From a470cc0ba8969a4b02cc648ee885c256b9b60797 Mon Sep 17 00:00:00 2001 From: Jan Rollmann Date: Wed, 4 Oct 2023 12:46:45 +0200 Subject: [PATCH] Add backoff for retry interval --- src/App.tsx | 4 +- src/library/base/Ux4iot.ts | 74 ++++++++++++++++++++++++++++---- src/library/base/constants.ts | 21 --------- src/library/base/types.ts | 2 + src/library/useMultiTelemetry.ts | 1 - 5 files changed, 69 insertions(+), 33 deletions(-) delete mode 100644 src/library/base/constants.ts diff --git a/src/App.tsx b/src/App.tsx index f77b912..8a94c06 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,8 +43,8 @@ function App(): JSX.Element | null { }} >
-
Connection Reason: ${connectionReason}
-
Connection Desc: ${connectionDesc}
+
Connection Reason: {connectionReason}
+
Connection Desc: {connectionDesc}
diff --git a/src/library/base/Ux4iot.ts b/src/library/base/Ux4iot.ts index 2274cbc..ceace92 100644 --- a/src/library/base/Ux4iot.ts +++ b/src/library/base/Ux4iot.ts @@ -36,14 +36,42 @@ import { isTelemetryMessage, } from './utils'; import { DeviceMethodParams } from 'azure-iothub'; -import { NETWORK_STATES, RECONNECT_TIMEOUT } from './constants'; import { AxiosError } from 'axios'; +import { ConnectionUpdateReason } from './types'; + +const RECONNECT_TIMEOUT = 5000; +const MAX_RECONNECT_TIMEOUT = 30000; +const NETWORK_STATES: Record = { + UX4IOT_OFFLINE: [ + 'ux4iot_unreachable', + 'Failed to fetch sessionId of ux4iot.', + ], + SERVER_UNAVAILABLE: [ + 'socket_connect_error', + 'Could not establish connection to ux4iot websocket', + ], + CLIENT_DISCONNECTED: ['socket_disconnect', 'Client manually disconnected'], + SERVER_DISCONNECTED: [ + 'socket_disconnect', + 'Disconnected / Error Connecting.', + ], + CONNECTED: ['socket_connect', 'Connected to ux4iot websocket'], +}; + +function nextTimeout( + timeout: number = RECONNECT_TIMEOUT, + maxTimeout: number = MAX_RECONNECT_TIMEOUT +) { + return timeout + timeout > maxTimeout ? maxTimeout : timeout + timeout; +} export class Ux4iot { sessionId = ''; socket: Socket | undefined; devMode: boolean; api: Ux4iotApi; + retryTimeout: number; + maxRetryTimeout: number; retryTimeoutAfterError?: NodeJS.Timeout; onSocketConnectionUpdate?: ConnectionUpdateFunction; onSessionId?: (sessionId: string) => void; @@ -57,6 +85,8 @@ export class Ux4iot { this.sessionId = sessionId; this.api = api; this.devMode = isDevOptions(options); + this.retryTimeout = options.reconnectTimeout ?? RECONNECT_TIMEOUT; + this.maxRetryTimeout = options.maxReconnectTimeout ?? MAX_RECONNECT_TIMEOUT; this.onSessionId = onSessionId; this.onSocketConnectionUpdate = options.onSocketConnectionUpdate; this.initializeSocket(); @@ -66,16 +96,36 @@ export class Ux4iot { options: InitializationOptions, onSessionId?: (sessionId: string) => void ): Promise { + const { onSocketConnectionUpdate, reconnectTimeout, maxReconnectTimeout } = + options; + const timeout = reconnectTimeout ?? RECONNECT_TIMEOUT; + const maxTimeout = maxReconnectTimeout ?? MAX_RECONNECT_TIMEOUT; const api = new Ux4iotApi(options); + let initializationTimeout: NodeJS.Timeout | undefined; try { const sessionId = await api.getSessionId(); api.setSessionId(sessionId); + clearTimeout(initializationTimeout); return new Ux4iot(options, sessionId, api, onSessionId); } catch (error) { const [reason, description] = NETWORK_STATES.UX4IOT_OFFLINE; - options.onSocketConnectionUpdate?.(reason, description); + onSocketConnectionUpdate?.(reason, description); + + console.warn( + `Trying to initialize again in ${timeout / 1000} seconds...` + ); + + const nextOptions = { + ...options, + reconnectTimeout: nextTimeout(timeout, maxTimeout), + maxReconnectTimeout: maxTimeout, + }; - throw NETWORK_STATES.UX4IOT_OFFLINE.join(); + return new Promise((resolve, reject) => { + initializationTimeout = setTimeout(() => { + return resolve(Ux4iot.create(nextOptions, onSessionId)); + }, timeout); + }); } } @@ -88,8 +138,10 @@ export class Ux4iot { this.socket.on('data', this.onData.bind(this)); } - private tryReconnect() { - clearTimeout(this.retryTimeoutAfterError as unknown as NodeJS.Timeout); + private tryReconnect(timeout: number = this.retryTimeout) { + clearTimeout(this.retryTimeoutAfterError); + + this.log(`Trying to reconnect in ${timeout / 1000} seconds...`); this.retryTimeoutAfterError = setTimeout(async () => { if (!this.socket) { @@ -101,12 +153,14 @@ export class Ux4iot { const [reason, description] = NETWORK_STATES.UX4IOT_OFFLINE; this.log(reason, description, error); this.onSocketConnectionUpdate?.(reason, description); - this.tryReconnect(); + this.tryReconnect( + nextTimeout(timeout ?? this.retryTimeout, this.maxRetryTimeout) + ); return; } this.initializeSocket(); } - }, RECONNECT_TIMEOUT); + }, timeout); } private async onConnect() { @@ -116,10 +170,11 @@ export class Ux4iot { console.log('onSessionId called with', this.sessionId); this.onSessionId?.(this.sessionId); // this callback should be used to reestablish all subscriptions this.onSocketConnectionUpdate?.(...NETWORK_STATES.CONNECTED); - clearTimeout(this.retryTimeoutAfterError as unknown as NodeJS.Timeout); + clearTimeout(this.retryTimeoutAfterError); } private onConnectError() { + this.log(`on connect error called`); const socketURL = this.api.getSocketURL(this.sessionId); this.log(`Failed to establish websocket to ${socketURL}`); const [reason, description] = NETWORK_STATES.SERVER_UNAVAILABLE; @@ -128,6 +183,7 @@ export class Ux4iot { } private onDisconnect(error: unknown) { + this.log(`on disconnect called`); if (error === 'io client disconnect') { // https://socket.io/docs/v4/client-api/#event-disconnect const [reason, description] = NETWORK_STATES.CLIENT_DISCONNECTED; @@ -145,7 +201,7 @@ export class Ux4iot { public async destroy(): Promise { this.socket?.disconnect(); this.socket = undefined; - clearTimeout(this.retryTimeoutAfterError as unknown as NodeJS.Timeout); + clearTimeout(this.retryTimeoutAfterError); this.log('socket with id', this.sessionId, 'destroyed'); } diff --git a/src/library/base/constants.ts b/src/library/base/constants.ts deleted file mode 100644 index 5738496..0000000 --- a/src/library/base/constants.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ConnectionUpdateReason } from './types'; - -export const RECONNECT_TIMEOUT = 5000; -export const TIMEOUT_SECONDS = RECONNECT_TIMEOUT / 1000; -export const NETWORK_STATES: Record = - { - UX4IOT_OFFLINE: [ - 'ux4iot_unreachable', - `Failed to fetch sessionId of ux4iot. Attempting again in ${TIMEOUT_SECONDS} seconds.`, - ], - SERVER_UNAVAILABLE: [ - 'socket_connect_error', - 'Could not establish connection to ux4iot websocket', - ], - CLIENT_DISCONNECTED: ['socket_disconnect', 'Client manually disconnected'], - SERVER_DISCONNECTED: [ - 'socket_disconnect', - `Disconnected / Error Connecting. Trying to reconnect in ${TIMEOUT_SECONDS} seconds.`, - ], - CONNECTED: ['socket_connect', 'Connected to ux4iot websocket'], - }; diff --git a/src/library/base/types.ts b/src/library/base/types.ts index ca032d6..4b30ce3 100644 --- a/src/library/base/types.ts +++ b/src/library/base/types.ts @@ -26,6 +26,8 @@ export type ConnectionUpdateFunction = ( export type InitializationOptions = { onSocketConnectionUpdate?: ConnectionUpdateFunction; + reconnectTimeout?: number; + maxReconnectTimeout?: number; } & (InitializeDevOptions | InitializeProdOptions); export type InitializeDevOptions = { adminConnectionString: string; diff --git a/src/library/useMultiTelemetry.ts b/src/library/useMultiTelemetry.ts index 3b7605c..1240ce4 100644 --- a/src/library/useMultiTelemetry.ts +++ b/src/library/useMultiTelemetry.ts @@ -35,7 +35,6 @@ type UseMultiTelemetryOutput = { type HookOptions = { initialSubscribers?: Subscribers; - useBulk?: boolean; onData?: TelemetryCallback; onGrantError?: GrantErrorCallback; onSubscriptionError?: SubscriptionErrorCallback;