From 4bd1fe0c633eb9cee23800026742bcf5390c82bb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 04:22:05 +0000 Subject: [PATCH 1/2] fix: prevent parallel reconnect loops by storing and clearing timer references (OPTI-2438) The 'disconnected' handler in setReconnect() scheduled a setTimeout without storing its ID, making it impossible to cancel. When 'failed' fired shortly after, the immediate reconnect() could fail and reset isReconnecting before the 1500ms timer fired, allowing a second independent retry loop to spawn. Changes: - Store _disconnectTimerId and _retryTimerId references - Clear both timers in stop() and at the start of reconnect() - Add _reconnectGeneration counter so stale retry callbacks are no-ops Co-Authored-By: craig.johnston --- .../millicast-sdk/src/utils/BaseWebRTC.js | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/millicast-sdk/src/utils/BaseWebRTC.js b/packages/millicast-sdk/src/utils/BaseWebRTC.js index c892f919c..62e8f8951 100644 --- a/packages/millicast-sdk/src/utils/BaseWebRTC.js +++ b/packages/millicast-sdk/src/utils/BaseWebRTC.js @@ -51,6 +51,9 @@ export default class BaseWebRTC extends EventEmitter { this.isReconnecting = false this.tokenGenerator = tokenGenerator this.options = null + this._disconnectTimerId = null + this._retryTimerId = null + this._reconnectGeneration = 0 } /** @@ -66,6 +69,10 @@ export default class BaseWebRTC extends EventEmitter { */ stop () { logger.info('Stopping') + clearTimeout(this._disconnectTimerId) + clearTimeout(this._retryTimerId) + this._disconnectTimerId = null + this._retryTimerId = null this.webRTCPeer.closeRTCPeer() this.signaling?.close() this.signaling = null @@ -106,7 +113,7 @@ export default class BaseWebRTC extends EventEmitter { this.reconnect({ error: new Error('Connection state change: RTCPeerConnectionState disconnected') }) } else if (state === 'disconnected') { this.alreadyDisconnected = true - setTimeout(() => this.reconnect({ error: new Error('Connection state change: RTCPeerConnectionState disconnected') }), 1500) + this._disconnectTimerId = setTimeout(() => this.reconnect({ error: new Error('Connection state change: RTCPeerConnectionState disconnected') }), 1500) } else { this.alreadyDisconnected = false } @@ -121,6 +128,11 @@ export default class BaseWebRTC extends EventEmitter { * @property {String} error - The value sent in the first [reconnect event]{@link BaseWebRTC#event:reconnect} within the error key of the payload */ async reconnect (data) { + clearTimeout(this._disconnectTimerId) + clearTimeout(this._retryTimerId) + this._disconnectTimerId = null + this._retryTimerId = null + const generation = ++this._reconnectGeneration try { logger.info('Attempting to reconnect...') if (!this.isActive() && !this.stopReconnection && !this.isReconnecting) { @@ -146,7 +158,11 @@ export default class BaseWebRTC extends EventEmitter { this.isReconnecting = false this.reconnectionInterval = nextReconnectInterval(this.reconnectionInterval) logger.error(`Reconnection failed, retrying in ${this.reconnectionInterval}ms. `, error) - setTimeout(() => this.reconnect({ error }), this.reconnectionInterval) + this._retryTimerId = setTimeout(() => { + if (this._reconnectGeneration === generation) { + this.reconnect({ error }) + } + }, this.reconnectionInterval) } } } From 98d4fb4faa66cca7ba00329ecd1b8fec9b145ef7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 04:32:32 +0000 Subject: [PATCH 2/2] fix: only increment reconnect generation for non-no-op calls Avoid incrementing _reconnectGeneration when reconnect() is called while isReconnecting is true (nested no-op calls from initConnection). This prevents stale generation checks from permanently breaking the retry loop. Co-Authored-By: craig.johnston --- packages/millicast-sdk/src/utils/BaseWebRTC.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/millicast-sdk/src/utils/BaseWebRTC.js b/packages/millicast-sdk/src/utils/BaseWebRTC.js index 62e8f8951..0a2620067 100644 --- a/packages/millicast-sdk/src/utils/BaseWebRTC.js +++ b/packages/millicast-sdk/src/utils/BaseWebRTC.js @@ -132,7 +132,7 @@ export default class BaseWebRTC extends EventEmitter { clearTimeout(this._retryTimerId) this._disconnectTimerId = null this._retryTimerId = null - const generation = ++this._reconnectGeneration + const generation = this.isReconnecting ? this._reconnectGeneration : ++this._reconnectGeneration try { logger.info('Attempting to reconnect...') if (!this.isActive() && !this.stopReconnection && !this.isReconnecting) {