webrpc.js
Version:
A reusable API server/client framework, running on socket.io
324 lines (275 loc) • 7.3 kB
JavaScript
'use strict';
(function(){
function WebRPCServer(io) {
if(this && (this instanceof WebRPCServer)) {
this.reset();
if(io) this.bind(io);
} else {
return new WebRPCServer(io);
}
};
WebRPCServer.prototype = {
reset: function() {
this.io = null;
this.rpcs = {};
this.logTraffic = false;
this.loginRequired = false;
this.isRunning = false;
this.timer = 0;
this.lastTick = 0;
this.sockets = {}; // sid -> socket
this.socketsCount = 0;
},
showTraffic: function(y) {
this.logTraffic = y;
return this;
},
requireLogin: function(y) {
this.loginRequired = y;
return this;
},
// func(req, reply(err, ret));
on: function(key, func) {
if(typeof func === 'function') {
var fList = this.rpcs[key];
if(!fList) fList = this.rpcs[key] = [];
fList.push(func);
}
return this;
},
off: function(key, func) {
if(key in this.rpcs) {
var fList = this.rpcs[key];
for(var i=fList.length-1; i>=0; i--) {
if(fList[i] === func) fList.slice(i,1);
}
if(fList.length === 0) delete this.rpcs[key];
}
return this;
},
bind: function(io) {
if(this.isRunning) throw new Error('WebRPCServer is already running.');
var server = this;
server.io = io;
io.on('connection', function(webSock){
if(server.logTraffic) console.log('rpc client connected, socket id: ' + webSock.id);
server.onConnected(webSock);
});
server.lastTick = Date.now();
server.isRunning = true;
// init tick() timer
server.tick();
server.timer = setInterval(function(){
server.tick();
}, 1000);
return this;
},
shutdown: function() {
if(!this.isRunning) return;
// clear tick() timer
if(this.timer) clearInterval(this.timer);
this.reset();
return this;
},
tick: function() {
var server = this;
server.lastTick = Date.now();
},
onDisconnected: function(webSock) {
var server = this;
delete server.sockets[ webSock.id ];
server.socketsCount --;
},
onConnected: function(webSock) {
var server = this;
webSock.on('disconnect', function(){
if(server.logTraffic) console.log('rpc client disconnected, socket id: ' + webSock.id);
server.onDisconnected(webSock);
});
// implement the rpc interface, so we can reuse webclient.js
/* {
uid, // optional
pin, // optional
seq: seq,
f: method,
args: args,
} */
webSock.on('rpc', function(req){
var f = req.f;
if(server.loginRequired) {
switch(f) {
case 'fastsignup':
case 'signup':
case 'login':
break;
default:
var _token = webSock._loginToken || { uid:'x', pin:'x' };
if(_token.pin !== req.pin) {
return webSock.emit('reply', {
seq: req.seq,
err: 403,
ret: 'Please login first',
});
}
break;
}
}
var callbacks = server.rpcs[f];
if(callbacks && callbacks.length > 0) {
var reply = function(err, ret) {
if((!err) && (f === 'login')) {
webSock._loginToken = ret.token || { uid:'x', pin:'x' };
}
webSock.emit('reply', {
seq: req.seq,
err: err,
ret: ret,
});
};
for(var i=callbacks.length-1; i>=0; i--) {
var func = callbacks[i];
if(typeof func === 'function') func(req.args, reply);
}
} else {
if(server.logTraffic) console.log('rpc unhandled: ', f);
}
});
server.sockets[webSock.id] = webSock;
server.socketsCount ++;
},
};
function WebRPCClient(sock) {
if(this && (this instanceof WebRPCClient)) {
this.reset();
if(sock) this.bind(sock);
} else {
return new WebRPCClient(sock);
}
};
WebRPCClient.prototype = {
reset: function() {
this.sock = null;
this.uid = '';
this.pin = '';
this.rpc_seq = 1;
this.rpc_callbacks = {};
this.events = {};
this.logTraffic = false;
},
showTraffic: function(y) {
this.logTraffic = y;
if(this.sock) this.sock.logTraffic = y;
},
on: function(event, func) {
var callbacks = this.events[event];
if(!callbacks) callbacks = this.events[event] = [];
callbacks.push(func);
return this;
},
off: function(event, func) {
if(func) {
var callbacks = this.events[event];
if(!callbacks) return;
var index = -1;
while((index = callbacks.indexOf(func)) >= 0) {
callbacks.splice(index, 1);
}
} else {
this.events[event] = [];
}
return this;
},
fireEvent: function(event, args) {
var callbacks = this.events[event];
if(!callbacks || !Array.isArray(callbacks)) return;
callbacks.forEach(function(func){
if(typeof func === 'function') func(event, args);
});
},
callback: function(seq, msg) {
if(this.sock.logTraffic) console.log(seq, msg);
var item = this.rpc_callbacks[seq];
if(item) {
delete this.rpc_callbacks[seq];
var func = item.func;
if(typeof func === 'function') func(msg.err, msg.ret);
}
},
bind: function(sock) {
var client = this;
this.sock = sock;
if(this.logTraffic) sock.logTraffic = this.logTraffic;
sock.on('hello', function(msg){
if(sock.logTraffic) console.log('hello', msg);
if(!msg || (typeof msg!=='object')) return;
client.fireEvent('hello', msg);
});
sock.on('notify', function(msg){
if(sock.logTraffic) console.log('notify', msg);
if(!msg || (typeof msg!=='object') || !msg.e) return;
client.fireEvent(msg.e, msg.args);
});
sock.on('reply', function(msg){
if(sock.logTraffic) console.log('reply', msg);
if(!msg || (typeof msg!=='object') || !msg.seq) return;
client.callback(msg.seq, msg);
});
},
/*
* accepted methods and args:
*
* fastsignup, 0
* signup, {uid, passwd, name, email, phone, uuid}
* login, {uid, passwd}
* logout, 0
*/
rpc: function(method, args, reply) {
if(!reply) {
reply = function dummy(err, ret){};
} else if(typeof reply !== 'function') {
throw new Error('rpc: callback func(err, ret) required');
}
var client = this;
var callback_func = reply;
switch(method) {
case 'login':
callback_func = function(err, ret) {
if(!err) {
client.uid = ret.token.uid;
client.pin = ret.token.pin;
}
reply(err, ret);
};
break;
case 'fastsignup':
case 'signup':
default:
break;
}
var seq = this.rpc_seq++;
this.rpc_callbacks[seq] = {
seq: seq,
func: callback_func,
t: Date.now(),
};
var req = {
seq: seq,
f: method,
args: args,
};
if(client.uid) req.uid = client.uid;
if(client.pin) req.pin = client.pin;
if(this.sock.logTraffic) console.log('rpc', req);
this.sock.emit('rpc', req);
return this;
},
};
if(typeof window === 'object') {
window.WebRPCClient = WebRPCClient;
} else {
exports = module.exports = {
Server: WebRPCServer,
Client: WebRPCClient,
};
}
})();