Merge pull request #262 from PR0M3TH3AN/codex/add-detailed-logging-for-watch-list-process

Add detailed logging for watch history flow
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-02 18:46:51 -04:00
commit 245f6bd0d1
5 files changed
+699 -59

No files matched your search

+65 -7
View File
@@ -6749,10 +6749,19 @@ class bitvidApp {
this.resetViewLoggingState();
}
const trimmedPubkey =
typeof pubkey === "string" && pubkey.trim() ? pubkey.trim() : "";
if (normalizedPubkey) {
this.pubkey = normalizedPubkey;
if (nostrClient && typeof nostrClient === "object") {
nostrClient.pubkey = normalizedPubkey;
}
} else {
this.pubkey = pubkey;
if (nostrClient && typeof nostrClient === "object") {
nostrClient.pubkey = trimmedPubkey ? trimmedPubkey.toLowerCase() : "";
}
}
this.currentUserNpub = this.safeEncodeNpub(this.pubkey);
@@ -7585,11 +7594,54 @@ class bitvidApp {
(async () => {
let viewResult;
try {
const watchHistoryEnabled =
watchHistoryService?.isEnabled?.() === true &&
typeof watchHistoryService.publishView === "function";
if (watchHistoryEnabled) {
viewResult = await watchHistoryService.publishView(thresholdPointer);
const canUseWatchHistoryService =
typeof watchHistoryService?.publishView === "function";
const resolveWatchActor = () => {
const normalizedUser = this.normalizeHexPubkey(this.pubkey);
if (normalizedUser) {
return normalizedUser;
}
const normalizedClient = this.normalizeHexPubkey(
nostrClient?.pubkey
);
if (normalizedClient) {
return normalizedClient;
}
const normalizedSession = this.normalizeHexPubkey(
nostrClient?.sessionActor?.pubkey
);
if (normalizedSession) {
return normalizedSession;
}
if (
typeof nostrClient?.pubkey === "string" &&
nostrClient.pubkey.trim()
) {
return nostrClient.pubkey.trim().toLowerCase();
}
if (
typeof nostrClient?.sessionActor?.pubkey === "string" &&
nostrClient.sessionActor.pubkey.trim()
) {
return nostrClient.sessionActor.pubkey.trim().toLowerCase();
}
return "";
};
const activeWatchActor = resolveWatchActor();
const watchMetadata = activeWatchActor ? { actor: activeWatchActor } : undefined;
if (canUseWatchHistoryService) {
viewResult = await watchHistoryService.publishView(
thresholdPointer,
undefined,
watchMetadata
);
} else if (typeof nostrClient?.recordVideoView === "function") {
viewResult = await nostrClient.recordVideoView(thresholdPointer);
} else {
@@ -11641,5 +11693,11 @@ function dedupeToNewestByRoot(videos) {
export const app = new bitvidApp();
export const appReady = app.init();
window.app = app;
window.appReady = appReady;
if (typeof window !== "undefined") {
window.app = app;
window.appReady = appReady;
window.bitvid = window.bitvid || {};
window.bitvid.app = app;
window.bitvid.appReady = appReady;
}
+49 -7
View File
@@ -16,7 +16,7 @@ export const WATCH_HISTORY_EMPTY_COPY =
"Your watch history is empty. Watch some videos to populate this list.";
export const WATCH_HISTORY_DISABLED_COPY =
"Encrypted watch history sync is disabled by this server. Local history stays on this device only.";
"Watch history sync is unavailable. Connect a NIP-07 extension or log in to enable encrypted syncing.";
const WATCH_HISTORY_METADATA_PREF_KEY =
"bitvid:watch-history:metadata-preference";
@@ -739,6 +739,19 @@ export function createWatchHistoryRenderer(config = {}) {
},
} = config;
const syncEnabled =
typeof watchHistoryService.isEnabled === "function"
? watchHistoryService.isEnabled() === true
: false;
const localSupported =
typeof watchHistoryService.supportsLocalHistory === "function"
? watchHistoryService.supportsLocalHistory() === true
: false;
const localOnly =
typeof watchHistoryService.isLocalOnly === "function"
? watchHistoryService.isLocalOnly() === true
: false;
const state = {
initialized: false,
actor: null,
@@ -763,7 +776,10 @@ export function createWatchHistoryRenderer(config = {}) {
? watchHistoryService.shouldStoreMetadata() !== false
: true,
sessionFallbackActive: false,
featureEnabled: watchHistoryService.isEnabled?.() === true,
syncEnabled,
localSupported,
localOnly,
featureEnabled: syncEnabled || localSupported,
};
let elements = {
@@ -895,18 +911,44 @@ export function createWatchHistoryRenderer(config = {}) {
}
function updateFeatureBanner() {
const enabled = watchHistoryService.isEnabled?.() === true;
state.featureEnabled = enabled;
const actor = typeof state.actor === "string" ? state.actor : undefined;
const syncEnabled =
typeof watchHistoryService.isEnabled === "function"
? watchHistoryService.isEnabled(actor) === true
: false;
const localSupported =
typeof watchHistoryService.supportsLocalHistory === "function"
? watchHistoryService.supportsLocalHistory(actor) === true
: false;
const localOnly =
typeof watchHistoryService.isLocalOnly === "function"
? watchHistoryService.isLocalOnly(actor) === true
: false;
state.syncEnabled = syncEnabled;
state.localSupported = localSupported;
state.localOnly = localOnly;
state.featureEnabled = syncEnabled || localSupported;
if (!(elements.featureBanner instanceof HTMLElement)) {
return;
}
if (enabled) {
if (syncEnabled) {
elements.featureBanner.textContent = "";
setHidden(elements.featureBanner, true);
return;
}
elements.featureBanner.textContent =
"Watch history sync is disabled on this server. Local history only.";
if (localOnly) {
elements.featureBanner.textContent =
"Watch history sync requires a logged-in Nostr account. This guest session is stored locally only.";
setHidden(elements.featureBanner, false);
return;
}
if (localSupported) {
elements.featureBanner.textContent =
"Watch history sync is disabled right now. We'll keep a local copy on this device.";
} else {
elements.featureBanner.textContent =
"Watch history is disabled on this server.";
}
setHidden(elements.featureBanner, false);
}
+92
View File
@@ -2319,6 +2319,19 @@ class NostrClient {
relays = Array.from(RELAY_URLS);
}
console.info(
"[nostr] Preparing to publish watch history snapshot.",
{
actor: actorKey,
snapshotId,
itemCount: canonicalItems.length,
chunkCount: chunks.length,
relaysRequested: relays,
attempt: options.attempt || 0,
source: options.source || "unknown",
}
);
const createdAtBase = Math.max(
Math.floor(Date.now() / 1000),
this.watchHistoryLastCreatedAt + 1,
@@ -2404,6 +2417,17 @@ class NostrClient {
return tag;
});
console.info(
"[nostr] Publishing watch history chunk.",
{
actor: actorKey,
snapshotId,
chunkIndex: index,
chunkSize: chunkItems.length,
relays,
}
);
const plaintext = JSON.stringify({
version: 2,
snapshot: snapshotId,
@@ -2504,6 +2528,15 @@ class NostrClient {
return { ok: false, error: "signing-failed", retryable: false };
}
console.info(
"[nostr] Publishing watch history pointer event.",
{
actor: actorKey,
snapshotId,
relays,
}
);
const pointerResults = await publishEventToRelays(
this.pool,
relays,
@@ -2588,6 +2621,16 @@ class NostrClient {
result.error = errorCode;
}
console.info("[nostr] Watch history snapshot publish result.", {
actor: actorKey,
snapshotId,
success,
partialAcceptance,
error: result.error || null,
pointerAcceptedCount,
chunkAcceptedCounts: chunkResults.map((entry) => entry.acceptedCount),
});
return result;
}
@@ -2628,6 +2671,13 @@ class NostrClient {
canonicalItems,
);
console.info("[nostr] Updating watch history list.", {
actor: resolvedActor,
incomingItemCount: incomingItems.length,
finalItemCount: canonicalItems.length,
replace: options.replace === true,
});
const publishResult = await this.publishWatchHistorySnapshot(
canonicalItems,
{
@@ -2637,6 +2687,13 @@ class NostrClient {
},
);
console.info("[nostr] Watch history list publish attempt finished.", {
actor: resolvedActor,
snapshotId: publishResult.snapshotId || null,
success: !!publishResult.ok,
retryable: !!publishResult.retryable,
});
const metadata = sanitizeWatchHistoryMetadata(cachedEntry.metadata);
metadata.updatedAt = Date.now();
metadata.status = publishResult.ok ? "ok" : "error";
@@ -2734,6 +2791,11 @@ class NostrClient {
return { pointerEvent: null, items: [], snapshotId: "" };
}
console.info("[nostr] Fetching watch history from relays.", {
actor: resolvedActor,
forceRefresh: options.forceRefresh === true,
});
const extension = window?.nostr;
const existingEntry = this.watchHistoryCache.get(actorKey);
@@ -2746,6 +2808,11 @@ class NostrClient {
Number.isFinite(existingEntry.savedAt) &&
now - existingEntry.savedAt < ttl
) {
console.info("[nostr] Using cached watch history entry.", {
actor: resolvedActor,
itemCount: Array.isArray(existingEntry.items) ? existingEntry.items.length : 0,
cacheAgeMs: now - existingEntry.savedAt,
});
return {
pointerEvent: existingEntry.pointerEvent || null,
items: existingEntry.items || [],
@@ -2754,6 +2821,7 @@ class NostrClient {
}
if (!this.pool) {
console.warn("[nostr] Cannot fetch watch history because relay pool is unavailable. Returning cached values.");
return {
pointerEvent: existingEntry?.pointerEvent || null,
items: existingEntry?.items || [],
@@ -2820,6 +2888,12 @@ class NostrClient {
}, null);
if (!pointerEvent) {
console.info(
"[nostr] No watch history pointer event found on relays. Falling back to storage.",
{
actor: resolvedActor,
}
);
const storageEntry = this.getWatchHistoryStorage().actors?.[actorKey];
const items = Array.isArray(storageEntry?.items)
? canonicalizeWatchHistoryItems(storageEntry.items, WATCH_HISTORY_MAX_ITEMS)
@@ -3080,6 +3154,11 @@ class NostrClient {
return [];
}
console.info("[nostr] Resolving watch history for actor.", {
actor: resolvedActor,
forceRefresh: options.forceRefresh === true,
});
const storage = this.getWatchHistoryStorage();
const fallbackItems = Array.isArray(storage.actors?.[actorKey]?.items)
? canonicalizeWatchHistoryItems(
@@ -3123,6 +3202,19 @@ class NostrClient {
canonicalItems,
);
console.info("[nostr] Watch history fetch complete.", {
actor: resolvedActor,
snapshotId: fetchResult.snapshotId || null,
pointerFound: !!fetchResult.pointerEvent,
itemCount: canonicalItems.length,
});
console.info("[nostr] Watch history resolved and cached.", {
actor: resolvedActor,
itemCount: canonicalItems.length,
snapshotId: fetchResult.snapshotId || null,
});
const entry = {
actor: resolvedActor,
items: canonicalItems,
+272 -44
View File
@@ -36,10 +36,10 @@ const state = {
},
};
function isFeatureEnabled() {
function resolveFlagEnabled() {
try {
if (typeof getWatchHistoryV2Enabled === "function") {
return getWatchHistoryV2Enabled();
return getWatchHistoryV2Enabled() === true;
}
} catch (error) {
if (isDevMode) {
@@ -52,6 +52,115 @@ function isFeatureEnabled() {
return FEATURE_WATCH_HISTORY_V2 === true;
}
function getLoggedInActorKey() {
const direct = normalizeActorKey(nostrClient?.pubkey);
if (direct) {
return direct;
}
if (typeof window !== "undefined") {
const appCandidate =
(window.bitvid && window.bitvid.app) || window.app || null;
if (appCandidate && typeof appCandidate === "object") {
if (typeof appCandidate.normalizeHexPubkey === "function") {
try {
const normalized = appCandidate.normalizeHexPubkey(
appCandidate.pubkey
);
if (normalized) {
return normalizeActorKey(normalized);
}
} catch (error) {
if (isDevMode) {
console.warn(
"[watchHistoryService] Failed to normalize app login pubkey:",
error
);
}
}
}
if (typeof appCandidate.pubkey === "string" && appCandidate.pubkey) {
const fallback = normalizeActorKey(appCandidate.pubkey);
if (fallback) {
return fallback;
}
}
}
}
return "";
}
function getSessionActorKey() {
const logged = getLoggedInActorKey();
if (logged) {
return "";
}
return normalizeActorKey(nostrClient?.sessionActor?.pubkey);
}
function resolveEffectiveActorKey(actorInput) {
const supplied =
typeof actorInput === "string" && actorInput.trim()
? normalizeActorKey(actorInput)
: "";
if (supplied) {
return supplied;
}
const logged = getLoggedInActorKey();
if (logged) {
return logged;
}
const session = getSessionActorKey();
if (session) {
return session;
}
return "";
}
function isFeatureEnabled(actorInput) {
const baseEnabled = resolveFlagEnabled();
const actorKey = resolveEffectiveActorKey(actorInput);
const logged = getLoggedInActorKey();
const session = getSessionActorKey();
if (actorKey && logged && actorKey === logged) {
return true;
}
if (actorKey && session && actorKey === session) {
return false;
}
if (!actorKey) {
if (logged) {
return true;
}
if (session) {
return false;
}
}
return baseEnabled;
}
function isLocalOnly(actorInput) {
const actorKey = resolveEffectiveActorKey(actorInput);
const session = getSessionActorKey();
if (!session) {
return false;
}
if (!actorKey) {
return true;
}
return actorKey === session;
}
function supportsLocalHistory(actorInput) {
return isLocalOnly(actorInput);
}
function getSessionStorage() {
try {
if (typeof window !== "undefined" && window.sessionStorage) {
@@ -454,9 +563,6 @@ function getOrCreateQueue(actorKey) {
}
function restoreQueueState() {
if (!isFeatureEnabled()) {
return;
}
if (state.restored) {
return;
}
@@ -560,16 +666,13 @@ function restoreQueueState() {
queue.lastSnapshotReason = null;
}
if (queue.pendingSnapshotId && isFeatureEnabled()) {
if (queue.pendingSnapshotId && isFeatureEnabled(actorKey)) {
scheduleRepublishForQueue(actorKey);
}
}
}
function ensureQueue(actorKey) {
if (!isFeatureEnabled()) {
return null;
}
if (!actorKey) {
return null;
}
@@ -580,9 +683,6 @@ function ensureQueue(actorKey) {
}
function persistQueueState() {
if (!isFeatureEnabled()) {
return;
}
const storage = getSessionStorage();
if (!storage) {
return;
@@ -647,9 +747,6 @@ function resolveActorKey(actorInput) {
}
function collectQueueItems(actorKey) {
if (!isFeatureEnabled()) {
return [];
}
const queue = state.queues.get(actorKey);
if (!queue) {
return [];
@@ -675,9 +772,6 @@ function collectQueueItems(actorKey) {
}
function notifyQueueChange(actorKey) {
if (!isFeatureEnabled()) {
return;
}
emit("queue-changed", {
actor: actorKey,
items: collectQueueItems(actorKey),
@@ -685,9 +779,6 @@ function notifyQueueChange(actorKey) {
}
function clearQueue(actorKey) {
if (!isFeatureEnabled()) {
return;
}
const queue = state.queues.get(actorKey);
if (!queue) {
return;
@@ -720,7 +811,7 @@ function resolveWatchedAt(...candidates) {
}
function scheduleRepublishForQueue(actorKey) {
if (!isFeatureEnabled()) {
if (!isFeatureEnabled(actorKey)) {
return;
}
const queue = state.queues.get(actorKey);
@@ -825,12 +916,21 @@ async function publishView(pointerInput, createdAt, metadata = {}) {
const viewResult = await nostrClient.recordVideoView(pointerInput, recordOptions);
if (!isFeatureEnabled()) {
return viewResult;
}
console.info(
"[watchHistoryService] Video view recorded. Preparing watch list update.",
{
pointer: pointerInput,
createdAt,
hasMetadata: !!metadata,
}
);
const pointer = normalizePointerInput(pointerInput);
if (!pointer) {
console.warn(
"[watchHistoryService] Skipping watch list update because pointer normalization failed.",
{ pointerInput }
);
return viewResult;
}
@@ -842,16 +942,30 @@ async function publishView(pointerInput, createdAt, metadata = {}) {
"";
const actorKey = normalizeActorKey(actorCandidate);
if (!actorKey) {
console.warn(
"[watchHistoryService] Unable to resolve actor for watch list update.",
{ actorCandidate }
);
return viewResult;
}
const queue = ensureQueue(actorKey);
if (!queue) {
console.warn(
"[watchHistoryService] Failed to resolve queue for actor.",
{ actorKey }
);
return viewResult;
}
const remoteEnabled = isFeatureEnabled(actorKey);
const normalizedPointer = normalizePointerInput(pointer);
if (!normalizedPointer) {
console.warn(
"[watchHistoryService] Pointer normalization failed during queue preparation.",
{ pointerInput }
);
return viewResult;
}
@@ -860,6 +974,18 @@ async function publishView(pointerInput, createdAt, metadata = {}) {
viewResult?.event?.created_at
);
const key = pointerKey(normalizedPointer);
console.info(
"[watchHistoryService] Watch list process triggered by video view.",
{
actor: actorKey,
pointerKey: key,
watchedAt: normalizedPointer.watchedAt,
sessionEvent: normalizedPointer.session === true,
remoteSyncEnabled: remoteEnabled,
}
);
const normalizedLogged = normalizeActorKey(nostrClient?.pubkey);
const normalizedEventActor = normalizeActorKey(
viewResult?.event?.pubkey || actorCandidate
@@ -868,8 +994,11 @@ async function publishView(pointerInput, createdAt, metadata = {}) {
normalizedPointer.session = true;
}
const key = pointerKey(normalizedPointer);
if (!key) {
console.warn(
"[watchHistoryService] Pointer key generation failed. Skipping queue update.",
{ pointer: normalizedPointer }
);
return viewResult;
}
@@ -896,6 +1025,14 @@ async function publishView(pointerInput, createdAt, metadata = {}) {
queue.throttle.set(key, now);
persistQueueState();
notifyQueueChange(actorKey);
console.info(
"[watchHistoryService] Updated existing watch list entry.",
{
actor: actorKey,
pointerKey: key,
watchedAt: existing.pointer.watchedAt,
}
);
return viewResult;
}
@@ -918,19 +1055,48 @@ async function publishView(pointerInput, createdAt, metadata = {}) {
persistQueueState();
notifyQueueChange(actorKey);
console.info(
"[watchHistoryService] Added pointer to pending watch list snapshot queue.",
{
actor: actorKey,
pointerKey: key,
queueSize: queue.items.size,
}
);
if (!remoteEnabled) {
console.info(
"[watchHistoryService] Watch history sync unavailable for this actor; retaining pointer in local session queue only.",
{
actor: actorKey,
pointerKey: key,
watchedAt: normalizedPointer.watchedAt,
}
);
}
return viewResult;
}
async function snapshot(items, options = {}) {
if (!isFeatureEnabled()) {
return { ok: true, skipped: true, reason: "feature-disabled" };
}
const reason = typeof options.reason === "string" ? options.reason : "manual";
const actorKey = resolveActorKey(options.actor);
if (!actorKey) {
return { ok: false, error: "missing-actor" };
}
if (!isFeatureEnabled(actorKey)) {
console.info(
"[watchHistoryService] Snapshot skipped because watch history sync is unavailable for this actor.",
{
actor: actorKey,
requestedItems: Array.isArray(items) ? items.length : 0,
reason,
}
);
return { ok: true, skipped: true, reason: "feature-disabled" };
}
if (state.inflightSnapshots.has(actorKey)) {
return state.inflightSnapshots.get(actorKey);
}
@@ -952,6 +1118,16 @@ async function snapshot(items, options = {}) {
const queue = ensureQueue(actorKey);
console.info(
"[watchHistoryService] Initiating watch list snapshot publish.",
{
actor: actorKey,
reason,
itemCount: payloadItems.length,
queued: queue?.items?.size ?? 0,
}
);
const run = (async () => {
emit("snapshot-start", { actor: actorKey, reason, items: payloadItems });
const publishResult = await nostrClient.publishWatchHistorySnapshot(
@@ -962,6 +1138,17 @@ async function snapshot(items, options = {}) {
}
);
console.info(
"[watchHistoryService] Watch list snapshot publish completed.",
{
actor: actorKey,
reason,
success: !!publishResult?.ok,
snapshotId: publishResult?.snapshotId || null,
retryable: !!publishResult?.retryable,
}
);
if (!publishResult?.ok) {
if (publishResult?.retryable && publishResult?.snapshotId) {
if (queue) {
@@ -988,6 +1175,14 @@ async function snapshot(items, options = {}) {
await updateFingerprintCache(actorKey, publishResult.items || payloadItems);
emit("snapshot-complete", { actor: actorKey, reason, result: publishResult });
console.info(
"[watchHistoryService] Watch list snapshot processed and fingerprint cache updated.",
{
actor: actorKey,
reason,
fingerprintUpdated: true,
}
);
return publishResult;
})()
.catch((error) => {
@@ -1005,26 +1200,59 @@ async function snapshot(items, options = {}) {
}
async function loadLatest(actorInput) {
if (!isFeatureEnabled()) {
return [];
}
const actorKey = resolveActorKey(actorInput);
if (!actorKey) {
return [];
}
if (!isFeatureEnabled(actorKey)) {
if (!state.restored) {
restoreQueueState();
}
const items = collectQueueItems(actorKey);
console.info(
"[watchHistoryService] loadLatest returning local-only watch history queue.",
{
actor: actorKey,
itemCount: items.length,
}
);
return items;
}
const now = Date.now();
const cacheEntry = state.fingerprintCache.get(actorKey);
console.info(
"[watchHistoryService] loadLatest invoked.",
{
actor: actorKey,
cacheHasItems: Array.isArray(cacheEntry?.items),
cacheExpiresAt: cacheEntry?.expiresAt || null,
}
);
if (
cacheEntry?.items &&
cacheEntry.expiresAt &&
cacheEntry.expiresAt > now &&
Array.isArray(cacheEntry.items)
) {
console.info(
"[watchHistoryService] Returning cached watch list items.",
{
actor: actorKey,
itemCount: cacheEntry.items.length,
}
);
return cacheEntry.items;
}
if (cacheEntry?.promise) {
console.info(
"[watchHistoryService] Awaiting in-flight watch list refresh.",
{
actor: actorKey,
}
);
return cacheEntry.promise;
}
@@ -1048,6 +1276,13 @@ async function loadLatest(actorInput) {
}
});
console.info(
"[watchHistoryService] Triggered nostr watch list refresh.",
{
actor: actorKey,
}
);
state.fingerprintCache.set(actorKey, {
...(cacheEntry || {}),
promise,
@@ -1057,13 +1292,13 @@ async function loadLatest(actorInput) {
}
async function getFingerprint(actorInput) {
if (!isFeatureEnabled()) {
return "";
}
const actorKey = resolveActorKey(actorInput);
if (!actorKey) {
return "";
}
if (!isFeatureEnabled(actorKey)) {
return "";
}
const now = Date.now();
const cacheEntry = state.fingerprintCache.get(actorKey);
if (
@@ -1086,9 +1321,6 @@ async function getFingerprint(actorInput) {
}
function resetProgress(actorInput) {
if (!isFeatureEnabled()) {
return;
}
const actorKey = normalizeActorKey(actorInput);
if (actorKey) {
state.queues.delete(actorKey);
@@ -1106,9 +1338,6 @@ function resetProgress(actorInput) {
}
function getQueuedPointers(actorInput) {
if (!isFeatureEnabled()) {
return [];
}
const actorKey = normalizeActorKey(actorInput);
if (!actorKey) {
return [];
@@ -1120,9 +1349,6 @@ function getQueuedPointers(actorInput) {
}
function getAllQueues() {
if (!isFeatureEnabled()) {
return {};
}
if (!state.restored) {
restoreQueueState();
}
@@ -1149,6 +1375,8 @@ function getSettings() {
const watchHistoryService = {
isEnabled: isFeatureEnabled,
supportsLocalHistory,
isLocalOnly,
publishView,
snapshot,
loadLatest,
+221 -1
View File
@@ -48,6 +48,9 @@ const originalWatchHistoryCache = nostrClient.watchHistoryCache;
const originalWatchHistoryStorage = nostrClient.watchHistoryStorage;
const originalScheduleWatchHistoryRepublish =
nostrClient.scheduleWatchHistoryRepublish;
const originalResolveWatchHistory = nostrClient.resolveWatchHistory;
const originalGetWatchHistoryFingerprint =
nostrClient.getWatchHistoryFingerprint;
nostrClient.watchHistoryCache = new Map();
nostrClient.watchHistoryStorage = {
@@ -628,7 +631,7 @@ async function testWatchHistoryPartialRelayRetry() {
const originalRecordView = nostrClient.recordVideoView;
try {
nostrClient.pubkey = "";
nostrClient.pubkey = actor;
nostrClient.sessionActor = { pubkey: actor, privateKey: "partial-priv" };
nostrClient.ensureSessionActor = async () => actor;
@@ -980,12 +983,226 @@ async function testWatchHistoryServiceIntegration() {
}
}
async function testWatchHistoryLocalFallbackWhenDisabled() {
console.log("Running watch history local fallback test...");
const actor = "local-fallback-actor";
const originalRecordView = nostrClient.recordVideoView;
const originalPub = nostrClient.pubkey;
const originalSession = nostrClient.sessionActor;
try {
setWatchHistoryV2Enabled(false);
localStorage.clear();
watchHistoryService.resetProgress();
nostrClient.pubkey = "";
nostrClient.sessionActor = { pubkey: actor, privateKey: "local-priv" };
nostrClient.recordVideoView = async (_pointer, options = {}) => ({
ok: true,
event: {
id: "local-view",
pubkey: actor,
created_at: options.created_at || 1_700_500_000,
},
});
const supported =
typeof watchHistoryService.supportsLocalHistory === "function"
? watchHistoryService.supportsLocalHistory(actor)
: true;
const localOnly =
typeof watchHistoryService.isLocalOnly === "function"
? watchHistoryService.isLocalOnly(actor)
: true;
const enabled =
typeof watchHistoryService.isEnabled === "function"
? watchHistoryService.isEnabled(actor)
: false;
assert.equal(
supported,
true,
"service should report local history support while sync disabled",
);
assert.equal(localOnly, true, "guest session should be treated as local only");
assert.equal(
enabled,
false,
"sync should be disabled for session actors when feature flag is off",
);
const createdAt = 1_700_500_000;
await watchHistoryService.publishView(
{ type: "e", value: "local-pointer" },
createdAt,
{ actor },
);
const queued = watchHistoryService.getQueuedPointers(actor);
assert.equal(
queued.length,
1,
"local queue should retain pointer when sync is disabled",
);
const latest = await watchHistoryService.loadLatest(actor);
assert.equal(
latest.length,
1,
"loadLatest should surface session queue entries when sync disabled",
);
assert.equal(latest[0]?.value, "local-pointer");
assert.equal(latest[0]?.type, "e");
assert(
Number.isFinite(latest[0]?.watchedAt) && latest[0].watchedAt > 0,
"local fallback entries should carry watchedAt timestamps",
);
} finally {
setWatchHistoryV2Enabled(true);
watchHistoryService.resetProgress();
nostrClient.recordVideoView = originalRecordView;
nostrClient.pubkey = originalPub;
nostrClient.sessionActor = originalSession;
}
}
async function testWatchHistorySyncEnabledForLoggedInUsers() {
console.log("Running watch history logged-in sync override test...");
const actor = "npub-logged-sync";
const originalPub = nostrClient.pubkey;
const originalSession = nostrClient.sessionActor;
const originalResolve = nostrClient.resolveWatchHistory;
const originalFingerprint = nostrClient.getWatchHistoryFingerprint;
try {
setWatchHistoryV2Enabled(false);
localStorage.clear();
watchHistoryService.resetProgress();
nostrClient.pubkey = actor;
nostrClient.sessionActor = null;
let resolveCalls = 0;
nostrClient.resolveWatchHistory = async (requestedActor, options = {}) => {
resolveCalls += 1;
assert.equal(
requestedActor,
actor,
"resolveWatchHistory should target the logged-in actor",
);
assert.equal(
options?.forceRefresh,
true,
"loadLatest should force a relay refresh for logged-in actors",
);
return [
{
type: "e",
value: "remote-pointer",
watchedAt: 1_700_600_000,
},
];
};
let fingerprintCalls = 0;
nostrClient.getWatchHistoryFingerprint = async (requestedActor, items) => {
fingerprintCalls += 1;
assert.equal(
requestedActor,
actor,
"fingerprint lookup should use the logged-in actor",
);
assert(Array.isArray(items), "fingerprint helper expects item array input");
return "fingerprint-logged";
};
const enabled =
typeof watchHistoryService.isEnabled === "function"
? watchHistoryService.isEnabled(actor)
: false;
assert.equal(
enabled,
true,
"sync should remain enabled for logged-in actors even when the flag is disabled",
);
const items = await watchHistoryService.loadLatest(actor);
assert.equal(
resolveCalls > 0,
true,
"loadLatest should fetch from relays for logged-in actors",
);
assert.equal(items.length, 1, "loadLatest should return relay data for logged-in actors");
assert.equal(items[0]?.value, "remote-pointer");
assert.equal(
fingerprintCalls > 0,
true,
"fingerprint cache should update for logged-in actors",
);
} finally {
setWatchHistoryV2Enabled(true);
watchHistoryService.resetProgress();
nostrClient.resolveWatchHistory = originalResolve;
nostrClient.getWatchHistoryFingerprint = originalFingerprint;
nostrClient.pubkey = originalPub;
nostrClient.sessionActor = originalSession;
}
}
async function testWatchHistoryAppLoginFallback() {
console.log("Running watch history app login fallback test...");
const actor = "f".repeat(64);
const originalPub = nostrClient.pubkey;
const originalSession = nostrClient.sessionActor;
const originalApp = typeof window.app === "undefined" ? undefined : window.app;
try {
setWatchHistoryV2Enabled(false);
localStorage.clear();
watchHistoryService.resetProgress();
nostrClient.pubkey = "";
nostrClient.sessionActor = null;
window.app = {
pubkey: actor,
normalizeHexPubkey(value) {
if (typeof value === "string" && value.trim()) {
return value.trim().toLowerCase();
}
return null;
},
};
const enabled =
typeof watchHistoryService.isEnabled === "function"
? watchHistoryService.isEnabled(actor)
: false;
assert.equal(
enabled,
true,
"sync should be enabled when the app reports a logged-in pubkey",
);
} finally {
setWatchHistoryV2Enabled(true);
watchHistoryService.resetProgress();
nostrClient.pubkey = originalPub;
nostrClient.sessionActor = originalSession;
if (typeof originalApp === "undefined") {
delete window.app;
} else {
window.app = originalApp;
}
}
}
await testPublishSnapshotCanonicalizationAndChunking();
await testPublishSnapshotUsesExtensionCrypto();
await testPublishSnapshotFailureRetry();
await testWatchHistoryPartialRelayRetry();
await testResolveWatchHistoryBatchingWindow();
await testWatchHistoryServiceIntegration();
await testWatchHistoryLocalFallbackWhenDisabled();
await testWatchHistorySyncEnabledForLoggedInUsers();
await testWatchHistoryAppLoginFallback();
console.log("watch-history.test.mjs completed successfully");
@@ -1004,6 +1221,9 @@ nostrClient.watchHistoryCache = originalWatchHistoryCache;
nostrClient.watchHistoryStorage = originalWatchHistoryStorage;
nostrClient.scheduleWatchHistoryRepublish =
originalScheduleWatchHistoryRepublish;
nostrClient.resolveWatchHistory = originalResolveWatchHistory;
nostrClient.getWatchHistoryFingerprint =
originalGetWatchHistoryFingerprint;
if (!originalFlag) {
setWatchHistoryV2Enabled(false);