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