/**
* 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;
});