Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/v1/signaling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,40 @@ describe("Signaling disconnect", () => {
expect((signaling as any).isReady).toBe(false);
});

// The client is built with `reconnect: true, max_reconnects: 0` and
// rpc-websockets only skips reconnecting on close code exactly 1000. Without
// this, a gateway-initiated 1001 close leaves an endless reconnect loop
// behind a client whose listeners have already been removed — an inert
// connection on the gateway that neither side ever reaps.
test("should disable auto-reconnect before closing on disconnect", async () => {
const signaling = new Signaling();
await signaling.connect({ endpointToken: "test-token" });

const ws = (signaling as any).ws;
const callOrder: string[] = [];
ws.setAutoReconnect.mockImplementation(() => callOrder.push("setAutoReconnect"));
ws.close.mockImplementation(() => callOrder.push("close"));

signaling.disconnect();

expect(ws.setAutoReconnect).toHaveBeenCalledWith(false);
expect(callOrder).toEqual(["setAutoReconnect", "close"]);
});

test("should still close when disabling auto-reconnect throws", async () => {
const signaling = new Signaling();
await signaling.connect({ endpointToken: "test-token" });

const ws = (signaling as any).ws;
ws.setAutoReconnect.mockImplementation(() => {
throw new Error("socket already gone");
});

expect(() => signaling.disconnect()).not.toThrow();
expect(ws.close).toHaveBeenCalled();
expect((signaling as any).ws).toBeNull();
});

test("should handle disconnect with diagnosticsBatcher", async () => {
const diagnosticsBatcher = new DiagnosticsBatcher();
const shutdownSpy = jest.spyOn(diagnosticsBatcher, "shutdown");
Expand Down
19 changes: 19 additions & 0 deletions src/v1/signaling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,25 @@ class Signaling extends EventEmitter {
logger.error("Error sending diagnostics... websocket may be disconnected", err);
}
}
// Stop auto-reconnect BEFORE closing. The client is constructed with
// `reconnect: true, max_reconnects: 0` (unlimited), and rpc-websockets
// only skips reconnecting when the close code is exactly 1000 — so any
// other code (notably 1001/StatusGoingAway, which the gateway sends
// whenever it wants the device to come back) puts the client into an
// endless reconnect loop. That loop outlives this disconnect: because
// removeAllListeners() has already run, the reconnected socket has no
// "open" handler, so it never calls setMediaPreferences, never creates
// peer connections, and never answers the heartbeat — an inert
// connection on the gateway that nothing on either side reaps.
//
// Reconnect stays enabled for the lifetime of a live connection (that is
// the point of it); it is only disabled here, where the caller has asked
// to disconnect and we are tearing this client down for good.
try {
this.ws.setAutoReconnect(false);
} catch (err) {
logger.error("Error disabling auto-reconnect", err);
}
this.ws.removeAllListeners();
try {
this.ws.close();
Expand Down