From 2c7105ec08ed74a952d470ebee2697ccd6584faf Mon Sep 17 00:00:00 2001 From: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Date: Tue, 28 Jul 2026 10:17:25 -0400 Subject: [PATCH] fix(desktop): preserve early relay auth challenges Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> --- desktop/src/shared/api/relayClientSession.ts | 9 +++++++- desktop/src/testing/e2eBridge.ts | 11 +++++++-- desktop/tests/e2e/relay-reconnect.spec.ts | 24 ++++++++++++++++++++ desktop/tests/helpers/bridge.ts | 2 ++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/desktop/src/shared/api/relayClientSession.ts b/desktop/src/shared/api/relayClientSession.ts index 84ee10b68d..7c1915b69d 100644 --- a/desktop/src/shared/api/relayClientSession.ts +++ b/desktop/src/shared/api/relayClientSession.ts @@ -536,7 +536,9 @@ export class RelayClient { ); const generation = ++this.connectionGeneration; + let pendingInbound: unknown[] | null = []; this.onMessageChannel = new Channel((message) => { + if (pendingInbound) return void pendingInbound.push(message); void this.handleWsMessage(message, generation).catch((error) => { if (generation !== this.connectionGeneration) return; this.resetConnection( @@ -560,7 +562,7 @@ export class RelayClient { } this.wsId = wsId; - await new Promise((resolve, reject) => { + const authentication = new Promise((resolve, reject) => { const timeout = window.setTimeout(() => { const error = new Error("Relay authentication timed out."); this.authRequest = null; @@ -576,6 +578,11 @@ export class RelayClient { }; }); + while (pendingInbound.length > 0) { + await this.handleWsMessage(pendingInbound.shift(), generation); + } + pendingInbound = null; + await authentication; this.stabilityTimer = window.setTimeout(() => { this.stabilityTimer = null; this.reconnectDelayMs = RECONNECT_BASE_DELAY_MS; diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 03c05fe877..e9008e4e75 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -286,6 +286,8 @@ type E2eConfig = { nostrBindSignDelayMs?: number; /** Reject successive mock WebSocket connect attempts, then resume. */ websocketConnectErrors?: string[]; + /** Deliver AUTH synchronously, before the mock connect command resolves. */ + websocketAuthBeforeConnectResolves?: boolean; stallWebsocketSends?: boolean; userSearchDelayMs?: number; // NIP-IA gate inputs — see tests/helpers/bridge.ts:MockBridgeOptions for @@ -8758,9 +8760,14 @@ async function connectMockSocket(args: { onMessage: unknown }) { subscriptions: new Map(), }); - window.setTimeout(() => { + if (getConfig()?.mock?.websocketAuthBeforeConnectResolves) { sendWsText(handler, ["AUTH", `mock-challenge-${wsId}`]); - }, 0); + await new Promise((resolve) => window.setTimeout(resolve, 50)); + } else { + window.setTimeout(() => { + sendWsText(handler, ["AUTH", `mock-challenge-${wsId}`]); + }, 0); + } return wsId; } diff --git a/desktop/tests/e2e/relay-reconnect.spec.ts b/desktop/tests/e2e/relay-reconnect.spec.ts index 6240cca0ba..5b30e38be6 100644 --- a/desktop/tests/e2e/relay-reconnect.spec.ts +++ b/desktop/tests/e2e/relay-reconnect.spec.ts @@ -94,6 +94,30 @@ test.beforeEach(async ({ page }) => { await installMockBridge(page); }); +test("AUTH arriving before connect resolves does not lose the first send", async ({ + page, +}) => { + await installMockBridge(page, { + websocketAuthBeforeConnectResolves: true, + }); + await page.goto("/"); + await expect + .poll( + () => + page.evaluate(() => window.__BUZZ_E2E_GET_RELAY_CONNECTION_STATE__?.()), + { timeout: 5_000 }, + ) + .toBe("connected"); + await page.getByTestId("channel-general").click(); + await expect(page.getByTestId("chat-title")).toHaveText("general"); + + const message = `first send after early auth ${Date.now()}`; + await page.getByTestId("message-input").fill(message); + await page.getByTestId("send-message").click(); + + await expect(page.getByTestId("message-timeline")).toContainText(message); +}); + test("failed initial relay dial retries automatically", async ({ page }) => { await installMockBridge(page, { websocketConnectErrors: ["mock relay pod unavailable"], diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index d65a260f30..9e78fe6741 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -276,6 +276,8 @@ type MockBridgeOptions = { nostrBindSignDelayMs?: number; /** Reject successive mock WebSocket connect attempts, then resume. */ websocketConnectErrors?: string[]; + /** Deliver AUTH synchronously, before the mock connect command resolves. */ + websocketAuthBeforeConnectResolves?: boolean; stallWebsocketSends?: boolean; userSearchDelayMs?: number; /**