Skip to content

Instantly share code, notes, and snippets.

@IUnknown68
Last active August 29, 2015 14:10
Show Gist options
  • Save IUnknown68/46a8a09379d89f103299 to your computer and use it in GitHub Desktop.
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.
//==============================================================================
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;
})();
@IUnknown68
Copy link
Author

Status:

strongly WIP

Requirements:

Q

Usage:

In the background script:

// this object will be available to clients
var someSharedObject = {
  foo: function(one, two) {
    return one + two;
  },
  bar: function() {
    throw new Error('This must fail');
  }
};

// create server
var connection = new RPC.Server();

// share object under name 'myObject'
connection.share('myObject', someSharedObject);

From the client script:

// connect to background
var connection = new RPC.Client();
var myObject = null;

// connect to 'myObject'...
connection
.get('myObject')

// ... store myObject and call 'foo'...
.then(function(obj) {
  myObject = obj;
  console.log('get myObject done:', myObject);
  return myObject.foo(3, 5);
})

//... then call 'bar'...
.then(function(result) {
  console.log('myObject.foo called:', result);
  return myObject.bar();
})

//... and handle any errors.
.fail(function(error) {
  console.log('Error:', error);
});

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:

connection.get(1, 'ctObject');

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:

var someSharedObject = {
  foo: function(one, two, connection) {
    connection.get('otherObject').then(...);
    return one + two;
  },
  ...
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment