Skip to content
Fetching contributors…
Cannot retrieve contributors at this time
433 lines (368 sloc) 12.3 KB
/**
* 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;
}
})();
Something went wrong with that request. Please try again.