From dab94a5cfe335e1ca6dba4d0db5bef248ebb529d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 18:51:34 +0000 Subject: [PATCH] Fix slow loading and NIP-07 decryption issues - Reduced `fetchListIncrementally` timeout to 4000ms to fail faster on bad relays. - Added 15s timeout to decryption calls in `HashtagPreferencesService`, `UserBlockListManager`, and `SubscriptionsManager` to prevent infinite hangs. - Added handling for "message channel closed" error in `nip07Adapter` with a backoff delay to allow extension recovery. - Prioritized preserving cached data on decryption failure instead of resetting to empty state. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- js/nostr/adapters/nip07Adapter.js | 4 +++- js/nostr/client.js | 4 ++-- js/services/hashtagPreferencesService.js | 15 ++++++++++++++- js/subscriptions.js | 15 ++++++++++++++- js/userBlocks.js | 13 ++++++++++++- 5 files changed, 45 insertions(+), 6 deletions(-) diff --git a/js/nostr/adapters/nip07Adapter.js b/js/nostr/adapters/nip07Adapter.js index 936130cf..dbdde2be 100644 --- a/js/nostr/adapters/nip07Adapter.js +++ b/js/nostr/adapters/nip07Adapter.js @@ -19,7 +19,9 @@ async function retryNip07Call(operation, label) { throw error; } if (i < attempts - 1) { - await new Promise((resolve) => setTimeout(resolve, 100)); + const isChannelClosed = error?.message?.includes("message channel closed"); + const delay = isChannelClosed ? 250 : 100; + await new Promise((resolve) => setTimeout(resolve, delay)); } } } diff --git a/js/nostr/client.js b/js/nostr/client.js index 3b2dcd46..a159a871 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -3643,7 +3643,7 @@ export class NostrClient { // Wrap fetch with a timeout to prevent hanging on slow relays let events = await withRequestTimeout( actualFetchFn(relayUrl, filter), - 6000, + 4000, null, `Fetch from ${relayUrl} timed out` ); @@ -3678,7 +3678,7 @@ export class NostrClient { delete filter.since; const events = await withRequestTimeout( actualFetchFn(relayUrl, filter), - 6000, + 4000, null, `Full fetch fallback from ${relayUrl} timed out` ); diff --git a/js/services/hashtagPreferencesService.js b/js/services/hashtagPreferencesService.js index 4d3722ee..261aa2d4 100644 --- a/js/services/hashtagPreferencesService.js +++ b/js/services/hashtagPreferencesService.js @@ -588,7 +588,20 @@ class HashtagPreferencesService { return; } - const decryptResult = await this.decryptEvent(latest, normalized); + let decryptResult; + try { + const decryptPromise = this.decryptEvent(latest, normalized); + const timeoutPromise = new Promise((_, reject) => + setTimeout( + () => reject(new Error("Decryption timed out after 15s")), + 15000, + ), + ); + decryptResult = await Promise.race([decryptPromise, timeoutPromise]); + } catch (error) { + decryptResult = { ok: false, error }; + } + if (!decryptResult.ok) { userLogger.warn( `${LOG_PREFIX} Failed to decrypt hashtag preferences`, diff --git a/js/subscriptions.js b/js/subscriptions.js index 246fdb21..609be1a9 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -467,7 +467,20 @@ class SubscriptionsManager { return; } - const decryptResult = await this.decryptSubscriptionEvent(newest, userPubkey); + let decryptResult; + try { + const decryptPromise = this.decryptSubscriptionEvent(newest, userPubkey); + const timeoutPromise = new Promise((_, reject) => + setTimeout( + () => reject(new Error("Decryption timed out after 15s")), + 15000, + ), + ); + decryptResult = await Promise.race([decryptPromise, timeoutPromise]); + } catch (error) { + decryptResult = { ok: false, error }; + } + if (!decryptResult.ok) { userLogger.error( "[SubscriptionsManager] Failed to decrypt subscription list:", diff --git a/js/userBlocks.js b/js/userBlocks.js index bb288fe7..c8aae597 100644 --- a/js/userBlocks.js +++ b/js/userBlocks.js @@ -1011,10 +1011,21 @@ class UserBlockListManager { let decryptionError = null; try { - const [muteList, blockList] = await Promise.all([ + const decryptPromise = Promise.all([ decryptEvent(newestMute), decryptEvent(newestBlock), ]); + const timeoutPromise = new Promise((_, reject) => + setTimeout( + () => reject(new Error("Decryption timed out after 15s")), + 15000, + ), + ); + + const [muteList, blockList] = await Promise.race([ + decryptPromise, + timeoutPromise, + ]); muteList.forEach((pk) => mergedPubkeys.add(pk)); blockList.forEach((pk) => mergedPubkeys.add(pk));