Commit | Line | Data |
---|---|---|
06e30f06 | 1 | var EngineioTools = { |
b42bd8f3 | 2 | ReconnectingSocket: function ReconnectingSocket(server_uri, socket_options) { |
06e30f06 D |
3 | var connected = false; |
4 | var is_reconnecting = false; | |
5 | ||
a37a697f | 6 | var reconnect_delay = 4000; |
06e30f06 D |
7 | var reconnect_last_delay = 0; |
8 | var reconnect_delay_exponential = true; | |
9 | var reconnect_max_attempts = 5; | |
10 | var reconnect_step = 0; | |
11 | var reconnect_tmr = null; | |
12 | ||
13 | var original_disconnect; | |
14 | var planned_disconnect = false; | |
15 | ||
16 | var socket = eio.apply(eio, arguments); | |
17 | socket.on('open', onOpen); | |
18 | socket.on('close', onClose); | |
19 | socket.on('error', onError); | |
20 | ||
21 | original_disconnect = socket.close; | |
22 | socket.close = close; | |
23 | ||
24 | // Apply any custom reconnection config | |
25 | if (socket_options) { | |
26 | if (typeof socket_options.reconnect_delay === 'number') | |
27 | reconnect_delay = socket_options.reconnect_delay; | |
28 | ||
29 | if (typeof socket_options.reconnect_max_attempts === 'number') | |
30 | reconnect_max_attempts = socket_options.reconnect_max_attempts; | |
31 | ||
32 | if (typeof socket_options.reconnect_delay_exponential !== 'undefined') | |
33 | reconnect_delay_exponential = !!socket_options.reconnect_delay_exponential; | |
34 | } | |
35 | ||
36 | ||
37 | function onOpen() { | |
38 | connected = true; | |
39 | is_reconnecting = false; | |
40 | planned_disconnect = false; | |
41 | ||
42 | reconnect_step = 0; | |
43 | reconnect_last_delay = 0; | |
44 | ||
45 | clearTimeout(reconnect_tmr); | |
46 | } | |
47 | ||
48 | ||
49 | function onClose() { | |
50 | connected = false; | |
51 | ||
a37a697f | 52 | if (!planned_disconnect && !is_reconnecting) |
06e30f06 D |
53 | reconnect(); |
54 | } | |
55 | ||
56 | ||
57 | function onError() { | |
58 | // This will be called when a reconnect fails | |
59 | if (is_reconnecting) | |
60 | reconnect(); | |
61 | } | |
62 | ||
63 | ||
64 | function close() { | |
65 | planned_disconnect = true; | |
66 | original_disconnect.call(socket); | |
67 | } | |
68 | ||
69 | ||
70 | function reconnect() { | |
71 | if (reconnect_step >= reconnect_max_attempts) { | |
72 | socket.emit('reconnecting_failed'); | |
73 | return; | |
74 | } | |
75 | ||
76 | var delay = reconnect_delay_exponential ? | |
77 | (reconnect_last_delay || reconnect_delay / 2) * 2 : | |
78 | reconnect_delay * reconnect_step; | |
79 | ||
80 | is_reconnecting = true; | |
81 | ||
82 | reconnect_tmr = setTimeout(function() { | |
83 | socket.open(); | |
84 | }, delay); | |
85 | ||
86 | reconnect_last_delay = delay; | |
87 | ||
88 | socket.emit('reconnecting', { | |
89 | attempt: reconnect_step + 1, | |
90 | max_attempts: reconnect_max_attempts, | |
91 | delay: delay | |
92 | }); | |
93 | ||
94 | reconnect_step++; | |
95 | } | |
96 | ||
97 | return socket; | |
98 | }, | |
99 | ||
100 | ||
101 | ||
102 | ||
103 | Rpc: (function(){ | |
b42bd8f3 D |
104 | /* |
105 | TODO: | |
106 | Create a document explaining the protocol | |
107 | Some way to expire unused callbacks? TTL? expireCallback() function? | |
108 | */ | |
06e30f06 | 109 | |
d718a0f9 D |
110 | /** |
111 | * Wrapper around creating a new WebsocketRpcCaller | |
112 | * This lets us use the WebsocketRpc object as a function | |
113 | */ | |
b42bd8f3 | 114 | function WebsocketRpc(eio_socket) { |
d718a0f9 D |
115 | var caller = new WebsocketRpcCaller(eio_socket); |
116 | var ret = function WebsocketRpcInstance() { | |
117 | return ret.makeCall.apply(ret, arguments); | |
118 | }; | |
119 | ||
120 | for(var prop in caller){ | |
121 | ret[prop] = caller[prop]; | |
122 | } | |
06e30f06 | 123 | |
d718a0f9 D |
124 | ret._mixinEmitter(); |
125 | ret._bindSocketListeners(); | |
126 | ||
c7cb4d54 D |
127 | // Keep a reference to the main Rpc object so namespaces can find calling functions |
128 | ret._rpc = ret; | |
129 | ||
d718a0f9 D |
130 | return ret; |
131 | } | |
132 | ||
133 | ||
134 | function WebsocketRpcCaller(eio_socket) { | |
b42bd8f3 | 135 | this._next_id = 0; |
f621d0ff | 136 | this._rpc_callbacks = {}; |
b42bd8f3 | 137 | this._socket = eio_socket; |
c7cb4d54 D |
138 | |
139 | this._rpc = this; | |
140 | this._namespace = ''; | |
141 | this._namespaces = []; | |
b42bd8f3 | 142 | } |
06e30f06 D |
143 | |
144 | ||
d718a0f9 | 145 | WebsocketRpcCaller.prototype._bindSocketListeners = function() { |
b42bd8f3 | 146 | var self = this; |
06e30f06 | 147 | |
b42bd8f3 D |
148 | // Proxy the onMessage listener |
149 | this._onMessageProxy = function rpcOnMessageBoundFunction(){ | |
150 | self._onMessage.apply(self, arguments); | |
151 | }; | |
152 | this._socket.on('message', this._onMessageProxy); | |
153 | }; | |
06e30f06 D |
154 | |
155 | ||
156 | ||
d718a0f9 | 157 | WebsocketRpcCaller.prototype.dispose = function() { |
b42bd8f3 D |
158 | if (this._onMessageProxy) { |
159 | this._socket.removeListener('message', this._onMessageProxy); | |
160 | delete this._onMessageProxy; | |
161 | } | |
06e30f06 | 162 | |
c7cb4d54 D |
163 | // Clean up any namespaces |
164 | for (var idx in this._namespaces) { | |
165 | this._namespaces[idx].dispose(); | |
166 | } | |
167 | ||
b42bd8f3 D |
168 | this.removeAllListeners(); |
169 | }; | |
06e30f06 D |
170 | |
171 | ||
172 | ||
c7cb4d54 D |
173 | WebsocketRpcCaller.prototype.namespace = function(namespace_name) { |
174 | var complete_namespace, namespace; | |
175 | ||
176 | if (this._namespace) { | |
177 | complete_namespace = this._namespace + '.' + namespace_name; | |
178 | } else { | |
179 | complete_namespace = namespace_name; | |
180 | } | |
181 | ||
182 | namespace = new this._rpc.Namespace(this._rpc, complete_namespace); | |
183 | this._rpc._namespaces.push(namespace); | |
184 | ||
185 | return namespace; | |
186 | }; | |
187 | ||
188 | ||
189 | ||
190 | // Find all namespaces that either matches or starts with namespace_name | |
191 | WebsocketRpcCaller.prototype._findRelevantNamespaces = function(namespace_name) { | |
192 | var found_namespaces = []; | |
193 | ||
194 | for(var idx in this._namespaces) { | |
195 | if (this._namespaces[idx]._namespace === namespace_name) { | |
196 | found_namespaces.push(this._namespaces[idx]); | |
197 | } | |
198 | ||
199 | if (this._namespaces[idx]._namespace.indexOf(namespace_name + '.') === 0) { | |
200 | found_namespaces.push(this._namespaces[idx]); | |
201 | } | |
202 | } | |
203 | ||
204 | return found_namespaces; | |
205 | }; | |
206 | ||
207 | ||
06e30f06 | 208 | |
b42bd8f3 D |
209 | /** |
210 | * The engine.io socket already has an emitter mixin so steal it from there | |
211 | */ | |
c7cb4d54 | 212 | WebsocketRpcCaller.prototype._mixinEmitter = function(target_obj) { |
b42bd8f3 | 213 | var funcs = ['on', 'once', 'off', 'removeListener', 'removeAllListeners', 'emit', 'listeners', 'hasListeners']; |
06e30f06 | 214 | |
c7cb4d54 D |
215 | target_obj = target_obj || this; |
216 | ||
b42bd8f3 D |
217 | for (var i=0; i<funcs.length; i++) { |
218 | if (typeof this._socket[funcs[i]] === 'function') | |
c7cb4d54 | 219 | target_obj[funcs[i]] = this._socket[funcs[i]]; |
b42bd8f3 D |
220 | } |
221 | }; | |
06e30f06 D |
222 | |
223 | ||
b42bd8f3 D |
224 | /** |
225 | * Check if a packet is a valid RPC call | |
226 | */ | |
d718a0f9 | 227 | WebsocketRpcCaller.prototype._isCall = function(packet) { |
b42bd8f3 D |
228 | return (typeof packet.method !== 'undefined' && |
229 | typeof packet.params !== 'undefined'); | |
230 | }; | |
06e30f06 D |
231 | |
232 | ||
b42bd8f3 D |
233 | /** |
234 | * Check if a packet is a valid RPC response | |
235 | */ | |
d718a0f9 | 236 | WebsocketRpcCaller.prototype._isResponse = function(packet) { |
b42bd8f3 D |
237 | return (typeof packet.id !== 'undefined' && |
238 | typeof packet.response !== 'undefined'); | |
239 | }; | |
06e30f06 D |
240 | |
241 | ||
242 | ||
b42bd8f3 D |
243 | /** |
244 | * Make an RPC call | |
245 | * First argument must be the method name to call | |
246 | * If the last argument is a function, it is used as a callback | |
247 | * All other arguments are passed to the RPC method | |
d718a0f9 | 248 | * Eg. Rpc.makeCall('namespace.method_name', 1, 2, 3, callbackFn) |
b42bd8f3 | 249 | */ |
d718a0f9 | 250 | WebsocketRpcCaller.prototype.makeCall = function(method) { |
b42bd8f3 | 251 | var params, callback, packet; |
06e30f06 | 252 | |
b42bd8f3 D |
253 | // Get a normal array of passed in arguments |
254 | params = Array.prototype.slice.call(arguments, 1, arguments.length); | |
255 | ||
256 | // If the last argument is a function, take it as a callback and strip it out | |
257 | if (typeof params[params.length-1] === 'function') { | |
258 | callback = params[params.length-1]; | |
259 | params = params.slice(0, params.length-1); | |
260 | } | |
261 | ||
262 | packet = { | |
263 | method: method, | |
264 | params: params | |
265 | }; | |
266 | ||
267 | if (typeof callback === 'function') { | |
268 | packet.id = this._next_id; | |
269 | ||
270 | this._next_id++; | |
f621d0ff | 271 | this._rpc_callbacks[packet.id] = callback; |
b42bd8f3 | 272 | } |
06e30f06 | 273 | |
b42bd8f3 D |
274 | this.send(packet); |
275 | }; | |
06e30f06 | 276 | |
06e30f06 | 277 | |
b42bd8f3 D |
278 | /** |
279 | * Encode the packet into JSON and send it over the websocket | |
280 | */ | |
d718a0f9 | 281 | WebsocketRpcCaller.prototype.send = function(packet) { |
b42bd8f3 D |
282 | if (this._socket) |
283 | this._socket.send(JSON.stringify(packet)); | |
284 | }; | |
06e30f06 | 285 | |
06e30f06 | 286 | |
b42bd8f3 D |
287 | /** |
288 | * Handler for the websocket `message` event | |
289 | */ | |
d718a0f9 | 290 | WebsocketRpcCaller.prototype._onMessage = function(message_raw) { |
b42bd8f3 D |
291 | var self = this, |
292 | packet, | |
a6b1c062 | 293 | returnFn, |
c7cb4d54 D |
294 | callback, |
295 | namespace, namespaces, idx; | |
b42bd8f3 D |
296 | |
297 | try { | |
298 | packet = JSON.parse(message_raw); | |
299 | if (!packet) throw 'Corrupt packet'; | |
300 | } catch(err) { | |
301 | return; | |
302 | } | |
303 | ||
304 | if (this._isResponse(packet)) { | |
305 | // If we have no callback waiting for this response, don't do anything | |
f621d0ff | 306 | if (typeof this._rpc_callbacks[packet.id] !== 'function') |
b42bd8f3 D |
307 | return; |
308 | ||
a6b1c062 D |
309 | // Delete the callback before calling it. If any exceptions accur within the callback |
310 | // we don't have to worry about the delete not happening | |
311 | callback = this._rpc_callbacks[packet.id]; | |
f621d0ff | 312 | delete this._rpc_callbacks[packet.id]; |
b42bd8f3 | 313 | |
a6b1c062 D |
314 | callback.apply(this, packet.response); |
315 | ||
b42bd8f3 D |
316 | } else if (this._isCall(packet)) { |
317 | // Calls with an ID may be responded to | |
318 | if (typeof packet.id !== 'undefined') { | |
319 | returnFn = this._createReturnCallFn(packet.id); | |
320 | } else { | |
321 | returnFn = this._noop; | |
322 | } | |
323 | ||
c7cb4d54 | 324 | this.emit.apply(this, ['all', packet.method, returnFn].concat(packet.params)); |
b42bd8f3 | 325 | this.emit.apply(this, [packet.method, returnFn].concat(packet.params)); |
c7cb4d54 D |
326 | |
327 | if (packet.method.indexOf('.') > 0) { | |
328 | namespace = packet.method.substring(0, packet.method.lastIndexOf('.')); | |
329 | namespaces = this._findRelevantNamespaces(namespace); | |
330 | for(idx in namespaces){ | |
331 | packet.method = packet.method.replace(namespaces[idx]._namespace + '.', ''); | |
332 | namespaces[idx].emit.apply(namespaces[idx], [packet.method, returnFn].concat(packet.params)); | |
333 | } | |
334 | } | |
b42bd8f3 D |
335 | } |
336 | }; | |
06e30f06 D |
337 | |
338 | ||
b42bd8f3 D |
339 | /** |
340 | * Returns a function used as a callback when responding to a call | |
341 | */ | |
d718a0f9 | 342 | WebsocketRpcCaller.prototype._createReturnCallFn = function(packet_id) { |
b42bd8f3 | 343 | var self = this; |
06e30f06 | 344 | |
b42bd8f3 D |
345 | return function returnCallFn() { |
346 | var value = Array.prototype.slice.call(arguments, 0); | |
06e30f06 | 347 | |
b42bd8f3 D |
348 | var ret_packet = { |
349 | id: packet_id, | |
350 | response: value | |
351 | }; | |
06e30f06 | 352 | |
b42bd8f3 D |
353 | self.send(ret_packet); |
354 | }; | |
355 | }; | |
06e30f06 | 356 | |
06e30f06 | 357 | |
06e30f06 | 358 | |
d718a0f9 | 359 | WebsocketRpcCaller.prototype._noop = function() {}; |
06e30f06 D |
360 | |
361 | ||
c7cb4d54 D |
362 | |
363 | WebsocketRpcCaller.prototype.Namespace = function(rpc, namespace) { | |
364 | var ret = function WebsocketRpcNamespaceInstance() { | |
365 | if (typeof arguments[0] === 'undefined') { | |
366 | return; | |
367 | } | |
368 | ||
369 | arguments[0] = ret._namespace + '.' + arguments[0]; | |
370 | return ret._rpc.apply(ret._rpc, arguments); | |
371 | }; | |
372 | ||
373 | ret._rpc = rpc; | |
374 | ret._namespace = namespace; | |
375 | ||
376 | ret.dispose = function() { | |
377 | ret.removeAllListeners(); | |
378 | ret._rpc = null; | |
379 | }; | |
380 | ||
381 | rpc._mixinEmitter(ret); | |
382 | ||
383 | return ret; | |
384 | }; | |
385 | ||
386 | ||
b42bd8f3 | 387 | return WebsocketRpc; |
06e30f06 D |
388 | |
389 | }()) | |
390 | }; |