socket.io-rpc
Version:
Minimalistic remote procedure call(RPC/RMI) library bootstrapped on socket.io
266 lines (247 loc) • 10.9 kB
JavaScript
angular.module('RPC', []).factory('$rpc', function ($rootScope, $q) {
var invocationCounter = 0;
var endCounter = 0;
var serverChannels = {};
var clientChannels = {};
var deferreds = [];
var baseURL;
var rpcMaster;
var serverRunDate; // used for invalidating the cache
var serverRunDateDeferred = $q.defer();
serverRunDateDeferred.promise.then(function (date) {
serverRunDate = new Date(date);
});
var callEnded = function (Id) {
if (deferreds[Id]) {
delete deferreds[Id];
endCounter++;
rpc.onEnd(endCounter);
if (endCounter == invocationCounter) {
rpc.onBatchEnd(endCounter);
invocationCounter = 0;
endCounter = 0;
}
}else {
console.warn("Deferred Id " + Id + " was resolved/rejected more than once, this should not occur.");
}
};
/**
* Generates a 'safe' key for storing cache in client's local storage
* @param name
* @returns {string}
*/
function getCacheKey(name) {
return 'SIORPC:' + baseURL + '/' + name;
}
function cacheIt(key, data) {
try{
localStorage[key] = JSON.stringify(data);
}catch(e){
console.warn("Error raised when writing to local storage: " + e); // probably quoata exceeded
}
}
var _loadChannel = function (name, handshakeData, deferred) {
rpcMaster.emit('load channel', {name: name, handshake: handshakeData});
if (!serverChannels.hasOwnProperty(name)) {
serverChannels[name] = {};
}
var channel = serverChannels[name];
channel._loadDef = deferred;
serverRunDateDeferred.promise.then(function () {
channel._socket = io.connect(baseURL + '/rpc-' + name, handshakeData)
.on('return', function (data) {
deferreds[data.Id].resolve(data.value);
$rootScope.$apply();
callEnded(data.Id);
})
.on('error', function (data) {
if (data && data.Id) {
if (deferreds[data.Id]) {
deferreds[data.Id].reject(data.reason);
$rootScope.$apply();
callEnded(data.Id);
} else {
console.warn('Deffered Id ' + data.Id + ' has been already resolved/rejected.');
}
} else {
console.error("Unknown error occured on RPC socket connection");
}
})
.on('connect_failed', function (reason) {
console.error('unable to connect to namespace ', reason);
channel._loadDef.reject(reason);
$rootScope.$apply();
})
.on('disconnect', function (data) {
delete serverChannels[name];
console.warn("Server channel " + name + " disconnected.");
});
var cacheKey = getCacheKey(name);
var cached = localStorage[cacheKey];
if (cached) {
cached = JSON.parse(cached);
if (serverRunDate < new Date(cached.cDate)) {
registerRemoteFunctions(cached, false); // will register functions from cached manifest
} else {
//cache has been invalidated
delete localStorage[cacheKey];
rpcMaster.emit('load channel', {name: name, handshake: handshakeData});
}
} else {
rpcMaster.emit('load channel', {name: name, handshake: handshakeData});
}
});
return channel._loadDef.promise;
};
var registerRemoteFunctions = function (data, storeInCache) {
var channelObj = serverChannels[data.name];
data.fnNames.forEach(function (fnName) {
channelObj[fnName] = function () {
invocationCounter++;
channelObj._socket.emit('call',
{Id: invocationCounter, fnName: fnName, args: Array.prototype.slice.call(arguments, 0)}
);
if (invocationCounter == 1) {
rpc.onBatchStarts(invocationCounter);
}
rpc.onCall(invocationCounter);
deferreds[invocationCounter] = $q.defer();
return deferreds[invocationCounter].promise;
};
});
channelObj._loadDef.resolve(channelObj);
if (storeInCache !== false) {
$rootScope.$apply();
data.cDate = new Date(); // here we make a note of when the channel cache was saved
cacheIt(getCacheKey(data.name), data)
}
};
var connect = function (url) {
if (!rpcMaster && url) {
baseURL = url;
rpcMaster = io.connect(url + '/rpc-master')
.on('serverRunDate', function (runDate) {
serverRunDateDeferred.resolve(runDate);
$rootScope.$apply();
})
.on('channelFns', registerRemoteFunctions)
.on('channelDoesNotExist', function (data) {
var channelObj = serverChannels[data.name];
channelObj._loadDef.reject();
console.warn("no channel under name: " + data.name);
$rootScope.$apply();
})
.on('client channel created', function (name) {
var channel = clientChannels[name];
var socket = io.connect(baseURL + '/rpcC-' + name + '/' + rpcMaster.socket.sessionid); //rpcC stands for rpc Client
channel.socket = socket;
socket.on('call', function (data) {
var exposed = channel.fns;
if (exposed.hasOwnProperty(data.fnName) && typeof exposed[data.fnName] === 'function') {
var that = exposed['this'] || exposed;
var retVal = exposed[data.fnName].apply(that, data.args);
if (retVal) {
//TODO investigate if the next block could be changed to $q.when() call
if (typeof retVal.then === 'function') { // this is async function, so we will emit 'return' after it finishes
//promise must be returned in order to be treated as async
retVal.then(function (asyncRetVal) {
socket.emit('return', { Id: data.Id, value: asyncRetVal });
}, function (error) {
socket.emit('error', { Id: data.Id, reason: error });
});
} else {
socket.emit('return', { Id: data.Id, value: retVal });
}
}
} else {
socket.emit('error', {Id: data.Id, reason: 'no such function has been exposed: ' + data.fnName });
}
});
channel.deferred.resolve(channel);
});
} else {
console.warn("ignoring connect command, either url of master null or already connected");
}
};
var rpc = {
connect: connect,
loadAllChannels: function () {
if (rpcMaster) {
rpcMaster.__channelListLoad = $q.defer();
rpcMaster.emit('load channelList');
rpcMaster
.on('channels', function (data) {
var name = data.list.pop();
while(name) {
serverChannels[name] = {};
_loadChannel(name);
name = data.list.pop();
}
rpcMaster.__channelListLoad.resolve(serverChannels);
$rootScope.$apply();
});
return rpcMaster.__channelListLoad.promise;
} else {
console.error("no connection to master");
}
},
/**
* for a particular channel this will connect and prepared the channel for use, if called more than once for one
* channel, it will return it's instance
* @param {string} name
* @param {*} [handshakeData] custom param for authentication
* @returns {promise}
*/
loadChannel: function (name, handshakeData) {
if (serverChannels.hasOwnProperty(name)) {
return serverChannels[name]._loadDef.promise;
} else {
var def = $q.defer();
_loadChannel(name, handshakeData, def);
return def.promise;
}
},
/**
* @param name {string}
* @param toExpose {Object} object with functions as values
* @returns {Promise} a promise saying that server is connected and can call the client
*/
expose: function (name, toExpose) { //
if (!clientChannels.hasOwnProperty(name)) {
clientChannels[name] = {};
}
var channel = clientChannels[name];
channel.fns = toExpose;
channel.deferred = $q.defer();
var fnNames = [];
for(var fn in toExpose)
{
fnNames.push(fn);
}
rpcMaster.emit('expose channel', {name: name, fns: fnNames});
return channel.deferred.promise;
}
};
rpc.onBatchStarts = function () {};
rpc.onBatchEnd = function () {};
rpc.onCall = function () {};
rpc.onEnd = function () {};
return rpc;
}).directive('rpcController', function ($controller, $q, $rpc) {
return {
scope: true,
link: function (scope, elm, attr) {
var ctrlName = attr.rpcController;
var promise = $rpc.loadChannel(attr.rpcChannel);
promise.then(function (channel) {
scope.rpc = channel;
var ctrl = $controller(ctrlName, {
$scope: scope
});
elm.children().data('$ngControllerController', ctrl);
}, function (err) {
console.error("Cannot instantiate rpc-controller - channel failed to load");
});
}
};
});