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