Permalink
Cannot retrieve contributors at this time
Fetching contributors…

/** | |
* 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; | |
} | |
})(); | |