From 6b9800faec0d204d9f7bfd9ee396e4daee331c32 Mon Sep 17 00:00:00 2001 From: dormouse-bot <287024035+dormouse-bot@users.noreply.github.com> Date: Wed, 19 Aug 2026 07:11:30 +0000 Subject: [PATCH 1/2] refactor(remote): drop the vacuous intentional-close check in PocketClient The generation guard already rejects every close that is not the live socket's, so the null check below it was unconditionally true and its comment described a mechanism no longer in play. --- lib/src/remote/client/pocket-client.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/src/remote/client/pocket-client.ts b/lib/src/remote/client/pocket-client.ts index 3f4e3dea..7c27a779 100644 --- a/lib/src/remote/client/pocket-client.ts +++ b/lib/src/remote/client/pocket-client.ts @@ -662,15 +662,16 @@ export class PocketClient { } #onClose(ws: PocketSocket): void { + // Generation guard, and the whole test for "was this close intentional?": + // `close()` tears down and nulls #ws *before* calling `ws.close()`, and a + // reconnect overwrites #ws with the new socket, so neither an intentional + // close nor a superseded socket's late close gets past this line. Anything + // that does is the socket dying on us (server restart, network drop). if (this.#ws !== ws) return; - // `close()` tears down and nulls #ws before the event fires, so a non-null - // #ws here means the socket died on us (server restart, network drop) rather - // than an intentional close. An unexpected drop of an established session is - // still host loss — the app must leave the wall instead of idling on a dead - // stream — even without a `host-gone` frame. - const unexpected = this.#ws !== null; - const hadSession = this.#connectedHostId !== null; - this.#teardown('relay socket closed', { notifyGone: unexpected && hadSession }); + // An unexpected drop of an established session is still host loss — the app + // must leave the wall instead of idling on a dead stream — even without a + // `host-gone` frame. + this.#teardown('relay socket closed', { notifyGone: this.#connectedHostId !== null }); } /** From a300b396ecd0c0c84c25ec8857956d268ab89a13 Mon Sep 17 00:00:00 2001 From: dormouse-bot <287024035+dormouse-bot@users.noreply.github.com> Date: Wed, 19 Aug 2026 07:15:29 +0000 Subject: [PATCH 2/2] docs(remote): point close()'s ordering comment at the generation guard close() tore down before ws.close() because #onClose read `#ws === null` as an intentional close. That read is gone; the ordering now matters because nulling #ws is what makes the generation guard reject the close, which is the sole thing keeping an intentional close from firing host-gone. Describe the mechanism that is actually there. --- lib/src/remote/client/pocket-client.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/remote/client/pocket-client.ts b/lib/src/remote/client/pocket-client.ts index 7c27a779..3e3c7c02 100644 --- a/lib/src/remote/client/pocket-client.ts +++ b/lib/src/remote/client/pocket-client.ts @@ -572,9 +572,11 @@ export class PocketClient { close(): void { const ws = this.#ws; - // Tear down BEFORE closing the socket: #onClose reads `#ws === null` as an - // intentional close (no host-gone), and while real sockets emit their close - // event asynchronously, test fakes may emit it synchronously from close(). + // Tear down BEFORE closing the socket: nulling #ws is what makes #onClose's + // generation guard reject the close that follows, which is the only thing + // keeping an intentional close from firing `host-gone`. Real sockets emit + // that event asynchronously, but test fakes may emit it synchronously from + // close(), so the ordering has to hold rather than merely usually hold. this.#teardown('relay socket closed', { notifyGone: false }); try { ws?.close();