Source: common/models/rpc.js

define(["lib/jquery",      "lib/underscore",           "common/network/dispatcher",
        "common/util/log", "common/util/binding",      "common/util/check"],
function($,                 _,                          Dispatcher,
         Log,               Binding,                    check) {
    "use strict";

    var logger = Log("RPC");

    // name -> [fn, valued] for all RPC this machine has defined
    var rpcs = {};

    // returns the names of a function's arguments
    // source: Prototype <http://prototypejs.org/>
    //         src/prototype/lang/function.js:38-43
    //         at commit a3cac7cfe755a81f419395b7819eff1f56cd9607
    var argumentNames = function(fn) {
        var names = fn.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1]
            .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, "")
            .replace(/\s+/g, "").split(",");
        return names.length === 1 && !names[0] ? [] : names;
    };

    // calls an RPC that has been defined by this app
    var invokeLocalRPC = function(data, callback) {
        // look up the RPC and make sure it exists
        var rpc = rpcs[data.name];

        if(!rpc) {
            logger.warn("Got a request for a nonexistant RPC: ", data);
        }

        var fn = rpc[0];
        var valued = rpc[1];

        // construct the context in which the function will be invoked
        var context = {
            originating_machine: data
        };

        if(valued) {
            context.return = function(value) {
                callback({return_value: value});
            };
            context.throw = function(message) {
                callback({error: message});
            };
        }

        // invoke the function
        try {
            var result = fn.apply(context, data.params);

            if(valued) {
                if(result !== undefined) {
                    // we have a valued RPC that returned a value, send
                    // that back
                    callback({return_value: result});
                }
                else {
                    // we have a valued RPC that didn't return a value,
                    // do nothing and wait for it to call context.return()
                }
            }
            else {
                // we have a void RPC, send back an empty response as soon
                // as execution is done
                callback({});
            }
        }
        catch(e) {
            // The RPC failed, send back the error message
            logger.warn("RPC invocation", data, "threw an error", e);
            callback({error: e});
        }
    };

    // client-side RPC that the bridge can call to get the result of an
    // RPC
    Dispatcher._bridge.registerAsyncRPC("rpc_called", invokeLocalRPC);

    // on unload, we need to deregister our RPCs
    $(window).unload(function() {
        _.each(rpcs, function(value, key) {
            Dispatcher._bridge.fireEvent("rpc_deregister", {
                name: key
            });
        });
    });

    /**
     * @privconstructor
     * @class Represents an RPC advertised by a LuXoR device. To register a new
     *        RPC or find an RPC advertised by another device, see
     *        {@link Lens.Machines} and {@link Machine}. See {@tutorial rpcs} for
     *        an overview of the RPC system.
     *
     * @property {String}        name    Name of this RPC.
     * @property {Machine}       machine The machine advertising this RPC.
     * @property {Boolean}       valued  If true, this RPC returns a value.
     * @property {Array<String>} params  A list of argument names the RPC takes
     *                                   when called.
     * @name RPC
     */
    var RPC = function(machine, name, props){
        /** @alias RPC.prototype */
        var self = {};

        self.machine = machine;
        self.name = name;

        props = props || {};

        if(props.valued === "true") {
            self.valued = true;
        }
        else if(props.valued === "false") {
            self.valued = false;
        }
        else {
            throw "Error parsing RPC message from Bridge: RPC property \"valued\" was not true or false";
        }

        self.params = JSON.parse(props.params);
        Object.freeze(self.params);

        /**
         * Invokes this RPC.
         *
         * @param {Array}    [args]     Array of arguments. Defaults to the
         *                              empty array.
         * @param {Function} [callback] Callback that will automatically be
         *                              attached to the returned Promise as a
         *                              done callback.
         *
         * @return {Promise} A jQuery Promise object that allows the caller to
         *                   attach callback for when the RPC completes or
         *                   fails. If this RPC is valued, any done callbacks
         *                   will get the result of the RPC as an argument. If
         *                   the RPC fails, the failure callbacks will get an
         *                   error message as an argument.
         */
        self.call = function(args, callback) {
            check(args, check.Match.Optional([check.Match.Serializable]));
            check(callback, check.Match.Optional(Function));

            // default is no arguments
            if(!args) {
                args = [];
            }

            // construct the Deferred that gets returned
            var deferred = $.Deferred();
            if(callback) {
                deferred.done(callback);
            }

            var rpcCallback = function(data) {
                if(data.return_value) {
                    deferred.resolve(data.return_value);
                }
                else if(data.error) {
                    deferred.reject(data.error);
                }
                else {
                    deferred.resolve();
                }
            };

            var params = {
                name: name,
                machine: machine.id,
                params: args
            };

            if(machine.isLocalhost()) {
                invokeLocalRPC(params, rpcCallback);
            }
            else {
                Dispatcher._bridge.callRPC("rpc_call", params, rpcCallback);
            }

            return deferred.promise();
        };


        Object.freeze(self);
        return self;
    };

    _.extend(RPC, /** @lends RPC */ {

        /**
         * Registers a RPC provided by this machine.
         * @param  {String}   name   Name of the RPC.
         * @param  {Function} fn     Function implementing the RPC.
         * @param  {Boolean}  valued True if the RPC returns a value.
         *
         * @return {Binding} A binding that can be cleared to deregister the RPC.
         * @private
         */
        _registerRPC: function(name, fn, valued) {
            if(rpcs[name]) {
                throw "RPC with name " + name + " already exists";
            }

            // add it to our internal map
            rpcs[name] = [fn, valued];

            // register it with the backend
            Dispatcher._bridge.fireEvent("rpc_register", {
                name: name,
                valued: valued,
                params: argumentNames(fn)
            });

            return Binding(function() {
                delete rpcs[name];
                Dispatcher._bridge.fireEvent("rpc_deregister", {
                    name: name
                });
            });
        }
    });

    return RPC;
});