API Docs for:
Show:

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;