From 7dcfc7c3e3bc40cf9196f825760e5b5073074f08 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Tue, 30 Jun 2026 09:15:02 -0400 Subject: [PATCH] Fix broadcast relay timeout: use 30s timeout for large broadcasts, distinguish timeouts from explicit rejections, only skip-mark explicit rejections, use live-tracked sets for results --- www/js/version.json | 6 ++-- www/ndk-worker.js | 85 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 69 insertions(+), 22 deletions(-) diff --git a/www/js/version.json b/www/js/version.json index 6893d93..8920f67 100644 --- a/www/js/version.json +++ b/www/js/version.json @@ -1,5 +1,5 @@ { - "VERSION": "v0.7.67", - "VERSION_NUMBER": "0.7.67", - "BUILD_DATE": "2026-06-30T11:59:52.299Z" + "VERSION": "v0.7.68", + "VERSION_NUMBER": "0.7.68", + "BUILD_DATE": "2026-06-30T13:15:02.809Z" } diff --git a/www/ndk-worker.js b/www/ndk-worker.js index 2678d47..f3f56a0 100644 --- a/www/ndk-worker.js +++ b/www/ndk-worker.js @@ -6214,7 +6214,8 @@ async function handlePublish(requestId, event, port) { // broadcast() so footers can show "๐Ÿ“ก 42/150 relays ยท relay.example.com". const publishSessionId = `pub_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; const publishedSoFar = new Set(); - const failedSoFar = new Set(); + const failedSoFar = new Set(); // explicit rejections (auth-required, blocked, etc.) + const timedOutSoFar = new Set(); // timeouts โ€” NOT skip-marked (could be slow connect) const totalTarget = targetRelaySet && targetRelaySet.relays ? targetRelaySet.relays.size : (targetRelaySet ? (targetRelaySet.size || 0) : 0); @@ -6251,32 +6252,61 @@ async function handlePublish(requestId, event, port) { ndkEvent.on('relay:publish:failed', (relay, err) => { const url = relay?.url || String(relay || ''); if (!url) return; - failedSoFar.add(url); + const errMsg = err?.message || String(err) || ''; + // Distinguish timeouts from explicit rejections. Timeouts with + // hundreds of relays often just mean the relay was slow to connect, + // not that it's broken โ€” so we track them separately and do NOT + // skip-mark them. Only explicit rejections go into failedSoFar. + const isTimeout = /timeout/i.test(errMsg); + if (isTimeout) { + timedOutSoFar.add(url); + } else { + failedSoFar.add(url); + } broadcast({ type: 'broadcastProgress', sessionId: publishSessionId, phase: 'progress', total: totalTarget, successful: publishedSoFar.size, - failed: failedSoFar.size, + failed: failedSoFar.size + timedOutSoFar.size, latestRelay: url, - latestError: err?.message || String(err), + latestError: errMsg, }); }); } - // Publish to relays (with broadcast relay set if broadcasting, else default outbox) - const relaySet = await ndkEvent.publish(targetRelaySet); + // 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. + // 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 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; + } // Final broadcast progress message with complete results. if (isBroadcast && totalTarget > 0) { + const totalFailed = failedSoFar.size + timedOutSoFar.size; broadcast({ type: 'broadcastProgress', sessionId: publishSessionId, phase: 'done', total: totalTarget, successful: publishedSoFar.size, - failed: failedSoFar.size, + failed: totalFailed, latestRelay: null, }); // Clean up listeners so the NDKEvent can be garbage-collected. @@ -6303,32 +6333,49 @@ async function handlePublish(requestId, event, port) { } } - // Check which connected relays didn't accept it - for (const relay of ndk.pool.relays.values()) { - if (relay.status >= 5 && !relayResults.successful.includes(relay.url)) { - relayResults.failed.push(relay.url); + // For broadcast publishes, use the live-tracked sets from the + // relay:published / relay:publish:failed events as the source of truth + // (they cover temporary relays that may not be in ndk.pool). For normal + // publishes, fall back to the pool-scan logic. + if (isBroadcast) { + relayResults.successful = Array.from(publishedSoFar); + // Include both explicit rejections and timeouts in the failed list + // for display purposes, but keep them distinguishable for skip-marking. + relayResults.failed = [...Array.from(failedSoFar), ...Array.from(timedOutSoFar)]; + } else { + // Check which connected relays didn't accept it + for (const relay of ndk.pool.relays.values()) { + if (relay.status >= 5 && !relayResults.successful.includes(relay.url)) { + relayResults.failed.push(relay.url); + } } } - console.log('[Worker] โœ… Published to relays:', relayResults.successful); - console.log('[Worker] โŒ Failed relays:', relayResults.failed); + console.log('[Worker] โœ… Published to relays:', relayResults.successful.length, relayResults.successful.slice(0, 10), relayResults.successful.length > 10 ? `... +${relayResults.successful.length - 10} more` : ''); + console.log('[Worker] โŒ Failed relays:', relayResults.failed.length); // --- Failure marking for broadcast relays --------------------------------- - // Persist skip-marks for broadcast relays that failed this publish so - // future publishes skip them. Debounced via scheduleSkipMarkSave() so a - // single post that fails on many relays produces one kind 10088 republish. + // Persist skip-marks ONLY for broadcast relays that EXPLICITLY rejected + // the publish (auth-required, blocked, invalid, etc.). We do NOT skip-mark + // relays that timed out โ€” with hundreds of relays, a timeout often means + // the relay was slow to connect, not that it's broken. The relay:publish:failed + // listener above separates timeouts (timedOutSoFar) from explicit rejections + // (failedSoFar) by checking the error message for "timeout". if (isBroadcast) { - const newSkips = relayResults.failed + 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-failed', + reason: 'publish-rejected', timestamp: Math.floor(Date.now() / 1000), }); } scheduleSkipMarkSave(); - console.log(`[Worker] Skip-marked ${newSkips.length} failed broadcast relays`); + console.log(`[Worker] Skip-marked ${newSkips.length} rejected broadcast relays ` + + `(${timedOutSoFar.size} timeouts excluded from skip-marking)`); + } else if (timedOutSoFar.size > 0) { + console.log(`[Worker] ${timedOutSoFar.size} broadcast relays timed out (not skip-marked โ€” will retry next publish)`); } }