perf: speed up nostr login by parallelizing list sync and decryption

Login with NIP-07 extensions was extremely slow because list loading
(blocks, subscriptions, hashtag preferences) happened sequentially and
each list's decryption tried encryption schemes one at a time.

Changes:
- Load blocks, subscriptions, and hashtags in parallel instead of
  sequentially (blocks no longer blocks other lists from starting)
- Use Promise.any() to try all decryption schemes simultaneously
  instead of trying nip44_v2 → nip44 → nip04 one-by-one
- Deduplicate same-family schemes (nip44/nip44_v2) to avoid redundant
  extension calls
- Increase NIP-07 request queue concurrency from 3 to 5 to support
  parallel decrypt operations during login
- Raise no-prompt decrypt timeout from 3s/5s to 8s since extensions
  that have already granted permission still need time to decrypt
- Increase feed sync timeout from 8s to 12s to give lists more time
  to settle before rendering unfiltered content

https://claude.ai/code/session_01V6m1gTecsppPrBHQnUyPnF
This commit is contained in:
Claude committed 2026-02-06 22:23:46 +00:00
1 parent 1b6d3c1fe8
commit 47e900ebef
6 files changed
+160 -105

No files matched your search

+44 -25
View File
@@ -999,10 +999,11 @@ class SubscriptionsManager {
decryptors.set(scheme, handler);
};
// Use a shorter per-call timeout during background/login loads (no
// permission prompt) so stalled extension calls fail fast. Interactive
// loads get more time because the user may be approving a popup.
const nip07DecryptTimeoutMs = allowPermissionPrompt ? 15000 : 5000;
// PERF: Previous 5s no-prompt timeout was too aggressive — extensions that
// have already granted permission still need time to decrypt. 8s gives a
// realistic window while still failing faster than the full 15s interactive
// timeout.
const nip07DecryptTimeoutMs = allowPermissionPrompt ? 15000 : 8000;
const signerDecryptOptions = {
priority: NIP07_PRIORITY.NORMAL,
timeoutMs: nip07DecryptTimeoutMs,
@@ -1107,37 +1108,55 @@ class SubscriptionsManager {
availableSchemes,
cachedScheme,
);
const attemptErrors = [];
// PERF: Try all decryption schemes in parallel via Promise.any().
// The first scheme to succeed wins, avoiding sequential 3-15s timeouts
// per scheme that previously made worst-case decryption take 45s+.
// Deduplicate schemes in the same family (nip44/nip44_v2 both call
// the same underlying signer.nip44Decrypt) to avoid redundant calls.
const getSchemeFamily = (s) =>
s === "nip44" || s === "nip44_v2" ? "nip44" : s;
const seenFamilies = new Set();
const attempts = order
.map((scheme) => {
const family = getSchemeFamily(scheme);
if (seenFamilies.has(family)) return null;
seenFamilies.add(family);
const decryptFn = decryptors.get(scheme);
if (!decryptFn) return null;
return decryptFn(ciphertext).then((plaintext) => {
if (typeof plaintext !== "string") {
throw new Error("Decryption returned a non-string payload.");
}
return { plaintext, scheme };
});
})
.filter(Boolean);
for (const scheme of order) {
const decryptFn = decryptors.get(scheme);
if (!decryptFn) {
continue;
}
if (attempts.length) {
try {
const plaintext = await decryptFn(ciphertext);
if (typeof plaintext !== "string") {
const error = new Error("Decryption returned a non-string payload.");
error.code = "subscriptions-invalid-plaintext";
attemptErrors.push({ scheme, error });
continue;
}
const result = await Promise.any(attempts);
decryptionSuccessCache.set(userPubkey, {
scheme,
scheme: result.scheme,
timestamp: Date.now(),
});
return { ok: true, plaintext, scheme };
} catch (error) {
attemptErrors.push({ scheme, error });
return { ok: true, plaintext: result.plaintext, scheme: result.scheme };
} catch (aggregateError) {
const attemptErrors = (aggregateError.errors || []).map((err, i) => ({
scheme: order[i] || "unknown",
error: err,
}));
const error = new Error("Failed to decrypt subscription list with available schemes.");
error.code = "subscriptions-decrypt-failed";
if (attemptErrors.length) {
error.cause = attemptErrors;
}
return { ok: false, error, errors: attemptErrors };
}
}
const error = new Error("Failed to decrypt subscription list with available schemes.");
error.code = "subscriptions-decrypt-failed";
if (attemptErrors.length) {
error.cause = attemptErrors;
}
return { ok: false, error, errors: attemptErrors };
return { ok: false, error, errors: [] };
}
async addChannel(channelHex, userPubkey) {