mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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]>
This commit is contained in:
1 parent
76ba750708
commit
e27c2fae39
2 files changed
+77
-66
No files matched your search
@@ -0,0 +1,72 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -39,7 +39,11 @@ import {
|
||||
computeIncrementalSinceWithOverlap,
|
||||
selectNewestListEvent,
|
||||
} from "../nostr/listEventOrdering.js";
|
||||
import { HEX64_REGEX } from "../utils/hex.js";
|
||||
import {
|
||||
isTransientDecryptError,
|
||||
computeRetryDelay,
|
||||
normalizeHexPubkey,
|
||||
} from "./hashtagPreferencesHelpers.js";
|
||||
import { SHORT_TIMEOUT_MS, NETWORK_RETRY_DELAY_MS } from "../constants.js";
|
||||
|
||||
const LOG_PREFIX = "[HashtagPreferences]";
|
||||
@@ -57,54 +61,6 @@ const DECRYPT_TIMEOUT_MS = 30000;
|
||||
// PERF: Reduced from 3s to 1.5s — extensions that already granted permission
|
||||
// should recover near-instantly. Shorter delay speeds up the login path.
|
||||
const DECRYPT_RETRY_DELAY_MS = NETWORK_RETRY_DELAY_MS;
|
||||
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",
|
||||
];
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
class TinyEventEmitter {
|
||||
constructor() {
|
||||
this.listeners = new Map();
|
||||
@@ -153,23 +109,6 @@ const EVENTS = Object.freeze({
|
||||
CHANGE: "change",
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function normalizeEncryptionToken(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
|
||||
Reference in new issue
Block a user