From 7703dd13aea3abc5c1dbfc0a54b7852253e7e88d Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 20 Oct 2025 13:05:36 -0400 Subject: [PATCH] Optimize admin list publish acceptance --- js/adminListStore.js | 131 ++++++++++++++++++++------------ tests/admin-list-store.test.mjs | 82 ++++++++++++++++++-- 2 files changed, 158 insertions(+), 55 deletions(-) diff --git a/js/adminListStore.js b/js/adminListStore.js index febf4120..d8a346a2 100644 --- a/js/adminListStore.js +++ b/js/adminListStore.js @@ -558,53 +558,90 @@ function publishListWithFirstAcceptance(pool, relays, event, options = {}) { : []; const resultsBuffer = new Array(relayPromises.length); - let resolved = false; - let remaining = relayPromises.length; + const resolveMessage = listKey + ? `Failed to publish ${listKey} list to any relay.` + : "Failed to publish admin list to any relay."; - const firstAcceptance = new Promise((resolve, reject) => { - if (!relayPromises.length) { - reject( - createError( - "publish-failed", - listKey - ? `Failed to publish ${listKey} list to any relay.` - : "Failed to publish admin list to any relay.", - ) - ); - return; + const normalizeFailureError = (value) => { + if (value instanceof Error) { + return value; } - relayPromises.forEach((promise, index) => { - promise - .then((result) => { - resultsBuffer[index] = result; - if (result?.success && !resolved) { - resolved = true; - resolve(result); - } - }) - .catch((error) => { - resultsBuffer[index] = { - url: Array.isArray(relays) ? relays[index] : "", - success: false, - error, - }; - }) - .finally(() => { - remaining -= 1; - if (!resolved && remaining === 0) { - reject( - createError( - "publish-failed", - listKey - ? `Failed to publish ${listKey} list to any relay.` - : "Failed to publish admin list to any relay.", - ) - ); - } - }); + if (value && typeof value === "object") { + if (value.error instanceof Error) { + return value.error; + } + + if (typeof value.error === "string" && value.error.trim()) { + return new Error(value.error.trim()); + } + } + + if (typeof value === "string" && value.trim()) { + return new Error(value.trim()); + } + + if (value && typeof value === "object" && typeof value.reason === "string") { + const trimmed = value.reason.trim(); + if (trimmed) { + return new Error(trimmed); + } + } + + return new Error("publish failed"); + }; + + const normalizeResult = (result, fallbackUrl = "") => { + if (result && typeof result === "object") { + const url = typeof result.url === "string" ? result.url : fallbackUrl; + const success = !!result.success; + const error = success ? null : normalizeFailureError(result); + return { url, success, error }; + } + + return { + url: fallbackUrl, + success: false, + error: normalizeFailureError(result), + }; + }; + + const acceptancePromises = relayPromises.map((promise, index) => + promise + .then((result) => { + const normalized = normalizeResult( + result, + Array.isArray(relays) ? relays[index] : "", + ); + resultsBuffer[index] = normalized; + if (normalized.success) { + return normalized; + } + + throw normalized; + }) + .catch((reason) => { + const normalized = normalizeResult( + reason, + Array.isArray(relays) ? relays[index] : "", + ); + if (typeof resultsBuffer[index] === "undefined") { + resultsBuffer[index] = normalized; + } + throw normalized; + }) + ); + + const firstAcceptance = (() => { + if (!relayPromises.length) { + return Promise.reject(createError("publish-failed", resolveMessage)); + } + + return Promise.any(acceptancePromises).catch((aggregateError) => { + const error = createError("publish-failed", resolveMessage, aggregateError); + throw error; }); - }); + })(); const allResults = Promise.allSettled(relayPromises).then((entries) => { for (let index = 0; index < entries.length; index += 1) { @@ -612,12 +649,8 @@ function publishListWithFirstAcceptance(pool, relays, event, options = {}) { const fallbackUrl = Array.isArray(relays) ? relays[index] : ""; const result = entry?.status === "fulfilled" - ? entry.value - : { - url: fallbackUrl, - success: false, - error: entry?.reason || new Error("publish failed"), - }; + ? normalizeResult(entry.value, fallbackUrl) + : normalizeResult(entry?.reason, fallbackUrl); if (typeof resultsBuffer[index] === "undefined") { resultsBuffer[index] = result; diff --git a/tests/admin-list-store.test.mjs b/tests/admin-list-store.test.mjs index 7e043ba0..dc42c515 100644 --- a/tests/admin-list-store.test.mjs +++ b/tests/admin-list-store.test.mjs @@ -132,6 +132,7 @@ const relays = [ "wss://relay2.example.com", ]; nostrClient.relays = [...relays]; +nostrClient.writeRelays = [...relays]; let publishBehaviors = []; let publishCallIndex = 0; @@ -155,14 +156,30 @@ nostrClient.pool = { } return { on(eventName, handler) { + const schedule = (fn) => { + const delay = + typeof behavior.delayMs === "number" && behavior.delayMs >= 0 + ? behavior.delayMs + : 0; + if (delay > 0) { + setTimeout(fn, delay); + } else { + queueMicrotask(fn); + } + }; + if (eventName === "ok" && behavior.success) { - handler(); + schedule(() => { + handler(); + }); return true; } if (eventName === "failed" && !behavior.success) { - handler( - behavior.error || new Error(`relay failure for ${url}`), - ); + schedule(() => { + handler( + behavior.error || new Error(`relay failure for ${url}`), + ); + }); return true; } return false; @@ -226,6 +243,59 @@ assert.equal( "should attempt to publish removals to every relay", ); +nostrClient.writeRelays = [relays[1]]; + +const writerelayBehaviors = [ + { url: relays[1], success: true }, +]; +setPublishBehaviors(writerelayBehaviors); + +await assert.doesNotReject( + () => + persistAdminState(ADMIN_SUPER_NPUB, { + whitelist: ["npub1writerelaymember"], + }), + "should publish whitelist updates to configured write relays", +); + +assert.equal( + publishCallIndex, + writerelayBehaviors.length, + "should target only write relays when available", +); + +nostrClient.writeRelays = []; + +const slowRelayDelayMs = 75; +const fastAcceptanceBehaviors = [ + { url: relays[0], success: true }, + { + url: relays[1], + success: false, + error: new Error("relay2 slow failure"), + delayMs: slowRelayDelayMs, + }, +]; +setPublishBehaviors(fastAcceptanceBehaviors); + +const startTime = Date.now(); + +await assert.doesNotReject( + () => + persistAdminState(ADMIN_SUPER_NPUB, { + whitelist: ["npub1fastacceptance"], + }), + "should resolve whitelist updates after the first relay acceptance", +); + +const durationMs = Date.now() - startTime; +assert.ok( + durationMs < slowRelayDelayMs, + `should not wait for slower relay failures (duration ${durationMs}ms >= ${slowRelayDelayMs}ms)`, +); + +await new Promise((resolve) => setTimeout(resolve, slowRelayDelayMs + 20)); + const relayFailureWarnings = capturedWarnings.filter((entry) => { const [, message] = entry; return ( @@ -247,8 +317,8 @@ const blacklistFailureCount = failureMessages.filter((message) => assert.equal( relayFailureWarnings.length, - 4, - "should warn once per relay failure across additions and removals", + 5, + "should warn for each relay failure across scenarios", ); assert.ok( whitelistFailureCount >= 2,