diff --git a/js/services/hashtagPreferencesService.js b/js/services/hashtagPreferencesService.js index 2fd72a70..9073120d 100644 --- a/js/services/hashtagPreferencesService.js +++ b/js/services/hashtagPreferencesService.js @@ -46,11 +46,14 @@ const DEFAULT_VERSION = 1; // nip-07 extension, and the main thread can be busy connecting to relays. 6s // proved too aggressive on real accounts (lists timed out and showed empty); // 15s matches the block-list timeout and lets decryption complete under load. -const DECRYPT_TIMEOUT_MS = 15000; +// Generous decrypt budget: real extensions can take 15-25s to answer a nip-07 +// decrypt under cold-login load even though the crypto is instant. A tighter +// budget times out mid-flight and retries forever (KNOWN_BUGS #0). +const DECRYPT_TIMEOUT_MS = 30000; // PERF: Reduced from 3s to 1.5s — extensions that already granted permission // should recover near-instantly. Shorter delay speeds up the login path. const DECRYPT_RETRY_DELAY_MS = NETWORK_RETRY_DELAY_MS; -const MAX_DECRYPT_RETRY_DELAY_MS = 30000; +const MAX_DECRYPT_RETRY_DELAY_MS = 60000; // Transient decrypt failures that must RETRY in the background rather than give // up: our own timeout, a severed nip-07 message port ("message channel closed"), @@ -1130,11 +1133,11 @@ class HashtagPreferencesService { sources.set(scheme, source); }; - // PERF: Reduced no-prompt timeout from 8s to 5s — the signer readiness - // gate guarantees the extension is ready before decryption starts, so - // decrypt calls should complete within 1-2s. 5s accommodates slow - // extensions while still failing fast for scheme fallback. - const nip07DecryptTimeoutMs = allowPermissionPrompt ? 6000 : SHORT_TIMEOUT_MS; + // Per-call nip-07 decrypt budget. A short value kills in-progress decrypts + // on slow extensions (15-25s under cold-login load), so the list never + // decrypts and retries forever. Give the interactive call real room; the + // background refresh stays tighter for fast scheme fallback. + const nip07DecryptTimeoutMs = allowPermissionPrompt ? 25000 : 10000; const signerDecryptOptions = { priority: NIP07_PRIORITY.NORMAL, timeoutMs: nip07DecryptTimeoutMs, diff --git a/js/subscriptions.js b/js/subscriptions.js index 7d433609..70341ffc 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -53,12 +53,15 @@ const SUBSCRIPTION_SET_KIND = // so decryption should succeed within 2-3s. 6s is generous enough for slow // relays while still failing fast enough for the scheme fallback to try the // next decryption method without stalling the login path. -const DECRYPT_TIMEOUT_MS = 15000; +// Generous decrypt budget: real extensions can take 15-25s to answer a nip-07 +// decrypt under cold-login load even though the crypto is instant. A tighter +// budget times out mid-flight and retries forever (KNOWN_BUGS #0). +const DECRYPT_TIMEOUT_MS = 30000; // PERF: Reduced from 3s to 1.5s — extensions that already granted permission // should recover near-instantly. The shorter delay prevents unnecessary wait // time during the critical login path when the first decrypt attempt fails. const DECRYPT_RETRY_DELAY_MS = 1500; -const MAX_DECRYPT_RETRY_DELAY_MS = 30000; +const MAX_DECRYPT_RETRY_DELAY_MS = 60000; // Transient decrypt failures that must RETRY rather than give up: our own // timeout, a severed nip-07 message port ("message channel closed"), and the @@ -1240,13 +1243,13 @@ class SubscriptionsManager { decryptors.set(scheme, handler); }; - // PERF: Reduced no-prompt timeout from 8s to 5s — the signer readiness - // gate now guarantees the extension is ready before decryption starts, so - // decrypt calls should complete within 1-2s. 5s accommodates slow - // extensions while still failing fast enough for scheme fallback. + // Per-call nip-07 decrypt budget. A short value kills in-progress decrypts + // on slow extensions (15-25s under cold-login load), so the list never + // decrypts and retries forever. Give the interactive call real room; the + // background refresh stays tighter for fast scheme fallback. const nip07DecryptTimeoutMs = allowPermissionPrompt - ? 6000 - : SHORT_TIMEOUT_MS; + ? 25000 + : 10000; const signerDecryptOptions = { priority: NIP07_PRIORITY.NORMAL, timeoutMs: nip07DecryptTimeoutMs, diff --git a/js/userBlocks.js b/js/userBlocks.js index 0f854b89..58006348 100644 --- a/js/userBlocks.js +++ b/js/userBlocks.js @@ -89,12 +89,21 @@ const FAST_BLOCKLIST_RELAY_LIMIT = 3; const FAST_BLOCKLIST_TIMEOUT_MS = 2500; const BACKGROUND_BLOCKLIST_TIMEOUT_MS = 6000; const MAX_BACKGROUND_BLOCKLIST_RELAY_LIMIT = 24; -// Keep interactive decrypt timeout above extension retry timeout window. -const DECRYPT_TIMEOUT_MS = Math.max(STANDARD_TIMEOUT_MS, 15000); -const BACKGROUND_DECRYPT_TIMEOUT_MS = 8000; +// Decrypt timeout budget. A nip-07 decrypt is a postMessage round-trip to the +// extension's (single-threaded, possibly sleeping) service worker — under +// cold-login load real extensions can take 15-25s to answer even though the +// crypto itself is instant. The harness (scripts/perf/nip07-channel-sim.mjs) +// shows lists fail to load the moment per-call latency crosses this budget, so +// keep it generous (30s) to give a slow-but-responsive signer room to complete +// rather than timing out and retrying forever (KNOWN_BUGS #0). +const DECRYPT_TIMEOUT_MS = Math.max(STANDARD_TIMEOUT_MS, 30000); +const BACKGROUND_DECRYPT_TIMEOUT_MS = 12000; // PERF: Reduced from 10s to 3s for faster recovery during login. const DECRYPT_RETRY_DELAY_MS = 3000; -const MAX_DECRYPT_RETRY_DELAY_MS = 30000; +// Cap the exponential backoff well above the decrypt budget so a persistently +// unresponsive signer settles into an occasional heartbeat instead of pinning +// the extension/CPU every ~15s indefinitely. +const MAX_DECRYPT_RETRY_DELAY_MS = 60000; // Transient decrypt failures that must KEEP the stale list and RETRY in the // background (the channel/extension recovers shortly), rather than giving up @@ -982,13 +991,12 @@ class UserBlockListManager { : allowPermissionPrompt ? DECRYPT_TIMEOUT_MS : BACKGROUND_DECRYPT_TIMEOUT_MS; - // A healthy nip04/nip44 decrypt is local crypto (~1s). The login-time value - // was 15s purely to tolerate a slow first-grant prompt — but permissions are - // pre-granted by the readiness gate, so a 15s timeout just monopolizes one of - // the two NIP-07 queue slots on a doomed call, starving the gate (real-env - // ~48s signer-ready — KNOWN_BUGS #0). 6s still tolerates a slow extension - // while freeing the slot ~2.5x faster. - const nip07DecryptTimeoutMs = allowPermissionPrompt ? 6000 : 6000; + // Per-call nip-07 decrypt budget. A 6s value (tried earlier) killed + // in-progress decrypts on slow extensions that legitimately need 15-25s to + // answer under cold-login load, so the list never decrypted and retried + // forever. Give the call real room (interactive 25s, background 12s) — it + // sits below the service-level DECRYPT_TIMEOUT_MS backstop. + const nip07DecryptTimeoutMs = allowPermissionPrompt ? 25000 : 12000; const emitStatus = (detail) => { if (!detail || typeof detail !== "object") { return; diff --git a/scripts/perf/nip07-channel-sim.mjs b/scripts/perf/nip07-channel-sim.mjs index 29ac0fb7..b67c873b 100644 --- a/scripts/perf/nip07-channel-sim.mjs +++ b/scripts/perf/nip07-channel-sim.mjs @@ -54,7 +54,13 @@ const MODES = { dead: { latency: 400, maxInflight: 1, dropCooldown: 10 ** 9 }, }; const HEALTHY = MODES.healthy; -const TARGET = MODES[MODE] || MODES.overload; +const TARGET = { ...(MODES[MODE] || MODES.overload) }; +// Allow overriding the per-call latency to model a genuinely slow extension +// (the real-env case: signer present but every decrypt sits in the nip-07 +// queue long enough to blow the 15s service timeout). +if (Number.isFinite(Number(process.env.LATENCY_MS))) { + TARGET.latency = Number(process.env.LATENCY_MS); +} const USER_SK = Uint8Array.from(Buffer.from("11".repeat(32), "hex")); const USER_PK = getPublicKey(USER_SK);