mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Merge pull request #1083 from PR0M3TH3AN/watch-history-hydration-improvement-14056519037250996205
Implement video hydration stage for watch history feed
This commit is contained in:
2 files changed
+171
-1
No files matched your search
@@ -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(),
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user