Commit | Line | Data |
---|---|---|
d99e7823 | 1 | /* |
b42bd8f3 | 2 | TODO: |
d99e7823 | 3 | Create a document explaining the protocol |
b42bd8f3 | 4 | Some way to expire unused callbacks? TTL? expireCallback() function? |
d99e7823 D |
5 | */ |
6 | ||
d718a0f9 D |
7 | /** |
8 | * Wrapper around creating a new WebsocketRpcCaller | |
9 | * This lets us use the WebsocketRpc object as a function | |
10 | */ | |
d99e7823 | 11 | function WebsocketRpc(eio_socket) { |
d718a0f9 D |
12 | var caller = new WebsocketRpcCaller(eio_socket); |
13 | var ret = function WebsocketRpcInstance() { | |
14 | return ret.makeCall.apply(ret, arguments); | |
15 | }; | |
16 | ||
17 | for(var prop in caller){ | |
18 | ret[prop] = caller[prop]; | |
19 | } | |
d99e7823 | 20 | |
d718a0f9 D |
21 | ret._mixinEmitter(); |
22 | ret._bindSocketListeners(); | |
23 | ||
c7cb4d54 D |
24 | // Keep a reference to the main Rpc object so namespaces can find calling functions |
25 | ret._rpc = ret; | |
26 | ||
d718a0f9 D |
27 | return ret; |
28 | } | |
29 | ||
30 | ||
31 | function WebsocketRpcCaller(eio_socket) { | |
d99e7823 | 32 | this._next_id = 0; |
f621d0ff | 33 | this._rpc_callbacks = {}; |
d99e7823 | 34 | this._socket = eio_socket; |
c7cb4d54 D |
35 | |
36 | this._rpc = this; | |
37 | this._namespace = ''; | |
38 | this._namespaces = []; | |
d99e7823 D |
39 | } |
40 | ||
41 | ||
d718a0f9 | 42 | WebsocketRpcCaller.prototype._bindSocketListeners = function() { |
d99e7823 D |
43 | var self = this; |
44 | ||
45 | // Proxy the onMessage listener | |
46 | this._onMessageProxy = function rpcOnMessageBoundFunction(){ | |
47 | self._onMessage.apply(self, arguments); | |
48 | }; | |
49 | this._socket.on('message', this._onMessageProxy); | |
50 | }; | |
51 | ||
52 | ||
53 | ||
d718a0f9 | 54 | WebsocketRpcCaller.prototype.dispose = function() { |
d99e7823 | 55 | if (this._onMessageProxy) { |
0fe7c000 | 56 | this._socket.removeListener('message', this._onMessageProxy); |
d99e7823 D |
57 | delete this._onMessageProxy; |
58 | } | |
0fe7c000 | 59 | |
c7cb4d54 D |
60 | // Clean up any namespaces |
61 | for (var idx in this._namespaces) { | |
62 | this._namespaces[idx].dispose(); | |
63 | } | |
64 | ||
0fe7c000 | 65 | this.removeAllListeners(); |
d99e7823 D |
66 | }; |
67 | ||
68 | ||
69 | ||
c7cb4d54 D |
70 | WebsocketRpcCaller.prototype.namespace = function(namespace_name) { |
71 | var complete_namespace, namespace; | |
72 | ||
73 | if (this._namespace) { | |
74 | complete_namespace = this._namespace + '.' + namespace_name; | |
75 | } else { | |
76 | complete_namespace = namespace_name; | |
77 | } | |
78 | ||
79 | namespace = new this._rpc.Namespace(this._rpc, complete_namespace); | |
80 | this._rpc._namespaces.push(namespace); | |
81 | ||
82 | return namespace; | |
83 | }; | |
84 | ||
85 | ||
86 | ||
87 | // Find all namespaces that either matches or starts with namespace_name | |
88 | WebsocketRpcCaller.prototype._findRelevantNamespaces = function(namespace_name) { | |
89 | var found_namespaces = []; | |
90 | ||
91 | for(var idx in this._namespaces) { | |
92 | if (this._namespaces[idx]._namespace === namespace_name) { | |
93 | found_namespaces.push(this._namespaces[idx]); | |
94 | } | |
95 | ||
96 | if (this._namespaces[idx]._namespace.indexOf(namespace_name + '.') === 0) { | |
97 | found_namespaces.push(this._namespaces[idx]); | |
98 | } | |
99 | } | |
100 | ||
101 | return found_namespaces; | |
102 | }; | |
103 | ||
104 | ||
d99e7823 D |
105 | |
106 | /** | |
107 | * The engine.io socket already has an emitter mixin so steal it from there | |
108 | */ | |
c7cb4d54 | 109 | WebsocketRpcCaller.prototype._mixinEmitter = function(target_obj) { |
d99e7823 D |
110 | var funcs = ['on', 'once', 'off', 'removeListener', 'removeAllListeners', 'emit', 'listeners', 'hasListeners']; |
111 | ||
c7cb4d54 D |
112 | target_obj = target_obj || this; |
113 | ||
d99e7823 | 114 | for (var i=0; i<funcs.length; i++) { |
a9cd4622 | 115 | if (typeof this._socket[funcs[i]] === 'function') |
c7cb4d54 | 116 | target_obj[funcs[i]] = this._socket[funcs[i]]; |
d99e7823 D |
117 | } |
118 | }; | |
119 | ||
120 | ||
121 | /** | |
122 | * Check if a packet is a valid RPC call | |
123 | */ | |
d718a0f9 | 124 | WebsocketRpcCaller.prototype._isCall = function(packet) { |
d99e7823 D |
125 | return (typeof packet.method !== 'undefined' && |
126 | typeof packet.params !== 'undefined'); | |
127 | }; | |
128 | ||
129 | ||
130 | /** | |
131 | * Check if a packet is a valid RPC response | |
132 | */ | |
d718a0f9 | 133 | WebsocketRpcCaller.prototype._isResponse = function(packet) { |
d99e7823 D |
134 | return (typeof packet.id !== 'undefined' && |
135 | typeof packet.response !== 'undefined'); | |
136 | }; | |
137 | ||
138 | ||
139 | ||
140 | /** | |
141 | * Make an RPC call | |
142 | * First argument must be the method name to call | |
143 | * If the last argument is a function, it is used as a callback | |
144 | * All other arguments are passed to the RPC method | |
d718a0f9 | 145 | * Eg. Rpc.makeCall('namespace.method_name', 1, 2, 3, callbackFn) |
d99e7823 | 146 | */ |
d718a0f9 | 147 | WebsocketRpcCaller.prototype.makeCall = function(method) { |
d99e7823 D |
148 | var params, callback, packet; |
149 | ||
b42bd8f3 | 150 | // Get a normal array of passed in arguments |
d99e7823 D |
151 | params = Array.prototype.slice.call(arguments, 1, arguments.length); |
152 | ||
b42bd8f3 | 153 | // If the last argument is a function, take it as a callback and strip it out |
d99e7823 D |
154 | if (typeof params[params.length-1] === 'function') { |
155 | callback = params[params.length-1]; | |
156 | params = params.slice(0, params.length-1); | |
157 | } | |
158 | ||
159 | packet = { | |
160 | method: method, | |
161 | params: params | |
162 | }; | |
163 | ||
164 | if (typeof callback === 'function') { | |
165 | packet.id = this._next_id; | |
166 | ||
167 | this._next_id++; | |
f621d0ff | 168 | this._rpc_callbacks[packet.id] = callback; |
d99e7823 D |
169 | } |
170 | ||
171 | this.send(packet); | |
172 | }; | |
173 | ||
174 | ||
175 | /** | |
176 | * Encode the packet into JSON and send it over the websocket | |
177 | */ | |
d718a0f9 | 178 | WebsocketRpcCaller.prototype.send = function(packet) { |
d99e7823 D |
179 | if (this._socket) |
180 | this._socket.send(JSON.stringify(packet)); | |
181 | }; | |
182 | ||
183 | ||
184 | /** | |
185 | * Handler for the websocket `message` event | |
186 | */ | |
d718a0f9 | 187 | WebsocketRpcCaller.prototype._onMessage = function(message_raw) { |
d99e7823 D |
188 | var self = this, |
189 | packet, | |
a6b1c062 | 190 | returnFn, |
c7cb4d54 D |
191 | callback, |
192 | namespace, namespaces, idx; | |
d99e7823 D |
193 | |
194 | try { | |
195 | packet = JSON.parse(message_raw); | |
196 | if (!packet) throw 'Corrupt packet'; | |
197 | } catch(err) { | |
198 | return; | |
199 | } | |
200 | ||
201 | if (this._isResponse(packet)) { | |
b42bd8f3 | 202 | // If we have no callback waiting for this response, don't do anything |
f621d0ff | 203 | if (typeof this._rpc_callbacks[packet.id] !== 'function') |
b42bd8f3 D |
204 | return; |
205 | ||
a6b1c062 D |
206 | // Delete the callback before calling it. If any exceptions accur within the callback |
207 | // we don't have to worry about the delete not happening | |
208 | callback = this._rpc_callbacks[packet.id]; | |
f621d0ff | 209 | delete this._rpc_callbacks[packet.id]; |
d99e7823 | 210 | |
a6b1c062 D |
211 | callback.apply(this, packet.response); |
212 | ||
d99e7823 D |
213 | } else if (this._isCall(packet)) { |
214 | // Calls with an ID may be responded to | |
215 | if (typeof packet.id !== 'undefined') { | |
b42bd8f3 | 216 | returnFn = this._createReturnCallFn(packet.id); |
d99e7823 | 217 | } else { |
b42bd8f3 | 218 | returnFn = this._noop; |
d99e7823 D |
219 | } |
220 | ||
0ca6adac | 221 | this.emit.apply(this, ['all', packet.method, returnFn].concat(packet.params)); |
d99e7823 | 222 | this.emit.apply(this, [packet.method, returnFn].concat(packet.params)); |
c7cb4d54 D |
223 | |
224 | if (packet.method.indexOf('.') > 0) { | |
225 | namespace = packet.method.substring(0, packet.method.lastIndexOf('.')); | |
226 | namespaces = this._findRelevantNamespaces(namespace); | |
227 | for(idx in namespaces){ | |
228 | packet.method = packet.method.replace(namespaces[idx]._namespace + '.', ''); | |
229 | namespaces[idx].emit.apply(namespaces[idx], [packet.method, returnFn].concat(packet.params)); | |
230 | } | |
231 | } | |
d99e7823 D |
232 | } |
233 | }; | |
234 | ||
235 | ||
b42bd8f3 D |
236 | /** |
237 | * Returns a function used as a callback when responding to a call | |
238 | */ | |
d718a0f9 | 239 | WebsocketRpcCaller.prototype._createReturnCallFn = function(packet_id) { |
b42bd8f3 D |
240 | var self = this; |
241 | ||
242 | return function returnCallFn() { | |
243 | var value = Array.prototype.slice.call(arguments, 0); | |
244 | ||
245 | var ret_packet = { | |
246 | id: packet_id, | |
247 | response: value | |
248 | }; | |
249 | ||
250 | self.send(ret_packet); | |
251 | }; | |
252 | }; | |
253 | ||
254 | ||
255 | ||
d718a0f9 | 256 | WebsocketRpcCaller.prototype._noop = function() {}; |
b42bd8f3 D |
257 | |
258 | ||
d99e7823 | 259 | |
c7cb4d54 D |
260 | WebsocketRpcCaller.prototype.Namespace = function(rpc, namespace) { |
261 | var ret = function WebsocketRpcNamespaceInstance() { | |
262 | if (typeof arguments[0] === 'undefined') { | |
263 | return; | |
264 | } | |
265 | ||
266 | arguments[0] = ret._namespace + '.' + arguments[0]; | |
267 | return ret._rpc.apply(ret._rpc, arguments); | |
268 | }; | |
269 | ||
270 | ret._rpc = rpc; | |
271 | ret._namespace = namespace; | |
272 | ||
273 | ret.dispose = function() { | |
274 | ret.removeAllListeners(); | |
275 | ret._rpc = null; | |
276 | }; | |
277 | ||
278 | rpc._mixinEmitter(ret); | |
279 | ||
280 | return ret; | |
281 | }; | |
282 | ||
283 | ||
284 | ||
d99e7823 D |
285 | |
286 | // If running a node module, set the exports | |
287 | if (typeof module === 'object' && typeof module.exports !== 'undefined') { | |
288 | module.exports = WebsocketRpc; | |
289 | } |