| 1 | /* |
| 2 | TODO: |
| 3 | Create a document explaining the protocol |
| 4 | Some way to expire unused callbacks? TTL? expireCallback() function? |
| 5 | */ |
| 6 | |
| 7 | function WebsocketRpc(eio_socket) { |
| 8 | var self = this; |
| 9 | |
| 10 | this._next_id = 0; |
| 11 | this._rpc_callbacks = {}; |
| 12 | this._socket = eio_socket; |
| 13 | |
| 14 | this._mixinEmitter(); |
| 15 | this._bindSocketListeners(); |
| 16 | } |
| 17 | |
| 18 | |
| 19 | WebsocketRpc.prototype._bindSocketListeners = function() { |
| 20 | var self = this; |
| 21 | |
| 22 | // Proxy the onMessage listener |
| 23 | this._onMessageProxy = function rpcOnMessageBoundFunction(){ |
| 24 | self._onMessage.apply(self, arguments); |
| 25 | }; |
| 26 | this._socket.on('message', this._onMessageProxy); |
| 27 | }; |
| 28 | |
| 29 | |
| 30 | |
| 31 | WebsocketRpc.prototype.dispose = function() { |
| 32 | if (this._onMessageProxy) { |
| 33 | this._socket.removeListener('message', this._onMessageProxy); |
| 34 | delete this._onMessageProxy; |
| 35 | } |
| 36 | |
| 37 | this.removeAllListeners(); |
| 38 | }; |
| 39 | |
| 40 | |
| 41 | |
| 42 | |
| 43 | /** |
| 44 | * The engine.io socket already has an emitter mixin so steal it from there |
| 45 | */ |
| 46 | WebsocketRpc.prototype._mixinEmitter = function() { |
| 47 | var funcs = ['on', 'once', 'off', 'removeListener', 'removeAllListeners', 'emit', 'listeners', 'hasListeners']; |
| 48 | |
| 49 | for (var i=0; i<funcs.length; i++) { |
| 50 | if (typeof this._socket[funcs[i]] === 'function') |
| 51 | this[funcs[i]] = this._socket[funcs[i]]; |
| 52 | } |
| 53 | }; |
| 54 | |
| 55 | |
| 56 | /** |
| 57 | * Check if a packet is a valid RPC call |
| 58 | */ |
| 59 | WebsocketRpc.prototype._isCall = function(packet) { |
| 60 | return (typeof packet.method !== 'undefined' && |
| 61 | typeof packet.params !== 'undefined'); |
| 62 | }; |
| 63 | |
| 64 | |
| 65 | /** |
| 66 | * Check if a packet is a valid RPC response |
| 67 | */ |
| 68 | WebsocketRpc.prototype._isResponse = function(packet) { |
| 69 | return (typeof packet.id !== 'undefined' && |
| 70 | typeof packet.response !== 'undefined'); |
| 71 | }; |
| 72 | |
| 73 | |
| 74 | |
| 75 | /** |
| 76 | * Make an RPC call |
| 77 | * First argument must be the method name to call |
| 78 | * If the last argument is a function, it is used as a callback |
| 79 | * All other arguments are passed to the RPC method |
| 80 | * Eg. Rpc.call('namespace.method_name', 1, 2, 3, callbackFn) |
| 81 | */ |
| 82 | WebsocketRpc.prototype.call = function(method) { |
| 83 | var params, callback, packet; |
| 84 | |
| 85 | // Get a normal array of passed in arguments |
| 86 | params = Array.prototype.slice.call(arguments, 1, arguments.length); |
| 87 | |
| 88 | // If the last argument is a function, take it as a callback and strip it out |
| 89 | if (typeof params[params.length-1] === 'function') { |
| 90 | callback = params[params.length-1]; |
| 91 | params = params.slice(0, params.length-1); |
| 92 | } |
| 93 | |
| 94 | packet = { |
| 95 | method: method, |
| 96 | params: params |
| 97 | }; |
| 98 | |
| 99 | if (typeof callback === 'function') { |
| 100 | packet.id = this._next_id; |
| 101 | |
| 102 | this._next_id++; |
| 103 | this._rpc_callbacks[packet.id] = callback; |
| 104 | } |
| 105 | |
| 106 | this.send(packet); |
| 107 | }; |
| 108 | |
| 109 | |
| 110 | /** |
| 111 | * Encode the packet into JSON and send it over the websocket |
| 112 | */ |
| 113 | WebsocketRpc.prototype.send = function(packet) { |
| 114 | if (this._socket) |
| 115 | this._socket.send(JSON.stringify(packet)); |
| 116 | }; |
| 117 | |
| 118 | |
| 119 | /** |
| 120 | * Handler for the websocket `message` event |
| 121 | */ |
| 122 | WebsocketRpc.prototype._onMessage = function(message_raw) { |
| 123 | var self = this, |
| 124 | packet, |
| 125 | returnFn, |
| 126 | callback; |
| 127 | |
| 128 | try { |
| 129 | packet = JSON.parse(message_raw); |
| 130 | if (!packet) throw 'Corrupt packet'; |
| 131 | } catch(err) { |
| 132 | return; |
| 133 | } |
| 134 | |
| 135 | if (this._isResponse(packet)) { |
| 136 | // If we have no callback waiting for this response, don't do anything |
| 137 | if (typeof this._rpc_callbacks[packet.id] !== 'function') |
| 138 | return; |
| 139 | |
| 140 | // Delete the callback before calling it. If any exceptions accur within the callback |
| 141 | // we don't have to worry about the delete not happening |
| 142 | callback = this._rpc_callbacks[packet.id]; |
| 143 | delete this._rpc_callbacks[packet.id]; |
| 144 | |
| 145 | callback.apply(this, packet.response); |
| 146 | |
| 147 | } else if (this._isCall(packet)) { |
| 148 | // Calls with an ID may be responded to |
| 149 | if (typeof packet.id !== 'undefined') { |
| 150 | returnFn = this._createReturnCallFn(packet.id); |
| 151 | } else { |
| 152 | returnFn = this._noop; |
| 153 | } |
| 154 | |
| 155 | this.emit.apply(this, [packet.method, returnFn].concat(packet.params)); |
| 156 | } |
| 157 | }; |
| 158 | |
| 159 | |
| 160 | /** |
| 161 | * Returns a function used as a callback when responding to a call |
| 162 | */ |
| 163 | WebsocketRpc.prototype._createReturnCallFn = function(packet_id) { |
| 164 | var self = this; |
| 165 | |
| 166 | return function returnCallFn() { |
| 167 | var value = Array.prototype.slice.call(arguments, 0); |
| 168 | |
| 169 | var ret_packet = { |
| 170 | id: packet_id, |
| 171 | response: value |
| 172 | }; |
| 173 | |
| 174 | self.send(ret_packet); |
| 175 | }; |
| 176 | }; |
| 177 | |
| 178 | |
| 179 | |
| 180 | WebsocketRpc.prototype._noop = function() {}; |
| 181 | |
| 182 | |
| 183 | |
| 184 | |
| 185 | // If running a node module, set the exports |
| 186 | if (typeof module === 'object' && typeof module.exports !== 'undefined') { |
| 187 | module.exports = WebsocketRpc; |
| 188 | } |