mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 17:02:28 +00:00
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:
5 files changed
+699
-59
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user