Improve Nostr watch history snapshot caching

This commit is contained in:
thePR0M3TH3AN committed 2025-10-22 20:13:10 -04:00
1 parent 9f79983d5d
commit 1ff3523bd0
7 files changed
+777 -39

No files matched your search

+14 -4
View File
@@ -2452,6 +2452,11 @@ export class NostrClient {
!!normalizedLogged &&
activeSigner &&
typeof activeSigner.signEvent === "function";
const canSignWithExtension =
!!normalizedLogged &&
activeSigner &&
typeof activeSigner.signEvent === "function" &&
shouldRequestExtensionPermissions(activeSigner);
if (!forceRenew && canSignWithActiveSigner) {
return normalizedLogged;
@@ -3237,6 +3242,10 @@ export class NostrClient {
if (!pubkey) throw new Error("Not logged in to publish video.");
const { videoData, nip71Metadata } = extractVideoPublishPayload(videoPayload);
const nip71EditedFlag =
videoPayload && typeof videoPayload === "object"
? videoPayload.nip71Edited
: null;
// NOTE: Keep the Upload, Edit, and Revert flows synchronized when
// updating shared fields. Changes here must be reflected in the modal
@@ -3274,6 +3283,7 @@ export class NostrClient {
const finalIsNsfw = videoData.isNsfw === true;
const finalIsForKids =
videoData.isForKids === true && !finalIsNsfw;
const wantPrivate = videoData.isPrivate === true;
const finalWs =
typeof videoData.ws === "string" ? videoData.ws.trim() : "";
const finalXs =
@@ -3389,10 +3399,10 @@ export class NostrClient {
description: contentObject.description,
url: contentObject.url,
magnet: contentObject.magnet,
thumbnail: contentObject.thumbnail,
mode: contentObject.mode,
isPrivate: contentObject.isPrivate,
isNsfw: contentObject.isNsfw,
thumbnail: contentObject.thumbnail,
mode: contentObject.mode,
isPrivate: wantPrivate,
isNsfw: contentObject.isNsfw,
isForKids: contentObject.isForKids,
};
+87 -6
View File
@@ -9,20 +9,61 @@ import {
import { buildRepostEvent, buildVideoMirrorEvent } from "../nostrEventSchemas.js";
import { normalizePointerInput } from "./watchHistory.js";
import { sanitizeRelayList } from "./nip46Client.js";
import { RELAY_URLS, getCachedNostrTools } from "./toolkit.js";
import {
RELAY_URLS,
getCachedNostrTools,
readToolkitFromScope,
} from "./toolkit.js";
import { DEFAULT_NIP07_PERMISSION_METHODS } from "./nip07Permissions.js";
import { devLogger, userLogger } from "../utils/logger.js";
const REBROADCAST_GUARD_PREFIX = "bitvid:rebroadcast:v1";
const rebroadcastAttemptMemory = new Map();
const HEX_PRIVATE_KEY_REGEX = /^[0-9a-f]{64}$/i;
export function signEventWithPrivateKey(event, privateKey) {
const tools = getCachedNostrTools();
const scopedTools = readToolkitFromScope();
const canonicalTools =
typeof globalThis !== "undefined" &&
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__
? globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__
: null;
const signFn =
typeof canonicalTools?.signEvent === "function"
? canonicalTools.signEvent
: typeof tools?.signEvent === "function"
? tools.signEvent
: typeof scopedTools?.signEvent === "function"
? scopedTools.signEvent
: null;
// console.debug('signEventWithPrivateKey', {
// signFn: typeof signFn,
// scopedSign: typeof scopedTools?.signEvent,
// cachedSign: typeof tools?.signEvent,
// });
const finalizeFn =
typeof canonicalTools?.finalizeEvent === "function"
? canonicalTools.finalizeEvent
: typeof tools?.finalizeEvent === "function"
? tools.finalizeEvent
: typeof scopedTools?.finalizeEvent === "function"
? scopedTools.finalizeEvent
: null;
const hashFn =
typeof canonicalTools?.getEventHash === "function"
? canonicalTools.getEventHash
: typeof tools?.getEventHash === "function"
? tools.getEventHash
: typeof scopedTools?.getEventHash === "function"
? scopedTools.getEventHash
: null;
if (
!privateKey ||
typeof privateKey !== "string" ||
!tools?.getEventHash ||
typeof tools.signEvent !== "function"
(!signFn && !finalizeFn)
) {
throw new Error("Missing signing primitives");
}
@@ -38,10 +79,50 @@ export function signEventWithPrivateKey(event, privateKey) {
tags,
content: typeof event.content === "string" ? event.content : "",
};
const id = tools.getEventHash(prepared);
const sig = tools.signEvent(prepared, privateKey);
return { ...prepared, id, sig };
if (signFn) {
const signature = signFn(prepared, privateKey);
if (signature && typeof signature === "object") {
const idValue =
typeof signature.id === "string"
? signature.id
: hashFn
? hashFn(prepared)
: null;
const sigValue =
typeof signature.sig === "string"
? signature.sig
: typeof signature.signature === "string"
? signature.signature
: null;
if (!idValue || !sigValue) {
throw new Error("Missing signing primitives");
}
return { ...prepared, id: idValue, sig: sigValue };
}
const sigValue = typeof signature === "string" ? signature : null;
if (!sigValue || !hashFn) {
throw new Error("Missing signing primitives");
}
const idValue = hashFn(prepared);
return { ...prepared, id: idValue, sig: sigValue };
}
const normalizedKey = privateKey.trim();
if (!HEX_PRIVATE_KEY_REGEX.test(normalizedKey)) {
throw new Error("Missing signing primitives");
}
const finalized = finalizeFn(prepared, normalizedKey);
if (
!finalized ||
typeof finalized.id !== "string" ||
typeof finalized.sig !== "string"
) {
throw new Error("Missing signing primitives");
}
return finalized;
}
export function deriveRebroadcastBucketIndex(referenceSeconds = null) {
+91 -12
View File
@@ -88,7 +88,9 @@ function mergePointerDetails(target, source) {
target.watchedAt = Math.max(0, Math.floor(source.watchedAt));
}
if (typeof source.relay === "string" && source.relay.trim()) {
target.relay = source.relay.trim();
if (!target.relay || !target.relay.trim()) {
target.relay = source.relay.trim();
}
}
if (!target.metadata && source.metadata && typeof source.metadata === "object") {
target.metadata = clonePointerMetadata(source.metadata);
@@ -175,11 +177,7 @@ export function pointerKey(pointer) {
if (!type || !value) {
return "";
}
const relay =
typeof pointer.relay === "string" && pointer.relay.trim()
? pointer.relay.trim().toLowerCase()
: "";
return relay ? `${type}:${value}:${relay}` : `${type}:${value}`;
return `${type}:${value}`;
}
function normalizePointerTag(tag) {
@@ -466,7 +464,10 @@ export function normalizeActorKey(actor) {
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
return trimmed.toLowerCase();
}
const decoder = getCachedNostrTools()?.nip19?.decode;
const toolkit = getCachedNostrTools();
const decoder =
(typeof window !== "undefined" ? window?.NostrTools?.nip19?.decode : null) ||
toolkit?.nip19?.decode;
if (typeof decoder === "function") {
try {
const decoded = decoder(trimmed);
@@ -729,7 +730,11 @@ export function isNip04EncryptedWatchHistoryEvent(pointerEvent, ciphertext) {
class WatchHistoryManager {
constructor(deps) {
this.deps = deps || {};
this.deps = {
ensureNostrTools,
getCachedNostrTools,
...(deps || {}),
};
this.cache = new Map();
this.storage = null;
this.republishTimers = new Map();
@@ -1225,11 +1230,51 @@ class WatchHistoryManager {
if (cachedNip04Tools) {
return cachedNip04Tools;
}
const tools = await ensureNostrTools();
if (tools?.nip04 && typeof tools.nip04.encrypt === "function") {
cachedNip04Tools = tools;
return cachedNip04Tools;
const ensureToolkit =
typeof this.deps.ensureNostrTools === "function"
? this.deps.ensureNostrTools
: ensureNostrTools;
if (ensureToolkit) {
try {
const ensured = await ensureToolkit();
if (
ensured?.nip04 &&
typeof ensured.nip04.encrypt === "function"
) {
cachedNip04Tools = ensured;
return cachedNip04Tools;
}
} catch (error) {
devLogger.warn(
"[nostr] Failed to resolve nostr-tools for watch history:",
error,
);
}
}
const readCachedToolkit =
typeof this.deps.getCachedNostrTools === "function"
? this.deps.getCachedNostrTools
: getCachedNostrTools;
if (readCachedToolkit) {
try {
const cached = await readCachedToolkit();
if (
cached?.nip04 &&
typeof cached.nip04.encrypt === "function"
) {
cachedNip04Tools = cached;
return cachedNip04Tools;
}
} catch (error) {
devLogger.warn(
"[nostr] Failed to read cached nostr-tools for watch history:",
error,
);
}
}
return null;
};
const encryptChunk = async (plaintext) => {
@@ -1483,6 +1528,40 @@ class WatchHistoryManager {
if (!success && errorCode) {
result.error = errorCode;
}
const shouldPersistLocally =
pointerAcceptedCount > 0 && !chunkRejectedEverywhere;
if (shouldPersistLocally) {
const storage = this.getStorage();
const previousEntry =
this.cache.get(actorKey) || storage.actors?.[actorKey] || {};
const metadata = sanitizeWatchHistoryMetadata(previousEntry.metadata);
metadata.updatedAt = Date.now();
metadata.status = success ? "ok" : "partial";
metadata.lastPublishResults = result.publishResults;
metadata.skippedCount = result.skippedCount || 0;
if (success) {
delete metadata.lastError;
} else {
metadata.lastError = result.error || "publish-partial";
}
const entry = {
actor: actorPubkey,
items: canonicalItems,
snapshotId,
pointerEvent: result.pointerEvent,
chunkEvents: result.chunkEvents,
savedAt: Date.now(),
fingerprint: await this.getFingerprint(actorPubkey, canonicalItems),
metadata,
};
this.cache.set(actorKey, entry);
this.persistEntry(actorKey, entry);
}
devLogger.info("[nostr] Watch history snapshot publish result.", {
actor: actorKey,
snapshotId,