mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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 <[email protected]>
This commit is contained in:
1 parent
37a675392f
commit
8871c4e3a1
2 files changed
+95
-83
No files matched your search
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user