From 777ab312a501e7ea634da45e469c5a8eea69136a Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Tue, 30 Jun 2026 10:02:00 -0400 Subject: [PATCH] Fix broadcast timeout: increase connection timeout to 15s for temporary relays, race publish against 45s overall deadline so one slow relay doesn't block --- www/js/version.json | 6 ++--- www/ndk-worker.js | 66 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/www/js/version.json b/www/js/version.json index 712b981..fa859c7 100644 --- a/www/js/version.json +++ b/www/js/version.json @@ -1,5 +1,5 @@ { - "VERSION": "v0.7.72", - "VERSION_NUMBER": "0.7.72", - "BUILD_DATE": "2026-06-30T13:37:36.886Z" + "VERSION": "v0.7.73", + "VERSION_NUMBER": "0.7.73", + "BUILD_DATE": "2026-06-30T14:02:00.201Z" } diff --git a/www/ndk-worker.js b/www/ndk-worker.js index 39d52c0..57b5200 100644 --- a/www/ndk-worker.js +++ b/www/ndk-worker.js @@ -6204,6 +6204,21 @@ async function handlePublish(requestId, event, port) { const allUrls = [...new Set([...outboxWriteUrls, ...activeBroadcastUrls])]; if (NDKRelaySet?.fromRelayUrls) { targetRelaySet = NDKRelaySet.fromRelayUrls(allUrls, ndk); + // Increase the connection timeout on all relays in the set. + // NDK's default connectionTimeout is 4400ms, which is too short + // for hundreds of temporary broadcast relays that need to + // establish WebSocket connections. Give them 15s to connect. + // The publish timeout (30s below) covers the full connect+publish + // cycle per relay. + if (targetRelaySet?.relays) { + for (const relay of targetRelaySet.relays) { + try { + if (relay.connectionTimeout !== undefined) { + relay.connectionTimeout = 15000; + } + } catch (_) { /* relay may be read-only */ } + } + } console.log(`[Worker] Broadcasting to ${allUrls.length} relays ` + `(${activeBroadcastUrls.length} broadcast + ${outboxWriteUrls.length} outbox, ` + `${skippedRelayUrls.size} skipped)`); @@ -6283,23 +6298,48 @@ async function handlePublish(requestId, event, port) { // Publish to relays (with broadcast relay set if broadcasting, else default outbox). // When broadcasting to many relays (potentially hundreds), use a much longer - // timeout than NDK's default 4400ms — temporary relays need time to establish - // WebSocket connections. Use 30s for broadcasts, default for normal publishes. + // per-relay timeout than NDK's default 4400ms — temporary relays need time to + // establish WebSocket connections. Use 30s per relay for broadcasts. // requiredRelayCount=1 so NDK doesn't throw if only 1 of 600 relays succeeds. - const BROADCAST_TIMEOUT_MS = 30000; - const publishTimeoutMs = isBroadcast ? BROADCAST_TIMEOUT_MS : undefined; + const BROADCAST_PER_RELAY_TIMEOUT_MS = 30000; + const publishTimeoutMs = isBroadcast ? BROADCAST_PER_RELAY_TIMEOUT_MS : undefined; const publishRequiredCount = isBroadcast ? 1 : undefined; let relaySet; - try { - relaySet = await ndkEvent.publish(targetRelaySet, publishTimeoutMs, publishRequiredCount); - } catch (publishError) { - // NDK throws NDKPublishError if requiredRelayCount isn't met. With - // requiredRelayCount=1 this shouldn't happen for broadcasts, but handle - // it gracefully — the live progress listeners already captured whatever - // succeeded. Log and continue so we still report results to the page. - console.warn('[Worker] publish() threw (some relays may still have succeeded):', publishError?.message || publishError); - relaySet = null; + if (isBroadcast) { + // For broadcasts, don't await the full Promise.all — NDK's publish() + // waits for ALL relays to resolve, so one slow relay blocks the entire + // operation. Instead, race the publish against an overall deadline. + // The live progress listeners (relay:published / relay:publish:failed) + // already track per-relay results in real time, so we can resolve early + // once the deadline hits. Relays that haven't responded by the deadline + // are simply not counted — they may still complete in the background. + const BROADCAST_OVERALL_DEADLINE_MS = 45000; // 45s overall cap + const publishPromise = ndkEvent.publish(targetRelaySet, publishTimeoutMs, publishRequiredCount) + .catch((err) => { + // NDKPublishError is expected when not all relays succeed — + // the live listeners already captured the successes. + console.warn('[Worker] Broadcast publish() resolved with error (expected):', err?.message || err); + return null; + }); + const deadlinePromise = new Promise((resolve) => { + setTimeout(() => resolve('__DEADLINE__'), BROADCAST_OVERALL_DEADLINE_MS); + }); + const result = await Promise.race([publishPromise, deadlinePromise]); + if (result === '__DEADLINE__') { + console.log(`[Worker] Broadcast overall deadline (${BROADCAST_OVERALL_DEADLINE_MS}ms) reached — ` + + `${publishedSoFar.size} succeeded, ${failedSoFar.size + timedOutSoFar.size} failed/timeout, ` + + `${totalTarget - publishedSoFar.size - failedSoFar.size - timedOutSoFar.size} still pending`); + } + relaySet = result && result !== '__DEADLINE__' ? result : null; + } else { + // Normal publish — await the full result as before. + try { + relaySet = await ndkEvent.publish(targetRelaySet, publishTimeoutMs, publishRequiredCount); + } catch (publishError) { + console.warn('[Worker] publish() threw:', publishError?.message || publishError); + relaySet = null; + } } // Final broadcast progress message with complete results.