From b48984792f4dcb60ccaf222c2c3c481e52ec313b Mon Sep 17 00:00:00 2001 From: Shakker Date: Fri, 31 Jul 2026 09:06:16 +0100 Subject: [PATCH] fix: use real Buzz relay EOSE --- .../buzz/src/relay-subscription.test.ts | 32 +++++++++++++++++ extensions/buzz/src/relay-subscription.ts | 34 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 extensions/buzz/src/relay-subscription.test.ts create mode 100644 extensions/buzz/src/relay-subscription.ts diff --git a/extensions/buzz/src/relay-subscription.test.ts b/extensions/buzz/src/relay-subscription.test.ts new file mode 100644 index 000000000000..8cb6d4e1bf3e --- /dev/null +++ b/extensions/buzz/src/relay-subscription.test.ts @@ -0,0 +1,32 @@ +import type { Filter, Relay } from "nostr-tools"; +import { describe, expect, it, vi } from "vitest"; +import { openBuzzRelaySubscription } from "./relay-subscription.js"; + +describe("openBuzzRelaySubscription", () => { + it("sends an explicit REQ without synthesizing EOSE", async () => { + vi.useFakeTimers(); + const oneose = vi.fn(); + const subscription = { + id: "sub:1", + close: vi.fn(), + } as unknown as ReturnType; + const relay = { + idleSince: Date.now(), + ongoingOperations: 0, + prepareSubscription: vi.fn(() => subscription), + send: vi.fn(async () => {}), + } as unknown as Relay; + const filters: Filter[] = [{ kinds: [0], authors: ["a".repeat(64)] }]; + + const opened = openBuzzRelaySubscription(relay, filters, { oneose }); + await vi.advanceTimersByTimeAsync(5_000); + + expect(opened).toBe(subscription); + expect(relay.prepareSubscription).toHaveBeenCalledWith(filters, { oneose }); + expect(relay.send).toHaveBeenCalledWith(JSON.stringify(["REQ", "sub:1", ...filters])); + expect(relay.ongoingOperations).toBe(1); + expect(relay.idleSince).toBeUndefined(); + expect(oneose).not.toHaveBeenCalled(); + vi.useRealTimers(); + }); +}); diff --git a/extensions/buzz/src/relay-subscription.ts b/extensions/buzz/src/relay-subscription.ts new file mode 100644 index 000000000000..fa7a2e6d0353 --- /dev/null +++ b/extensions/buzz/src/relay-subscription.ts @@ -0,0 +1,34 @@ +import type { Filter, Relay } from "nostr-tools"; + +type BuzzRelaySubscriptionParams = Omit[1], "abort">; + +export function openBuzzRelaySubscription( + relay: Relay, + filters: Filter[], + params: BuzzRelaySubscriptionParams, +): ReturnType { + // Relay.subscribe() synthesizes EOSE after 4.4 seconds. Buzz needs the relay's + // real EOSE before replacing or closing subscriptions, otherwise an async REQ + // can register after CLOSE and remain orphaned on the server. + relay.idleSince = undefined; + relay.ongoingOperations += 1; + + let subscription: ReturnType; + try { + subscription = relay.prepareSubscription(filters, params); + } catch (error) { + relay.ongoingOperations -= 1; + if (relay.ongoingOperations === 0) { + relay.idleSince = Date.now(); + relay.scheduleIdleClose(); + } + throw error; + } + + const frame = JSON.stringify(["REQ", subscription.id, ...filters]); + void relay.send(frame).catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + subscription.close(`Buzz relay subscription request failed: ${message}`); + }); + return subscription; +}