This repository has been archived by the owner on May 29, 2020. It is now read-only.
forked from Densaugeo/PersistentWS
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathPersistentWS.js
220 lines (175 loc) · 7.84 KB
/
PersistentWS.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
(function(root, factory) {
if(typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], factory);
}
if(typeof exports === 'object') {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
}
// Browser globals (root is window)
root.PersistentWS = factory();
}(this, function() {
/**
* @description This script provides a persistent WebSocket that attempts to reconnect after disconnections
*/
/**
* @module PersistentWS
* @description This is a WebSocket that attempts to reconnect after disconnections
* @description Reconnection times start at ~5s, double after each failed attempt, and are randomized +/- 10%
* @description Exposes standard WebSocket API (https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
*
* @example var persistentConnection = new PersistentWS('wss://foo.bar/');
* @example
* @example persistentConnection.addEventListener('message', function(e) {
* @example console.log('Received: ' + e.data);
* @example });
* @example
* @example // Options may be supplied as a *third* parameter, after the rarely-used protocols argument
* @example var anotherConnection = new PersistentWS('wss://foo.bar/', undefined, {verbose: true});
*/
var PersistentWS = function PersistentWS(url, protocols, options) {
var self = this;
// @prop Boolean verbose -- console.log() info about connections and disconnections
// @option Boolean verbose -- Sets .verbose
this.verbose = Boolean(options && options.verbose) || false;
// @prop Number initialRetryTime -- Delay for first retry attempt, in milliseconds. Always an integer >= 100
// @option Number initialRetryTime -- Sets .initialRetryTime
this.initialRetryTime = Number(options && options.initialRetryTime) || 5000;
// @prop Boolean persistence -- If false, disables reconnection
// @option Boolean persistence -- Sets .persistence
this.persistence = options === undefined || options.persistence === undefined || Boolean(options.persistence);
// @prop Number attempts -- Retry attempt # since last disconnect
this.attempts = 0;
// @prop WebSocket socket -- The actual WebSocket. Events registered directly to the raw socket will be lost after reconnections
this.socket = {};
// @method undefined _onopen(Event e) -- For internal use. Calls to .onopen() and handles reconnection cleanup
this._onopen = function(e) {
if(self.onopen) {
self.onopen(e);
}
}
// @method undefined _onmessage(Event e) -- For internal use. Calls to .onmessage()
this._onmessage = function(e) {
if(self.onmessage) {
self.onmessage(e);
}
}
// @method undefined _onerror(Error e) -- For internal use. Calls to .onerror()
this._onerror = function(e) {
if(self.onerror) {
self.onerror(e);
}
}
// @method undefined _onclose(Event e) -- For internal use. Calls to .onclose() and ._reconnect() where appropriate
this._onclose = function(e) {
if(self.persistence) {
self._reconnect();
}
if(self.onclose) {
self.onclose(e);
}
}
// @prop [[String, Function, Boolean]] _listeners -- For internal use. Array of .addEventListener arguments
this._listeners = [
['open', this._onopen],
['message', this._onmessage],
['error', this._onerror],
['close', this._onclose]
];
// @method undefined _connect() -- For internal use. Connects and copies in event listeners
this._connect = function _connect() {
if(self.verbose) {
console.log('Opening WebSocket to ' + url);
}
var binaryType = self.socket.binaryType;
self.socket = new WebSocket(url, protocols);
self.socket.binaryType = binaryType || self.socket.binaryType;
// Reset .attempts counter on successful connection
self.socket.addEventListener('open', function() {
if(self.verbose) {
console.log('WebSocket connected to ' + self.url);
}
self.attempts = 0;
});
self._listeners.forEach(function(v) {
self.socket.addEventListener.apply(self.socket, v);
});
}
this._connect();
}
PersistentWS.CONNECTING = WebSocket.CONNECTING;
PersistentWS.OPEN = WebSocket.OPEN;
PersistentWS.CLOSING = WebSocket.CLOSING;
PersistentWS.CLOSED = WebSocket.CLOSED;
PersistentWS.prototype.CONNECTING = WebSocket.CONNECTING;
PersistentWS.prototype.OPEN = WebSocket.OPEN;
PersistentWS.prototype.CLOSING = WebSocket.CLOSING;
PersistentWS.prototype.CLOSED = WebSocket.CLOSED;
var webSocketProperties = ['binaryType', 'bufferedAmount', 'extensions', 'protocol', 'readyState', 'url'];
webSocketProperties.forEach(function(v) {
Object.defineProperty(PersistentWS.prototype, v, {
get: function() {
return this.socket[v];
},
set: function(x) {
return (this.socket[v] = x);
}
});
});
PersistentWS.prototype.close = function(code, reason) {
this.socket.close(code, reason);
}
PersistentWS.prototype.send = function(data) {
this.socket.send(data);
}
PersistentWS.prototype.addEventListener = function addEventListener(type, listener, useCapture) {
this.socket.addEventListener(type, listener, useCapture);
var alreadyStored = this._getListenerIndex(type, listener, useCapture) !== -1;
if(!alreadyStored) {
// Store optional parameter useCapture as Boolean, for consistency with
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
var useCaptureBoolean = Boolean(useCapture);
this._listeners.push([type, listener, useCaptureBoolean]);
}
}
PersistentWS.prototype.removeEventListener = function removeEventListener(type, listener, useCapture) {
this.socket.removeEventListener(type, listener, useCapture);
var indexToRemove = this._getListenerIndex(type, listener, useCapture);
if(indexToRemove !== -1) {
this._listeners.splice(indexToRemove, 1);
}
}
// @method proto Boolean dispatchEvent(Event event) -- Same as calling .dispatchEvent() on .socket
PersistentWS.prototype.dispatchEvent = function(event) {
return this.socket.dispatchEvent(event);
}
// @method proto Number _getListenerIndex(String type, Function listener[, Boolean useCapture]) -- For internal use. Returns index of a listener in ._listeners
PersistentWS.prototype._getListenerIndex = function _getListenerIndex(type, listener, useCapture) {
// Store optional parameter useCapture as Boolean, for consistency with
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
var useCaptureBoolean = Boolean(useCapture);
var result = -1;
this._listeners.forEach(function(v, i) {
if(v[0] === type && v[1] === listener && v[2] === useCaptureBoolean) {
result = i;
}
});
return result;
}
// @method proto undefined _reconnect() -- For internal use. Begins the reconnection timer
PersistentWS.prototype._reconnect = function() {
// Retty time falls of exponentially
var retryTime = this.initialRetryTime*Math.pow(2, this.attempts++);
// Retry time is randomized +/- 10% to prevent clients reconnecting at the exact same time after a server event
retryTime += Math.floor(Math.random()*retryTime/5 - retryTime/10);
if(this.verbose) {
console.log('WebSocket disconnected, attempting to reconnect in ' + retryTime + 'ms...');
}
setTimeout(this._connect, retryTime);
}
// Only one object to return, so no need for module object to hold it
return PersistentWS;
})); // Module pattern