Last active
August 29, 2015 14:11
-
-
Save jason-s13r/0ac511e0a3ec0cd5cf4a to your computer and use it in GitHub Desktop.
WebSocket RPC
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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