mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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]>
This commit is contained in:
1 parent
0031d68b67
commit
6984fb2ef9
2 files changed
+92
No files matched your search
@@ -131,6 +131,34 @@ export function publishEventToRelay(pool, url, event, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// nostr-tools SimplePool.publish(relays, event) returns an ARRAY of
|
||||
// per-relay promises (one per url). Each MUST be consumed or a relay that
|
||||
// rejects ("blocked", "auth-required", "publish timed out", websocket
|
||||
// error, …) surfaces as an uncaught promise rejection. With a large/flaky
|
||||
// relay list that floods the event loop and can freeze the tab. We pass a
|
||||
// single [url], so the array has one promise; allSettled never rejects, so
|
||||
// every rejection is consumed here.
|
||||
if (Array.isArray(pub)) {
|
||||
Promise.allSettled(pub).then((settled) => {
|
||||
clearTimeout(timeoutId);
|
||||
const anyOk = settled.some((entry) => entry.status === "fulfilled");
|
||||
if (anyOk) {
|
||||
finalize(true);
|
||||
return;
|
||||
}
|
||||
const firstRejection = settled.find(
|
||||
(entry) => entry.status === "rejected",
|
||||
);
|
||||
finalize(
|
||||
false,
|
||||
firstRejection?.reason instanceof Error
|
||||
? firstRejection.reason
|
||||
: new Error(String(firstRejection?.reason || "publish failed")),
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pub && typeof pub.then === "function") {
|
||||
pub
|
||||
.then(() => {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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);
|
||||
});
|
||||
Reference in new issue
Block a user