fix(client): clear the scheduled retry when a connect attempt opens a transport

An explicit connect() during reconnect backoff opened a new transport but
left the scheduled retry armed. When it fired on the healthy connection it
disposed the transport and reconnected, dropping requests in flight. This
is why owned-subscriptions-old-daemon could still fail after #4858 added
the explicit reconnect. attemptConnect now owns the rule ensureConnected
already applied; the new unit test fails on the old client.
This commit is contained in:
Mohamed Boudra
2026-09-15 02:06:50 +02:00
parent 94706b2d31
commit f786504cb1
2 changed files with 49 additions and 4 deletions
+43
View File
@@ -989,6 +989,49 @@ test("ensureConnected reconnects immediately without leaving the scheduled retry
}
});
test("connect during a scheduled retry reconnects immediately without leaving the retry armed", async () => {
useHeartbeatClock();
try {
const first = createMockTransport();
const second = createMockTransport();
const third = createMockTransport();
const transports = [first, second, third];
let transportIndex = 0;
const client = new DaemonClient({
url: "ws://test",
clientId: "clsk_explicit_reconnect",
reconnect: { enabled: true, baseDelayMs: 1_500, maxDelayMs: 1_500 },
transportFactory: () => {
const transport = transports[transportIndex];
if (!transport) throw new Error("unexpected extra reconnect");
transportIndex += 1;
return transport.transport;
},
});
clients.push(client);
const initialConnect = client.connect();
first.triggerOpen();
await initialConnect;
first.triggerClose({ code: 1001, reason: "daemon restarted" });
expect(client.getConnectionState().status).toBe("disconnected");
const reconnect = client.connect();
expect(client.getConnectionState().status).toBe("connecting");
expect(transportIndex).toBe(2);
second.triggerOpen();
await reconnect;
expect(client.getConnectionState().status).toBe("connected");
await vi.advanceTimersByTimeAsync(1_500);
expect(client.getConnectionState().status).toBe("connected");
expect(transportIndex).toBe(2);
} finally {
vi.useRealTimers();
}
});
test("disabling reconnect cancels a pending retry until explicitly resumed", async () => {
useHeartbeatClock();
try {
+6 -4
View File
@@ -1255,6 +1255,12 @@ export class DaemonClient {
if (this.connectionState.status === "connecting") {
return;
}
// This attempt supersedes any retry the last disconnect scheduled. Left
// armed, that retry would tear down the connection this attempt opens.
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
}
const headers: Record<string, string> = {};
const password = normalizePassword(this.config.password);
@@ -1464,10 +1470,6 @@ export class DaemonClient {
) {
return;
}
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
}
if (this.connectPromise) {
this.attemptConnect();
return;