UNPKG

7.29 kBPlain TextView Raw
1/*!
2 * Reconnecting WebSocket
3 * by Pedro Ladaria <pedro.ladaria@gmail.com>
4 * https://github.com/pladaria/reconnecting-websocket
5 * License MIT
6 */
7type 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
17const isWebSocket = constructor =>
18 constructor && constructor.CLOSING === 2;
19
20const isGlobalWebSocket = () =>
21 typeof WebSocket !== 'undefined' && isWebSocket(WebSocket);
22
23const 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
33const 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
42const initReconnectionDelay = (config: Options) =>
43 (config.minReconnectionDelay + Math.random() * config.minReconnectionDelay);
44
45const updateReconnectionDelay = (config: Options, previousDelay: number) => {
46 const newDelay = previousDelay * config.reconnectionDelayGrowFactor;
47 return (newDelay > config.maxReconnectionDelay)
48 ? config.maxReconnectionDelay
49 : newDelay;
50};
51
52const LEVEL_0_EVENTS = ['onopen', 'onclose', 'onmessage', 'onerror'];
53
54const 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
65const 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 // require new to construct
79 if (!(this instanceof ReconnectingWebsocket)) {
80 throw new TypeError("Failed to construct 'ReconnectingWebSocket': Please use the 'new' operator");
81 }
82
83 // Set config. Not using `Object.assign` because of IE11
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 * Not using dispatchEvent, otherwise we must use a DOM Event object
97 * Deferred because we want to handle the close event before this
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 // @todo move to constant
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 // because when closing with fastClose=true, it is saved and set to null to avoid double calls
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 // execute close listeners soon with a fake closeEvent
189 // and remove them from the WS instance so they
190 // don't get fired on the real close.
191 handleClose();
192 ws.removeEventListener('close', handleClose);
193
194 // run and remove level2
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 // run and remove level0
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
234export = ReconnectingWebsocket;