From f9fffe241bd506308ec00ce73fccbfe183865acd Mon Sep 17 00:00:00 2001 From: wangmiao0668000666 Date: Thu, 2 Jul 2026 09:10:38 +0800 Subject: [PATCH] fix(nostr): clear per-relay publish timeout timer to prevent dangling handles (#98720) (cherry picked from commit a7a444e7ef1e75218d3a14f39ac8018b40ff4750) --- extensions/nostr/src/nostr-profile.test.ts | 74 +++++++++++++++++++++- extensions/nostr/src/nostr-profile.ts | 7 +- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/extensions/nostr/src/nostr-profile.test.ts b/extensions/nostr/src/nostr-profile.test.ts index 8d3e1620c7a5..82b6e952081f 100644 --- a/extensions/nostr/src/nostr-profile.test.ts +++ b/extensions/nostr/src/nostr-profile.test.ts @@ -1,5 +1,5 @@ // Nostr tests cover nostr profile plugin behavior. -import { verifyEvent, getPublicKey } from "nostr-tools"; +import { verifyEvent, getPublicKey, type SimplePool } from "nostr-tools"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { NostrProfile } from "./config-schema.js"; import { @@ -8,6 +8,7 @@ import { contentToProfile, validateProfile, sanitizeProfileForDisplay, + publishProfile, type ProfileContent, } from "./nostr-profile.js"; import { TEST_HEX_PRIVATE_KEY_BYTES } from "./test-fixtures.js"; @@ -414,3 +415,74 @@ describe("edge cases", () => { expect(verifyEvent(event)).toBe(true); }); }); + +// ============================================================================ +// Profile Publishing Tests +// ============================================================================ + +describe("publishProfile", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + function createFakePool(publishResult: unknown): SimplePool { + return { + publish: vi.fn(() => [publishResult]), + } as unknown as SimplePool; + } + + it("clears the per-relay timeout timer after a successful publish", async () => { + const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout"); + const profile: NostrProfile = { name: "test" }; + const pool = createFakePool(Promise.resolve()); + + const result = await publishProfile( + pool, + TEST_HEX_PRIVATE_KEY_BYTES, + ["wss://relay.example"], + profile, + ); + + expect(result.successes).toEqual(["wss://relay.example"]); + expect(clearTimeoutSpy).toHaveBeenCalledTimes(1); + }); + + it("clears the per-relay timeout timer after a publish timeout", async () => { + const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout"); + const profile: NostrProfile = { name: "test" }; + const pool = createFakePool(new Promise(() => {})); + + const promise = publishProfile( + pool, + TEST_HEX_PRIVATE_KEY_BYTES, + ["wss://relay.example"], + profile, + ); + vi.advanceTimersByTime(6_000); + const result = await promise; + + expect(result.failures).toHaveLength(1); + expect(result.failures[0]?.error).toContain("timeout"); + expect(clearTimeoutSpy).toHaveBeenCalledTimes(1); + }); + + it("does not add dangling timers when publishing to multiple relays", async () => { + vi.spyOn(globalThis, "setTimeout").mockClear(); + const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout"); + const profile: NostrProfile = { name: "test" }; + const pool = createFakePool(Promise.resolve()); + + await publishProfile( + pool, + TEST_HEX_PRIVATE_KEY_BYTES, + ["wss://relay.a", "wss://relay.b"], + profile, + ); + + expect(clearTimeoutSpy).toHaveBeenCalledTimes(2); + }); +}); diff --git a/extensions/nostr/src/nostr-profile.ts b/extensions/nostr/src/nostr-profile.ts index e0f897db419b..fff1dab0f63b 100644 --- a/extensions/nostr/src/nostr-profile.ts +++ b/extensions/nostr/src/nostr-profile.ts @@ -98,9 +98,10 @@ async function publishProfileEvent( // Publish to each relay in parallel with timeout const publishPromises = relays.map(async (relay) => { + let timer: ReturnType | undefined; try { const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error("timeout")), RELAY_PUBLISH_TIMEOUT_MS); + timer = setTimeout(() => reject(new Error("timeout")), RELAY_PUBLISH_TIMEOUT_MS); }); await Promise.race([...pool.publish([relay], event), timeoutPromise]); @@ -109,6 +110,10 @@ async function publishProfileEvent( } catch (err) { const errorMessage = formatErrorMessage(err); failures.push({ relay, error: errorMessage }); + } finally { + if (timer) { + clearTimeout(timer); + } } });