Files
bitvid/js/watchHistoryMetadata.js
T

307 lines
8.8 KiB
JavaScript

// js/watchHistoryMetadata.js
import { getApplication } from "./applicationContext.js";
import nostrService from "./services/nostrService.js";
import { nostrClient } from "./nostr.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,
};
}