Remove metadata storage and handling from watch history

Removes the metadata argument from `WatchHistoryService.publishView` and cleans up the associated caching and resolution logic in `watchHistory.js` and `watchHistoryFeed.js`. Deletes `js/watchHistoryMetadata.js` as it is no longer used.

This simplification ensures watch history strictly manages pointers and timestamps, leaving metadata resolution to higher-level consumers. It also reduces the payload size and complexity of persisted history records.

Tests have been updated to reflect the API changes.
This commit is contained in:
google-labs-jules[bot] committed 2026-01-05 16:58:59 +00:00
1 parent bb17ed86e3
commit 4c7230f540
7 files changed
+26 -839

No files matched your search

+2 -224
View File
@@ -3,7 +3,6 @@
import { pointerKey, normalizePointerInput } from "../nostr/watchHistory.js";
import watchHistoryService from "../watchHistoryService.js";
import nostrService from "../services/nostrService.js";
import { createWatchHistoryMetadataResolver } from "../watchHistoryMetadata.js";
import {
isWatchHistoryDebugEnabled,
logWatchHistoryDebug,
@@ -203,15 +202,8 @@ function createWatchHistorySource({ service = watchHistoryService } = {}) {
};
}
function createWatchHistoryHydratorStage({
service = watchHistoryService,
metadataResolver,
} = {}) {
const resolver = metadataResolver || createWatchHistoryMetadataResolver();
function createWatchHistoryHydratorStage({ service = watchHistoryService } = {}) {
return async function watchHistoryHydratorStage(items = [], context = {}) {
const shouldStoreMetadata = false;
const results = [];
for (const item of Array.isArray(items) ? items : []) {
@@ -224,150 +216,12 @@ function createWatchHistoryHydratorStage({
? item.pointerKey
: pointerKey(item.pointer);
const pointerType =
typeof item?.pointer?.type === "string" ? item.pointer.type : null;
const pointerValue =
typeof item?.pointer?.value === "string" ? item.pointer.value : null;
const watchedAtValue = Number.isFinite(item?.watchedAt)
? Math.max(0, Math.floor(Number(item.watchedAt)))
: Number.isFinite(item?.metadata?.watchedAt)
? Math.max(0, Math.floor(Number(item.metadata.watchedAt)))
: 0;
let video = item.video || null;
let profile = item.metadata?.profile || null;
if (isWatchHistoryDebugEnabled()) {
debugInfo("Hydrating watch history item.", {
pointerKey: pointerKeyValue || null,
pointerType,
pointerValue,
hasInitialVideo: Boolean(video),
hasInitialProfile: Boolean(profile),
watchedAt: watchedAtValue || null,
});
}
if (pointerKeyValue && typeof service?.getLocalMetadata === "function") {
try {
const stored = service.getLocalMetadata(pointerKeyValue);
if (stored) {
const hadVideo = Boolean(video);
const hadProfile = Boolean(profile);
video = video || stored.video || null;
profile = profile || stored.profile || null;
if (isWatchHistoryDebugEnabled()) {
debugInfo("Loaded cached metadata for pointer.", {
pointerKey: pointerKeyValue,
hadVideo,
hadProfile,
cachedVideo: Boolean(stored.video),
cachedProfile: Boolean(stored.profile),
resultingVideo: Boolean(video),
resultingProfile: Boolean(profile),
});
}
}
} catch (error) {
context?.log?.(
"[watch-history-feed] Failed to read cached metadata",
error,
);
if (isWatchHistoryDebugEnabled()) {
debugWarn("Failed to read cached metadata for pointer.", {
pointerKey: pointerKeyValue || null,
error,
});
}
}
}
if (!video && resolver?.resolveVideo) {
if (isWatchHistoryDebugEnabled()) {
debugInfo("Resolving video via metadata resolver.", {
pointerKey: pointerKeyValue || null,
pointerType,
pointerValue,
});
}
try {
video = await resolver.resolveVideo(item.pointer);
if (isWatchHistoryDebugEnabled()) {
if (video) {
debugInfo("Metadata resolver returned video candidate.", {
pointerKey: pointerKeyValue || null,
videoId: typeof video.id === "string" ? video.id : null,
title:
typeof video.title === "string" && video.title.trim()
? video.title
: null,
invalid: Boolean(video.invalid),
});
} else {
debugWarn("Metadata resolver returned no video candidate.", {
pointerKey: pointerKeyValue || null,
});
}
}
} catch (error) {
context?.log?.(
"[watch-history-feed] Failed to resolve video from pointer",
error,
);
if (isWatchHistoryDebugEnabled()) {
debugWarn("Metadata resolver threw while resolving video.", {
pointerKey: pointerKeyValue || null,
error,
});
}
}
}
if (video?.pubkey && !profile && resolver?.resolveProfile) {
if (isWatchHistoryDebugEnabled()) {
debugInfo("Resolving profile for video author.", {
pointerKey: pointerKeyValue || null,
pubkey: video.pubkey,
});
}
try {
profile = resolver.resolveProfile(video.pubkey) || null;
if (isWatchHistoryDebugEnabled()) {
if (profile) {
const label =
profile.display_name ||
profile.name ||
profile.username ||
profile.nip05 ||
null;
debugInfo("Resolved profile for video author.", {
pointerKey: pointerKeyValue || null,
pubkey: video.pubkey,
label: label || null,
});
} else {
debugWarn("Resolver returned no profile for video author.", {
pointerKey: pointerKeyValue || null,
pubkey: video.pubkey,
});
}
}
} catch (error) {
context?.log?.(
"[watch-history-feed] Failed to resolve profile for pointer",
error,
);
if (isWatchHistoryDebugEnabled()) {
debugWarn("Profile resolver threw while hydrating metadata.", {
pointerKey: pointerKeyValue || null,
pubkey: video.pubkey,
error,
});
}
}
}
const metadata = {
...(isPlainObject(item.metadata) ? item.metadata : {}),
pointerKey: pointerKeyValue,
@@ -375,88 +229,15 @@ function createWatchHistoryHydratorStage({
resumeAt: item.metadata?.resumeAt ?? null,
completed: item.metadata?.completed ?? false,
session: item.metadata?.session === true,
video: video || null,
profile: profile || null,
};
if (isWatchHistoryDebugEnabled()) {
const summary = {
pointerKey: pointerKeyValue || null,
pointerType,
pointerValue,
watchedAt: watchedAtValue || null,
videoId: typeof video?.id === "string" ? video.id : null,
title:
typeof video?.title === "string" && video.title?.trim()
? video.title
: null,
invalid: Boolean(video?.invalid),
hasProfile: Boolean(profile),
};
if (video) {
debugInfo("Hydrated watch history entry metadata.", summary);
} else {
debugWarn("Hydrated entry is missing resolved video metadata.", summary);
}
}
const nextItem = {
...item,
pointerKey: pointerKeyValue,
watchedAt: watchedAtValue,
video: video || null,
metadata,
};
results.push(nextItem);
if (pointerKeyValue) {
if (shouldStoreMetadata) {
try {
service?.setLocalMetadata?.(pointerKeyValue, {
video: metadata.video,
profile: metadata.profile,
});
if (isWatchHistoryDebugEnabled()) {
debugInfo("Stored metadata cache entry for pointer.", {
pointerKey: pointerKeyValue,
cachedVideo: Boolean(metadata.video),
cachedProfile: Boolean(metadata.profile),
});
}
} catch (error) {
context?.log?.(
"[watch-history-feed] Failed to persist metadata cache",
error,
);
if (isWatchHistoryDebugEnabled()) {
debugWarn("Failed to persist metadata cache entry.", {
pointerKey: pointerKeyValue,
error,
});
}
}
} else if (typeof service?.removeLocalMetadata === "function") {
try {
service.removeLocalMetadata(pointerKeyValue);
if (isWatchHistoryDebugEnabled()) {
debugInfo("Cleared metadata cache entry for pointer.", {
pointerKey: pointerKeyValue,
});
}
} catch (error) {
context?.log?.(
"[watch-history-feed] Failed to clear cached metadata",
error,
);
if (isWatchHistoryDebugEnabled()) {
debugWarn("Failed to clear cached metadata entry.", {
pointerKey: pointerKeyValue,
error,
});
}
}
}
}
}
return results;
@@ -484,11 +265,8 @@ function createWatchHistorySorter() {
export function createWatchHistoryFeedDefinition({
service = watchHistoryService,
nostr = nostrService,
metadataResolverFactory = createWatchHistoryMetadataResolver,
shouldIncludeVideo,
} = {}) {
const metadataResolver = metadataResolverFactory({ nostr });
const blacklistStage = createBlacklistFilterStage({
shouldIncludeVideo:
typeof shouldIncludeVideo === "function"
@@ -502,7 +280,7 @@ export function createWatchHistoryFeedDefinition({
return {
source: createWatchHistorySource({ service }),
stages: [
createWatchHistoryHydratorStage({ service, metadataResolver }),
createWatchHistoryHydratorStage({ service }),
blacklistStage,
createWatchHistorySuppressionStage(),
],
+11 -144
View File
@@ -39,42 +39,6 @@ const WATCH_HISTORY_ENCRYPTION_FALLBACK_ORDER = Object.freeze([
"nip04",
]);
function cloneVideoMetadata(video) {
if (!video || typeof video !== "object") {
return null;
}
try {
return JSON.parse(JSON.stringify(video));
} catch (error) {
devLogger.warn("[nostr] Failed to clone watch history video metadata", error);
return { ...video };
}
}
function cloneProfileMetadata(profile) {
if (!profile || typeof profile !== "object") {
return null;
}
try {
return JSON.parse(JSON.stringify(profile));
} catch (error) {
devLogger.warn("[nostr] Failed to clone watch history profile metadata", error);
return { ...profile };
}
}
function clonePointerMetadata(metadata) {
if (!metadata || typeof metadata !== "object") {
return null;
}
try {
return JSON.parse(JSON.stringify(metadata));
} catch (error) {
devLogger.warn("[nostr] Failed to clone watch history pointer metadata", error);
return { ...metadata };
}
}
function mergePointerDetails(target, source) {
if (!target || typeof target !== "object" || !source || typeof source !== "object") {
return target;
@@ -96,19 +60,6 @@ function mergePointerDetails(target, source) {
target.relay = source.relay.trim();
}
}
if (!target.metadata && source.metadata && typeof source.metadata === "object") {
target.metadata = clonePointerMetadata(source.metadata);
} else if (target.metadata && source.metadata) {
const merged = clonePointerMetadata(target.metadata) || {};
const additional = clonePointerMetadata(source.metadata) || {};
target.metadata = { ...merged, ...additional };
}
if (!target.video && source.video) {
target.video = cloneVideoMetadata(source.video);
}
if (!target.profile && source.profile) {
target.profile = cloneProfileMetadata(source.profile);
}
return target;
}
@@ -146,29 +97,6 @@ function clonePointerItem(pointer) {
cloned.session = true;
}
const metadata = clonePointerMetadata(pointer.metadata);
if (metadata) {
cloned.metadata = metadata;
}
const video = cloneVideoMetadata(pointer.video) || metadata?.video || null;
if (video) {
cloned.video = video;
}
const profile = cloneProfileMetadata(pointer.profile) || metadata?.profile || null;
if (profile) {
cloned.profile = profile;
}
if (pointer.completed === true || metadata?.completed === true) {
cloned.completed = true;
}
if (!Number.isFinite(cloned.resumeAt) && Number.isFinite(metadata?.resumeAt)) {
cloned.resumeAt = metadata.resumeAt;
}
return cloned;
}
@@ -544,15 +472,7 @@ function canonicalizeWatchHistoryItems(rawItems, maxItems = WATCH_HISTORY_MAX_IT
}
function sanitizeWatchHistoryMetadata(metadata) {
if (!metadata || typeof metadata !== "object") {
return {};
}
try {
return JSON.parse(JSON.stringify(metadata));
} catch (error) {
devLogger.warn("[nostr] Failed to sanitize watch history metadata:", error);
return {};
}
return {};
}
function serializeWatchHistoryItems(items) {
@@ -580,47 +500,14 @@ function serializeWatchHistoryItems(items) {
if (watchedAt !== undefined) {
payload.watchedAt = watchedAt;
}
const metadata = clonePointerMetadata(item?.metadata);
if (metadata) {
payload.metadata = metadata;
}
const video = metadata?.video || cloneVideoMetadata(item?.video);
if (video) {
if (payload.metadata) {
payload.metadata.video = payload.metadata.video || video;
} else {
payload.video = video;
}
}
const profile = metadata?.profile || cloneProfileMetadata(item?.profile);
if (profile) {
if (payload.metadata) {
payload.metadata.profile = payload.metadata.profile || profile;
} else {
payload.profile = profile;
}
}
const resumeAt = Number.isFinite(item?.resumeAt)
? Math.max(0, Math.floor(item.resumeAt))
: undefined;
if (resumeAt !== undefined) {
if (payload.metadata) {
if (!Number.isFinite(payload.metadata.resumeAt)) {
payload.metadata.resumeAt = resumeAt;
}
} else {
payload.resumeAt = resumeAt;
}
payload.resumeAt = resumeAt;
}
if (item?.completed === true) {
if (payload.metadata) {
payload.metadata.completed = true;
} else {
payload.completed = true;
}
}
if (payload.metadata && !Object.keys(payload.metadata).length) {
delete payload.metadata;
payload.completed = true;
}
return payload;
})
@@ -1106,7 +993,6 @@ class WatchHistoryManager {
items = Object.values(records).flat();
}
const metadata = sanitizeWatchHistoryMetadata(entry?.metadata);
const snapshotId = typeof entry?.snapshotId === "string" ? entry.snapshotId : "";
const fingerprint = typeof entry?.fingerprint === "string" ? entry.fingerprint : "";
sanitizedActors[actorKey] = {
@@ -1119,7 +1005,7 @@ class WatchHistoryManager {
savedAt,
records,
items,
metadata,
metadata: {},
};
}
const storage = {
@@ -1169,7 +1055,6 @@ class WatchHistoryManager {
records = canonicalizeWatchHistoryItems(entry.items, WATCH_HISTORY_MAX_ITEMS);
}
const metadata = sanitizeWatchHistoryMetadata(entry.metadata);
const snapshotId = typeof entry.snapshotId === "string" ? entry.snapshotId : "";
const fingerprint = typeof entry.fingerprint === "string" ? entry.fingerprint : "";
const savedAt = Number.isFinite(entry.savedAt) && entry.savedAt > 0 ? entry.savedAt : now;
@@ -1183,7 +1068,7 @@ class WatchHistoryManager {
fingerprint,
savedAt,
records,
metadata,
metadata: {},
};
mutated = true;
}
@@ -1362,17 +1247,13 @@ class WatchHistoryManager {
return fetchResult;
}
const storageEntry = this.getStorage().actors?.[actorKey];
const metadata = sanitizeWatchHistoryMetadata(storageEntry?.metadata);
const alreadyAttempted = metadata.autoSnapshotAttempted === true;
const records = storageEntry?.records || {};
const flatItems = Object.values(records).flat();
if (!flatItems.length || alreadyAttempted) {
if (!flatItems.length) {
return fetchResult;
}
metadata.autoSnapshotAttempted = true;
metadata.autoSnapshotAttemptedAt = Date.now();
// We publish all records
const publishResult = await this.publishRecords(records, {
@@ -1390,7 +1271,7 @@ class WatchHistoryManager {
chunkEvents: [],
savedAt: Date.now(),
fingerprint,
metadata,
metadata: {},
};
this.cache.set(actorKey, entry);
this.persistEntry(actorKey, entry);
@@ -1776,16 +1657,6 @@ class WatchHistoryManager {
success: !!publishResult.ok,
retryable: !!publishResult.retryable,
});
const metadata = sanitizeWatchHistoryMetadata(cachedEntry.metadata);
metadata.updatedAt = Date.now();
metadata.status = publishResult.ok ? "ok" : "error";
metadata.lastPublishResults = publishResult.publishResults; // This will likely be array or complex obj
metadata.skippedCount = publishResult.skippedCount || 0;
if (!publishResult.ok) {
metadata.lastError = publishResult.error || "publish-failed";
} else {
delete metadata.lastError;
}
const entry = {
actor: resolvedActor,
records,
@@ -1794,7 +1665,7 @@ class WatchHistoryManager {
pointerEvent: publishResult.pointerEvent || cachedEntry.pointerEvent || null,
savedAt: Date.now(),
fingerprint,
metadata,
metadata: {},
};
this.cache.set(actorKey, entry);
this.persistEntry(actorKey, entry);
@@ -1938,7 +1809,7 @@ class WatchHistoryManager {
pointerEvent: null,
savedAt: now,
fingerprint,
metadata: sanitizeWatchHistoryMetadata(storageEntry?.metadata),
metadata: {},
};
this.cache.set(actorKey, entry);
this.persistEntry(actorKey, entry);
@@ -2094,10 +1965,6 @@ class WatchHistoryManager {
resolvedActor,
records,
);
const metadata = sanitizeWatchHistoryMetadata(
this.getStorage().actors?.[actorKey]?.metadata,
);
metadata.lastFetchedAt = now;
const entry = {
actor: resolvedActor,
@@ -2107,7 +1974,7 @@ class WatchHistoryManager {
pointerEvent: latestEvent,
savedAt: now,
fingerprint,
metadata,
metadata: {},
};
this.cache.set(actorKey, entry);
this.persistEntry(actorKey, entry);
@@ -2196,7 +2063,7 @@ class WatchHistoryManager {
pointerEvent: fetchResult.pointerEvent || null,
savedAt: Date.now(),
fingerprint,
metadata: sanitizeWatchHistoryMetadata(storage.actors?.[actorKey]?.metadata),
metadata: {},
};
this.cache.set(actorKey, entry);
this.persistEntry(actorKey, entry);
+3 -97
View File
@@ -72,7 +72,6 @@ export default class WatchHistoryTelemetry {
? Math.max(0, viewThresholdSeconds)
: DEFAULT_VIEW_THRESHOLD_SECONDS;
this.watchHistoryMetadataEnabled = false;
this.watchHistoryPreferenceUnsubscribe = null;
this.playbackTelemetryState = null;
this.loggedViewPointerKeys = new Set();
@@ -81,17 +80,14 @@ export default class WatchHistoryTelemetry {
destroy() {
this.resetPlaybackLoggingState();
this._clearPreferenceSubscription();
this.watchHistoryMetadataEnabled = false;
}
async initPreferenceSync() {
this.watchHistoryMetadataEnabled = false;
return null;
}
refreshPreferenceSettings() {
this.watchHistoryMetadataEnabled = false;
return this.watchHistoryMetadataEnabled;
return false;
}
isMetadataPreferenceEnabled() {
@@ -99,54 +95,11 @@ export default class WatchHistoryTelemetry {
}
persistMetadataForVideo(video, pointerInfo) {
if (
!this.watchHistoryMetadataEnabled ||
!pointerInfo ||
!pointerInfo.key ||
typeof this.watchHistoryService?.setLocalMetadata !== "function"
) {
return;
}
if (!video || typeof video !== "object") {
return;
}
const metadata = {
// Persist only sanitized metadata so local history never stores playback
// transports or magnet fingerprints.
video: sanitizeVideoMetadata(video),
};
try {
this.watchHistoryService.setLocalMetadata(pointerInfo.key, metadata);
} catch (error) {
devLogger.warn(
"[watchHistoryTelemetry] Failed to persist local metadata for pointer:",
pointerInfo.key,
error,
);
}
// Deprecated
}
dropMetadata(pointerKey) {
if (!pointerKey || typeof pointerKey !== "string") {
return;
}
if (typeof this.watchHistoryService?.removeLocalMetadata !== "function") {
return;
}
try {
this.watchHistoryService.removeLocalMetadata(pointerKey);
} catch (error) {
devLogger.warn(
"[watchHistoryTelemetry] Failed to remove cached metadata for pointer:",
pointerKey,
error,
);
}
// Deprecated
}
async handleRemoval(payload = {}) {
@@ -272,58 +225,11 @@ export default class WatchHistoryTelemetry {
try {
const canUseWatchHistoryService =
typeof this.watchHistoryService?.publishView === "function";
const resolveWatchActor = () => {
const normalizedUser = this.normalizeHexPubkey(
this.getActiveUserPubkey(),
);
if (normalizedUser) {
return normalizedUser;
}
const normalizedClient = this.normalizeHexPubkey(
this.nostrClient?.pubkey,
);
if (normalizedClient) {
return normalizedClient;
}
const normalizedSession = this.normalizeHexPubkey(
this.nostrClient?.sessionActor?.pubkey,
);
if (normalizedSession) {
return normalizedSession;
}
if (normalizeString(this.nostrClient?.pubkey)) {
return normalizeString(this.nostrClient.pubkey).toLowerCase();
}
if (normalizeString(this.nostrClient?.sessionActor?.pubkey)) {
return normalizeString(
this.nostrClient.sessionActor.pubkey,
).toLowerCase();
}
return "";
};
const activeWatchActor = resolveWatchActor();
const watchMetadata = {};
if (activeWatchActor) {
watchMetadata.actor = activeWatchActor;
}
if (state.videoMetadata) {
watchMetadata.video = state.videoMetadata;
}
const metadataPayload = Object.keys(watchMetadata).length
? watchMetadata
: undefined;
if (canUseWatchHistoryService) {
viewResult = await this.watchHistoryService.publishView(
thresholdPointer,
undefined,
metadataPayload,
);
} else if (typeof this.nostrClient?.recordVideoView === "function") {
viewResult = await this.nostrClient.recordVideoView(
-306
View File
@@ -1,306 +0,0 @@
// js/watchHistoryMetadata.js
import { getApplication } from "./applicationContext.js";
import nostrService from "./services/nostrService.js";
import { nostrClient } from "./nostrClientFacade.js";
import { logWatchHistoryDebug } from "./watchHistoryDebug.js";
function logInfo(message, details) {
logWatchHistoryDebug("watchHistoryMetadata", "info", message, details);
}
function logWarn(message, details) {
logWatchHistoryDebug("watchHistoryMetadata", "warn", message, details);
}
function normalizeVideoCandidate(candidate) {
if (!candidate || typeof candidate !== "object") {
logInfo("Discarded non-object video candidate while normalizing.");
return null;
}
if (candidate.deleted) {
logInfo("Discarded deleted video candidate while normalizing.", {
eventId: candidate.id || null,
});
return null;
}
return candidate;
}
export function createWatchHistoryMetadataResolver({
appResolver = getApplication,
nostr = nostrService,
client = nostrClient,
} = {}) {
const caches = {
activeVideos: null,
catalogPromise: null,
};
function getAppInstance() {
try {
return typeof appResolver === "function" ? appResolver() : getApplication();
} catch (error) {
logWarn("Failed to resolve application instance.", { error });
return null;
}
}
async function resolveVideo(pointer) {
if (!pointer || typeof pointer !== "object") {
logInfo("Received invalid pointer when resolving video metadata.", {
pointerType: typeof pointer,
});
return null;
}
const app = getAppInstance();
const type = pointer.type === "a" ? "a" : "e";
const value = typeof pointer.value === "string" ? pointer.value.trim() : "";
logInfo("Attempting to resolve watch history pointer.", {
pointerType: type,
pointerValue: value || null,
hasAppInstance: Boolean(app),
});
if (!value) {
logInfo("Pointer value was empty after trimming. Skipping resolution.");
return null;
}
if (type === "e") {
const fromAppCache = app?.videosMap?.get?.(value);
const normalizedAppCache = normalizeVideoCandidate(fromAppCache);
if (normalizedAppCache) {
logInfo("Resolved pointer from application videos map.", {
pointerValue: value,
});
return normalizedAppCache;
}
const fromClientCache = client?.allEvents instanceof Map
? client.allEvents.get(value)
: null;
const normalizedClientCache = normalizeVideoCandidate(fromClientCache);
if (normalizedClientCache) {
logInfo("Resolved pointer from nostr client event cache.", {
pointerValue: value,
});
return normalizedClientCache;
}
if (typeof app?.getOldEventById === "function") {
try {
const fetched = await app.getOldEventById(value);
const normalized = normalizeVideoCandidate(fetched);
if (normalized) {
logInfo("Resolved pointer via app.getOldEventById.", {
pointerValue: value,
});
return normalized;
}
} catch (error) {
logWarn("Failed to fetch old event by id.", {
pointerValue: value,
error,
});
}
}
if (typeof client?.getEventById === "function") {
try {
const fetched = await client.getEventById(value);
const normalized = normalizeVideoCandidate(fetched);
if (normalized) {
logInfo("Resolved pointer via nostrClient.getEventById.", {
pointerValue: value,
});
return normalized;
}
} catch (error) {
logWarn("Failed to fetch event by id via nostrClient.", {
pointerValue: value,
error,
});
}
}
logInfo("Failed to resolve pointer by event id.", { pointerValue: value });
return null;
}
const compareAddress = (candidate) => {
if (!candidate || typeof candidate !== "object") {
return false;
}
if (typeof app?.getVideoAddressPointer !== "function") {
return false;
}
try {
const address = app.getVideoAddressPointer(candidate);
const isMatch = typeof address === "string" && address.trim() === value;
if (isMatch) {
logInfo("Candidate matched address pointer.", {
pointerValue: value,
candidateId: candidate.id || null,
});
}
return isMatch;
} catch (error) {
logWarn("Failed to compute address pointer for candidate.", {
pointerValue: value,
error,
});
return false;
}
};
if (app?.videosMap instanceof Map) {
for (const cached of app.videosMap.values()) {
if (compareAddress(cached)) {
const normalized = normalizeVideoCandidate(cached);
if (normalized) {
return normalized;
}
}
}
}
const blacklist = app?.blacklistedEventIds instanceof Set
? app.blacklistedEventIds
: undefined;
const filterOptions = {
blacklistedEventIds: blacklist,
isAuthorBlocked: (pubkey) => {
try {
return (
typeof app?.isAuthorBlocked === "function" &&
app.isAuthorBlocked(pubkey)
);
} catch (error) {
logWarn("Failed to evaluate isAuthorBlocked callback.", {
pointerValue: value,
error,
});
return false;
}
},
};
if (!Array.isArray(caches.activeVideos)) {
try {
caches.activeVideos = nostr.getFilteredActiveVideos
? nostr.getFilteredActiveVideos(filterOptions)
: [];
logInfo("Loaded active videos cache for pointer resolution.", {
pointerValue: value,
candidateCount: Array.isArray(caches.activeVideos)
? caches.activeVideos.length
: 0,
});
} catch (error) {
logWarn("Failed to read active videos cache.", {
pointerValue: value,
error,
});
caches.activeVideos = [];
}
}
for (const candidate of Array.isArray(caches.activeVideos)
? caches.activeVideos
: []) {
if (compareAddress(candidate)) {
const normalized = normalizeVideoCandidate(candidate);
if (normalized) {
logInfo("Resolved pointer from active videos cache.", {
pointerValue: value,
candidateId: normalized.id || null,
});
return normalized;
}
}
}
if (!caches.catalogPromise && typeof nostr.fetchVideos === "function") {
logInfo("Fetching video catalog for pointer resolution.", {
pointerValue: value,
});
caches.catalogPromise = nostr
.fetchVideos(filterOptions)
.catch((error) => {
logWarn("Failed to fetch video catalog while resolving pointer.", {
pointerValue: value,
error,
});
return [];
});
}
if (caches.catalogPromise) {
try {
const catalog = await caches.catalogPromise;
for (const candidate of Array.isArray(catalog) ? catalog : []) {
if (compareAddress(candidate)) {
const normalized = normalizeVideoCandidate(candidate);
if (normalized) {
logInfo("Resolved pointer from fetched video catalog.", {
pointerValue: value,
candidateId: normalized.id || null,
});
return normalized;
}
}
}
} catch (error) {
logWarn("Failed to resolve pointer from fetched catalog.", {
pointerValue: value,
error,
});
}
}
logInfo("Failed to resolve pointer after exhausting lookup strategies.", {
pointerValue: value,
});
return null;
}
function resolveProfile(pubkey) {
if (typeof pubkey !== "string" || !pubkey.trim()) {
return null;
}
const app = getAppInstance();
if (!app) {
return null;
}
try {
const cacheEntry =
typeof app.getProfileCacheEntry === "function"
? app.getProfileCacheEntry(pubkey)
: null;
if (cacheEntry && typeof cacheEntry === "object") {
logInfo("Resolved profile from application cache.", { pubkey });
return cacheEntry.profile || null;
}
} catch (error) {
logWarn("Failed to read profile cache entry.", { pubkey, error });
}
logInfo("Profile cache miss during watch history hydration.", { pubkey });
return null;
}
function reset() {
caches.activeVideos = null;
logInfo("Reset active videos cache for watch history metadata resolver.");
caches.catalogPromise = null;
logInfo("Reset catalog promise for watch history metadata resolver.");
}
return {
resolveVideo,
resolveProfile,
reset,
caches,
};
}
+1 -15
View File
@@ -698,22 +698,11 @@ async function updateFingerprintCache(actorKey, items) {
return fingerprint;
}
async function publishView(pointerInput, createdAt, metadata = {}) {
async function publishView(pointerInput, createdAt) {
const recordOptions = {};
if (Number.isFinite(createdAt)) {
recordOptions.created_at = createdAt;
}
if (metadata && typeof metadata === "object") {
if (Array.isArray(metadata.additionalTags)) {
recordOptions.additionalTags = metadata.additionalTags;
}
if (Array.isArray(metadata.relays)) {
recordOptions.relays = metadata.relays;
}
if (metadata.content != null) {
recordOptions.content = metadata.content;
}
}
const viewResult = await nostrClient.recordVideoView(pointerInput, recordOptions);
@@ -722,7 +711,6 @@ async function publishView(pointerInput, createdAt, metadata = {}) {
{
pointer: pointerInput,
createdAt,
hasMetadata: !!metadata,
}
);
@@ -736,7 +724,6 @@ async function publishView(pointerInput, createdAt, metadata = {}) {
}
const actorCandidate =
(typeof metadata.actor === "string" && metadata.actor.trim()) ||
viewResult?.event?.pubkey ||
nostrClient?.pubkey ||
nostrClient?.sessionActor?.pubkey ||
@@ -775,7 +762,6 @@ async function publishView(pointerInput, createdAt, metadata = {}) {
viewResult?.event?.created_at
);
delete normalizedPointer.metadata;
delete normalizedPointer.video;
delete normalizedPointer.profile;
delete normalizedPointer.resumeAt;
-8
View File
@@ -1160,7 +1160,6 @@ async function testWatchHistoryPartialRelayRetry() {
await watchHistoryService.publishView(
{ type: "e", value: "partial-pointer" },
Math.floor(Date.now() / 1000),
{ actor },
);
const queuedBefore = watchHistoryService.getQueuedPointers(actor);
@@ -1359,7 +1358,6 @@ async function testWatchHistorySnapshotRetainsNewQueueEntriesDuringPublish() {
await watchHistoryService.publishView(
{ type: "e", value: "inflight-initial" },
createdAt,
{ actor },
);
const queuedBefore = watchHistoryService.getQueuedPointers(actor);
@@ -1401,7 +1399,6 @@ async function testWatchHistorySnapshotRetainsNewQueueEntriesDuringPublish() {
await watchHistoryService.publishView(
{ type: "e", value: "inflight-new" },
createdAt,
{ actor },
);
const queuedDuring = watchHistoryService.getQueuedPointers(actor);
@@ -1566,19 +1563,16 @@ async function testWatchHistoryServiceIntegration() {
await watchHistoryService.publishView(
{ type: "e", value: "video-one" },
viewCreatedAt,
{ actor, video: pointerVideo },
);
viewCreatedAt += 60;
await watchHistoryService.publishView(
{ type: "e", value: "video-one" },
viewCreatedAt,
{ actor, video: pointerVideo },
);
viewCreatedAt += 30;
await watchHistoryService.publishView(
{ type: "a", value: "30023:pub:episode" },
viewCreatedAt,
{ actor },
);
const queued = watchHistoryService.getQueuedPointers(actor);
@@ -1823,7 +1817,6 @@ async function testWatchHistorySnapshotMergesQueuedWithCachedItems() {
await watchHistoryService.publishView(
{ type: "e", value: "fresh-entry" },
1_700_000_200,
{ actor },
);
const mergeResult = await watchHistoryService.snapshot(null, {
@@ -2366,7 +2359,6 @@ async function testWatchHistoryLocalFallbackWhenDisabled() {
await watchHistoryService.publishView(
{ type: "e", value: "local-pointer" },
createdAt,
{ actor },
);
const queued = watchHistoryService.getQueuedPointers(actor);
@@ -83,39 +83,10 @@ async function flushAsyncOperations() {
}
async function testMetadataPreferenceSync() {
let subscribeTopic = null;
let subscribeHandler = null;
let cleared = false;
const { telemetry } = createTelemetry({
serviceOverrides: {
subscribe: (topic, handler) => {
subscribeTopic = topic;
subscribeHandler = handler;
return () => {};
},
clearLocalMetadata: () => {
cleared = true;
},
getSettings: () => ({ metadata: { storeLocally: true } }),
},
options: {
getActiveUserPubkey: () => null,
},
});
const { telemetry } = createTelemetry();
const unsubscribe = await telemetry.initPreferenceSync();
assert.equal(subscribeTopic, "metadata-preference");
assert.equal(typeof unsubscribe, "function");
assert.equal(telemetry.isMetadataPreferenceEnabled(), true);
subscribeHandler({ enabled: false });
assert.equal(unsubscribe, null, "initPreferenceSync should no-op");
assert.equal(telemetry.isMetadataPreferenceEnabled(), false);
assert.equal(cleared, true);
subscribeHandler({ enabled: true });
assert.equal(telemetry.isMetadataPreferenceEnabled(), true);
}
async function testPreparePlaybackLoggingPublishesOnce() {
@@ -127,8 +98,8 @@ async function testPreparePlaybackLoggingPublishesOnce() {
const { telemetry } = createTelemetry({
serviceOverrides: {
publishView: async (ptr, _relay, metadata) => {
publishCalls.push({ ptr, metadata });
publishView: async (ptr, _relay) => {
publishCalls.push({ ptr });
return {
ok: true,
event: { pubkey: hexPubkey },
@@ -174,7 +145,7 @@ async function testPreparePlaybackLoggingPublishesOnce() {
);
}
async function testPreparePlaybackLoggingIncludesVideoMetadata() {
async function testPreparePlaybackLoggingIgnoresMetadata() {
const publishCalls = [];
const pointer = { type: "e", value: "event-id" };
@@ -182,8 +153,8 @@ async function testPreparePlaybackLoggingIncludesVideoMetadata() {
const { telemetry } = createTelemetry({
serviceOverrides: {
publishView: async (ptr, _relay, metadata) => {
publishCalls.push({ ptr, metadata });
publishView: async (ptr, _relay) => {
publishCalls.push({ ptr });
return {
ok: true,
event: { pubkey: hexPubkey },
@@ -216,14 +187,7 @@ async function testPreparePlaybackLoggingIncludesVideoMetadata() {
assert.equal(publishCalls.length, 1, "publishView should be invoked once");
assert.equal(publishCalls[0].ptr, pointer);
assert.ok(publishCalls[0].metadata, "metadata payload should exist");
assert.deepEqual(publishCalls[0].metadata.video, {
id: "evt1",
title: "Example Video",
thumbnail: "https://example.com/thumb.jpg",
pubkey: hexPubkey,
created_at: 1_700_000_000,
});
assert.equal(publishCalls[0].metadata, undefined, "metadata should NOT be passed");
}
async function testCancelPlaybackLoggingFlushes() {
@@ -254,7 +218,7 @@ async function testCancelPlaybackLoggingFlushes() {
await testMetadataPreferenceSync();
await testPreparePlaybackLoggingPublishesOnce();
await testPreparePlaybackLoggingIncludesVideoMetadata();
await testPreparePlaybackLoggingIgnoresMetadata();
await testCancelPlaybackLoggingFlushes();
console.log("watch-history-telemetry tests passed");