Source: ARCore/machines.js

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