From fb4b622f3c755d43c7beacc8b46c1eb24cc51127 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 6 May 2026 23:43:13 +0200 Subject: [PATCH] fix frozen publish --- src/constants.ts | 6 +++ src/services/client.service.ts | 74 ++++++++++++++++++++++------------ 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 3623161c..c93bdd38 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -126,6 +126,12 @@ export const PUBLIC_MESSAGE_RSVP_PUBLISH_AUTHOR_WRITE_CAP = 10 /** After a publish wave, failed NIP-65 write (outbox) relays are retried once after this delay. */ export const OUTBOX_PUBLISH_RETRY_DELAY_MS = 5000 +/** + * After the first relay accepts a publish, resolve {@link ClientService.publishEvent} after this many ms + * so the UI does not wait for every slow or dead relay (callers typically only need ≥1 success). + */ +export const EARLY_PUBLISH_SUCCESS_GRACE_MS = 1200 + /** * Cap how long we wait on NIP-65 / inbox relay-list resolution (including `fetchRelayLists` network phase * and kind-10432 fetch) before publishing or falling back to IndexedDB-only merge. diff --git a/src/services/client.service.ts b/src/services/client.service.ts index c4f5e154..7340e1c3 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -23,6 +23,7 @@ import { RELAY_READ_ONLY_POOL_CONNECT_TIMEOUT_MS, TIMELINE_SHARD_SUBSCRIBE_CONCURRENCY, OUTBOX_PUBLISH_RETRY_DELAY_MS, + EARLY_PUBLISH_SUCCESS_GRACE_MS, DEFAULT_FAVORITE_RELAYS, NIP66_DISCOVERY_RELAY_URLS, PROFILE_FETCH_RELAY_URLS, @@ -570,6 +571,19 @@ class ClientService extends EventTarget { const failedOutboxes = userOutboxUrls.filter((u) => !hadSuccess.has(norm(u))) if (failedOutboxes.length === 0) return + const anyRelaySucceeded = relayStatuses.some((r) => r.success) + if ( + anyRelaySucceeded && + failedOutboxes.length > 0 && + failedOutboxes.every((u) => isLocalNetworkUrl(u)) + ) { + logger.info( + '[Publish] Skipping NIP-65 outbox retry: unreachable local write relay(s) only; note already reached at least one relay.', + { failedOutboxes } + ) + return + } + const statusHint = (url: string): string => { const n = norm(url) const forUrl = relayStatuses.filter((r) => norm(r.url) === n) @@ -1521,6 +1535,7 @@ class ClientService extends EventTarget { slotCap }) let hasResolved = false + let earlyGraceTimer: ReturnType | null = null const globalTimeout = setTimeout(() => { if (hasResolved) { @@ -1548,6 +1563,10 @@ class ClientService extends EventTarget { // Ensure we resolve even if not all relays finished if (!hasResolved) { + if (earlyGraceTimer != null) { + clearTimeout(earlyGraceTimer) + earlyGraceTimer = null + } hasResolved = true logger.debug('[PublishEvent] Resolving due to timeout', { success: successCount >= uniqueRelayUrls.length / 3, @@ -1704,7 +1723,11 @@ class ClientService extends EventTarget { break } catch (wsErr) { const msg = wsErr instanceof Error ? wsErr.message : String(wsErr) + const localConnDead = + isLocal && + /Local relay connection timeout|connection timed out/i.test(msg) const retriable = + !localConnDead && wsAttempt === 0 && /Remote relay connection timeout|Local relay connection timeout|Publish timeout after|publish timed out|websocket closed|connection failed|relay connection closed|SendingOnClosedConnection/i.test( msg @@ -1759,6 +1782,10 @@ class ClientService extends EventTarget { this.emitNewEvent(event) } if (currentFinished >= uniqueRelayUrls.length && !hasResolved) { + if (earlyGraceTimer != null) { + clearTimeout(earlyGraceTimer) + earlyGraceTimer = null + } hasResolved = true logger.debug('[PublishEvent] All relays finished, resolving', { success: successCount >= uniqueRelayUrls.length / 3, @@ -1774,32 +1801,27 @@ class ClientService extends EventTarget { successCount, totalCount: uniqueRelayUrls.length }) - } - - // Also resolve early if we have enough successes (1/3 of relays) - // This prevents waiting for slow/failing relays - if (!hasResolved && successCount >= Math.max(1, Math.ceil(uniqueRelayUrls.length / 3)) && currentFinished >= Math.max(1, Math.ceil(uniqueRelayUrls.length / 3))) { - // Wait a bit more to see if more relays succeed quickly - setTimeout(() => { - if (!hasResolved) { - hasResolved = true - logger.debug('[PublishEvent] Resolving early with enough successes', { - success: true, - successCount, - totalCount: uniqueRelayUrls.length, - finishedCount: currentFinished, - relayStatusesCount: relayStatuses.length - }) - clearTimeout(globalTimeout) - flushPublishOpBatch('early_success_threshold') - resolve({ - success: true, - relayStatuses, - successCount, - totalCount: uniqueRelayUrls.length - }) - } - }, 2000) // Wait 2 more seconds for quick responses + } else if (!hasResolved && successCount >= 1 && earlyGraceTimer == null) { + earlyGraceTimer = setTimeout(() => { + earlyGraceTimer = null + if (hasResolved) return + hasResolved = true + clearTimeout(globalTimeout) + flushPublishOpBatch('early_any_success_grace') + logger.debug('[PublishEvent] Resolving after first success grace', { + success: successCount >= uniqueRelayUrls.length / 3, + successCount, + totalCount: uniqueRelayUrls.length, + finishedRelays: currentFinished, + graceMs: EARLY_PUBLISH_SUCCESS_GRACE_MS + }) + resolve({ + success: successCount >= uniqueRelayUrls.length / 3, + relayStatuses, + successCount, + totalCount: uniqueRelayUrls.length + }) + }, EARLY_PUBLISH_SUCCESS_GRACE_MS) } } })