Merge pull request #1856 from PR0M3TH3AN/codex/increase-decrypt-timeout-and-add-retry-logic

Increase decrypt timeouts and add background retry for user state decryption
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-27 17:05:33 -05:00
commit a60e16d1f2
3 files changed
+141 -6

No files matched your search

+46 -2
View File
@@ -23,6 +23,8 @@ const LOG_PREFIX = "[HashtagPreferences]";
const HASHTAG_IDENTIFIER = "bitvid:tag-preferences";
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
const DEFAULT_VERSION = 1;
const DECRYPT_TIMEOUT_MS = 90000;
const DECRYPT_RETRY_DELAY_MS = 10000;
class TinyEventEmitter {
constructor() {
@@ -235,6 +237,7 @@ class HashtagPreferencesService {
this.eventCreatedAt = null;
this.loaded = false;
this.preferencesVersion = DEFAULT_VERSION;
this.decryptRetryTimeoutId = null;
profileCache.subscribe((event, detail) => {
if (event === "profileChanged") {
@@ -262,6 +265,10 @@ class HashtagPreferencesService {
this.eventCreatedAt = null;
this.loaded = false;
this.preferencesVersion = DEFAULT_VERSION;
if (this.decryptRetryTimeoutId) {
clearTimeout(this.decryptRetryTimeoutId);
this.decryptRetryTimeoutId = null;
}
this.emitChange("reset");
}
@@ -416,6 +423,29 @@ class HashtagPreferencesService {
}
}
scheduleDecryptRetry(pubkey, error) {
const normalized = normalizeHexPubkey(pubkey);
if (!normalized) {
return;
}
if (this.decryptRetryTimeoutId) {
clearTimeout(this.decryptRetryTimeoutId);
}
this.decryptRetryTimeoutId = setTimeout(() => {
this.decryptRetryTimeoutId = null;
if (this.activePubkey && this.activePubkey !== normalized) {
return;
}
this.load(normalized).catch((retryError) => {
userLogger.warn(`${LOG_PREFIX} Decryption retry failed`, retryError);
});
}, DECRYPT_RETRY_DELAY_MS);
devLogger.log(
`${LOG_PREFIX} Decryption timed out; scheduling retry in ${DECRYPT_RETRY_DELAY_MS / 1000}s.`,
error,
);
}
async load(pubkey) {
const normalized = normalizeHexPubkey(pubkey);
let wasLoadedForUser =
@@ -610,8 +640,14 @@ class HashtagPreferencesService {
const decryptPromise = this.decryptEvent(latest, normalized);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Decryption timed out after 15s")),
15000,
() => {
const timeoutError = new Error(
`Decryption timed out after ${DECRYPT_TIMEOUT_MS / 1000}s`,
);
timeoutError.code = "hashtag-preferences-decrypt-timeout";
reject(timeoutError);
},
DECRYPT_TIMEOUT_MS,
),
);
decryptResult = await Promise.race([decryptPromise, timeoutPromise]);
@@ -625,6 +661,14 @@ class HashtagPreferencesService {
decryptResult.error,
);
if (decryptResult.error?.code === "hashtag-preferences-decrypt-timeout") {
if (!wasLoadedForUser && !this.loaded) {
this.loaded = true;
}
this.scheduleDecryptRetry(normalized, decryptResult.error);
return;
}
// If we already have loaded preferences (e.g. from cache), preserve them
// rather than wiping everything just because the remote update couldn't be decrypted.
if (wasLoadedForUser) {
+45 -2
View File
@@ -33,6 +33,8 @@ import { profileCache } from "./state/profileCache.js";
const SUBSCRIPTION_SET_KIND =
getNostrEventSchema(NOTE_TYPES.SUBSCRIPTION_LIST)?.kind ?? 30000;
const DECRYPT_TIMEOUT_MS = 90000;
const DECRYPT_RETRY_DELAY_MS = 10000;
function normalizeHexPubkey(value) {
if (typeof value !== "string") {
@@ -335,6 +337,7 @@ class SubscriptionsManager {
this.isRunningFeed = false;
this.hasRenderedOnce = false;
this.emitter = new TinyEventEmitter();
this.decryptRetryTimeoutId = null;
this.ensureNostrServiceListener();
profileCache.subscribe((event, detail) => {
@@ -397,6 +400,29 @@ class SubscriptionsManager {
return this.emitter.on(eventName, handler);
}
scheduleDecryptRetry(userPubkey, error) {
const normalized = normalizeHexPubkey(userPubkey) || userPubkey;
if (!normalized) {
return;
}
if (this.decryptRetryTimeoutId) {
clearTimeout(this.decryptRetryTimeoutId);
}
this.decryptRetryTimeoutId = setTimeout(() => {
this.decryptRetryTimeoutId = null;
if (this.currentUserPubkey && this.currentUserPubkey !== normalized) {
return;
}
this.updateFromRelays(normalized).catch((retryError) => {
userLogger.warn("[SubscriptionsManager] Decryption retry failed:", retryError);
});
}, DECRYPT_RETRY_DELAY_MS);
devLogger.log(
`[SubscriptionsManager] Decryption timed out; scheduling retry in ${DECRYPT_RETRY_DELAY_MS / 1000}s.`,
error,
);
}
async updateFromRelays(userPubkey) {
if (!userPubkey) return;
@@ -538,8 +564,14 @@ class SubscriptionsManager {
const decryptPromise = this.decryptSubscriptionEvent(newest, userPubkey);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Decryption timed out after 15s")),
15000,
() => {
const timeoutError = new Error(
`Decryption timed out after ${DECRYPT_TIMEOUT_MS / 1000}s`,
);
timeoutError.code = "subscriptions-decrypt-timeout";
reject(timeoutError);
},
DECRYPT_TIMEOUT_MS,
),
);
decryptResult = await Promise.race([decryptPromise, timeoutPromise]);
@@ -548,6 +580,13 @@ class SubscriptionsManager {
}
if (!decryptResult.ok) {
if (decryptResult.error?.code === "subscriptions-decrypt-timeout") {
if (!this.loaded && !cachedSnapshot.hasSnapshot) {
this.loaded = true;
}
this.scheduleDecryptRetry(normalizedUserPubkey, decryptResult.error);
return;
}
userLogger.error(
"[SubscriptionsManager] Failed to decrypt subscription list:",
decryptResult.error,
@@ -614,6 +653,10 @@ class SubscriptionsManager {
this.lastRunOptions = null;
this.lastResult = null;
this.hasRenderedOnce = false;
if (this.decryptRetryTimeoutId) {
clearTimeout(this.decryptRetryTimeoutId);
this.decryptRetryTimeoutId = null;
}
this.emitter.emit("change", { action: "reset", subscribedPubkeys: [] });
}
+50 -2
View File
@@ -72,6 +72,8 @@ export const USER_BLOCK_EVENTS = Object.freeze({
const FAST_BLOCKLIST_RELAY_LIMIT = 3;
const FAST_BLOCKLIST_TIMEOUT_MS = 2500;
const BACKGROUND_BLOCKLIST_TIMEOUT_MS = 6000;
const DECRYPT_TIMEOUT_MS = 90000;
const DECRYPT_RETRY_DELAY_MS = 10000;
function sanitizeRelayList(candidate) {
return Array.isArray(candidate)
@@ -643,6 +645,7 @@ class UserBlockListManager {
this.loaded = false;
this.emitter = new TinyEventEmitter();
this.seedStateCache = new Map();
this.decryptRetryTimeoutId = null;
profileCache.subscribe((event, detail) => {
if (event === "profileChanged") {
@@ -664,6 +667,10 @@ class UserBlockListManager {
this.muteEventId = null;
this.muteEventCreatedAt = null;
this.loaded = false;
if (this.decryptRetryTimeoutId) {
clearTimeout(this.decryptRetryTimeoutId);
this.decryptRetryTimeoutId = null;
}
}
on(eventName, handler) {
@@ -674,6 +681,32 @@ class UserBlockListManager {
return Array.from(this.blockedPubkeys);
}
scheduleDecryptRetry(userPubkey, error) {
if (!userPubkey) {
return;
}
if (this.decryptRetryTimeoutId) {
clearTimeout(this.decryptRetryTimeoutId);
}
this.decryptRetryTimeoutId = setTimeout(() => {
this.decryptRetryTimeoutId = null;
if (this.activePubkey !== userPubkey) {
return;
}
this.loadBlocks(userPubkey, {
since: Number.isFinite(this.blockEventCreatedAt)
? this.blockEventCreatedAt
: undefined,
}).catch((retryError) => {
userLogger.warn("[UserBlockList] Decryption retry failed:", retryError);
});
}, DECRYPT_RETRY_DELAY_MS);
devLogger.log(
`[UserBlockList] Decryption timed out; scheduling retry in ${DECRYPT_RETRY_DELAY_MS / 1000}s.`,
error,
);
}
isBlocked(pubkey) {
const normalized = normalizeHex(pubkey);
if (!normalized) {
@@ -1064,8 +1097,14 @@ class UserBlockListManager {
]);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Decryption timed out after 15s")),
15000,
() => {
const timeoutError = new Error(
`Decryption timed out after ${DECRYPT_TIMEOUT_MS / 1000}s`,
);
timeoutError.code = "user-blocklist-decrypt-timeout";
reject(timeoutError);
},
DECRYPT_TIMEOUT_MS,
),
);
@@ -1082,6 +1121,15 @@ class UserBlockListManager {
}
if (decryptionError) {
if (decryptionError.code === "user-blocklist-decrypt-timeout") {
this.blockedPubkeys = previousState.blockedPubkeys;
this.blockEventId = previousState.blockEventId;
this.blockEventCreatedAt = previousState.blockEventCreatedAt;
this.loaded = true;
emitStatus({ status: "settled" });
this.scheduleDecryptRetry(normalized, decryptionError);
return;
}
userLogger.warn(
"[UserBlockList] Decryption failed during merge.",
decryptionError,