socket.io-rpc
Version:
Minimalistic remote procedure call(RPC/RMI) library bootstrapped on socket.io
233 lines (216 loc) • 9.19 kB
JavaScript
// RPC client
var RPC = (function (rpc) {
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 = when.defer();
serverRunDateDeferred.promise.then(function (date) {
serverRunDate = new Date(date);
});
rpc.onBatchStarts = function () {};
rpc.onBatchEnd = function () {};
rpc.onCall = function () {};
rpc.onEnd = function () {};
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) {
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);
callEnded(data.Id);
})
.on('error', function (data) {
if (data && data.Id) {
deferreds[data.Id].reject(data.reason);
callEnded(data.Id);
} 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);
})
.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] = when.defer();
return deferreds[invocationCounter].promise;
};
});
channelObj._loadDef.resolve(channelObj);
if (storeInCache !== false) {
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);
})
.on('channelFns', registerRemoteFunctions)
.on('channelDoesNotExist', function (data) {
var channelObj = serverChannels[data.name];
channelObj._loadDef.reject();
console.warn("no channel under name: " + data.name);
})
.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) {
if (when.isPromise(retVal)) { // 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");
}
};
rpc.connect = connect;
rpc.loadAllChannels = function () {
if (rpcMaster) {
rpcMaster.__channelListLoad = when.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);
});
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 {when.promise}
*/
rpc.loadChannel = function (name, handshakeData) {
if (serverChannels.hasOwnProperty(name)) {
return serverChannels[name]._loadDef.promise;
} else {
var def = when.defer();
_loadChannel(name, handshakeData, def);
return def.promise;
}
};
/**
* @param name {string}
* @param toExpose {Object} object with functions as values
* @returns {Promise} when.js promise
*/
rpc.expose = function (name, toExpose) { //
if (!clientChannels.hasOwnProperty(name)) {
clientChannels[name] = {};
}
var channel = clientChannels[name];
channel.fns = toExpose;
channel.deferred = when.defer();
var fnNames = [];
for(var fn in toExpose)
{
fnNames.push(fn);
}
rpcMaster.emit('expose channel', {name: name, fns: fnNames});
return channel.deferred.promise;
};
return rpc;
}(RPC || {}));