Merge pull request #1083 from PR0M3TH3AN/watch-history-hydration-improvement-14056519037250996205

Implement video hydration stage for watch history feed
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-05 15:47:30 -05:00
commit 5ad9dee85b
2 files changed
+171 -1

No files matched your search

+70
View File
@@ -1,8 +1,10 @@
// js/feedEngine/watchHistoryFeed.js
import { pointerKey, normalizePointerInput } from "../nostr/watchHistory.js";
import { convertEventToVideo } from "../nostr/index.js";
import watchHistoryService from "../watchHistoryService.js";
import nostrService from "../services/nostrService.js";
import { nostrClient } from "../nostrClientFacade.js";
import {
isWatchHistoryDebugEnabled,
logWatchHistoryDebug,
@@ -202,6 +204,73 @@ function createWatchHistorySource({ service = watchHistoryService } = {}) {
};
}
function createWatchHistoryHydrationStage() {
return async function watchHistoryHydrationStage(items = [], context = {}) {
const missing = [];
const idMap = new Map();
for (const item of items) {
if (item.video) {
continue;
}
const pointer = item.pointer;
if (pointer?.type === "e" && pointer.value) {
missing.push(pointer.value);
if (!idMap.has(pointer.value)) {
idMap.set(pointer.value, []);
}
idMap.get(pointer.value).push(item);
}
}
if (!missing.length) {
return items;
}
const uniqueIds = Array.from(new Set(missing));
const relays =
Array.isArray(nostrClient?.relays) && nostrClient.relays.length
? nostrClient.relays
: null;
if (!relays || !nostrClient?.pool) {
return items;
}
try {
const events = await nostrClient.pool.list(relays, [
{ ids: uniqueIds },
]);
for (const event of events) {
if (!event || !event.id) {
continue;
}
try {
const video = convertEventToVideo(event);
if (video && !video.invalid) {
const targets = idMap.get(event.id);
if (targets) {
for (const target of targets) {
target.video = video;
if (target.metadata) {
target.metadata.video = video;
}
}
}
}
} catch (err) {
debugWarn(`[watchHistoryFeed] Failed to convert event ${event.id}:`, err);
}
}
} catch (error) {
debugWarn("[watchHistoryFeed] Hydration fetch failed:", error);
}
return items;
};
}
function createWatchHistoryHydratorStage({ service = watchHistoryService } = {}) {
return async function watchHistoryHydratorStage(items = [], context = {}) {
const results = [];
@@ -281,6 +350,7 @@ export function createWatchHistoryFeedDefinition({
source: createWatchHistorySource({ service }),
stages: [
createWatchHistoryHydratorStage({ service }),
createWatchHistoryHydrationStage(),
blacklistStage,
createWatchHistorySuppressionStage(),
],
+101 -1
View File
@@ -23,10 +23,14 @@ const { nostrClient, setActiveSigner, getActiveSigner, clearActiveSigner } = awa
const { rememberNostrTools } = await import("../js/nostr/toolkit.js");
const { normalizeActorKey } = await import("../js/nostr/watchHistory.js");
const { watchHistoryService } = await import("../js/watchHistoryService.js");
const { buildHistoryCard } = await import("../js/historyView.js");
const { buildHistoryCard, createWatchHistoryRenderer } = await import("../js/historyView.js");
const { getApplication, setApplication } = await import(
"../js/applicationContext.js"
);
const { createWatchHistoryFeedDefinition } = await import(
"../js/feedEngine/watchHistoryFeed.js"
);
const { createFeedEngine } = await import("../js/feedEngine/engine.js");
if (typeof globalThis.window === "undefined") {
globalThis.window = {};
@@ -2514,6 +2518,102 @@ async function testWatchHistoryAppLoginFallback() {
}
}
async function testWatchHistoryFeedHydration() {
console.log("Running watch history feed hydration test...");
poolHarness.reset();
watchHistoryService.resetProgress();
const actor = "feed-hydration-actor";
const videoId = "hydration-video-id";
const videoTitle = "Hydrated Video Title";
const videoEvent = {
id: videoId,
kind: 30078,
pubkey: "video-author",
created_at: Math.floor(Date.now() / 1000) - 100,
tags: [["d", "hydration-d-tag"], ["t", "video"]],
content: JSON.stringify({
title: videoTitle,
videoRootId: "root-id",
version: 2
})
};
const originalPoolList = nostrClient.pool.list;
const originalApp = getApplication();
const originalPubkey = nostrClient.pubkey;
try {
nostrClient.pubkey = actor;
const engine = createFeedEngine();
engine.registerFeed("watch-history", createWatchHistoryFeedDefinition({ service: watchHistoryService }));
setApplication({
feedEngine: { run: (name, opts) => engine.runFeed(name, opts) },
isAuthorBlocked: () => false,
getHashtagPreferences: () => ({ interests: [], disinterests: [] })
});
// Mock pool.list to return the video event when requested by ID
nostrClient.pool.list = async (relays, filters) => {
const results = [];
for (const filter of filters) {
if (filter.ids && filter.ids.includes(videoId)) {
results.push(videoEvent);
}
if (filter.kinds && filter.kinds.includes(WATCH_HISTORY_KIND)) {
results.push({
id: "history-event-id",
kind: WATCH_HISTORY_KIND,
pubkey: actor,
created_at: Math.floor(Date.now() / 1000),
tags: [["d", "2023-11"]],
content: JSON.stringify({
version: 2,
events: [videoId],
watchedAt: { [videoId]: Math.floor(Date.now() / 1000) }
})
});
}
}
return results;
};
// Simulate renderer fetching via feed engine
const renderer = createWatchHistoryRenderer({
fetchHistory: async (actorInput, { cursor = 0 } = {}) => {
const runtime = {
watchHistory: { actor: actorInput, cursor },
blacklistedEventIds: new Set(),
isAuthorBlocked: () => false
};
return engine.run("watch-history", { runtime });
},
getActor: async () => actor
});
await renderer.init({ actor, force: true });
const state = renderer.getState();
assert.equal(state.items.length, 1, "Should have 1 item");
const item = state.items[0];
// Check if hydration worked
assert.ok(item.video, "Item should have video object populated");
assert.equal(item.video.title, videoTitle, "Video title should be hydrated from relay event");
assert.equal(item.video.id, videoId, "Video ID should match");
} finally {
nostrClient.pubkey = originalPubkey;
nostrClient.pool.list = originalPoolList;
setApplication(originalApp);
watchHistoryService.resetProgress();
}
}
await testWatchHistoryFeedHydration();
await testPublishSnapshotCanonicalizationAndChunking();
await testPublishSnapshotUsesPlaintext();
// await testPublishSnapshotFallsBackToNip04WhenNip44Unavailable(); // Obsolete with plaintext