mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-09 14:22:53 +00:00
Improve Nostr watch history snapshot caching
This commit is contained in:
1 parent
9f79983d5d
commit
1ff3523bd0
7 files changed
+777
-39
No files matched your search
+14
-4
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user