Files
bitvid/js/services/hashtagPreferencesHelpers.js
T
thePR0M3TH3ANandClaude Opus 4.8 e27c2fae39 refactor(hashtags): extract pure helpers from hashtagPreferencesService (2/10)
File-size decomposition (no behavior change). Move the pure, state-free helpers
isTransientDecryptError, computeRetryDelay (+ their transient-error tables and
MAX_DECRYPT_RETRY_DELAY_MS) and normalizeHexPubkey out into a new
js/services/hashtagPreferencesHelpers.js and import them back. HEX64_REGEX is now
imported by the helper module instead of the service.

hashtagPreferencesService.js: 1541 -> 1480 lines (back under its 1484 limit).
Baseline left at 1434 (file fits its original allowance again; not bumped up).

Build + full unit suite green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-19 11:09:34 -04:00

73 lines
2.3 KiB
JavaScript

// js/services/hashtagPreferencesHelpers.js
//
// Pure helpers extracted from hashtagPreferencesService.js to keep that service
// under the file-size budget. No module state — safe to test in isolation.
import { HEX64_REGEX } from "../utils/hex.js";
const MAX_DECRYPT_RETRY_DELAY_MS = 60000;
// Transient decrypt failures that must RETRY in the background rather than give
// up: our own timeout, a severed nip-07 message port ("message channel closed"),
// and the circuit breaker's fast-fail. Without this a brief channel drop during
// the login burst abandons hashtag prefs until a refresh (KNOWN_BUGS #0).
const TRANSIENT_DECRYPT_ERROR_CODES = new Set([
"hashtag-preferences-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() : "";
if (TRANSIENT_DECRYPT_MESSAGE_PATTERNS.some((p) => message.includes(p))) {
return true;
}
// decryptEvent wraps the per-scheme Promise.any failures in a generic
// "decrypt-failed" error and nests the real causes under .errors/.cause —
// recurse so a channel-death sub-error still triggers a retry.
const nested = Array.isArray(error.errors)
? error.errors
: Array.isArray(error.cause)
? error.cause
: null;
if (nested) {
return nested.some((entry) => isTransientDecryptError(entry?.error || entry));
}
return false;
}
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);
}
export function normalizeHexPubkey(pubkey) {
if (typeof pubkey !== "string") {
return null;
}
const trimmed = pubkey.trim();
if (!trimmed) {
return null;
}
if (HEX64_REGEX.test(trimmed)) {
return trimmed.toLowerCase();
}
return null;
}