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