feat(core): improve reconnect handling (#15458)

#### PR Dependency Tree


* **PR #15458** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved realtime connection recovery with progressively longer retry
delays, capped to prevent excessive waiting.
* Connection retries now reset after successful reconnection and stop
cleanly when no longer needed.
* Reduced false error alerts by confirming repeated failures before
notifying users.
* Added cancellation handling when connection context changes or the
service is closed.

* **User Experience**
* Realtime connection alerts now identify authentication, network,
server, and timeout issues with localized messages.
* Active alerts are automatically dismissed when the connection
recovers.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-08-10 21:44:24 +08:00
committed by GitHub
parent 25de261c9e
commit 749c83cd8e
8 changed files with 142 additions and 24 deletions
@@ -117,9 +117,12 @@ test('retry when connect failed', async () => {
class TestConnection extends AutoReconnectConnection {
override retryDelay = 300;
connectCount = 0;
retryDelayFor(retryCount: number) {
return this.getRetryDelay(retryCount);
}
override async doConnect() {
this.connectCount++;
if (this.connectCount === 3) {
if (this.connectCount >= 3) {
return { hello: 'world' };
}
throw new Error('not connected, count: ' + this.connectCount);
@@ -127,9 +130,15 @@ test('retry when connect failed', async () => {
override doDisconnect() {
return Promise.resolve();
}
triggerError(error: Error) {
this.error = error;
}
}
const connection = new TestConnection();
expect([0, 1, 2, 8].map(count => connection.retryDelayFor(count))).toEqual([
300, 600, 1200, 60000,
]);
connection.connect();
await vitest.waitFor(() => {
@@ -149,6 +158,12 @@ test('retry when connect failed', async () => {
expect(connection.status).toBe('connected');
expect(connection.error).toBeUndefined();
});
connection.triggerError(new Error('disconnected'));
await vitest.waitFor(() => {
expect(connection.connectCount).toBe(4);
expect(connection.status).toBe('connected');
});
});
test('retry when error', async () => {
@@ -31,7 +31,9 @@ export abstract class AutoReconnectConnection<
private _status: ConnectionStatus = 'idle';
private _error: Error | undefined = undefined;
retryDelay = 3000;
maxRetryDelay = 60000;
connectingTimeout = 15000;
private retryCount = 0;
private refCount = 0;
private connectingAbort?: AbortController;
private reconnectingAbort?: AbortController;
@@ -103,6 +105,7 @@ export abstract class AutoReconnectConnection<
clearTimeout(timeout);
if (!signal.aborted) {
this._inner = value;
this.retryCount = 0;
this.setStatus('connected');
} else {
try {
@@ -150,16 +153,21 @@ export abstract class AutoReconnectConnection<
this.reconnectingAbort = new AbortController();
const signal = this.reconnectingAbort.signal;
const retryDelay = this.getRetryDelay(this.retryCount++);
const timeout = setTimeout(() => {
if (!signal.aborted) {
this.innerConnect();
}
}, this.retryDelay);
}, retryDelay);
signal.addEventListener('abort', () => {
clearTimeout(timeout);
});
}
protected getRetryDelay(retryCount: number) {
return Math.min(this.retryDelay * 2 ** retryCount, this.maxRetryDelay);
}
connect() {
this.refCount++;
if (this.refCount === 1) {
@@ -175,6 +183,7 @@ export abstract class AutoReconnectConnection<
}
if (this.refCount === 0) {
this.innerDisconnect();
this.retryCount = 0;
this.setStatus('closed');
}
}