Last active
August 29, 2015 14:10
-
-
Save IUnknown68/46a8a09379d89f103299 to your computer and use it in GitHub Desktop.
RPC for chrome. Connects objects between background/content/extension scripts in the most simple way.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//============================================================================== | |
var RPC = (function(){ | |
var RPC = {}; | |
var _nextConnectionId = 1; | |
var _nextMessageId = 1; | |
var _nextObjectId = 1; | |
/*** | |
* Class _SharedObject | |
*/ | |
function _SharedObject(objectName, object) { | |
this.target = object; | |
this.getMethods = function() { | |
var ar = []; | |
for (var i in object) { | |
if (object.hasOwnProperty(i) && 'function' === typeof object[i]) { | |
ar.push(i); | |
} | |
} | |
return ar; | |
} | |
this.onGetMethods = function(message, connection) { | |
return connection.sendResponseSucceeded(message, {methods: this.getMethods()}); | |
} | |
this.onCall = function(message, connection) { | |
try { | |
var methodName = message.methodName; | |
if (!methodName || !object.hasOwnProperty(methodName) || 'function' !== typeof object[methodName]) { | |
throw new Error('Method ' + methodName + ' does not exist on ' + message.objectName); | |
} | |
message.arguments = message.arguments || []; | |
// append connection to arguments | |
message.arguments.push(connection); | |
return connection.sendResponseSucceeded(message, object[methodName].apply(object[methodName], message.arguments)); | |
} | |
catch(e) { | |
console.error(e); | |
return connection.sendResponseFailed(message, {message: e.message}); | |
} | |
} | |
} | |
/*** | |
* Class _Proxy | |
*/ | |
function _Proxy(connection, objectName, methods) { | |
methods.forEach(function(methodName) { | |
this[methodName] = _Proxy.createMethod(connection, objectName, methodName); | |
}.bind(this)); | |
} | |
_Proxy.createMethod = function(connection, objectName, methodName) { | |
return function() { | |
return connection.callMethod(objectName, methodName, Array.prototype.slice.call(arguments)); | |
}; | |
}; | |
function _proxyfy(objectName, methods, connection, object) { | |
if (object) { | |
_Proxy.call(object, connection, objectName, methods); | |
return _unpackProperties(object, connection); | |
} | |
return new _Proxy(connection, objectName, methods); | |
} | |
function _unpackProperties(object, connection) { | |
for (var i in object) { | |
if (object.hasOwnProperty(i)) { | |
if ('object' === typeof object[i]) { | |
var property = object[i]; | |
// this is an RPC Proxy - evaluate | |
if (property.__objectName && property.__methods) { | |
object[i] = _proxyfy(property.__objectName, property.__methods, connection, property); | |
delete property.__objectName; | |
delete property.__methods; | |
} | |
} | |
} | |
} | |
return object; | |
} | |
/*** | |
* Class _Connection | |
*/ | |
function _Connection(_port, _onMessage, _onDisconnect) { | |
this.id = _nextConnectionId++; | |
var _requests = {}; | |
this.sharedObjects = {}; // public! | |
function _send(message) { | |
// if message.id is present, it's a reply | |
message.id = message.id || _nextMessageId++; | |
var request = _requests[message.id] = { | |
message: message, | |
q: Q.defer() | |
}; | |
_port.postMessage(message); | |
return request.q.promise; | |
} | |
var _onResponse = function (message) { | |
var id = message.id; | |
if (_requests[id]) { | |
var originalMessage = _requests[id].message; | |
var response = message.response; | |
switch(originalMessage.type) { | |
case 'get': // query a target | |
response = (message.response.methods) ? | |
_proxyfy(originalMessage.objectName, message.response.methods, this) : | |
message.response; // error | |
break; | |
case 'call': // call a method | |
_unpackProperties(response, this); | |
break; | |
} | |
var q = _requests[id].q; | |
delete _requests[id]; | |
_handled = true; | |
if ('error' === message.result) { | |
q.reject(response); | |
} | |
else { | |
q.resolve(response); | |
} | |
} | |
}.bind(this); | |
var _onGetMethods = function(message) { | |
var objectName = message.objectName; | |
var target = this.sharedObjects[objectName]; | |
if (target) { | |
return target.onGetMethods(message, this); | |
} | |
return this.sendResponseFailed(message, {message: 'Object ' + objectName + ' not found'}); | |
}.bind(this); | |
var _onCall = function (message) { | |
var objectName = message.objectName; | |
var target = this.sharedObjects[objectName]; | |
if (target) { | |
return target.onCall(message, this); | |
} | |
return false; | |
}.bind(this); | |
this.share = function(objectName, object) { | |
this.sharedObjects[objectName] = new _SharedObject(objectName, object); | |
} | |
this.get = function(objectName) { | |
return _send({id: _nextMessageId++, type: 'get', objectName: objectName}); | |
}; | |
this.callMethod = function(objectName, methodName, arguments) { | |
return _send({id: _nextMessageId++, type: 'call', objectName: objectName, methodName: methodName, arguments: arguments}); | |
}; | |
this.sendResponseSucceeded = function(message, data) { | |
if (!message.id) { | |
throw new Error('Response: Message must have an id'); | |
} | |
return _send({id: message.id, type: 'reply', response: data, result: 'ok'}); | |
}; | |
this.sendResponseFailed = function(message, data) { | |
if (!message.id) { | |
throw new Error('Response: Message must have an id'); | |
} | |
return _send({id: message.id, type: 'reply', response: data, result: 'error'}); | |
}; | |
_port.onMessage.addListener(function(message) { | |
if (_onMessage) { | |
// pass to parent | |
if (_onMessage(message, this)) { | |
// handled | |
return; | |
} | |
} | |
switch(message.type) { | |
case 'get': // query a target | |
_onGetMethods(message); | |
break; | |
case 'call': // call a method | |
_onCall(message); | |
break; | |
case 'reply': // any reply to a request | |
_onResponse(message); | |
break; | |
} | |
}.bind(this)); | |
_port.onDisconnect.addListener(function () { | |
if (_onDisconnect) { | |
_onDisconnect(this); | |
} | |
}.bind(this)); | |
}; | |
/*** | |
* Class RPC.Client | |
*/ | |
RPC.Client = function() { | |
var args = Array.prototype.slice.call(arguments); | |
var crt = chrome.tabs || chrome.runtime; | |
var _port = crt.connect.apply(crt, args); | |
_Connection.call(this, _port/*, _onMessage, _onDisconnect*/); | |
}; | |
/*** | |
* Class RPC.Server | |
*/ | |
RPC.Server = function() { | |
var _connections = {}; | |
var _sharedObjects = {}; | |
this.share = function(objectName, object) { | |
_sharedObjects[objectName] = new _SharedObject(objectName, object); | |
} | |
function _onDisconnect(connection) { | |
delete _connections[connection.id]; | |
} | |
this.get = function(connectionId, objectName) { | |
if (_connections[connectionId]) { | |
return _connections[connectionId].get(objectName); | |
} | |
var q = Q.defer(); | |
q.reject('Invalid connection id'); | |
return q.promise; | |
} | |
chrome.runtime.onConnect.addListener( | |
function(port) { | |
var tabId = port.sender.tab.id; | |
var connection = new _Connection( | |
port, | |
/*_onMessage,*/ null, | |
_onDisconnect | |
); | |
connection.sharedObjects = _sharedObjects; | |
_connections[connection.id] = connection; | |
} | |
); | |
}; | |
/*** | |
* Class RPC.Object | |
*/ | |
RPC.Object = function(connection, object) { | |
var objectName = 'Object_' + (_nextObjectId++); | |
var shared = new _SharedObject(objectName, object); | |
connection.sharedObjects[objectName] = shared; | |
this.__objectName = objectName; | |
this.__methods = shared.getMethods(); | |
for (var i in object) { | |
if (object.hasOwnProperty(i)) { | |
this[i] = object[i]; | |
} | |
} | |
}; | |
return RPC; | |
})(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Status:
strongly WIP
Requirements:
Q
Usage:
In the background script:
From the client script:
Of course you can also share an object from a client script the same way. If you want to connect from your background script to an object shared from a content script you need the connection id:
Currently the ID is not accessible - but:
Each function that is called on
someObject
receives an additional argument - the last argument:connection
. This can be used to connect back to any object shared by the other side: