mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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:
3 files changed
+141
-6
No files matched your search
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user