From 8871c4e3a1ad0466e6a50afeb5b10c0a34b26b0a Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:35:44 -0400 Subject: [PATCH] refactor(watch-history): extract pure codec helpers from watchHistory (5/10) File-size decomposition (no behavior change). Move the dependency-free encode/ decode helpers (sanitizeWatchHistoryMetadata, serializeWatchHistoryItems, bytesToHex, looksLikeJsonStructure, hexToBytesCompat) into a new js/nostr/watchHistoryCodec.js and import them back. watchHistory.js: 2512 -> 2435 lines (back under its 2438 limit). Build + full unit suite green. Co-Authored-By: Claude Opus 4.8 --- js/nostr/watchHistory.js | 90 +++-------------------------------- js/nostr/watchHistoryCodec.js | 88 ++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 83 deletions(-) create mode 100644 js/nostr/watchHistoryCodec.js diff --git a/js/nostr/watchHistory.js b/js/nostr/watchHistory.js index a1a177cd..67d678e8 100644 --- a/js/nostr/watchHistory.js +++ b/js/nostr/watchHistory.js @@ -14,6 +14,13 @@ import { NOTE_TYPES, } from "../nostrEventSchemas.js"; import { CACHE_POLICIES } from "./cachePolicies.js"; +import { + sanitizeWatchHistoryMetadata, + serializeWatchHistoryItems, + bytesToHex, + looksLikeJsonStructure, + hexToBytesCompat, +} from "./watchHistoryCodec.js"; import { publishEventToRelays } from "../nostrPublish.js"; import { RELAY_URLS, @@ -368,55 +375,6 @@ function canonicalizeWatchHistoryItems(rawItems, maxItems = WATCH_HISTORY_MAX_IT return buckets; } -function sanitizeWatchHistoryMetadata(metadata) { - return {}; -} - -function serializeWatchHistoryItems(items) { - if (!Array.isArray(items) || items.length === 0) { - return "[]"; - } - const normalized = items - .map((item) => { - const type = item?.type === "a" ? "a" : "e"; - const value = typeof item?.value === "string" ? item.value : ""; - if (!type || !value) { - return null; - } - const relay = - typeof item?.relay === "string" && item.relay.trim() - ? item.relay.trim() - : undefined; - const watchedAt = Number.isFinite(item?.watchedAt) - ? Math.max(0, Math.floor(item.watchedAt)) - : undefined; - const payload = { type, value }; - if (relay) { - payload.relay = relay; - } - if (watchedAt !== undefined) { - payload.watchedAt = watchedAt; - } - const resumeAt = Number.isFinite(item?.resumeAt) - ? Math.max(0, Math.floor(item.resumeAt)) - : undefined; - if (resumeAt !== undefined) { - payload.resumeAt = resumeAt; - } - if (item?.completed === true) { - payload.completed = true; - } - return payload; - }) - .filter(Boolean); - return JSON.stringify(normalized); -} - -function bytesToHex(bytes) { - return Array.from(bytes) - .map((byte) => byte.toString(16).padStart(2, "0")) - .join(""); -} async function computeWatchHistoryFingerprintForItems(itemsOrBuckets) { // Check if buckets @@ -454,40 +412,6 @@ async function computeWatchHistoryFingerprintForItems(itemsOrBuckets) { return `fallback:${serialized}`; } -function looksLikeJsonStructure(content) { - if (typeof content !== "string") { - return false; - } - const trimmed = content.trim(); - if (!trimmed) { - return false; - } - const first = trimmed[0]; - return first === "{" || first === "["; -} - -function hexToBytesCompat(hex, tools = null) { - if (typeof hex !== "string") { - throw new Error("Invalid hex input."); - } - const trimmed = hex.trim(); - if (!trimmed || trimmed.length % 2 !== 0) { - throw new Error("Invalid hex input."); - } - if (tools?.utils && typeof tools.utils.hexToBytes === "function") { - return tools.utils.hexToBytes(trimmed); - } - const bytes = new Uint8Array(trimmed.length / 2); - for (let index = 0; index < trimmed.length; index += 2) { - const byte = Number.parseInt(trimmed.slice(index, index + 2), 16); - if (Number.isNaN(byte)) { - throw new Error("Invalid hex input."); - } - bytes[index / 2] = byte; - } - return bytes; -} - // Memoizes NIP-44 conversation keys across an entire load. Deriving a // conversation key is an ECDH operation (~12ms each in-bundle); without this // cache the watch-history decrypt path re-derived the *same* key once per diff --git a/js/nostr/watchHistoryCodec.js b/js/nostr/watchHistoryCodec.js new file mode 100644 index 00000000..9c297862 --- /dev/null +++ b/js/nostr/watchHistoryCodec.js @@ -0,0 +1,88 @@ +// js/nostr/watchHistoryCodec.js +// +// Pure, dependency-free encode/decode helpers extracted from watchHistory.js to +// keep that module under the file-size budget. No behavior change. + +export function sanitizeWatchHistoryMetadata(metadata) { + return {}; +} + +export function serializeWatchHistoryItems(items) { + if (!Array.isArray(items) || items.length === 0) { + return "[]"; + } + const normalized = items + .map((item) => { + const type = item?.type === "a" ? "a" : "e"; + const value = typeof item?.value === "string" ? item.value : ""; + if (!type || !value) { + return null; + } + const relay = + typeof item?.relay === "string" && item.relay.trim() + ? item.relay.trim() + : undefined; + const watchedAt = Number.isFinite(item?.watchedAt) + ? Math.max(0, Math.floor(item.watchedAt)) + : undefined; + const payload = { type, value }; + if (relay) { + payload.relay = relay; + } + if (watchedAt !== undefined) { + payload.watchedAt = watchedAt; + } + const resumeAt = Number.isFinite(item?.resumeAt) + ? Math.max(0, Math.floor(item.resumeAt)) + : undefined; + if (resumeAt !== undefined) { + payload.resumeAt = resumeAt; + } + if (item?.completed === true) { + payload.completed = true; + } + return payload; + }) + .filter(Boolean); + return JSON.stringify(normalized); +} + +export function bytesToHex(bytes) { + return Array.from(bytes) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); +} + +export function looksLikeJsonStructure(content) { + if (typeof content !== "string") { + return false; + } + const trimmed = content.trim(); + if (!trimmed) { + return false; + } + const first = trimmed[0]; + return first === "{" || first === "["; +} + +export function hexToBytesCompat(hex, tools = null) { + if (typeof hex !== "string") { + throw new Error("Invalid hex input."); + } + const trimmed = hex.trim(); + if (!trimmed || trimmed.length % 2 !== 0) { + throw new Error("Invalid hex input."); + } + if (tools?.utils && typeof tools.utils.hexToBytes === "function") { + return tools.utils.hexToBytes(trimmed); + } + const bytes = new Uint8Array(trimmed.length / 2); + for (let index = 0; index < trimmed.length; index += 2) { + const byte = Number.parseInt(trimmed.slice(index, index + 2), 16); + if (Number.isNaN(byte)) { + throw new Error("Invalid hex input."); + } + bytes[index / 2] = byte; + } + return bytes; +}