diff --git a/www/js/version.json b/www/js/version.json index 5151d6d..2aa5070 100644 --- a/www/js/version.json +++ b/www/js/version.json @@ -1,5 +1,5 @@ { - "VERSION": "v0.7.77", - "VERSION_NUMBER": "0.7.77", - "BUILD_DATE": "2026-06-30T14:21:55.493Z" + "VERSION": "v0.7.78", + "VERSION_NUMBER": "0.7.78", + "BUILD_DATE": "2026-06-30T14:33:14.964Z" } diff --git a/www/ndk-worker.js b/www/ndk-worker.js index d897e0a..045ed4e 100644 --- a/www/ndk-worker.js +++ b/www/ndk-worker.js @@ -6280,35 +6280,71 @@ async function handlePublish(requestId, event, port) { let relaySet = null; if (isBroadcast && totalTarget > 0 && NDKRelaySet?.fromRelayUrls) { - // Split all URLs into batches. - const batches = []; - for (let i = 0; i < allBroadcastUrls.length; i += BATCH_SIZE) { - batches.push(allBroadcastUrls.slice(i, i + BATCH_SIZE)); - } - console.log(`[Worker] Publishing in ${batches.length} batches of ${BATCH_SIZE} (per-relay timeout: ${PER_RELAY_TIMEOUT_MS}ms)`); - - for (let bi = 0; bi < batches.length; bi++) { - const batch = batches[bi]; - const batchSet = NDKRelaySet.fromRelayUrls(batch, ndk); - // Set connection timeout on each relay in this batch. - if (batchSet?.relays) { - for (const relay of batchSet.relays) { - try { - if (relay.connectionTimeout !== undefined) { - relay.connectionTimeout = PER_RELAY_TIMEOUT_MS; - } - } catch (_) { /* relay may be read-only */ } - } - } + // --- Phase 1: Publish to the user's standard outbox relays first --- + // This ensures the post lands on the user's primary relays quickly, + // and lets us send the response back to the page so the composer + // clears immediately. The broadcast relay batches run afterward. + if (outboxWriteUrls.length > 0) { + console.log(`[Worker] Phase 1: Publishing to ${outboxWriteUrls.length} outbox relays first`); try { - await ndkEvent.publish(batchSet, PER_RELAY_TIMEOUT_MS, 1); - } catch (batchErr) { - // NDKPublishError is expected when not all relays in the batch succeed. - // The live listeners already captured successes/failures. + const outboxSet = NDKRelaySet.fromRelayUrls(outboxWriteUrls, ndk); + await ndkEvent.publish(outboxSet, 10000, 1); + } catch (outboxErr) { + console.warn('[Worker] Outbox publish error (expected if some relays fail):', outboxErr?.message || outboxErr); + } + console.log(`[Worker] Phase 1 complete: ${publishedSoFar.size} outbox relays succeeded`); + } + + // --- Send response back to the page NOW so the composer clears --- + // The post is already on the user's outbox relays. The broadcast + // batches continue in the background. + const earlyRelayResults = { + successful: Array.from(publishedSoFar), + failed: [], + }; + port.postMessage({ + type: 'response', + requestId: requestId, + data: { + success: true, + relayResults: earlyRelayResults, + totalRelays: earlyRelayResults.successful.length, + } + }); + console.log('[Worker] Sent early publish response — composer can clear now'); + + // --- Phase 2: Broadcast to remaining relays in batches (background) --- + // Only publish to relays that aren't already in the outbox set + // (those were already published in Phase 1). + const broadcastOnlyUrls = activeBroadcastUrls.filter(u => !outboxWriteUrls.includes(u)); + if (broadcastOnlyUrls.length > 0) { + const batches = []; + for (let i = 0; i < broadcastOnlyUrls.length; i += BATCH_SIZE) { + batches.push(broadcastOnlyUrls.slice(i, i + BATCH_SIZE)); + } + console.log(`[Worker] Phase 2: Broadcasting to ${broadcastOnlyUrls.length} relays in ${batches.length} batches of ${BATCH_SIZE}`); + + for (let bi = 0; bi < batches.length; bi++) { + const batch = batches[bi]; + const batchSet = NDKRelaySet.fromRelayUrls(batch, ndk); + if (batchSet?.relays) { + for (const relay of batchSet.relays) { + try { + if (relay.connectionTimeout !== undefined) { + relay.connectionTimeout = PER_RELAY_TIMEOUT_MS; + } + } catch (_) { /* relay may be read-only */ } + } + } + try { + await ndkEvent.publish(batchSet, PER_RELAY_TIMEOUT_MS, 1); + } catch (batchErr) { + // Expected — some relays in this batch may fail. + } + console.log(`[Worker] Batch ${bi + 1}/${batches.length} complete — ` + + `${publishedSoFar.size}/${totalTarget} total succeeded, ` + + `${failedSoFar.size + timedOutSoFar.size} failed/timeout so far`); } - console.log(`[Worker] Batch ${bi + 1}/${batches.length} complete — ` + - `${publishedSoFar.size}/${totalTarget} succeeded, ` + - `${failedSoFar.size + timedOutSoFar.size} failed/timeout so far`); } // Final broadcast progress message with complete results. @@ -6325,14 +6361,30 @@ async function handlePublish(requestId, event, port) { latestRelay: null, relayUrls: Array.from(publishedSoFar), }); - // Clean up listeners so the NDKEvent can be garbage-collected. try { ndkEvent.removeAllListeners('relay:published'); ndkEvent.removeAllListeners('relay:publish:failed'); - } catch (_cleanupErr) { - // removeAllListeners may not exist on all EventEmitter impls; ignore. + } catch (_cleanupErr) { /* ignore */ } + + // --- Failure marking (background, after all batches) --- + const newSkips = Array.from(failedSoFar) + .filter(url => broadcastRelayUrls.has(url) && !skippedRelayUrls.has(url)); + if (newSkips.length > 0) { + for (const url of newSkips) { + skippedRelayUrls.set(url, { + reason: 'publish-rejected', + timestamp: Math.floor(Date.now() / 1000), + }); + } + scheduleSkipMarkSave(); + console.log(`[Worker] Skip-marked ${newSkips.length} rejected broadcast relays ` + + `(${timedOutSoFar.size} timeouts excluded)`); } - // relaySet stays null for broadcasts — we use publishedSoFar for results. + + // Return early — we already sent the response above. The rest of + // handlePublish (relayResults, kind 10002 sync, etc.) is skipped for + // broadcasts since we've handled everything here. + return; } else { // Normal (non-broadcast) publish — await the full result as before. try {