This commit is contained in:
thePR0M3TH3AN committed 2026-02-21 16:08:33 -05:00
1 parent 576513ecdb
commit 152b7fbf1a
6 files changed
+90 -23

No files matched your search

+4 -4
View File
@@ -99,10 +99,10 @@ class Nip07RequestQueue {
// This prevents "message channel closed" errors caused by race conditions
// when multiple components (blocks, DMs, auth) query the extension simultaneously,
// while allowing critical tasks (e.g. blocklist decryption) to jump the line.
// PERF: Increased from 3 to 5 to allow more parallel decrypt operations
// during login when blocks, subscriptions, and hashtag preferences all need
// to decrypt simultaneously.
const requestQueue = new Nip07RequestQueue(5);
// Stability: keep extension request concurrency conservative. Some NIP-07
// providers/content scripts drop channels under high parallel load, which
// manifests as "message channel closed" and repeated decrypt timeouts.
const requestQueue = new Nip07RequestQueue(2);
export function getEnableVariantTimeoutMs() {
const overrideValue =
+17 -2
View File
@@ -45,6 +45,15 @@ const DECRYPT_TIMEOUT_MS = 6000;
// 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;
function computeRetryDelay(baseDelayMs, attempt) {
const normalizedAttempt =
Number.isFinite(attempt) && attempt > 0 ? Math.floor(attempt) : 0;
const multiplier = 2 ** normalizedAttempt;
const nextDelay = Math.max(250, Math.floor(baseDelayMs * multiplier));
return Math.min(MAX_DECRYPT_RETRY_DELAY_MS, nextDelay);
}
class TinyEventEmitter {
constructor() {
@@ -260,6 +269,7 @@ class HashtagPreferencesService {
this.backgroundLoading = false;
this.preferencesVersion = DEFAULT_VERSION;
this.decryptRetryTimeoutId = null;
this.decryptRetryAttempt = 0;
this.loadPromise = null;
this.loadingPubkey = null;
this.subscriptionKey = null;
@@ -303,6 +313,7 @@ class HashtagPreferencesService {
clearTimeout(this.decryptRetryTimeoutId);
this.decryptRetryTimeoutId = null;
}
this.decryptRetryAttempt = 0;
this.loadPromise = null;
this.loadingPubkey = null;
this.emitChange("reset");
@@ -525,6 +536,9 @@ class HashtagPreferencesService {
if (this.decryptRetryTimeoutId) {
clearTimeout(this.decryptRetryTimeoutId);
}
const attempt = this.decryptRetryAttempt;
const retryDelayMs = computeRetryDelay(DECRYPT_RETRY_DELAY_MS, attempt);
this.decryptRetryAttempt = attempt + 1;
this.decryptRetryTimeoutId = setTimeout(() => {
this.decryptRetryTimeoutId = null;
if (this.activePubkey && this.activePubkey !== normalized) {
@@ -533,9 +547,9 @@ class HashtagPreferencesService {
this.load(normalized, options).catch((retryError) => {
userLogger.warn(`${LOG_PREFIX} Decryption retry failed`, retryError);
});
}, DECRYPT_RETRY_DELAY_MS);
}, retryDelayMs);
devLogger.log(
`${LOG_PREFIX} Decryption timed out; scheduling retry in ${DECRYPT_RETRY_DELAY_MS / 1000}s.`,
`${LOG_PREFIX} Decryption timed out; scheduling retry in ${retryDelayMs / 1000}s.`,
error,
);
}
@@ -939,6 +953,7 @@ class HashtagPreferencesService {
this.loaded = true;
this.loadedFromCache = false;
this.lastSuccessfulSyncAt = Date.now();
this.decryptRetryAttempt = 0;
if (this.backgroundLoading) {
this.backgroundLoading = false;
}
+18 -2
View File
@@ -54,6 +54,15 @@ const DECRYPT_TIMEOUT_MS = 6000;
// 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;
function computeRetryDelay(baseDelayMs, attempt) {
const normalizedAttempt =
Number.isFinite(attempt) && attempt > 0 ? Math.floor(attempt) : 0;
const multiplier = 2 ** normalizedAttempt;
const nextDelay = Math.max(250, Math.floor(baseDelayMs * multiplier));
return Math.min(MAX_DECRYPT_RETRY_DELAY_MS, nextDelay);
}
function normalizeHexPubkey(value) {
if (typeof value !== "string") {
@@ -369,6 +378,7 @@ class SubscriptionsManager {
this.hasRenderedOnce = false;
this.emitter = new TinyEventEmitter();
this.decryptRetryTimeoutId = null;
this.decryptRetryAttempt = 0;
this.ensureNostrServiceListener();
this.subscriptionKey = null;
@@ -458,6 +468,9 @@ class SubscriptionsManager {
if (this.decryptRetryTimeoutId) {
clearTimeout(this.decryptRetryTimeoutId);
}
const attempt = this.decryptRetryAttempt;
const retryDelayMs = computeRetryDelay(DECRYPT_RETRY_DELAY_MS, attempt);
this.decryptRetryAttempt = attempt + 1;
this.decryptRetryTimeoutId = setTimeout(() => {
this.decryptRetryTimeoutId = null;
if (this.currentUserPubkey && this.currentUserPubkey !== normalized) {
@@ -466,9 +479,9 @@ class SubscriptionsManager {
this.updateFromRelays(normalized, options).catch((retryError) => {
userLogger.warn("[SubscriptionsManager] Decryption retry failed:", retryError);
});
}, DECRYPT_RETRY_DELAY_MS);
}, retryDelayMs);
devLogger.log(
`[SubscriptionsManager] Decryption timed out; scheduling retry in ${DECRYPT_RETRY_DELAY_MS / 1000}s.`,
`[SubscriptionsManager] Decryption timed out; scheduling retry in ${retryDelayMs / 1000}s.`,
error,
);
}
@@ -741,6 +754,8 @@ class SubscriptionsManager {
return;
}
this.decryptRetryAttempt = 0;
const decryptedStr = decryptResult.plaintext;
const normalized = parseSubscriptionPlaintext(decryptedStr);
@@ -820,6 +835,7 @@ class SubscriptionsManager {
clearTimeout(this.decryptRetryTimeoutId);
this.decryptRetryTimeoutId = null;
}
this.decryptRetryAttempt = 0;
this.emitter.emit("change", { action: "reset", subscribedPubkeys: [] });
}
+28 -3
View File
@@ -3342,8 +3342,17 @@ export class ProfileModalController {
const failedTasks = Array.isArray(listsDetail.tasks)
? listsDetail.tasks.filter((task) => task && task.ok === false)
: [];
const permissionFailureTasks = failedTasks.filter((task) => {
const code =
task?.error && typeof task.error.code === "string"
? task.error.code.trim()
: "";
return code.endsWith("-permission-required");
});
const loadedFromCacheTasks = failedTasks.filter((task) => task.fromCache === true);
const hardFailTasks = failedTasks.filter((task) => task.fromCache !== true);
const onlyPermissionFailures =
failedTasks.length > 0 && permissionFailureTasks.length === failedTasks.length;
if (loadedFromCacheTasks.length > 0 && hardFailTasks.length === 0) {
this.setSubscriptionsStatus(
@@ -3357,6 +3366,18 @@ export class ProfileModalController {
this.setBlockListLoadingState("error", {
message: "Blocked creators loaded from cache; syncing latest in background.",
});
} else if (onlyPermissionFailures) {
this.setSubscriptionsStatus(
"Waiting for extension decrypt permissions to sync your lists.",
"warning",
);
this.hashtagController.setHashtagStatus(
"Waiting for extension decrypt permissions to sync your lists.",
"warning",
);
this.setBlockListLoadingState("error", {
message: "Waiting for extension decrypt permissions to sync your blocked creators.",
});
} else if (hardFailTasks.length > 0) {
this.setSubscriptionsStatus(
"Some profile lists failed to sync. Use Retry sync.",
@@ -3375,11 +3396,15 @@ export class ProfileModalController {
this.setPermissionPromptCtaState({
visible: true,
message:
loadedFromCacheTasks.length > 0
onlyPermissionFailures
? "NIP-07 decrypt permissions are needed to finish syncing your profile lists."
: loadedFromCacheTasks.length > 0
? "Some lists are currently from cache. Retry now to sync with relays."
: "Failed to sync required profile lists. Retry now.",
buttonLabel: "Retry list sync",
action: "retry-auth-sync",
buttonLabel: onlyPermissionFailures
? "Enable permissions"
: "Retry list sync",
action: onlyPermissionFailures ? "permission" : "retry-auth-sync",
busy: false,
});
return;
+17 -2
View File
@@ -90,6 +90,15 @@ const DECRYPT_TIMEOUT_MS = STANDARD_TIMEOUT_MS;
const BACKGROUND_DECRYPT_TIMEOUT_MS = 8000;
// PERF: Reduced from 10s to 3s for faster recovery during login.
const DECRYPT_RETRY_DELAY_MS = 3000;
const MAX_DECRYPT_RETRY_DELAY_MS = 30000;
function computeRetryDelay(baseDelayMs, attempt) {
const normalizedAttempt =
Number.isFinite(attempt) && attempt > 0 ? Math.floor(attempt) : 0;
const multiplier = 2 ** normalizedAttempt;
const nextDelay = Math.max(250, Math.floor(baseDelayMs * multiplier));
return Math.min(MAX_DECRYPT_RETRY_DELAY_MS, nextDelay);
}
function sanitizeRelayList(candidate) {
return Array.isArray(candidate)
@@ -699,6 +708,7 @@ class UserBlockListManager {
this.emitter = new TinyEventEmitter();
this.seedStateCache = new Map();
this.decryptRetryTimeoutId = null;
this.decryptRetryAttempt = 0;
this.loadPromise = null;
this.loadingPubkey = null;
this.blockListSubscriptionKey = null;
@@ -730,6 +740,7 @@ class UserBlockListManager {
clearTimeout(this.decryptRetryTimeoutId);
this.decryptRetryTimeoutId = null;
}
this.decryptRetryAttempt = 0;
this.loadPromise = null;
this.loadingPubkey = null;
}
@@ -805,6 +816,9 @@ class UserBlockListManager {
if (this.decryptRetryTimeoutId) {
clearTimeout(this.decryptRetryTimeoutId);
}
const attempt = this.decryptRetryAttempt;
const retryDelayMs = computeRetryDelay(DECRYPT_RETRY_DELAY_MS, attempt);
this.decryptRetryAttempt = attempt + 1;
const retryMeta = detail && typeof detail === "object" ? detail : {};
const signerStatus = retryMeta.signerStatus || "unknown";
this.decryptRetryTimeoutId = setTimeout(() => {
@@ -820,9 +834,9 @@ class UserBlockListManager {
}).catch((retryError) => {
userLogger.warn("[UserBlockList] Decryption retry failed:", retryError);
});
}, DECRYPT_RETRY_DELAY_MS);
}, retryDelayMs);
devLogger.log(
`[UserBlockList] Decryption stalled (${signerStatus}); retrying in ${DECRYPT_RETRY_DELAY_MS / 1000}s.`,
`[UserBlockList] Decryption stalled (${signerStatus}); retrying in ${retryDelayMs / 1000}s.`,
retryMeta.error || retryMeta,
);
}
@@ -1528,6 +1542,7 @@ class UserBlockListManager {
reason: effectiveReason,
events: [newestStandard?.id, newestLegacy?.id].filter(Boolean),
});
this.decryptRetryAttempt = 0;
if (effectiveReason === "empty-events") {
emitStatus({ status: "applied-empty", event: newestOverall, source });
+6 -10
View File
@@ -21,7 +21,7 @@ describe("NIP-07 Concurrency Queue", () => {
global.window = originalWindow;
});
test("runs up to 5 tasks concurrently", async () => {
test("runs up to 2 tasks concurrently", async () => {
const start = Date.now();
const delays = [200, 200, 200, 200, 200, 100];
const log = [];
@@ -44,12 +44,11 @@ describe("NIP-07 Concurrency Queue", () => {
const starts = log.filter(e => e.status === 'start');
// Task 1-5 should start almost immediately (concurrency 5)
assert.ok(Math.abs(starts[0].time - starts[4].time) < 50, "Task 1-5 should start concurrently");
// Task 1-2 should start almost immediately (concurrency 2)
assert.ok(Math.abs(starts[0].time - starts[1].time) < 50, "Task 1-2 should start concurrently");
// Task 6 should start after roughly 200ms (when slot opens)
// Note: Node's setTimeout is not precise, so we use loose bounds
assert.ok(starts[5].time >= 150, "Task 6 should wait for a slot");
// Task 3 should start only after the first slot opens (~200ms).
assert.ok(starts[2].time >= 150, "Task 3 should wait for a slot");
});
test("respects priority", async () => {
@@ -62,12 +61,9 @@ describe("NIP-07 Concurrency Queue", () => {
return id;
};
// Fill the queue (size 5)
// Fill the queue (size 2)
runNip07WithRetry(task('blocker1', 100));
runNip07WithRetry(task('blocker2', 100));
runNip07WithRetry(task('blocker3', 100));
runNip07WithRetry(task('blocker4', 100));
runNip07WithRetry(task('blocker5', 100));
// Enqueue low priority
const pLow = runNip07WithRetry(task('low', 50), { priority: NIP07_PRIORITY.LOW });