| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436 |
- /*!
- * Reconnecting WebSocket
- * by Pedro Ladaria <pedro.ladaria@gmail.com>
- * https://github.com/pladaria/reconnecting-websocket
- * License MIT
- */
- import {CloseEvent, ErrorEvent, Event, EventListener, WebSocketEventMap} from './events';
- import RNTimer from "react-native-background-timer";
- const _setTimeout = RNTimer ? (callback: () => void, time: number) => RNTimer.setTimeout(callback, time) : setTimeout;
- const _clearTimeout = RNTimer ? (time: number) => RNTimer.clearTimeout(time) : clearTimeout;
- const getGlobalWebSocket = (): WebSocket | undefined => {
- if (typeof WebSocket !== 'undefined') {
- // @ts-ignore
- return WebSocket;
- }
- };
- /**
- * Returns true if given argument looks like a WebSocket class
- */
- const isWebSocket = (w: any) => typeof w === 'function' && w.CLOSING === 2;
- export type Options = {
- WebSocket?: any;
- maxReconnectionDelay?: number;
- minReconnectionDelay?: number;
- reconnectionDelayGrowFactor?: number;
- minUptime?: number;
- connectionTimeout?: number;
- maxRetries?: number;
- debug?: boolean;
- };
- const DEFAULT = {
- maxReconnectionDelay: 10000,
- minReconnectionDelay: 1000 + Math.random() * 4000,
- minUptime: 5000,
- reconnectionDelayGrowFactor: 1.3,
- connectionTimeout: 4000,
- maxRetries: Infinity,
- debug: false,
- };
- export type UrlProvider = string | (() => string) | (() => Promise<string>);
- export type ListenersMap = {
- error: Array<((event: ErrorEvent) => void)>;
- message: Array<((event: MessageEvent) => void)>;
- open: Array<((event: Event) => void)>;
- close: Array<((event: CloseEvent) => void)>;
- };
- export default class ReconnectingWebSocket {
- private _ws?: WebSocket;
- private _listeners: ListenersMap = {
- error: [],
- message: [],
- open: [],
- close: [],
- };
- private _retryCount = -1;
- private _uptimeTimeout: any;
- private _connectTimeout: any;
- private _shouldReconnect = true;
- private _connectLock = false;
- private _binaryType = 'blob';
- private readonly _url: UrlProvider;
- private readonly _protocols?: string | string[];
- private readonly _options: Options;
- private readonly eventToHandler = new Map<keyof WebSocketEventMap, any>([
- ['open', this._handleOpen.bind(this)],
- ['close', this._handleClose.bind(this)],
- ['error', this._handleError.bind(this)],
- ['message', this._handleMessage.bind(this)],
- ]);
- constructor(url: UrlProvider, protocols?: string | string[], options: Options = {}) {
- this._url = url;
- this._protocols = protocols;
- this._options = options;
- this._connect();
- }
- static get CONNECTING() {
- return 0;
- }
- static get OPEN() {
- return 1;
- }
- static get CLOSING() {
- return 2;
- }
- static get CLOSED() {
- return 3;
- }
- get CONNECTING() {
- return ReconnectingWebSocket.CONNECTING;
- }
- get OPEN() {
- return ReconnectingWebSocket.OPEN;
- }
- get CLOSING() {
- return ReconnectingWebSocket.CLOSING;
- }
- get CLOSED() {
- return ReconnectingWebSocket.CLOSED;
- }
- get binaryType(): string {
- return this._ws ? this._ws.binaryType : this._binaryType;
- }
- set binaryType(value: string) {
- this._binaryType = value;
- if (this._ws) {
- // @ts-ignore
- this._ws.binaryType = value;
- }
- }
- /**
- * Returns the number or connection retries
- */
- get retryCount(): number {
- return Math.max(this._retryCount, 0);
- }
- /**
- * The number of bytes of data that have been queued using calls to send() but not yet
- * transmitted to the network. This value resets to zero once all queued data has been sent.
- * This value does not reset to zero when the connection is closed; if you keep calling send(),
- * this will continue to climb. Read only
- */
- get bufferedAmount(): number {
- return this._ws ? this._ws.bufferedAmount : 0;
- }
- /**
- * The extensions selected by the server. This is currently only the empty string or a list of
- * extensions as negotiated by the connection
- */
- get extensions(): string {
- return this._ws ? this._ws.extensions : '';
- }
- /**
- * A string indicating the name of the sub-protocol the server selected;
- * this will be one of the strings specified in the protocols parameter when creating the
- * WebSocket object
- */
- get protocol(): string {
- return this._ws ? this._ws.protocol : '';
- }
- /**
- * The current state of the connection; this is one of the Ready state constants
- */
- get readyState(): number {
- return this._ws ? this._ws.readyState : ReconnectingWebSocket.CONNECTING;
- }
- /**
- * The URL as resolved by the constructor
- */
- get url(): string {
- return this._ws ? this._ws.url : '';
- }
- /**
- * An event listener to be called when the WebSocket connection's readyState changes to CLOSED
- */
- public onclose?: (event: CloseEvent) => void = undefined;
- /**
- * An event listener to be called when an error occurs
- */
- public onerror?: (event: Event) => void = undefined;
- /**
- * An event listener to be called when a message is received from the server
- */
- public onmessage?: (event: MessageEvent) => void = undefined;
- /**
- * An event listener to be called when the WebSocket connection's readyState changes to OPEN;
- * this indicates that the connection is ready to send and receive data
- */
- public onopen?: (event: Event) => void = undefined;
- /**
- * Closes the WebSocket connection or connection attempt, if any. If the connection is already
- * CLOSED, this method does nothing
- */
- public close(code?: number, reason?: string) {
- this._shouldReconnect = false;
- if (!this._ws || this._ws.readyState === this.CLOSED) {
- return;
- }
- this._ws.close(code, reason);
- }
- /**
- * Closes the WebSocket connection or connection attempt and connects again.
- * Resets retry counter;
- */
- public reconnect(code?: number, reason?: string) {
- this._shouldReconnect = true;
- this._retryCount = -1;
- if (!this._ws || this._ws.readyState === this.CLOSED) {
- this._connect();
- }
- this._disconnect(code, reason);
- this._connect();
- }
- /**
- * Enqueues the specified data to be transmitted to the server over the WebSocket connection
- */
- public send(data: string | ArrayBuffer | Blob | ArrayBufferView) {
- if (this._ws) {
- this._ws.send(data);
- }
- }
- /**
- * Register an event handler of a specific event type
- */
- public addEventListener<K extends keyof WebSocketEventMap>(
- type: K,
- listener: ((event: WebSocketEventMap[K]) => void),
- ): void {
- if (this._listeners[type]) {
- // @ts-ignore
- this._listeners[type].push(listener);
- }
- }
- /**
- * Removes an event listener
- */
- public removeEventListener<K extends keyof WebSocketEventMap>(
- type: K,
- listener: ((event: WebSocketEventMap[K]) => void),
- ): void {
- if (this._listeners[type]) {
- // @ts-ignore
- this._listeners[type] = this._listeners[type].filter(l => l !== listener);
- }
- }
- private _debug(...params: any[]) {
- if (this._options.debug) {
- // tslint:disable-next-line
- console.log('RWS>', ...params);
- }
- }
- private _getNextDelay() {
- let delay = 0;
- if (this._retryCount > 0) {
- const {
- reconnectionDelayGrowFactor = DEFAULT.reconnectionDelayGrowFactor,
- minReconnectionDelay = DEFAULT.minReconnectionDelay,
- maxReconnectionDelay = DEFAULT.maxReconnectionDelay,
- } = this._options;
- delay =
- minReconnectionDelay + Math.pow(this._retryCount - 1, reconnectionDelayGrowFactor);
- if (delay > maxReconnectionDelay) {
- delay = maxReconnectionDelay;
- }
- }
- this._debug('next delay', delay);
- return delay;
- }
- private _wait(): Promise<void> {
- return new Promise(resolve => {
- _setTimeout(resolve, this._getNextDelay());
- });
- }
- /**
- * @return Promise<string>
- */
- private _getNextUrl(urlProvider: UrlProvider): Promise<string> {
- if (typeof urlProvider === 'string') {
- return Promise.resolve(urlProvider);
- }
- if (typeof urlProvider === 'function') {
- const url = urlProvider();
- if (typeof url === 'string') {
- return Promise.resolve(url);
- }
- if (url.then) {
- return url;
- }
- }
- throw Error('Invalid URL');
- }
- private _connect() {
- if (this._connectLock) {
- return;
- }
- this._connectLock = true;
- const {
- maxRetries = DEFAULT.maxRetries,
- connectionTimeout = DEFAULT.connectionTimeout,
- WebSocket = getGlobalWebSocket(),
- } = this._options;
- if (this._retryCount >= maxRetries) {
- this._debug('max retries reached', this._retryCount, '>=', maxRetries);
- return;
- }
- this._retryCount++;
- this._debug('connect', this._retryCount);
- this._removeListeners();
- if (!isWebSocket(WebSocket)) {
- throw Error('No valid WebSocket class provided');
- }
- this._wait()
- .then(() => this._getNextUrl(this._url))
- .then(url => {
- this._debug('connect', {url, protocols: this._protocols});
- this._ws = new WebSocket(url, this._protocols);
- // @ts-ignore
- this._ws!.binaryType = this._binaryType;
- this._connectLock = false;
- this._addListeners();
- this._connectTimeout = _setTimeout(() => this._handleTimeout(), connectionTimeout);
- });
- }
- private _handleTimeout() {
- this._debug('timeout event');
- this._handleError(new ErrorEvent(Error('TIMEOUT'), this));
- }
- private _disconnect(code?: number, reason?: string) {
- _clearTimeout(this._connectTimeout);
- if (!this._ws) {
- return;
- }
- this._removeListeners();
- try {
- this._ws.close(code, reason);
- this._handleClose(new CloseEvent(code, reason, this));
- } catch (error) {
- // ignore
- }
- }
- private _acceptOpen() {
- this._retryCount = 0;
- }
- private _handleOpen(event: Event) {
- this._debug('open event');
- const {minUptime = DEFAULT.minUptime} = this._options;
- _clearTimeout(this._connectTimeout);
- this._uptimeTimeout = _setTimeout(() => this._acceptOpen(), minUptime);
- this._debug('assign binary type');
- // @ts-ignore
- this._ws!.binaryType = this._binaryType;
- if (this.onopen) {
- this.onopen(event);
- }
- this._listeners.open.forEach(listener => listener(event));
- }
- private _handleMessage(event: MessageEvent) {
- this._debug('message event');
- if (this.onmessage) {
- this.onmessage(event);
- }
- this._listeners.message.forEach(listener => listener(event));
- }
- private _handleError(event: ErrorEvent) {
- this._debug('error event', event.message);
- this._disconnect(undefined, event.message === 'TIMEOUT' ? 'timeout' : undefined);
- if (this.onerror) {
- this.onerror(event);
- }
- this._debug('exec error listeners');
- this._listeners.error.forEach(listener => listener(event));
- this._connect();
- }
- private _handleClose(event: CloseEvent) {
- this._debug('close event');
- if (this.onclose) {
- this.onclose(event);
- }
- this._listeners.close.forEach(listener => listener(event));
- }
- /**
- * Remove event listeners to WebSocket instance
- */
- private _removeListeners() {
- if (!this._ws) {
- return;
- }
- this._debug('removeListeners');
- for (const [type, handler] of this.eventToHandler) {
- this._ws.removeEventListener(type, handler);
- }
- }
- /**
- * Assign event listeners to WebSocket instance
- */
- private _addListeners() {
- this._debug('addListeners');
- for (const [type, handler] of this.eventToHandler) {
- this._ws!.addEventListener(type, handler);
- }
- }
- }
|