reconnecting-websocket.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. /*!
  2. * Reconnecting WebSocket
  3. * by Pedro Ladaria <pedro.ladaria@gmail.com>
  4. * https://github.com/pladaria/reconnecting-websocket
  5. * License MIT
  6. */
  7. import {CloseEvent, ErrorEvent, Event, EventListener, WebSocketEventMap} from './events';
  8. import RNTimer from "react-native-background-timer";
  9. const _setTimeout = RNTimer ? (callback: () => void, time: number) => RNTimer.setTimeout(callback, time) : setTimeout;
  10. const _clearTimeout = RNTimer ? (time: number) => RNTimer.clearTimeout(time) : clearTimeout;
  11. const getGlobalWebSocket = (): WebSocket | undefined => {
  12. if (typeof WebSocket !== 'undefined') {
  13. // @ts-ignore
  14. return WebSocket;
  15. }
  16. };
  17. /**
  18. * Returns true if given argument looks like a WebSocket class
  19. */
  20. const isWebSocket = (w: any) => typeof w === 'function' && w.CLOSING === 2;
  21. export type Options = {
  22. WebSocket?: any;
  23. maxReconnectionDelay?: number;
  24. minReconnectionDelay?: number;
  25. reconnectionDelayGrowFactor?: number;
  26. minUptime?: number;
  27. connectionTimeout?: number;
  28. maxRetries?: number;
  29. debug?: boolean;
  30. };
  31. const DEFAULT = {
  32. maxReconnectionDelay: 10000,
  33. minReconnectionDelay: 1000 + Math.random() * 4000,
  34. minUptime: 5000,
  35. reconnectionDelayGrowFactor: 1.3,
  36. connectionTimeout: 4000,
  37. maxRetries: Infinity,
  38. debug: false,
  39. };
  40. export type UrlProvider = string | (() => string) | (() => Promise<string>);
  41. export type ListenersMap = {
  42. error: Array<((event: ErrorEvent) => void)>;
  43. message: Array<((event: MessageEvent) => void)>;
  44. open: Array<((event: Event) => void)>;
  45. close: Array<((event: CloseEvent) => void)>;
  46. };
  47. export default class ReconnectingWebSocket {
  48. private _ws?: WebSocket;
  49. private _listeners: ListenersMap = {
  50. error: [],
  51. message: [],
  52. open: [],
  53. close: [],
  54. };
  55. private _retryCount = -1;
  56. private _uptimeTimeout: any;
  57. private _connectTimeout: any;
  58. private _shouldReconnect = true;
  59. private _connectLock = false;
  60. private _binaryType = 'blob';
  61. private readonly _url: UrlProvider;
  62. private readonly _protocols?: string | string[];
  63. private readonly _options: Options;
  64. private readonly eventToHandler = new Map<keyof WebSocketEventMap, any>([
  65. ['open', this._handleOpen.bind(this)],
  66. ['close', this._handleClose.bind(this)],
  67. ['error', this._handleError.bind(this)],
  68. ['message', this._handleMessage.bind(this)],
  69. ]);
  70. constructor(url: UrlProvider, protocols?: string | string[], options: Options = {}) {
  71. this._url = url;
  72. this._protocols = protocols;
  73. this._options = options;
  74. this._connect();
  75. }
  76. static get CONNECTING() {
  77. return 0;
  78. }
  79. static get OPEN() {
  80. return 1;
  81. }
  82. static get CLOSING() {
  83. return 2;
  84. }
  85. static get CLOSED() {
  86. return 3;
  87. }
  88. get CONNECTING() {
  89. return ReconnectingWebSocket.CONNECTING;
  90. }
  91. get OPEN() {
  92. return ReconnectingWebSocket.OPEN;
  93. }
  94. get CLOSING() {
  95. return ReconnectingWebSocket.CLOSING;
  96. }
  97. get CLOSED() {
  98. return ReconnectingWebSocket.CLOSED;
  99. }
  100. get binaryType(): string {
  101. return this._ws ? this._ws.binaryType : this._binaryType;
  102. }
  103. set binaryType(value: string) {
  104. this._binaryType = value;
  105. if (this._ws) {
  106. // @ts-ignore
  107. this._ws.binaryType = value;
  108. }
  109. }
  110. /**
  111. * Returns the number or connection retries
  112. */
  113. get retryCount(): number {
  114. return Math.max(this._retryCount, 0);
  115. }
  116. /**
  117. * The number of bytes of data that have been queued using calls to send() but not yet
  118. * transmitted to the network. This value resets to zero once all queued data has been sent.
  119. * This value does not reset to zero when the connection is closed; if you keep calling send(),
  120. * this will continue to climb. Read only
  121. */
  122. get bufferedAmount(): number {
  123. return this._ws ? this._ws.bufferedAmount : 0;
  124. }
  125. /**
  126. * The extensions selected by the server. This is currently only the empty string or a list of
  127. * extensions as negotiated by the connection
  128. */
  129. get extensions(): string {
  130. return this._ws ? this._ws.extensions : '';
  131. }
  132. /**
  133. * A string indicating the name of the sub-protocol the server selected;
  134. * this will be one of the strings specified in the protocols parameter when creating the
  135. * WebSocket object
  136. */
  137. get protocol(): string {
  138. return this._ws ? this._ws.protocol : '';
  139. }
  140. /**
  141. * The current state of the connection; this is one of the Ready state constants
  142. */
  143. get readyState(): number {
  144. return this._ws ? this._ws.readyState : ReconnectingWebSocket.CONNECTING;
  145. }
  146. /**
  147. * The URL as resolved by the constructor
  148. */
  149. get url(): string {
  150. return this._ws ? this._ws.url : '';
  151. }
  152. /**
  153. * An event listener to be called when the WebSocket connection's readyState changes to CLOSED
  154. */
  155. public onclose?: (event: CloseEvent) => void = undefined;
  156. /**
  157. * An event listener to be called when an error occurs
  158. */
  159. public onerror?: (event: Event) => void = undefined;
  160. /**
  161. * An event listener to be called when a message is received from the server
  162. */
  163. public onmessage?: (event: MessageEvent) => void = undefined;
  164. /**
  165. * An event listener to be called when the WebSocket connection's readyState changes to OPEN;
  166. * this indicates that the connection is ready to send and receive data
  167. */
  168. public onopen?: (event: Event) => void = undefined;
  169. /**
  170. * Closes the WebSocket connection or connection attempt, if any. If the connection is already
  171. * CLOSED, this method does nothing
  172. */
  173. public close(code?: number, reason?: string) {
  174. this._shouldReconnect = false;
  175. if (!this._ws || this._ws.readyState === this.CLOSED) {
  176. return;
  177. }
  178. this._ws.close(code, reason);
  179. }
  180. /**
  181. * Closes the WebSocket connection or connection attempt and connects again.
  182. * Resets retry counter;
  183. */
  184. public reconnect(code?: number, reason?: string) {
  185. this._shouldReconnect = true;
  186. this._retryCount = -1;
  187. if (!this._ws || this._ws.readyState === this.CLOSED) {
  188. this._connect();
  189. }
  190. this._disconnect(code, reason);
  191. this._connect();
  192. }
  193. /**
  194. * Enqueues the specified data to be transmitted to the server over the WebSocket connection
  195. */
  196. public send(data: string | ArrayBuffer | Blob | ArrayBufferView) {
  197. if (this._ws) {
  198. this._ws.send(data);
  199. }
  200. }
  201. /**
  202. * Register an event handler of a specific event type
  203. */
  204. public addEventListener<K extends keyof WebSocketEventMap>(
  205. type: K,
  206. listener: ((event: WebSocketEventMap[K]) => void),
  207. ): void {
  208. if (this._listeners[type]) {
  209. // @ts-ignore
  210. this._listeners[type].push(listener);
  211. }
  212. }
  213. /**
  214. * Removes an event listener
  215. */
  216. public removeEventListener<K extends keyof WebSocketEventMap>(
  217. type: K,
  218. listener: ((event: WebSocketEventMap[K]) => void),
  219. ): void {
  220. if (this._listeners[type]) {
  221. // @ts-ignore
  222. this._listeners[type] = this._listeners[type].filter(l => l !== listener);
  223. }
  224. }
  225. private _debug(...params: any[]) {
  226. if (this._options.debug) {
  227. // tslint:disable-next-line
  228. console.log('RWS>', ...params);
  229. }
  230. }
  231. private _getNextDelay() {
  232. let delay = 0;
  233. if (this._retryCount > 0) {
  234. const {
  235. reconnectionDelayGrowFactor = DEFAULT.reconnectionDelayGrowFactor,
  236. minReconnectionDelay = DEFAULT.minReconnectionDelay,
  237. maxReconnectionDelay = DEFAULT.maxReconnectionDelay,
  238. } = this._options;
  239. delay =
  240. minReconnectionDelay + Math.pow(this._retryCount - 1, reconnectionDelayGrowFactor);
  241. if (delay > maxReconnectionDelay) {
  242. delay = maxReconnectionDelay;
  243. }
  244. }
  245. this._debug('next delay', delay);
  246. return delay;
  247. }
  248. private _wait(): Promise<void> {
  249. return new Promise(resolve => {
  250. _setTimeout(resolve, this._getNextDelay());
  251. });
  252. }
  253. /**
  254. * @return Promise<string>
  255. */
  256. private _getNextUrl(urlProvider: UrlProvider): Promise<string> {
  257. if (typeof urlProvider === 'string') {
  258. return Promise.resolve(urlProvider);
  259. }
  260. if (typeof urlProvider === 'function') {
  261. const url = urlProvider();
  262. if (typeof url === 'string') {
  263. return Promise.resolve(url);
  264. }
  265. if (url.then) {
  266. return url;
  267. }
  268. }
  269. throw Error('Invalid URL');
  270. }
  271. private _connect() {
  272. if (this._connectLock) {
  273. return;
  274. }
  275. this._connectLock = true;
  276. const {
  277. maxRetries = DEFAULT.maxRetries,
  278. connectionTimeout = DEFAULT.connectionTimeout,
  279. WebSocket = getGlobalWebSocket(),
  280. } = this._options;
  281. if (this._retryCount >= maxRetries) {
  282. this._debug('max retries reached', this._retryCount, '>=', maxRetries);
  283. return;
  284. }
  285. this._retryCount++;
  286. this._debug('connect', this._retryCount);
  287. this._removeListeners();
  288. if (!isWebSocket(WebSocket)) {
  289. throw Error('No valid WebSocket class provided');
  290. }
  291. this._wait()
  292. .then(() => this._getNextUrl(this._url))
  293. .then(url => {
  294. this._debug('connect', {url, protocols: this._protocols});
  295. this._ws = new WebSocket(url, this._protocols);
  296. // @ts-ignore
  297. this._ws!.binaryType = this._binaryType;
  298. this._connectLock = false;
  299. this._addListeners();
  300. this._connectTimeout = _setTimeout(() => this._handleTimeout(), connectionTimeout);
  301. });
  302. }
  303. private _handleTimeout() {
  304. this._debug('timeout event');
  305. this._handleError(new ErrorEvent(Error('TIMEOUT'), this));
  306. }
  307. private _disconnect(code?: number, reason?: string) {
  308. _clearTimeout(this._connectTimeout);
  309. if (!this._ws) {
  310. return;
  311. }
  312. this._removeListeners();
  313. try {
  314. this._ws.close(code, reason);
  315. this._handleClose(new CloseEvent(code, reason, this));
  316. } catch (error) {
  317. // ignore
  318. }
  319. }
  320. private _acceptOpen() {
  321. this._retryCount = 0;
  322. }
  323. private _handleOpen(event: Event) {
  324. this._debug('open event');
  325. const {minUptime = DEFAULT.minUptime} = this._options;
  326. _clearTimeout(this._connectTimeout);
  327. this._uptimeTimeout = _setTimeout(() => this._acceptOpen(), minUptime);
  328. this._debug('assign binary type');
  329. // @ts-ignore
  330. this._ws!.binaryType = this._binaryType;
  331. if (this.onopen) {
  332. this.onopen(event);
  333. }
  334. this._listeners.open.forEach(listener => listener(event));
  335. }
  336. private _handleMessage(event: MessageEvent) {
  337. this._debug('message event');
  338. if (this.onmessage) {
  339. this.onmessage(event);
  340. }
  341. this._listeners.message.forEach(listener => listener(event));
  342. }
  343. private _handleError(event: ErrorEvent) {
  344. this._debug('error event', event.message);
  345. this._disconnect(undefined, event.message === 'TIMEOUT' ? 'timeout' : undefined);
  346. if (this.onerror) {
  347. this.onerror(event);
  348. }
  349. this._debug('exec error listeners');
  350. this._listeners.error.forEach(listener => listener(event));
  351. this._connect();
  352. }
  353. private _handleClose(event: CloseEvent) {
  354. this._debug('close event');
  355. if (this.onclose) {
  356. this.onclose(event);
  357. }
  358. this._listeners.close.forEach(listener => listener(event));
  359. }
  360. /**
  361. * Remove event listeners to WebSocket instance
  362. */
  363. private _removeListeners() {
  364. if (!this._ws) {
  365. return;
  366. }
  367. this._debug('removeListeners');
  368. for (const [type, handler] of this.eventToHandler) {
  369. this._ws.removeEventListener(type, handler);
  370. }
  371. }
  372. /**
  373. * Assign event listeners to WebSocket instance
  374. */
  375. private _addListeners() {
  376. this._debug('addListeners');
  377. for (const [type, handler] of this.eventToHandler) {
  378. this._ws!.addEventListener(type, handler);
  379. }
  380. }
  381. }