1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | type Options = {
|
8 | constructor?: new(url: string, protocols?: string | string[]) => WebSocket;
|
9 | maxReconnectionDelay?: number;
|
10 | minReconnectionDelay?: number;
|
11 | reconnectionDelayGrowFactor?: number;
|
12 | connectionTimeout?: number;
|
13 | maxRetries?: number;
|
14 | debug?: boolean;
|
15 | };
|
16 |
|
17 | const isWebSocket = constructor =>
|
18 | constructor && constructor.CLOSING === 2;
|
19 |
|
20 | const isGlobalWebSocket = () =>
|
21 | typeof WebSocket !== 'undefined' && isWebSocket(WebSocket);
|
22 |
|
23 | const getDefaultOptions = () => <Options>({
|
24 | constructor: isGlobalWebSocket() ? WebSocket : null,
|
25 | maxReconnectionDelay: 10000,
|
26 | minReconnectionDelay: 1500,
|
27 | reconnectionDelayGrowFactor: 1.3,
|
28 | connectionTimeout: 4000,
|
29 | maxRetries: Infinity,
|
30 | debug: false,
|
31 | });
|
32 |
|
33 | const bypassProperty = (src, dst, name: string) => {
|
34 | Object.defineProperty(dst, name, {
|
35 | get: () => src[name],
|
36 | set: (value) => {src[name] = value},
|
37 | enumerable: true,
|
38 | configurable: true,
|
39 | });
|
40 | };
|
41 |
|
42 | const initReconnectionDelay = (config: Options) =>
|
43 | (config.minReconnectionDelay + Math.random() * config.minReconnectionDelay);
|
44 |
|
45 | const updateReconnectionDelay = (config: Options, previousDelay: number) => {
|
46 | const newDelay = previousDelay * config.reconnectionDelayGrowFactor;
|
47 | return (newDelay > config.maxReconnectionDelay)
|
48 | ? config.maxReconnectionDelay
|
49 | : newDelay;
|
50 | };
|
51 |
|
52 | const LEVEL_0_EVENTS = ['onopen', 'onclose', 'onmessage', 'onerror'];
|
53 |
|
54 | const reassignEventListeners = (ws: WebSocket, oldWs: WebSocket, listeners) => {
|
55 | Object.keys(listeners).forEach(type => {
|
56 | listeners[type].forEach(([listener, options]) => {
|
57 | ws.addEventListener(type, listener, options);
|
58 | });
|
59 | });
|
60 | if (oldWs) {
|
61 | LEVEL_0_EVENTS.forEach(name => {ws[name] = oldWs[name]});
|
62 | }
|
63 | };
|
64 |
|
65 | const ReconnectingWebsocket = function(
|
66 | url: string,
|
67 | protocols?: string|string[],
|
68 | options = <Options>{}
|
69 | ) {
|
70 | let ws: WebSocket;
|
71 | let connectingTimeout;
|
72 | let reconnectDelay = 0;
|
73 | let retriesCount = 0;
|
74 | let shouldRetry = true;
|
75 | let savedOnClose = null;
|
76 | const listeners: any = {};
|
77 |
|
78 |
|
79 | if (!(this instanceof ReconnectingWebsocket)) {
|
80 | throw new TypeError("Failed to construct 'ReconnectingWebSocket': Please use the 'new' operator");
|
81 | }
|
82 |
|
83 |
|
84 | const config = getDefaultOptions();
|
85 | Object.keys(config)
|
86 | .filter(key => options.hasOwnProperty(key))
|
87 | .forEach(key => config[key] = options[key]);
|
88 |
|
89 | if (!isWebSocket(config.constructor)) {
|
90 | throw new TypeError('Invalid WebSocket constructor. Set `options.constructor`');
|
91 | }
|
92 |
|
93 | const log = config.debug ? (...params) => console.log('RWS:', ...params) : () => {};
|
94 |
|
95 | |
96 |
|
97 |
|
98 |
|
99 | const emitError = (code: string, msg: string) => setTimeout(() => {
|
100 | const err = <any>new Error(msg);
|
101 | err.code = code;
|
102 | if (Array.isArray(listeners.error)) {
|
103 | listeners.error.forEach(([fn]) => fn(err));
|
104 | }
|
105 | if (ws.onerror) {
|
106 | ws.onerror(err);
|
107 | }
|
108 | }, 0);
|
109 |
|
110 | const handleClose = () => {
|
111 | log('close');
|
112 | retriesCount++;
|
113 | log('retries count:', retriesCount);
|
114 | if (retriesCount > config.maxRetries) {
|
115 | emitError('EHOSTDOWN', 'Too many failed connection attempts');
|
116 | return;
|
117 | }
|
118 | if (!reconnectDelay) {
|
119 | reconnectDelay = initReconnectionDelay(config);
|
120 | } else {
|
121 | reconnectDelay = updateReconnectionDelay(config, reconnectDelay);
|
122 | }
|
123 | log('reconnectDelay:', reconnectDelay);
|
124 |
|
125 | if (shouldRetry) {
|
126 | setTimeout(connect, reconnectDelay);
|
127 | }
|
128 | };
|
129 |
|
130 | const connect = () => {
|
131 | if (!shouldRetry) {
|
132 | return;
|
133 | }
|
134 |
|
135 | log('connect');
|
136 | const oldWs = ws;
|
137 | ws = new (<any>config.constructor)(url, protocols);
|
138 |
|
139 | connectingTimeout = setTimeout(() => {
|
140 | log('timeout');
|
141 | ws.close();
|
142 | emitError('ETIMEDOUT', 'Connection timeout');
|
143 | }, config.connectionTimeout);
|
144 |
|
145 | log('bypass properties');
|
146 | for (let key in ws) {
|
147 |
|
148 | if (['addEventListener', 'removeEventListener', 'close', 'send'].indexOf(key) < 0) {
|
149 | bypassProperty(ws, this, key);
|
150 | }
|
151 | }
|
152 |
|
153 | ws.addEventListener('open', () => {
|
154 | clearTimeout(connectingTimeout);
|
155 | log('open');
|
156 | reconnectDelay = initReconnectionDelay(config);
|
157 | log('reconnectDelay:', reconnectDelay);
|
158 | retriesCount = 0;
|
159 | });
|
160 |
|
161 | ws.addEventListener('close', handleClose);
|
162 |
|
163 | reassignEventListeners(ws, oldWs, listeners);
|
164 |
|
165 |
|
166 | ws.onclose = ws.onclose || savedOnClose;
|
167 | savedOnClose = null;
|
168 | };
|
169 |
|
170 | log('init');
|
171 | connect();
|
172 |
|
173 | this.close = (code = 1000, reason = '', {keepClosed = false, fastClose = true, delay = 0} = {}) => {
|
174 | if (delay) {
|
175 | reconnectDelay = delay;
|
176 | }
|
177 | shouldRetry = !keepClosed;
|
178 |
|
179 | ws.close(code, reason);
|
180 |
|
181 | if (fastClose) {
|
182 | const fakeCloseEvent = <CloseEvent>{
|
183 | code,
|
184 | reason,
|
185 | wasClean: true,
|
186 | };
|
187 |
|
188 |
|
189 |
|
190 |
|
191 | handleClose();
|
192 | ws.removeEventListener('close', handleClose);
|
193 |
|
194 |
|
195 | if (Array.isArray(listeners.close)) {
|
196 | listeners.close.forEach(([listener, options]) => {
|
197 | listener(fakeCloseEvent);
|
198 | ws.removeEventListener('close', listener, options);
|
199 | });
|
200 | }
|
201 |
|
202 |
|
203 | if (ws.onclose) {
|
204 | savedOnClose = ws.onclose;
|
205 | ws.onclose(fakeCloseEvent);
|
206 | ws.onclose = null;
|
207 | }
|
208 | }
|
209 | };
|
210 |
|
211 | this.send = (data) => {
|
212 | ws.send(data)
|
213 | };
|
214 |
|
215 | this.addEventListener = (type: string, listener: EventListener, options: any) => {
|
216 | if (Array.isArray(listeners[type])) {
|
217 | if (!listeners[type].some(([l]) => l === listener)) {
|
218 | listeners[type].push([listener, options]);
|
219 | }
|
220 | } else {
|
221 | listeners[type] = [[listener, options]];
|
222 | }
|
223 | ws.addEventListener(type, listener, options);
|
224 | };
|
225 |
|
226 | this.removeEventListener = (type: string, listener: EventListener, options: any) => {
|
227 | if (Array.isArray(listeners[type])) {
|
228 | listeners[type] = listeners[type].filter(([l]) => l !== listener);
|
229 | }
|
230 | ws.removeEventListener(type, listener, options);
|
231 | };
|
232 | };
|
233 |
|
234 | export = ReconnectingWebsocket;
|