From f683380392d80882029b32971dc37a5159bbb257 Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Wed, 9 Sep 2026 14:23:01 -0400 Subject: [PATCH 1/2] fix(signaling): only retry on close code 1001, tear down on everything else rpc-websockets' unlimited auto-reconnect only special-cases close code 1000 - every other code, including ones this SDK doesn't recognize yet, was silently retried forever. Flip the default: 1001 (Going Away) is the only code the gateway sends to mean "come back on this session"; everything else - 1000 (endpoint gone), 4409 (superseded by a newer connection from this same device, pv-gateway PR pv-gateway#123), 1011 (internal error, where the right recovery is minting a new endpoint, not retrying this connection), and any future/unknown code - now disables auto-reconnect and tears the connection down. The gateway-initiated codes (4409, 1011) race the "close" event: the underlying client schedules its reconnect timer synchronously inside the raw WebSocket's own close listener, before it defers emitting the "close" event this file listens on. setAutoReconnect(false) alone is too late for those; the already-queued reconnect_timer_id has to be cleared directly. Co-Authored-By: Claude Sonnet 5 --- src/v1/signaling.test.ts | 35 +++++++++++++++++++++++++++++++++-- src/v1/signaling.ts | 27 ++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/v1/signaling.test.ts b/src/v1/signaling.test.ts index a993a41..f9a317f 100644 --- a/src/v1/signaling.test.ts +++ b/src/v1/signaling.test.ts @@ -184,7 +184,7 @@ describe("Signaling websocket event handlers", () => { expect(ws.setAutoReconnect).not.toHaveBeenCalled(); }); - test("should clear ping interval and set isReady false on close", async () => { + test("should clear ping interval on close", async () => { // Trigger open first to set up pingInterval const openCallback = getWsCallback("open"); await openCallback(); @@ -192,7 +192,7 @@ describe("Signaling websocket event handlers", () => { const closeCallback = getWsCallback("close"); expect(closeCallback).toBeDefined(); - closeCallback(4000); + closeCallback(1001); expect((signaling as any).isReady).toBe(false); }); @@ -207,6 +207,37 @@ describe("Signaling websocket event handlers", () => { expect((signaling as any).ws).toBeNull(); expect((signaling as any).isReady).toBe(false); }); + + // 1001 is the only close code the gateway sends to say "come back on this + // same session" (drain eviction, lost media server, etc). Everything else + // must tear the connection down rather than let rpc-websockets' unlimited + // auto-reconnect keep hammering a connection that isn't coming back. + test("should not disconnect on the retryable close code 1001", async () => { + const closeCallback = getWsCallback("close"); + expect(closeCallback).toBeDefined(); + + closeCallback(1001); + + expect((signaling as any).ws).not.toBeNull(); + const ws = (signaling as any).ws; + expect(ws.setAutoReconnect).not.toHaveBeenCalled(); + }); + + test.each([ + [4409, "superseded by a newer connection from the same device"], + [1011, "gateway internal error"], + [4000, "an unrecognized close code"], + ])("should disable auto-reconnect and disconnect on close code %d (%s)", async (code) => { + const ws = (signaling as any).ws; + const closeCallback = getWsCallback("close"); + expect(closeCallback).toBeDefined(); + + closeCallback(code); + + expect(ws.setAutoReconnect).toHaveBeenCalledWith(false); + expect((signaling as any).ws).toBeNull(); + expect((signaling as any).isReady).toBe(false); + }); }); describe("Signaling disconnect", () => { diff --git a/src/v1/signaling.ts b/src/v1/signaling.ts index cd369bf..a069517 100644 --- a/src/v1/signaling.ts +++ b/src/v1/signaling.ts @@ -32,6 +32,21 @@ const FATAL_HANDSHAKE_ERRORS: Record Date: Wed, 9 Sep 2026 14:25:38 -0400 Subject: [PATCH 2/2] fix(signaling): trim comments to public SDK-facing language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop pv-gateway internal implementation detail (drain eviction, media server loss, etc.) from comments — this is a public SDK, comments should describe SDK behavior, not the internals of one server that happens to implement the protocol. Co-Authored-By: Claude Sonnet 5 --- src/v1/signaling.test.ts | 10 +--------- src/v1/signaling.ts | 26 ++++++-------------------- 2 files changed, 7 insertions(+), 29 deletions(-) diff --git a/src/v1/signaling.test.ts b/src/v1/signaling.test.ts index f9a317f..29de410 100644 --- a/src/v1/signaling.test.ts +++ b/src/v1/signaling.test.ts @@ -208,10 +208,6 @@ describe("Signaling websocket event handlers", () => { expect((signaling as any).isReady).toBe(false); }); - // 1001 is the only close code the gateway sends to say "come back on this - // same session" (drain eviction, lost media server, etc). Everything else - // must tear the connection down rather than let rpc-websockets' unlimited - // auto-reconnect keep hammering a connection that isn't coming back. test("should not disconnect on the retryable close code 1001", async () => { const closeCallback = getWsCallback("close"); expect(closeCallback).toBeDefined(); @@ -223,11 +219,7 @@ describe("Signaling websocket event handlers", () => { expect(ws.setAutoReconnect).not.toHaveBeenCalled(); }); - test.each([ - [4409, "superseded by a newer connection from the same device"], - [1011, "gateway internal error"], - [4000, "an unrecognized close code"], - ])("should disable auto-reconnect and disconnect on close code %d (%s)", async (code) => { + test.each([4409, 1011, 4000])("should disable auto-reconnect and disconnect on close code %d", async (code) => { const ws = (signaling as any).ws; const closeCallback = getWsCallback("close"); expect(closeCallback).toBeDefined(); diff --git a/src/v1/signaling.ts b/src/v1/signaling.ts index a069517..bb5d386 100644 --- a/src/v1/signaling.ts +++ b/src/v1/signaling.ts @@ -33,17 +33,9 @@ const FATAL_HANDSHAKE_ERRORS: Record