/** * Contains the JailedSite object used both by the application * site, and by each plugin */ (function(){ /** * JailedSite object represents a single site in the * communication protocol between the application and the plugin * * @param {Object} connection a special object allowing to send * and receive messages from the opposite site (basically it * should only provide send() and onMessage() methods) */ JailedSite = function(connection) { this._interface = {}; this._remote = null; this._remoteUpdateHandler = function(){}; this._getInterfaceHandler = function(){}; this._interfaceSetAsRemoteHandler = function(){}; this._disconnectHandler = function(){}; this._store = new ReferenceStore; var me = this; this._connection = connection; this._connection.onMessage( function(data){ me._processMessage(data); } ); this._connection.onDisconnect( function(m){ me._disconnectHandler(m); } ); } /** * Set a handler to be called when the remote site updates its * interface * * @param {Function} handler */ JailedSite.prototype.onRemoteUpdate = function(handler) { this._remoteUpdateHandler = handler; } /** * Set a handler to be called when received a responce from the * remote site reporting that the previously provided interface * has been succesfully set as remote for that site * * @param {Function} handler */ JailedSite.prototype.onInterfaceSetAsRemote = function(handler) { this._interfaceSetAsRemoteHandler = handler; } /** * Set a handler to be called when the remote site requests to * (re)send the interface. Used to detect an initialzation * completion without sending additional request, since in fact * 'getInterface' request is only sent by application at the last * step of the plugin initialization * * @param {Function} handler */ JailedSite.prototype.onGetInterface = function(handler) { this._getInterfaceHandler = handler; } /** * @returns {Object} set of remote interface methods */ JailedSite.prototype.getRemote = function() { return this._remote; } /** * Sets the interface of this site making it available to the * remote site by sending a message with a set of methods names * * @param {Object} _interface to set */ JailedSite.prototype.setInterface = function(_interface) { this._interface = _interface; this._sendInterface(); } /** * Sends the actual interface to the remote site upon it was * updated or by a special request of the remote site */ JailedSite.prototype._sendInterface = function() { var names = []; for (var name in this._interface) { if (this._interface.hasOwnProperty(name)) { names.push(name); } } this._connection.send({type:'setInterface', api: names}); } /** * Handles a message from the remote site */ JailedSite.prototype._processMessage = function(data) { switch(data.type) { case 'method': var method = this._interface[data.name]; var args = this._unwrap(data.args); method.apply(null, args); break; case 'callback': var method = this._store.fetch(data.id)[data.num]; var args = this._unwrap(data.args); method.apply(null, args); break; case 'setInterface': this._setRemote(data.api); break; case 'getInterface': this._sendInterface(); this._getInterfaceHandler(); break; case 'interfaceSetAsRemote': this._interfaceSetAsRemoteHandler(); break; case 'disconnect': this._disconnectHandler(); this._connection.disconnect(); break; } } /** * Sends a requests to the remote site asking it to provide its * current interface */ JailedSite.prototype.requestRemote = function() { this._connection.send({type:'getInterface'}); } /** * Sets the new remote interface provided by the other site * * @param {Array} names list of function names */ JailedSite.prototype._setRemote = function(names) { this._remote = {}; var i, name; for (i = 0; i < names.length; i++) { name = names[i]; this._remote[name] = this._genRemoteMethod(name); } this._remoteUpdateHandler(); this._reportRemoteSet(); } /** * Generates the wrapped function corresponding to a single remote * method. When the generated function is called, it will send the * corresponding message to the remote site asking it to execute * the particular method of its interface * * @param {String} name of the remote method * * @returns {Function} wrapped remote method */ JailedSite.prototype._genRemoteMethod = function(name) { var me = this; var remoteMethod = function() { me._connection.send({ type: 'method', name: name, args: me._wrap(arguments) }); }; return remoteMethod; } /** * Sends a responce reporting that interface just provided by the * remote site was sucessfully set by this site as remote */ JailedSite.prototype._reportRemoteSet = function() { this._connection.send({type:'interfaceSetAsRemote'}); } /** * Prepares the provided set of remote method arguments for * sending to the remote site, replaces all the callbacks with * identifiers * * @param {Array} args to wrap * * @returns {Array} wrapped arguments */ JailedSite.prototype._wrap = function(args) { var wrapped = []; var callbacks = {}; var callbacksPresent = false; for (var i = 0; i < args.length; i++) { if (typeof args[i] == 'function') { callbacks[i] = args[i]; wrapped[i] = {type: 'callback', num : i}; callbacksPresent = true; } else { wrapped[i] = {type: 'argument', value : args[i]}; } } var result = {args: wrapped}; if (callbacksPresent) { result.callbackId = this._store.put(callbacks); } return result; } /** * Unwraps the set of arguments delivered from the remote site, * replaces all callback identifiers with a function which will * initiate sending that callback identifier back to other site * * @param {Object} args to unwrap * * @returns {Array} unwrapped args */ JailedSite.prototype._unwrap = function(args) { var called = false; // wraps each callback so that the only one could be called var once = function(cb) { return function() { if (!called) { called = true; cb.apply(this, arguments); } else { var msg = 'A callback from this set has already been executed'; throw new Error(msg); } }; } var result = []; var i, arg, cb, me = this; for (i = 0; i < args.args.length; i++) { arg = args.args[i]; if (arg.type == 'argument') { result.push(arg.value); } else { cb = once( this._genRemoteCallback(args.callbackId, i) ); result.push(cb); } } return result; } /** * Generates the wrapped function corresponding to a single remote * callback. When the generated function is called, it will send * the corresponding message to the remote site asking it to * execute the particular callback previously saved during a call * by the remote site a method from the interface of this site * * @param {Number} id of the remote callback to execute * @param {Number} argNum argument index of the callback * * @returns {Function} wrapped remote callback */ JailedSite.prototype._genRemoteCallback = function(id, argNum) { var me = this; var remoteCallback = function() { me._connection.send({ type : 'callback', id : id, num : argNum, args : me._wrap(arguments) }); }; return remoteCallback; } /** * Sends the notification message and breaks the connection */ JailedSite.prototype.disconnect = function() { this._connection.send({type: 'disconnect'}); this._connection.disconnect(); } /** * Set a handler to be called when received a disconnect message * from the remote site * * @param {Function} handler */ JailedSite.prototype.onDisconnect = function(handler) { this._disconnectHandler = handler; } /** * ReferenceStore is a special object which stores other objects * and provides the references (number) instead. This reference * may then be sent over a json-based communication channel (IPC * to another Node.js process or a message to the Worker). Other * site may then provide the reference in the responce message * implying the given object should be activated. * * Primary usage for the ReferenceStore is a storage for the * callbacks, which therefore makes it possible to initiate a * callback execution by the opposite site (which normally cannot * directly execute functions over the communication channel). * * Each stored object can only be fetched once and is not * available for the second time. Each stored object must be * fetched, since otherwise it will remain stored forever and * consume memory. * * Stored object indeces are simply the numbers, which are however * released along with the objects, and are later reused again (in * order to postpone the overflow, which should not likely happen, * but anyway). */ var ReferenceStore = function() { this._store = {}; // stored object this._indices = [0]; // smallest available indices } /** * @function _genId() generates the new reference id * * @returns {Number} smallest available id and reserves it */ ReferenceStore.prototype._genId = function() { var id; if (this._indices.length == 1) { id = this._indices[0]++; } else { id = this._indices.shift(); } return id; } /** * Releases the given reference id so that it will be available by * another object stored * * @param {Number} id to release */ ReferenceStore.prototype._releaseId = function(id) { for (var i = 0; i < this._indices.length; i++) { if (id < this._indices[i]) { this._indices.splice(i, 0, id); break; } } // cleaning-up the sequence tail for (i = this._indices.length-1; i >= 0; i--) { if (this._indices[i]-1 == this._indices[i-1]) { this._indices.pop(); } else { break; } } } /** * Stores the given object and returns the refernce id instead * * @param {Object} obj to store * * @returns {Number} reference id of the stored object */ ReferenceStore.prototype.put = function(obj) { var id = this._genId(); this._store[id] = obj; return id; } /** * Retrieves previously stored object and releases its reference * * @param {Number} id of an object to retrieve */ ReferenceStore.prototype.fetch = function(id) { var obj = this._store[id]; this._store[id] = null; delete this._store[id]; this._releaseId(id); return obj; } })();