From 121f3bbe2902313efda2cb8d9a710d40af951a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Thu, 15 Jul 2021 20:50:13 +0200 Subject: [PATCH] implement reconnecter class. --- src/component/utils/reconnecter.ts | 149 +++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 src/component/utils/reconnecter.ts diff --git a/src/component/utils/reconnecter.ts b/src/component/utils/reconnecter.ts new file mode 100644 index 00000000..10aec3f3 --- /dev/null +++ b/src/component/utils/reconnecter.ts @@ -0,0 +1,149 @@ +import EventEmitter from 'eventemitter3' + +export interface ReconnecterAbstractEvents { + connect: () => void + disconnect: (error?: Error) => void +} + +export abstract class ReconnecterAbstract extends EventEmitter { + constructor() { + super() + + if (this.constructor == ReconnecterAbstract) { + throw new Error("Abstract classes can't be instantiated."); + } + } + + public async connect() { + throw new Error("Method 'connect()' must be implemented."); + } + + public async disconnect() { + throw new Error("Method 'disconnect()' must be implemented."); + } +} + +export interface ReconnecterEvents { + open: () => void + connect: () => void + disconnect: () => void + close: (error?: Error) => void +} + +export interface ReconnecterConfig { + max_reconnects: number + timeout_ms: number + backoff_ms: number +} + +export class Reconnecter extends EventEmitter { + private _conn: ReconnecterAbstract + private _config: ReconnecterConfig + private _timeout: number | undefined + + private _open = false + private _connected = false + private _total_reconnects = 0 + private _last_connected: Date | undefined + + constructor(conn: ReconnecterAbstract, config?: ReconnecterConfig) { + super() + + this._conn = conn + this._config = { + max_reconnects: 10, + timeout_ms: 1500, + backoff_ms: 750, + ...config, + } + + this._conn.on('connect', () => { + if (this._timeout) { + window.clearTimeout(this._timeout) + this._timeout = undefined + } + + this._connected = true + + if (this._open) { + this._last_connected = new Date() + this.emit('connect') + } else { + this._conn.disconnect() + } + }) + + this._conn.on('disconnect', () => { + if (this._timeout) { + window.clearTimeout(this._timeout) + this._timeout = undefined + } + + this._connected = false + + if (this._open) { + this.emit('disconnect') + this.reconnect() + } + }) + } + + public get isOpen(): boolean { + return this._open + } + + public get isConnected(): boolean { + return this._connected + } + + public get totalReconnects(): number { + return this._total_reconnects + } + + public get lastConnected(): Date | undefined { + return this._last_connected + } + + public open(): void { + if (this._open) { + throw new Error('connection is already open') + } + + this._open = true + this.emit('open') + + this._conn.connect() + this._timeout = window.setTimeout(() => { + this._conn.disconnect() + this.reconnect() + }, this._config.timeout_ms) + } + + public close(error?: Error): void { + if (!this._open) { + throw new Error('connection is already closed') + } + + this._open = false + this._last_connected = undefined + this.emit('close', error) + + if (this._connected) { + this._conn.disconnect() + } + } + + public reconnect(): void { + if (this._connected) { + throw new Error('connection is already connected') + } + + this._total_reconnects++ + + if (this._config.max_reconnects < this._total_reconnects || this._total_reconnects < 0) { + setTimeout(this._conn.connect, this._config.backoff_ms) + } else { + this.close(new Error('reconnection failed')) + } + } +}