feat(data-connect): Align SQL Connect Stream Transport Exponential Backoff Parameters with Other Platform SDKs (#10221)

* udpate exponential backoff params to match other SDKs

* add changeset

* update changeset

* Apply reviewer suggestion

* yarn run format

---------

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
Stephen Rosa
2026-07-28 13:48:25 -07:00
committed by GitHub
co-authored by Hanzo Dev
parent 18d46d06dd
commit 6628ce177d
3 changed files with 68 additions and 15 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@firebase/data-connect': patch
---
Align SQL Connect Stream Transport exponential backoff parameters with other platform SDKs. No need for public release note.
@@ -54,13 +54,9 @@ const IDLE_CONNECTION_TIMEOUT_MS = 15 * 1000; // 15 seconds
/** Initial reconnect delay in ms */
const INITIAL_RECONNECT_DELAY_MS = 1000; // 1 second
/** Max reconnect delay in ms */
const MAX_RECONNECT_DELAY_MS = 30 * 1000; // 30 seconds
/** Max random jitter to add to reconnect delay in ms */
const MAX_RECONNECT_JITTER_MS = 0.5 * 1000; // 0.5 seconds
const MAX_RECONNECT_DELAY_MS = 60 * 1000; // 60 seconds
/** Factor to multiply delay by on failure */
const RECONNECT_BACKOFF_FACTOR = 1.3;
/** Max number of reconnection attempts before giving up */
const MAX_RECONNECT_ATTEMPTS = 10;
const RECONNECT_BACKOFF_FACTOR = 1.5;
/**
* A promise returned to the user when invoking an operation via {@linkcode AbstractDataConnectStreamTransport.invokeQuery | invokeQuery}
@@ -445,19 +441,13 @@ export abstract class AbstractDataConnectStreamTransport extends AbstractDataCon
if (this.reconnectTimer) {
return;
}
if (this.reconnectAttempts++ >= MAX_RECONNECT_ATTEMPTS) {
const errorString =
'Stream disconnected and could not reconnect - max stream reconnection attempts reached.';
logError(errorString);
void this.cleanupAndTerminate(Code.OTHER, errorString);
return;
}
this.reconnectAttempts++;
const delay = this.reconnectDelayMs;
this.reconnectDelayMs = Math.min(
this.reconnectDelayMs * RECONNECT_BACKOFF_FACTOR,
MAX_RECONNECT_DELAY_MS
);
const jitter = Math.random() * MAX_RECONNECT_JITTER_MS;
const jitter = (Math.random() - 0.5) * delay;
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
@@ -22,7 +22,7 @@ import * as sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { DataConnectOptions } from '../../src/api/DataConnect';
import { Code } from '../../src/core/error';
import { Code, DataConnectError } from '../../src/core/error';
import { AuthTokenProvider } from '../../src/core/FirebaseAuthProvider';
import * as logger from '../../src/logger';
import {
@@ -1993,6 +1993,64 @@ describe('AbstractDataConnectStreamTransport', () => {
delete (global as any).document;
}
});
it('should exponentially back off with 1.5 multiplier and [-0.5x, +0.5x] jitter capped at 60s', async () => {
ensureConnectionStub.rejects(
new DataConnectError(Code.OTHER, 'Connection failed')
);
const randomStub = sinon.stub(Math, 'random');
const observer = {
onData: sinon.spy(),
onDisconnect: sinon.spy(),
onError: sinon.spy()
};
transport.invokeSubscribe(observer, queryName1, variables1);
// Attempt 1: base delay 1000ms, random=0 -> jitter -500ms -> delay 500ms
randomStub.returns(0);
transport.onStreamClose(1006, 'Abnormal Closure');
await clock.tickAsync(499);
expect(ensureConnectionStub).to.not.have.been.called;
// Set random=1 before attempt 1 failure triggers startReconnectBackoff for attempt 2
// Attempt 2: base delay 1500ms, random=1 -> jitter +750ms -> delay 2250ms
randomStub.returns(1);
await clock.tickAsync(1);
expect(ensureConnectionStub).to.have.been.calledOnce;
await clock.tickAsync(2249);
expect(ensureConnectionStub).to.have.been.calledOnce;
// Set random=0.5 before attempt 2 failure triggers startReconnectBackoff for attempt 3
// Attempt 3: base delay 2250ms, random=0.5 -> jitter 0ms -> delay 2250ms
randomStub.returns(0.5);
await clock.tickAsync(1);
expect(ensureConnectionStub).to.have.been.calledTwice;
await clock.tickAsync(2250);
expect(ensureConnectionStub).to.have.been.calledThrice;
});
it('should attempt reconnection indefinitely without a max attempt limit', async () => {
ensureConnectionStub.rejects(
new DataConnectError(Code.OTHER, 'Connection failed')
);
const observer = {
onData: sinon.spy(),
onDisconnect: sinon.spy(),
onError: sinon.spy()
};
transport.invokeSubscribe(observer, queryName1, variables1);
transport.onStreamClose(1006, 'Abnormal Closure');
// Fast-forward past 15 reconnection attempts
for (let i = 0; i < 15; i++) {
await clock.tickAsync(65000);
}
expect(ensureConnectionStub.callCount).to.be.at.least(15);
});
});
});