diff --git a/components/iframe_forms/iframe-application-form.html b/components/iframe_forms/iframe-application-form.html index 8e97e113..e44a132e 100644 --- a/components/iframe_forms/iframe-application-form.html +++ b/components/iframe_forms/iframe-application-form.html @@ -284,6 +284,7 @@ "wss://relay.snort.social", "wss://relay.damus.io", "wss://relay.primal.net", + "wss://nos.lol", ]; const pool = new SimplePool(); diff --git a/components/iframe_forms/iframe-bug-fix-form.html b/components/iframe_forms/iframe-bug-fix-form.html index b47dc5fd..3040a75d 100644 --- a/components/iframe_forms/iframe-bug-fix-form.html +++ b/components/iframe_forms/iframe-bug-fix-form.html @@ -266,6 +266,7 @@ "wss://relay.snort.social", "wss://relay.damus.io", "wss://relay.primal.net", + "wss://nos.lol", ]; const pool = new SimplePool(); diff --git a/components/iframe_forms/iframe-content-appeals-form.html b/components/iframe_forms/iframe-content-appeals-form.html index 96b23c89..12dafd62 100644 --- a/components/iframe_forms/iframe-content-appeals-form.html +++ b/components/iframe_forms/iframe-content-appeals-form.html @@ -211,6 +211,7 @@ "wss://relay.snort.social", "wss://relay.damus.io", "wss://relay.primal.net", + "wss://nos.lol", ]; const pool = new SimplePool(); diff --git a/components/iframe_forms/iframe-feedback-form.html b/components/iframe_forms/iframe-feedback-form.html index 0149284c..37933bbe 100644 --- a/components/iframe_forms/iframe-feedback-form.html +++ b/components/iframe_forms/iframe-feedback-form.html @@ -202,6 +202,7 @@ "wss://relay.snort.social", "wss://relay.damus.io", "wss://relay.primal.net", + "wss://nos.lol", ]; const pool = new SimplePool(); diff --git a/components/iframe_forms/iframe-request-form.html b/components/iframe_forms/iframe-request-form.html index 8302d4a8..4470ab20 100644 --- a/components/iframe_forms/iframe-request-form.html +++ b/components/iframe_forms/iframe-request-form.html @@ -226,6 +226,7 @@ "wss://relay.snort.social", "wss://relay.damus.io", "wss://relay.primal.net", + "wss://nos.lol", ]; const pool = new SimplePool(); diff --git a/js/submissions/submissionFacade.js b/js/submissions/submissionFacade.js index 4e8c448d..0589f680 100644 --- a/js/submissions/submissionFacade.js +++ b/js/submissions/submissionFacade.js @@ -14,6 +14,7 @@ import { SUBMISSION_KIND, parseSubmissionEvent, } from "./submissionService.js"; +import { publishEventToRelays } from "../nostrPublish.js"; import { devLogger } from "../utils/logger.js"; export const RESOLVED_LIST_KIND = 30000; @@ -33,6 +34,28 @@ function getManagerAndRelays(client) { return { manager, relays }; } +// Keep pending submissions alive. They're authored by throwaway ephemeral keys +// and public relays prune events from pubkeys with no social graph, so the queue +// quietly evaporates over time. A signed event can be re-published by anyone, so +// on each fetch we re-broadcast the still-pending events back to our relays to +// refresh them. Best-effort and fire-and-forget — never blocks or fails a fetch. +function mirrorPendingSubmissions(rawEvents, client) { + try { + const pool = client?.pool; + const relays = Array.isArray(client?.relays) ? client.relays : []; + if (!pool || !relays.length || !Array.isArray(rawEvents) || !rawEvents.length) { + return; + } + for (const event of rawEvents) { + Promise.resolve() + .then(() => publishEventToRelays(pool, relays, event)) + .catch(() => {}); + } + } catch (error) { + // best effort — mirroring must never break the submissions view + } +} + function newestByCreatedAt(events) { let newest = null; for (const event of Array.isArray(events) ? events : []) { @@ -93,7 +116,16 @@ export async function fetchPendingSubmissions({ byApplicant.set(key, submission); } } - return [...byApplicant.values()]; + const pending = [...byApplicant.values()]; + + // Refresh the still-pending events on our relays so they don't get pruned. + const pendingIds = new Set(pending.map((submission) => submission.eventId)); + const rawPending = (Array.isArray(subEvents) ? subEvents : []).filter( + (event) => event && pendingIds.has(event.id), + ); + mirrorPendingSubmissions(rawPending, client); + + return pending; } /** diff --git a/tests/submission-facade.test.mjs b/tests/submission-facade.test.mjs index d0fd9e39..7fd3b124 100644 --- a/tests/submission-facade.test.mjs +++ b/tests/submission-facade.test.mjs @@ -94,6 +94,19 @@ test("fetchPendingSubmissions returns pending, excludes resolved, dedupes by app assert.equal(alice.id, "s2", "newest application for alice wins"); }); +test("fetchPendingSubmissions still returns pending when a pool is present (mirror is best-effort)", async () => { + const events = [ + submissionEvent({ id: "m1", applicant: "npub-x", created_at: 100 }), + ]; + const client = mockClient({ events }); + // Presence of a pool triggers the re-broadcast mirror path; any publish + // failure is swallowed and must not affect the returned queue. + client.pool = {}; + const pending = await fetchPendingSubmissions({ adminHex: ADMIN, client }); + assert.equal(pending.length, 1); + assert.equal(pending[0].applicant, "npub-x"); +}); + test("fetchPendingSubmissions returns [] for a blank admin or no relays", async () => { assert.deepEqual(await fetchPendingSubmissions({ adminHex: "", client: mockClient({}) }), []); const noRelays = { ...mockClient({}), relays: [] };