Files
bitvid/tests/nostr-publish-array-return.test.mjs
thePR0M3TH3ANandClaude Opus 4.8 6984fb2ef9 fix(publish): handle nostr-tools array-return; stop uncaught rejection flood
publishEventToRelay expected the old nostr-tools publish handle (.on/.then), but
nostr-tools 2.17 SimplePool.publish(relays, event) returns an ARRAY of per-relay
promises. The array has neither .on nor .then, so the code (a) fell through to
the "optimistic success" path (publishes were never truly confirmed) and (b)
never attached a .catch to the inner promise — so every relay that rejected
("blocked", "auth-required", "publish timed out", websocket error, …) leaked an
unhandled promise rejection. With a large/flaky relay list (e.g. a 35-relay
account hitting many paid/restricted relays) that floods the event loop and can
freeze the tab — observed when republishing the relay list and when publishing a
video.

Fix: detect the array return and Promise.allSettled it — any fulfilled = success,
otherwise a failure result; every rejection is consumed. Confirmed publishes now
resolve from real acks instead of the optimistic fallback.

New tests assert a rejecting array-promise becomes a failure result with NO
unhandled rejection leak, a fulfilling one succeeds, and an all-rejecting publish
still resolves. Mutation-verified; existing publish tests pass under the suite.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 08:27:21 -04:00

65 lines
2.5 KiB
JavaScript

// nostr-tools SimplePool.publish(relays, event) returns an ARRAY of per-relay
// promises. publishEventToRelay must consume them: a relay that rejects
// ("blocked", "auth-required", "publish timed out", websocket error, …) must be
// turned into a {success:false} result, NOT leak as an unhandled promise
// rejection. With a large/flaky relay list, leaked rejections flood the event
// loop and can freeze the tab (observed when republishing a 35-relay list).
import "./test-helpers/setup-localstorage.mjs";
import assert from "node:assert/strict";
import test from "node:test";
import { publishEventToRelay } from "../js/nostrPublish.js";
const EVENT = { id: "e1", kind: 1, content: "hi" };
// A pool whose publish() returns an array of promises, like nostr-tools 2.17.
function arrayReturningPool(makePromise) {
return {
publish(urls) {
return urls.map(() => makePromise());
},
};
}
test("a rejecting array-promise becomes a failure result (no unhandled rejection)", async () => {
const unhandled = [];
const onUnhandled = (reason) => unhandled.push(reason);
process.on("unhandledRejection", onUnhandled);
try {
const pool = arrayReturningPool(() =>
Promise.reject(new Error("blocked: pubkey is not allowed to publish")),
);
const result = await publishEventToRelay(pool, "wss://relay.example", EVENT);
assert.equal(result.success, false, "a rejecting publish is a failure result");
assert.equal(result.url, "wss://relay.example");
assert.ok(result.error, "the rejection reason is captured on the result");
// Let any leaked microtask rejections surface before asserting.
await new Promise((r) => setTimeout(r, 10));
assert.equal(
unhandled.length,
0,
`no unhandled rejections should leak (saw ${unhandled.length})`,
);
} finally {
process.off("unhandledRejection", onUnhandled);
}
});
test("a fulfilling array-promise becomes a success result", async () => {
const pool = arrayReturningPool(() => Promise.resolve("ok"));
const result = await publishEventToRelay(pool, "wss://relay.example", EVENT);
assert.equal(result.success, true);
assert.equal(result.url, "wss://relay.example");
});
test("all-rejecting publish still resolves (does not hang) with a failure", async () => {
const pool = arrayReturningPool(() => Promise.reject(new Error("auth-required")));
const result = await publishEventToRelay(pool, "wss://relay.example", EVENT, {
timeoutMs: 2000,
});
assert.equal(result.success, false);
});