Skip to content

Instantly share code, notes, and snippets.

@jason-s13r
Last active August 29, 2015 14:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jason-s13r/0ac511e0a3ec0cd5cf4a to your computer and use it in GitHub Desktop.
Save jason-s13r/0ac511e0a3ec0cd5cf4a to your computer and use it in GitHub Desktop.
WebSocket RPC
<html>
<head>
<script src="WebSocketRpc.js"></script>
<script>
// Setup RPC
var rpc = new WebSocketRpc('ws://echo.websocket.org'/*, true */); // pass true to disable sending batched calls.
// Want to do stuff on open, error and close.
rpc.on('ready', function(event){ console.log('socket is ready'); })
.on('error', function(event){ console.log('oh my an error occurred', event); })
.on('close', function(event){ console.log('socket is closed'); })
// We'll use this for handling JSON-RPC 2.0 notification result/error messages.
rpc.on('notification', function(error, results, response, event){
if (error) {
console.error('what, an error notification?', error);
return;
}
console.log('oh good, a notification', results);
})
// Let's set up a method that the server can call for the client to do.
rpc.expose('add', function(params, callback) {
console.log('exposed add:', params);
var result = params[0] + params[1];
callback(null, result);
});
// Let's set up another one, which we'll use as a notification.
rpc.expose('notification', function(params, callback){
console.log('exposed notification:', params);
callback();
});
// Can also set up a namedspaced object of methods.
rpc.expose('math', {
add: function(params, callback) {
callback(null, params[0] + params[1]);
},
subtract: function(params, callback) {
callback(null, params[0] - params[1]);
},
multiply: function(params, callback) {
callback(null, params[0] * params[1]);
}
});
// Call a method on the server.
rpc.call('add', [5, 10], function(error, result, response, request, event){
console.log(arguments);
});
// Send the notification to the server.
rpc.call('notification', 'Still Alive');
// Call a namespaced method.
rpc.call('math.multiply', [21, 34], function(error, results){
console.log('math.multiply', results);
});
rpc.batch(['math.subtract', [5, 3], function(error, result) {
console.log('subtract', result);
}], ['math.divide', [5, 3], function(error, result) {
if (error) {
console.error('divide', error);
return;
}
console.log('divide', result);
}]);
rpc.connect();
</script>
</head>
<body>
Look in the JS console.
</body>
</html>
function WebSocketRpc(url, batchOperationsDisabled) {
'use strict';
var self = this;
var socket = null;
var queue = [];
var history = {};
var methods = {};
var noop = function () {};
var noopFactory = function () {
return noop;
};
var eventHandlers = {
error: noop,
ready: noop,
close: noop,
notification: noop,
message: noop
};
var exceptions = {
ParseError: {
code: -32700,
message: 'Parse error'
},
MethodNotFound: {
code: -32601,
message: 'Method not found'
}
};
init();
function init() {
self.call = function () {
queue.push([].slice.call(arguments));
return self;
};
self.batch = function () {
if (batchOperationsDisabled) {
throw new Error('Batch Operations Disabled');
}
queue = queue.concat([].slice.call(arguments));
return self;
};
self.connect = connect;
self.expose = expose;
self.exceptions = exceptions;
}
function connect() {
socket = new WebSocket(url);
self.ws = socket;
socket.onopen = opened;
socket.onerror = error;
socket.onmessage = handler;
socket.onclose = onclose;
return self;
}
function opened(event) {
eventHandlers.ready.call(self, event);
if (batchOperationsDisabled) {
while (queue.length > 0) {
call.apply(self, queue.shift());
}
} else {
batch.apply(self, queue);
}
self.call = call;
self.batch = batch;
self.connect = noop;
}
function error(event) {
eventHandlers.error.call(self, event);
}
function handler(event) {
eventHandlers.message.call(self, event);
var data;
try {
data = JSON.parse(event.data);
} catch (e) {
console.error(e);
var request = formatResponse(exceptions.ParseError, null, {
id: null
});
socket.send(JSON.stringify(request));
}
if (!data) {
return;
}
if (Object.prototype.toString.call(data) === '[object Array]') {
new Batched(data, event).handle();
return;
}
new Single(data, event).handle();
}
function onclose(event) {
eventHandlers.close.call(self, event);
}
function Batched(responses, event) {
var _self = this;
var batchRequest = [];
var batchNofications = [];
_self.handle = handle;
function trySend() {
var combined = batchRequest.length + batchNofications.length;
if (combined >= responses.length) {
socket.send(JSON.stringify(batchRequest));
return;
}
}
function next(res) {
return function (error, result) {
var request = formatResponse(error, result, res);
batchRequest.push(request);
trySend();
};
}
function notify(res) {
return function (error, result) {
batchNofications.push(res);
};
}
function handle() {
responses.forEach(function (response) {
new Single(response, event, next, notify).handle();
});
return _self;
}
return _self;
}
function Single(response, event, _next, _nextNoop) {
var _self = this;
_nextNoop = _nextNoop || noopFactory;
_self.next = next;
_self.handle = handle;
_self.method = method;
_self.notification = notification;
_self.results = results;
function next(res) {
return function (error, result) {
var request = formatResponse(error, result, res);
socket.send(JSON.stringify(request));
};
}
function handle() {
if (response.method !== undefined && response.params !== undefined) {
return !!method(response, event);
}
if (response.id === null) {
return !!notification(response, event);
}
results(response, event);
return _self;
}
function method(response, event) {
var cb = methods[response.method] || rpcException(exceptions.MethodNotFound);
var n = _next || next;
if (response.id === null) {
n = _nextNoop || noopFactory;
}
cb.call(self, response.params, n(response), response, response.id === null);
return _self;
}
function notification(response, event) {
response.id = response.error === undefined ? response.id : response.error.id;
response.id = response.id || null;
if (response.id === null) {
console.log('notification:', response);
eventHandlers.notification.call(self, response.error, response.result, response, event);
return _self;
}
handle(response, event);
return _self;
}
function results(response, event) {
var item = history[response.id] || {};
item.response = response;
item.callback.call(self, item.response.error, item.response.result, item.response, item.request, event);
return _self;
}
return _self;
}
function formatResponse(error, result, response) {
var request = {
jsonrpc: '2.0',
id: response.id || null
};
if ( !! error) {
request.error = error;
}
if ( !! result) {
request.result = result;
}
return request;
}
function onErrorCallback(error) {
return function (params, callback, response, isNotification) {
callback(error);
var cb = onError || noop;
cb.call(self, error);
};
}
function rpcException(error) {
return function (params, callback, response, isNotification) {
callback(error);
};
}
function send(id) {
var request = collateRequest(id);
socket.send(JSON.stringify(request));
}
function collateRequest(id) {
var ids = id;
if (typeof id === 'string') {
return prepareRequest(id);
}
return ids.map(prepareRequest);
}
function prepareRequest(id) {
var item = history[id];
if (typeof item.callback !== 'function') {
item.request.id = null;
}
return item.request;
}
function identity(size, v) {
v = new Array(size || 20).join('.').split('').map(function () {
return Math.round(Math.random() * 100);
}).join('');
return (1 * v).toString(36);
}
function createRequest(method, parameters, callback, id) {
id = id || identity();
var request = {
jsonrpc: '2.0',
id: id || null,
method: method,
params: parameters
};
history[id] = {
method: method,
request: request,
callback: callback,
response: null
};
return id;
}
function call(method, parameters, callback, id) {
id = createRequest(method, parameters, callback, id);
send(id);
return self;
}
function batch(requests) {
if (batchOperationsDisabled) {
throw new Error('Batch Operations Disabled');
}
requests = [].slice.call(arguments);
if (requests.length === 1) {
call.apply(self, requests[0]);
return;
}
var ids = requests.map(function (request) {
return createRequest.apply(self, request);
});
send(ids);
return self;
}
function expose(method, fn) {
if (typeof fn !== 'function') {
Object.keys(fn).forEach(function (key) {
methods[method + '.' + key] = fn[key];
}, methods);
}
methods[method] = fn;
return self;
}
function on(eventName, handler) {
eventHandlers[eventName] = handler;
return self;
}
function off(eventName) {
eventHandlers[eventName] = noop;
return self;
}
function close() {
if (socket !== null) {
socket.close();
}
}
self.close = close;
self.on = on;
self.off = off;
self.ws = socket;
return self;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment