/** * Lens.Machines allows you interact with the LuXoR device your app is * running on, as well as discover and interact with nearby machines. * * LuXoR devices have capabilities, which you can turn on and off from your * apps. In addition, apps can define RPCs, which can be invoked by apps * running on nearby machines. * * See {@tutorial rpcs} for more information on defining and calling RPCs. * * @name Lens.Machines * @namespace */ define(["lib/jquery", "lib/underscore", "common/network/dispatcher", "common/models/machine", "common/util/binding_list", "common/util/log", "lib/meteor", "common/util/dep_map", "cgi-bin/hostname", "common/util/check", "common/constants"], function($, _, Dispatcher, Machine, BindingList, Log, meteor, DepMap, hostname, check, Constants) { "use strict"; var Deps = meteor.deps.Deps; var logger = Log("Capabilities"); var localhost = Machine("localhost", hostname); var subscribers = BindingList(); var machinesById = {}; var machinesDep = new Deps.Dependency(); var idDeps = DepMap(); // machine ids var capDeps = DepMap(); // capability names var rpcDeps = DepMap(); // rpc names var dbDeps = DepMap(); // db name var allMachines = function() { return _.values(machinesById).concat(localhost); }; var validFilterState = check.Match.Where(function(state) { if((state !== "added") && (state !== "removed")) { throw check.Match.Error("\"" + state + "\" is not a valid value for filter.state"); } return true; }); var validFilterType = check.Match.Where(function(type) { if((type !== "rpc") && (type !== "db") && (type !== "capability")) { throw check.Match.Error("\"" + type + "\" is not a valid value for filter.type"); } return true; }); var Machines = /** @lends Lens.Machines */ { /** * Subscribes a function to be called when capabilities or RPCs are * added and/or removed. * * @param {Function} fn * A callback function, invoked when a capability is added or * removed. Gets the {@link Capability}, {@link Machine}, or * {@link Lens.DB} as the first argument, the state (either "added" * or "removed") as the second argument, and the type ("rpc", "db" * or "capability") as the third argument. * @param {Object} [filter] * Filters to limit which capability additions/removals the * subscriber function is called for. * @param {Machine | String | RegExp} [filter.machine] * A {@link Machine}, or a string or RegExp to match the machine * name against. * @param {String | RegExp} [filter.name] * A string or RegExp to match the capability name against. * @param {String} [filter.state] * Either "added" or "removed", if you only want to be notified of * that state. * @param {String} [filter.type] * "rpc", "db", or "capability", if you only want to be notified of * that type. * * @return {Binding} A binding that can be used to unsubscribe. */ subscribe: function() { check(arguments[0], Function); check(arguments[1], check.Match.Optional({ machine: check.Match.Optional(check.Match.OneOf(String, RegExp, check.Match.Machine)), name: check.Match.Optional(check.Match.OneOf(String, RegExp)), state: check.Match.Optional(validFilterState), type: check.Match.Optional(validFilterType) })); if(arguments.length === 1) { return subscribers.add([arguments[0], null]); } else if(arguments.length === 2) { var fn = arguments[0]; var filter = arguments[1]; return subscribers.add([fn, filter]); } throw "Lens.Capabilities.subscribe expected 1 or 2 arguments, got " + arguments.length; }, /** * Returns a list of all nearby machines. * * @return {Machine[]} */ all: function() { machinesDep.depend(); return allMachines(); }, /** * Finds a machine, given its name. * * @param {String} name * @return {Machine} The Machine with the given name, or null if no * such machine exists. */ machine: function(name) { check(name, String); if(name === "localhost") { return localhost; } else { var m = _.find(this.all(), function(machine) { return machine.name() === name; }); return m || null; } }, /** * Finds a machine, given its id. * * @param {String} id * @return {Machine} The Machine with the given id, or null if no * such machine exists. */ findById: function(id) { check(id, String); if(id === "localhost") { return localhost; } else { idDeps.depend(id); return machinesById[id] || null; } }, /** * Finds all machines with the given capability. * * @param {String} name Name of the capability * @return {Array<Machine>} Array of matching {@link Machine}s */ findByCapability: function(name) { check(name, String); capDeps.depend(name); return _.filter(allMachines(), function(machine) { return machine.capability(name) !== null; }); }, /** * Finds all machines with the given RPC. * * @param {String} name Name of the RPC * @return {Array<Machine>} Array of matching {@link Machine}s */ findByRPC: function(name) { check(name, String); rpcDeps.depend(name); return _.filter(allMachines(), function(machine) { return machine.rpc(name) !== null; }); }, /** * Finds all machines with the given DB. * * @param {String} name Name of the DB * @return {Array<Machine>} Array of matching {@link Machine}s */ findByDB: function(name) { check(name, String); dbDeps.depend(Constants.db.PUBLISHED_PREFIX + Constants.db.NAME_SEPARATOR + name); return _.filter(allMachines(), function(machine) { return machine.db(name) !== null; }); }, /** * The local LuminAR machine. * @type{Machine} * @memberOf Lens.Machines */ localhost: localhost }; // target is a string to test // pattern is either a string or RegExp. If it's a string, it's tested for // equality with target. If it's a RegExp, it's tested for a match against // target. var matchString = function(target, pattern) { if(pattern.constructor === RegExp) { return pattern.test(target); } else { return pattern === target; } }; // given a subscription filter capability and state // of an event, returns whether the event matches the filter. var matchFilter = function(filter, capability, state, type) { // no filter -> always true if(!filter) { return true; } // type filter if(filter.type) { if(!matchString(type, filter.type)) { return false; } } // machine filter if(filter.machine) { var machineFilter = filter.machine; if(machineFilter._isLensMachine) { machineFilter = machineFilter.name(); } if(!matchString(capability.machine.name(), machineFilter)) { return false; } } // capability name filter if(filter.name) { if(!matchString(capability.name, filter.name)) { return false; } } // action filter if(filter.state && (filter.state !== state)) { return false; } // if it's passed all the filters, return true return true; }; var notifySubscribers = function(capability, state, type) { subscribers.each(function(subscriber) { var fn = subscriber[0]; var filter = subscriber[1]; if(matchFilter(filter, capability, state, type)) { fn(capability, state, type); } }); }; Dispatcher._bridge.subscribe("capability_added", function(data) { var machine = Machines.findById(data.machine); // create the machine if we need to if(!machine) { machine = Machine(data.machine); machinesById[machine.id] = machine; machinesDep.changed(); idDeps.changed(machine.id); } // add Capability var capabilityInfo = machine._addCap(data.name, data.properties); var capability = capabilityInfo[0]; var type = capabilityInfo[1]; if(type === "rpc") { rpcDeps.changed(data.name); } else if(type === "db") { dbDeps.changed(data.name); } else { capDeps.changed(data.name); } // notify subscribers notifySubscribers(capability, "added", type); }); Dispatcher._bridge.subscribe("capability_removed", function(data) { // find the Machine var machine = Machines.findById(data.machine); if(machine) { // remove the capability var removedCapabilityInfo = machine._removeCap(data.name); var removedCapability = removedCapabilityInfo[0]; var type = removedCapabilityInfo[1]; if(removedCapability) { if(machine.capabilities().length === 0) { // Machine has no more capabilities, remove it delete machinesById[machine.id]; } if(type === "rpc") { rpcDeps.changed(data.name); } else if(type === "db") { dbDeps.changed(data.name); } else { capDeps.changed(data.name); } notifySubscribers(removedCapability, "removed", type); } else { logger.warn("Warning non-existant capability " + data.name + " removed from " + data.machine); } } else { logger.warn("Warning: capability removed from non-existant machine " + data.machine); } }); // ask the Bridge to send us the status of every capability Dispatcher._bridge.fireEvent("capability_refresh"); Lens._addMember(Machines, "Machines"); return Machines; });