define(["lib/jquery", "lib/underscore", "common/network/dispatcher",
"common/util/log", "common/util/id_generator", "common/models/capability",
"common/models/rpc", "ARCore/machines", "require",
"lib/meteor", "ARCore/db", "common/constants",
"common/util/check"],
function($, _, Dispatcher,
Log, IDGenerator, Capability,
RPC, Machines, require,
meteor, DB, Constants,
check) {
"use strict";
var Deps = meteor.deps.Deps;
var hasPrefix = function(prefix, string) {
var stringPrefix = string.substring(0, prefix.length);
return stringPrefix === prefix;
};
var stripPrefix = function(prefix, string) {
if(hasPrefix(prefix, string)) {
string = string.substring(prefix.length);
}
return string;
};
// descriptive shortcuts for hasPrefix and stripPrefix, made via partial
// application
var capIsRPC = _.bind(hasPrefix, window, Constants.db.RPC_CAP_PREFIX);
var stripRPCPrefix = _.bind(stripPrefix, window, Constants.db.RPC_CAP_PREFIX);
var capIsDB = _.bind(hasPrefix, window, Constants.db.DB_CAP_PREFIX);
var stripDBPrefix = _.bind(stripPrefix, window, Constants.db.DB_CAP_PREFIX);
var dbIsPublished = _.bind(hasPrefix, window, Constants.db.PUBLISHED_PREFIX+Constants.db.NAME_SEPARATOR);
var stripPublishedPrefix = _.bind(stripPrefix, window, Constants.db.PUBLISHED_PREFIX+Constants.db.NAME_SEPARATOR);
var dbIsShared = _.bind(hasPrefix, window, Constants.db.SHARED_PREFIX+Constants.db.NAME_SEPARATOR);
var stripSharedPrefix = _.bind(stripPrefix, window, Constants.db.SHARED_PREFIX+Constants.db.NAME_SEPARATOR);
/**
* @privconstructor
* @class Represents a LuXoR device. To find devices, see {@link Lens.Machines}
*
* @property {String} id UUID of this machine, or "localhost".
* @property {String} hostname The hostname of the machine. Only guarenteed
* to be available on localhost; may be null for
* other machines.
* @name Machine
*/
var Machine = function(id, hostname) {
check(id, String);
/** @alias Machine.prototype */
var self = {};
// capability name -> Capability
var capMap = {};
// rpc name -> RPC
var rpcMap = {};
// published db name -> DB
var dbMap = {};
// shared db name -> DB
var sharedDBMap = {};
var nameDep = new Deps.Dependency();
var capsDep = new Deps.Dependency();
hostname = hostname || null;
// property so we can check if an object is a Machine
self._isLensMachine = true;
/**
* Returns the name of this machine: Either the hostname, or, if the
* hostname is unavailable, the UUID. For localhost, always returns
* "localhost".
*
* @return {String}
*/
self.name = function() {
nameDep.depend();
if(self.isLocalhost()) {
return "localhost";
}
return this.hostname || this.id;
};
/**
* Returns the capabilities advertised by this machine.
*
* @return {Array<Capability>} An array of {@link Capability}
*/
self.capabilities = function() {
capsDep.depend();
return _.values(capMap);
};
/**
* Finds a capability by name.
*
* @param {String} name The name of the capability.
*
* @return {Capability} A {@link Capability}, or null if there is no
* such capability.
*/
self.capability = function(name) {
check(name, String);
capsDep.depend();
return capMap[name] || null;
};
/**
* Returns the RPCs advertised by this machine.
*
* @return {Array<RPC>} An array of {@link RPC}
*/
self.rpcs = function() {
capsDep.depend();
return _.values(rpcMap);
};
/**
* Finds an RPC by name.
*
* @param {String} name The name of the RPC.
*
* @return {RPC} An {@link RPC}, or null if there is no such RPC.
*/
self.rpc = function(name) {
check(name, String);
capsDep.depend();
return rpcMap[name] || null;
};
/**
* Returns the databases published by this machine.
*
* @return {Array<Lens.DB>} An array of {@link Lens.DB}
*/
self.dbs = function() {
capsDep.depend();
return _.values(dbMap);
};
/**
* Finds a database published by this machine.
*
* @param {String} name The name of the database.
*
* @return {Lens.DB} A {@link Lens.DB}, or null if there is no such
* database.
*/
self.db = function(name) {
check(name, String);
capsDep.depend();
return dbMap[name] || null;
};
/**
* Finds a shared database that this machine is the master for.
*
* @param {String} name The name of the database.
*
* @return {Lens.DB} A {@link Lens.DB}, or null if there is no such
* database.
*
* @private
*/
self._sharedDB = function(name) {
return sharedDBMap[name] || null;
};
/**
* Identical to {@link Lens.Capabilities.subscribe}, but adds
* this machine as filter.machine.
*/
self.subscribe = function() {
var filter, fn;
if(arguments.length === 1) {
filter = {machine: self};
fn = arguments[0];
} else {
fn = arguments[0];
filter = arguments[1];
check(filter, Object);
filter.machine = self;
}
return require("ARCore/machines").subscribe(fn, filter);
};
/**
* Returns true if this is the local LuXoR device.
*
* @return {Boolean}
*/
self.isLocalhost = function() {
return (id === "localhost");
};
/**
* Ensures that this Machine has the given capability. Succeeds if the
* capability is already enabled or if a request to enable it succeeds;
* fails if a request to enable it fails.
* @param {String} capName Name of the capability.
* @param {Function} [callback] This callback is invoked if the
* request is successful.
*
* @return {Promise} A jQuery promise. You can attach callbacks using
* this promise or use jQuery's tools to do
* parallel requests. If you passed a callback,
* that callback will automatically be registered as
* a success handler for this promise.
*/
self.ensure = function(capName, callback) {
check(capName, String);
check(callback, check.Match.Optional(Function));
var deferred = $.Deferred();
var ensureCap = function() {
self.capability(capName).ensure(callback).done(function() {
deferred.resolve();
}).fail(function() {
deferred.reject();
});
};
if(self.capability(capName)) {
// it exists, we can ensure it immediately
ensureCap();
}
else {
// no such capability, wait for it to exist.
self.subscribe(ensureCap, {name: capName, state: "added"});
}
return deferred.promise();
};
/**
* Registers a new RPC that doesn't return a value. Nearby machines will
* be able to call this RPC.
*
* Throws an error if called on a machine other than localhost.
*
* See {@tutorial rpcs} for more information on defining RPCs.
*
* @param {String} name Name of the RPC.
* @param {Function} fn A function that implements the RPC.
*
* @return {Binding} A binding. Clearing this binding will deregister
* the RPC.
*/
self.registerVoidRPC = function(name, fn) {
check(name, String);
check(fn, Function);
if(!self.isLocalhost()) {
throw "Cannot define RPC on non-local machine";
}
return RPC._registerRPC(name, fn, false);
};
/**
* Registers a new RPC that returns a value. Nearby machines will
* be able to call this RPC.
*
* Throws an error if called on a machine other than localhost.
*
* See {@tutorial rpcs} for information on defining RPCs.
*
* @param {String} name Name of the RPC.
* @param {Function} fn A function that implements the RPC.
*
* @return {Binding} A binding. Clearing this binding will deregister
* the RPC.
*/
self.registerValuedRPC = function(name, fn) {
check(name, String);
check(fn, Function);
if(!this.isLocalhost()) {
throw "Cannot define RPC on non-local machine";
}
return RPC._registerRPC(name, fn, true);
};
/**
* Adds a capability to this machine.
*
* @param {String} name Name for the Capability
* @param {Object<String, String>} props Arbitrary properties
*
* @return {Capability} capability The new capability
* @private
*/
self._addCap = function(name, props) {
capsDep.changed();
var cap, type;
if(capIsRPC(name)) {
name = stripRPCPrefix(name);
cap = RPC(self, name, props);
type = "rpc";
rpcMap[name] = cap;
}
else if(capIsDB(name)) {
name = stripDBPrefix(name);
cap = DB._connect(self, name);
type = "db";
if(dbIsPublished(name)) {
dbMap[stripPublishedPrefix(name)] = cap;
}
else if(dbIsShared(name)) {
sharedDBMap[stripSharedPrefix(name)] = cap;
}
else {
throw "Invalid database name: " + name;
}
}
else {
cap = Capability(self, name, props);
type = "capability";
capMap[name] = cap;
if(name === "Nearby" && props.name) {
hostname = props.name;
nameDep.changed();
}
}
return [cap, type];
};
/**
* Removes a capability from this machine.
*
* @param {String} name Name of the capability.
*
* @return {Capability} capability The removed capability
*
* @private
*/
self._removeCap = function(name) {
capsDep.changed();
var removedCapability;
var type;
if(capIsRPC(name)) {
type = "rpc";
name = stripRPCPrefix(name);
removedCapability = rpcMap[name];
delete rpcMap[name];
}
if(capIsDB(name)) {
type = "db";
name = stripDBPrefix(name);
if(dbMap[name]) {
removedCapability = dbMap[name];
delete dbMap[name];
}
else if(sharedDBMap[name]) {
removedCapability = sharedDBMap[name];
delete sharedDBMap[name];
}
}
else {
type = "capability";
var oldName = self.name();
removedCapability = capMap[name];
delete capMap[name];
if(self.name() !== oldName) {
nameDep.changed();
}
}
return [removedCapability, type];
};
self.id = id;
Object.defineProperty(self, "hostname", {get: function() { return hostname; }});
Object.freeze(self);
return self;
};
return Machine;
});