File: src/registry.js
var Rpc = require('./rpc');
var WasabiError = require('./wasabi_error');
/**
* Extract the name of a function using .wsbFnName, .name, or parsing .toString()
*/
function _parseFunctionName(fn) {
var match;
var name;
if (typeof fn !== 'function') {
return false;
}
if (typeof fn.wsbFnName === 'string') {
return fn.wsbFnName;
}
/**
* Another apparent toString parsing hack that isn't.
* Function.prototype.toString is specified to return a string
* representation of the function with the syntax of a function declaration
* since ECMAScript 1 (15.3.4.2)
*/
match = /function\s*(\w*)/.exec(fn.toString());
if (match && match[1] && match[1].length > 0) {
name = match[1];
} else {
name = fn.name;
}
fn.wsbFnName = name;
return fn.wsbFnName;
}
/**
* Manages the registration of classes for consistent
* serialization/unserialization
* @class Registry
* @constructor
*/
function Registry() {
// hash <-> klass
this.klassToHash = {};
this.hashToKlass = {};
// hash <-> RPC
this.rpcToHash = {};
this.hashToRpc = {};
// objects by serial number
this.objects = {};
this.nextSerialNumber = 1;
}
Registry.prototype = {
constructor: Registry,
/**
* Return a unique hash from one or two functions suitable for entering into
* the registry's klass or rpc tables. Note that the supplied functions must
* have a valid `name` property
* @method hash
* @param {Function} fn1 The first function to hash
* @param {Function} fn2 The (optional) second function to hash
* @return {Number} The XOR hash of the characters of
* klass.prototype.constructor.name
*/
hash: function (fn1, fn2) {
var result = 0;
var name = _parseFunctionName(fn1);
var i;
if (fn2) {
var name2 = _parseFunctionName(fn2);
name += '.' + name2;
}
// compute a 16 bit hash using wrap-around bitshifting and XOR
for (i = 0; i < name.length; i++) {
// save the lowest bit, and wrap it around to the front, and shift
// all other bits down by 1, filling with zeros from the left
result = ((result & 1) << 15) + (result >>> 1);
// then XOR the current character into the hash
result ^= name.charCodeAt(i);
}
return result;
},
/**
* Register a class with Wasabi, allowing it to transmit instances of
* this class through a Connection
* @method addClass
* @param {Function} klass The constructor of the class to add
* @param {Wasabi} instance The Wasabi instance to invoke the RPC through
*/
addClass: function (klass, instance) {
var k;
var fn;
var keyOfRealFunction;
var hash;
var args;
var name = _parseFunctionName(klass);
// wasabi requires the `name` property to be set and valid
if (typeof name !== 'string' || name.length < 1) {
throw new WasabiError('Attempt to add anonymous class. Give it a name with "function NAME () { ... }"');
}
hash = this.hash(klass);
// detect hash collisions (library error) or class redefinition (user error)
if (this.hashToKlass[hash] !== undefined) {
throw new WasabiError('Invalid attempt to redefine class ' + klass.name + ' with hash ' + hash);
}
// add the klass to the hash map
this.klassToHash[klass] = hash;
this.hashToKlass[hash] = klass;
// register this class's RPCs
for (k in klass.prototype) {
// RPCs are functions starting with rpc, c2s, or s2c, and not ending
// with Args
if (typeof klass.prototype[k] === 'function' && !(/Args$/.test(k)) && /^(rpc|c2s|s2c)/.test(k)) {
fn = klass.prototype[k];
// find the Args function if it exists (for rpcFoo this would be
// rpcFooArgs)
args = klass.prototype[k + 'Args'];
// if this class was already added to a different Wasabi
// instance, we'll use the real method instead of the
// replacement we create later in this function. This usually
// only happens in the test suite
keyOfRealFunction = 'wsbReal_' + k;
if (klass.prototype[keyOfRealFunction] !== undefined) {
fn = klass.prototype[keyOfRealFunction];
} else {
klass.prototype[keyOfRealFunction] = fn;
}
// use property name if the function name can't be found
fn.wsbFnName = _parseFunctionName(fn) || k;
// replace the definition on the klass's prototype with the
// invocation stub generated by mkRpc
klass.prototype[k] = this.mkRpc(klass, fn, args, instance);
}
}
},
/**
* Create an RPC from the supplied procedure function and serialize
* function. `instance` must be a {{#crossLink "Wasabi"}}{{/crossLink}}
* instance
* @method mkRpc
* @param {Function} klass The klass this rpc is associated with, or `false`
* for static RPCs
* @param {Function} fn The local function to call when the RPC is invoked
* on a remote host
* @param {Function} serialize A serialize function describing the arguments
* used by this RPC
* @param {Wasabi} instance The Wasabi instance to invoke this RPC through
* @return {Function} The function you should call remotely to invoke the
* RPC on a connection
*/
mkRpc: function (klass, fn, serialize, instance) {
var hash = this.hash(klass, fn);
var rpc;
// detect anonymous static RPCs
if (typeof fn.wsbFnName !== 'string' || fn.wsbFnName.length < 1) {
throw new WasabiError('Attempt to add anonymous RPC. Give it a name with "function NAME () { ... }"');
}
// detect hash collisions (library error) or class redefinition (user error)
if (this.hashToRpc[hash] !== undefined) {
throw new WasabiError('Invalid attempt to redefine RPC ' + (klass ? klass.name + '#' : '') + fn.name + ' with hash ' + hash);
}
// create a new RPC definition
rpc = new Rpc(fn, klass, serialize);
// update the hash <-> rpc mapping
this.rpcToHash[rpc] = hash;
this.hashToRpc[hash] = rpc;
// if klass is truthy, create a method RPC. `this` will refer to the
// object which the RPC is invoked on locally, and we should send the
// invocation through instance which the object belongs to. otherwise
// the method is static, and we should send the RPC through the wasabi
// instance it was defined on
return function () {
var args = Array.prototype.slice.call(arguments);
if (klass) {
this.wsbInstance._invokeRpc(rpc, this, args);
} else {
instance._invokeRpc(rpc, false, args);
}
};
},
/**
* Register an instance of a klass
* @method addObject
* @param {NetObject} obj The object to add to the registry
* @param {Nunmber} serial The serial number to assign to this object. If
* falsy, the nextSerialNumber will be used
*/
addObject: function (obj, serial) {
obj.wsbSerialNumber = serial || this.nextSerialNumber;
this.nextSerialNumber += 1;
this.objects[obj.wsbSerialNumber] = obj;
},
/**
* Remove an instance of a klass
* @method removeObject
* @param {NetObject|number} arg The object or serial number of an object to
* remove from the registry
*/
removeObject: function (arg) {
var k;
if (typeof arg === 'number') {
delete this.objects[arg];
} else {
for (k in this.objects) {
if (this.objects.hasOwnProperty(k) && this.objects[k] === arg) {
delete this.objects[k];
return;
}
}
}
},
/**
* Get an instance of a klass by serial number
* @method getObject
*/
getObject: function (serial) {
return this.objects[serial];
},
/**
* Get the function/constructor/klass represented by the given hash
* @method getClass
*/
getClass: function (hash) {
return this.hashToKlass[hash];
},
/**
* get the RPC function associated with the hash
* @method getRpc
*/
getRpc: function (hash) {
return this.hashToRpc[hash];
}
};
module.exports = Registry;