refactor(blocks): extract helpers from userBlocks (3/10)

File-size decomposition (no behavior change). Move the state-free TinyEventEmitter
and the decrypt-retry helpers (isTransientDecryptError, computeRetryDelay + their
transient-error tables and MAX_DECRYPT_RETRY_DELAY_MS) into js/userBlocksHelpers.js.

userBlocks.js: 2520 -> 2443 lines (back under its 2478 limit).

Build + full unit suite green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-19 11:33:35 -04:00
1 parent e27c2fae39
commit b068854c94
2 files changed
+97 -81

No files matched your search

+5 -81
View File
@@ -12,6 +12,11 @@ import {
KIND_MUTE_LIST,
} from "./nostrEventSchemas.js";
import { devLogger, userLogger } from "./utils/logger.js";
import {
TinyEventEmitter,
isTransientDecryptError,
computeRetryDelay,
} from "./userBlocksHelpers.js";
import {
noteSignerDecryptTimeout,
noteSignerDecryptSuccess,
@@ -40,50 +45,6 @@ import {
selectNewestListEvent,
} from "./nostr/listEventOrdering.js";
class TinyEventEmitter {
constructor() {
this.listeners = new Map();
}
on(eventName, handler) {
if (typeof eventName !== "string" || typeof handler !== "function") {
return () => {};
}
if (!this.listeners.has(eventName)) {
this.listeners.set(eventName, new Set());
}
const handlers = this.listeners.get(eventName);
handlers.add(handler);
return () => {
handlers.delete(handler);
if (!handlers.size) {
this.listeners.delete(eventName);
}
};
}
emit(eventName, detail) {
const handlers = this.listeners.get(eventName);
if (!handlers || !handlers.size) {
return;
}
for (const handler of Array.from(handlers)) {
try {
handler(detail);
} catch (error) {
userLogger.warn(
`[UserBlockList] listener for "${eventName}" threw an error`,
error,
);
}
}
}
}
export const USER_BLOCK_EVENTS = Object.freeze({
CHANGE: "change",
STATUS: "status",
@@ -107,43 +68,6 @@ const DECRYPT_RETRY_DELAY_MS = 3000;
// Cap the exponential backoff well above the decrypt budget so a persistently
// unresponsive signer settles into an occasional heartbeat instead of pinning
// the extension/CPU every ~15s indefinitely.
const MAX_DECRYPT_RETRY_DELAY_MS = 60000;
// Transient decrypt failures that must KEEP the stale list and RETRY in the
// background (the channel/extension recovers shortly), rather than giving up
// permanently. Besides our own timeout, this covers a severed nip-07 message
// port ("message channel closed" — KNOWN_BUGS #0) and the circuit breaker's
// fast-fail while it probes for recovery. Without this, a brief channel drop
// during the login burst abandons the block list until a page refresh, even
// though the channel comes back seconds later.
const TRANSIENT_DECRYPT_ERROR_CODES = new Set([
"user-blocklist-decrypt-timeout",
"nip07-channel-unresponsive",
]);
const TRANSIENT_DECRYPT_MESSAGE_PATTERNS = [
"message channel closed",
"could not establish connection",
"receiving end does not exist",
"extension context invalidated",
"connection lost",
"channel is unresponsive",
];
function isTransientDecryptError(error) {
if (!error) return false;
if (error.code && TRANSIENT_DECRYPT_ERROR_CODES.has(error.code)) {
return true;
}
const message =
typeof error.message === "string" ? error.message.toLowerCase() : "";
return TRANSIENT_DECRYPT_MESSAGE_PATTERNS.some((p) => message.includes(p));
}
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) {
if (!Array.isArray(candidate)) return [];
const seen = new Set();
+92
View File
@@ -0,0 +1,92 @@
// js/userBlocksHelpers.js
//
// State-free helpers extracted from userBlocks.js to keep that module under the
// file-size budget. No behavior change.
import { userLogger } from "./utils/logger.js";
export class TinyEventEmitter {
constructor() {
this.listeners = new Map();
}
on(eventName, handler) {
if (typeof eventName !== "string" || typeof handler !== "function") {
return () => {};
}
if (!this.listeners.has(eventName)) {
this.listeners.set(eventName, new Set());
}
const handlers = this.listeners.get(eventName);
handlers.add(handler);
return () => {
handlers.delete(handler);
if (!handlers.size) {
this.listeners.delete(eventName);
}
};
}
emit(eventName, detail) {
const handlers = this.listeners.get(eventName);
if (!handlers || !handlers.size) {
return;
}
for (const handler of Array.from(handlers)) {
try {
handler(detail);
} catch (error) {
userLogger.warn(
`[UserBlockList] listener for "${eventName}" threw an error`,
error,
);
}
}
}
}
// Cap the exponential backoff well above the decrypt budget so a persistently
// unresponsive signer settles into an occasional heartbeat instead of pinning
// the extension/CPU every ~15s indefinitely.
const MAX_DECRYPT_RETRY_DELAY_MS = 60000;
// Transient decrypt failures that must KEEP the stale list and RETRY in the
// background (the channel/extension recovers shortly), rather than giving up
// permanently. Besides our own timeout, this covers a severed nip-07 message
// port ("message channel closed" — KNOWN_BUGS #0) and the circuit breaker's
// fast-fail while it probes for recovery. Without this, a brief channel drop
// during the login burst abandons the block list until a page refresh, even
// though the channel comes back seconds later.
const TRANSIENT_DECRYPT_ERROR_CODES = new Set([
"user-blocklist-decrypt-timeout",
"nip07-channel-unresponsive",
]);
const TRANSIENT_DECRYPT_MESSAGE_PATTERNS = [
"message channel closed",
"could not establish connection",
"receiving end does not exist",
"extension context invalidated",
"connection lost",
"channel is unresponsive",
];
export function isTransientDecryptError(error) {
if (!error) return false;
if (error.code && TRANSIENT_DECRYPT_ERROR_CODES.has(error.code)) {
return true;
}
const message =
typeof error.message === "string" ? error.message.toLowerCase() : "";
return TRANSIENT_DECRYPT_MESSAGE_PATTERNS.some((p) => message.includes(p));
}
export 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);
}