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 | ||
6 | var reconnect_delay = 2000; | |
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 | ||
52 | if (!planned_disconnect) | |
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 | |
b42bd8f3 D |
110 | function WebsocketRpc(eio_socket) { |
111 | var self = this; | |
06e30f06 | 112 | |
b42bd8f3 | 113 | this._next_id = 0; |
f621d0ff | 114 | this._rpc_callbacks = {}; |
b42bd8f3 | 115 | this._socket = eio_socket; |
06e30f06 | 116 | |
b42bd8f3 D |
117 | this._mixinEmitter(); |
118 | this._bindSocketListeners(); | |
119 | } | |
06e30f06 D |
120 | |
121 | ||
b42bd8f3 D |
122 | WebsocketRpc.prototype._bindSocketListeners = function() { |
123 | var self = this; | |
06e30f06 | 124 | |
b42bd8f3 D |
125 | // Proxy the onMessage listener |
126 | this._onMessageProxy = function rpcOnMessageBoundFunction(){ | |
127 | self._onMessage.apply(self, arguments); | |
128 | }; | |
129 | this._socket.on('message', this._onMessageProxy); | |
130 | }; | |
06e30f06 D |
131 | |
132 | ||
133 | ||
b42bd8f3 D |
134 | WebsocketRpc.prototype.dispose = function() { |
135 | if (this._onMessageProxy) { | |
136 | this._socket.removeListener('message', this._onMessageProxy); | |
137 | delete this._onMessageProxy; | |
138 | } | |
06e30f06 | 139 | |
b42bd8f3 D |
140 | this.removeAllListeners(); |
141 | }; | |
06e30f06 D |
142 | |
143 | ||
144 | ||
145 | ||
b42bd8f3 D |
146 | /** |
147 | * The engine.io socket already has an emitter mixin so steal it from there | |
148 | */ | |
149 | WebsocketRpc.prototype._mixinEmitter = function() { | |
150 | var funcs = ['on', 'once', 'off', 'removeListener', 'removeAllListeners', 'emit', 'listeners', 'hasListeners']; | |
06e30f06 | 151 | |
b42bd8f3 D |
152 | for (var i=0; i<funcs.length; i++) { |
153 | if (typeof this._socket[funcs[i]] === 'function') | |
154 | this[funcs[i]] = this._socket[funcs[i]]; | |
155 | } | |
156 | }; | |
06e30f06 D |
157 | |
158 | ||
b42bd8f3 D |
159 | /** |
160 | * Check if a packet is a valid RPC call | |
161 | */ | |
162 | WebsocketRpc.prototype._isCall = function(packet) { | |
163 | return (typeof packet.method !== 'undefined' && | |
164 | typeof packet.params !== 'undefined'); | |
165 | }; | |
06e30f06 D |
166 | |
167 | ||
b42bd8f3 D |
168 | /** |
169 | * Check if a packet is a valid RPC response | |
170 | */ | |
171 | WebsocketRpc.prototype._isResponse = function(packet) { | |
172 | return (typeof packet.id !== 'undefined' && | |
173 | typeof packet.response !== 'undefined'); | |
174 | }; | |
06e30f06 D |
175 | |
176 | ||
177 | ||
b42bd8f3 D |
178 | /** |
179 | * Make an RPC call | |
180 | * First argument must be the method name to call | |
181 | * If the last argument is a function, it is used as a callback | |
182 | * All other arguments are passed to the RPC method | |
183 | * Eg. Rpc.call('namespace.method_name', 1, 2, 3, callbackFn) | |
184 | */ | |
185 | WebsocketRpc.prototype.call = function(method) { | |
186 | var params, callback, packet; | |
06e30f06 | 187 | |
b42bd8f3 D |
188 | // Get a normal array of passed in arguments |
189 | params = Array.prototype.slice.call(arguments, 1, arguments.length); | |
190 | ||
191 | // If the last argument is a function, take it as a callback and strip it out | |
192 | if (typeof params[params.length-1] === 'function') { | |
193 | callback = params[params.length-1]; | |
194 | params = params.slice(0, params.length-1); | |
195 | } | |
196 | ||
197 | packet = { | |
198 | method: method, | |
199 | params: params | |
200 | }; | |
201 | ||
202 | if (typeof callback === 'function') { | |
203 | packet.id = this._next_id; | |
204 | ||
205 | this._next_id++; | |
f621d0ff | 206 | this._rpc_callbacks[packet.id] = callback; |
b42bd8f3 | 207 | } |
06e30f06 | 208 | |
b42bd8f3 D |
209 | this.send(packet); |
210 | }; | |
06e30f06 | 211 | |
06e30f06 | 212 | |
b42bd8f3 D |
213 | /** |
214 | * Encode the packet into JSON and send it over the websocket | |
215 | */ | |
216 | WebsocketRpc.prototype.send = function(packet) { | |
217 | if (this._socket) | |
218 | this._socket.send(JSON.stringify(packet)); | |
219 | }; | |
06e30f06 | 220 | |
06e30f06 | 221 | |
b42bd8f3 D |
222 | /** |
223 | * Handler for the websocket `message` event | |
224 | */ | |
225 | WebsocketRpc.prototype._onMessage = function(message_raw) { | |
226 | var self = this, | |
227 | packet, | |
a6b1c062 D |
228 | returnFn, |
229 | callback; | |
b42bd8f3 D |
230 | |
231 | try { | |
232 | packet = JSON.parse(message_raw); | |
233 | if (!packet) throw 'Corrupt packet'; | |
234 | } catch(err) { | |
235 | return; | |
236 | } | |
237 | ||
238 | if (this._isResponse(packet)) { | |
239 | // If we have no callback waiting for this response, don't do anything | |
f621d0ff | 240 | if (typeof this._rpc_callbacks[packet.id] !== 'function') |
b42bd8f3 D |
241 | return; |
242 | ||
a6b1c062 D |
243 | // Delete the callback before calling it. If any exceptions accur within the callback |
244 | // we don't have to worry about the delete not happening | |
245 | callback = this._rpc_callbacks[packet.id]; | |
f621d0ff | 246 | delete this._rpc_callbacks[packet.id]; |
b42bd8f3 | 247 | |
a6b1c062 D |
248 | callback.apply(this, packet.response); |
249 | ||
b42bd8f3 D |
250 | } else if (this._isCall(packet)) { |
251 | // Calls with an ID may be responded to | |
252 | if (typeof packet.id !== 'undefined') { | |
253 | returnFn = this._createReturnCallFn(packet.id); | |
254 | } else { | |
255 | returnFn = this._noop; | |
256 | } | |
257 | ||
258 | this.emit.apply(this, [packet.method, returnFn].concat(packet.params)); | |
259 | } | |
260 | }; | |
06e30f06 D |
261 | |
262 | ||
b42bd8f3 D |
263 | /** |
264 | * Returns a function used as a callback when responding to a call | |
265 | */ | |
266 | WebsocketRpc.prototype._createReturnCallFn = function(packet_id) { | |
267 | var self = this; | |
06e30f06 | 268 | |
b42bd8f3 D |
269 | return function returnCallFn() { |
270 | var value = Array.prototype.slice.call(arguments, 0); | |
06e30f06 | 271 | |
b42bd8f3 D |
272 | var ret_packet = { |
273 | id: packet_id, | |
274 | response: value | |
275 | }; | |
06e30f06 | 276 | |
b42bd8f3 D |
277 | self.send(ret_packet); |
278 | }; | |
279 | }; | |
06e30f06 | 280 | |
06e30f06 | 281 | |
06e30f06 | 282 | |
b42bd8f3 | 283 | WebsocketRpc.prototype._noop = function() {}; |
06e30f06 D |
284 | |
285 | ||
b42bd8f3 | 286 | return WebsocketRpc; |
06e30f06 D |
287 | |
288 | }()) | |
289 | }; |