Files
bitvid/js/utils/signerHealthNotice.js
T
thePR0M3TH3ANandClaude Opus 4.8 8f39b2c479 feat(signer): surface an actionable notice when the signer goes unresponsive
A raw window.nostr probe in the user's browser (getPublicKey → nip04.encrypt
→ nip04.decrypt, bypassing ALL of bitvid) HUNG forever, with the extension's
own console showing `ObjectMultiplex - orphaned data for "background-liveness"`
+ `malformed chunk` + listener-leak warnings. That is the NIP-07 extension's
content-script ↔ background-service-worker channel being dead (classic MV3
worker death) — an extension/environment failure, not a bitvid bug. No client
change can force an unresponsive signer to answer; the fix is to reload/unlock
the extension.

What bitvid CAN do is stop failing silently. Add js/utils/signerHealthNotice.js:
after a few consecutive list-decrypt timeouts while the signer reports itself
present, emit ONE rate-limited, actionable user notice ("your signer isn't
responding to decryption — unlock/reload the extension and refresh") instead of
the silent 15-minute retry loop. Reset on any decrypt success so a transient
blip doesn't warn. Wired into blocks, hashtags, and subscriptions.

Also documents the definitive root-cause + all resilience increments in
KNOWN_BUGS #0. Test: tests/signer-health-notice.test.mjs (threshold, rate-limit,
success-reset). Full unit suite green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 14:18:46 -04:00

69 lines
2.3 KiB
JavaScript

// Shared "is the signer actually answering?" notice.
//
// When encrypted lists (blocks, subscriptions, hashtags, DMs) repeatedly time
// out DESPITE a resolved signer (signerStatus "present"), the cause is almost
// always the NIP-07 extension's background/service-worker channel being
// unresponsive — not bitvid (confirmed by a raw window.nostr probe hanging;
// see KNOWN_BUGS #0). Silently retrying forever leaves the user with no idea
// why their lists never load. This surfaces ONE clear, actionable message so
// they can unlock/reload the extension, then stays quiet so it never spams.
import { userLogger } from "./logger.js";
const CONSECUTIVE_TIMEOUT_THRESHOLD = 3;
const NOTICE_COOLDOWN_MS = 120_000;
const NOTICE_MESSAGE =
"Your Nostr signer extension isn't responding to decryption requests, so " +
"encrypted data (hashtags, subscriptions, block list, DMs) can't load. This " +
"is usually the extension's background worker going to sleep or getting " +
"stuck. Try unlocking it and/or reloading the extension (toggle it off and " +
"on from your browser's extensions page), then refresh bitvid.";
let consecutiveTimeouts = 0;
let lastNoticeAt = 0;
let lastDetail = null;
/**
* Record that a list decrypt timed out while the signer reported itself present.
* After a few consecutive such timeouts, emit ONE user-facing notice (rate
* limited) telling the user their signer is unresponsive.
*/
export function noteSignerDecryptTimeout(context = "list") {
consecutiveTimeouts += 1;
lastDetail = { context, consecutiveTimeouts, at: Date.now() };
const now = Date.now();
if (
consecutiveTimeouts >= CONSECUTIVE_TIMEOUT_THRESHOLD &&
now - lastNoticeAt > NOTICE_COOLDOWN_MS
) {
lastNoticeAt = now;
userLogger.error(`[signer-unresponsive] ${NOTICE_MESSAGE}`, {
context,
consecutiveTimeouts,
});
}
}
/**
* Record that a signer call succeeded — the channel is alive, so reset the
* streak (a single later timeout shouldn't immediately re-warn).
*/
export function noteSignerDecryptSuccess() {
consecutiveTimeouts = 0;
}
export function getSignerHealthState() {
return { consecutiveTimeouts, lastNoticeAt, lastDetail };
}
export const __testExports = {
CONSECUTIVE_TIMEOUT_THRESHOLD,
NOTICE_COOLDOWN_MS,
reset() {
consecutiveTimeouts = 0;
lastNoticeAt = 0;
lastDetail = null;
},
};