Merge pull request #283 from PR0M3TH3AN/codex/integrate-feed-engine-in-app-setup

Integrate feed engine into recent video flow
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-04 16:42:27 -04:00
commit a940d0cdfd
3 files changed
+213 -12

No files matched your search

+159 -10
View File
@@ -18,6 +18,13 @@ import { attachUrlHealthBadges } from "./urlHealthObserver.js";
import { ADMIN_INITIAL_EVENT_BLACKLIST } from "./lists.js";
import { userBlocks } from "./userBlocks.js";
import { relayManager } from "./relayManager.js";
import {
createFeedEngine,
createActiveNostrSource,
createBlacklistFilterStage,
createDedupeByRootStage,
createChronologicalSorter,
} from "./feedEngine/index.js";
import watchHistoryService from "./watchHistoryService.js";
import r2Service from "./services/r2Service.js";
import PlaybackService from "./services/playbackService.js";
@@ -163,9 +170,19 @@ class Application {
this.videoListViewBlacklistHandler = null;
this.boundVideoListShareListener = null;
this.boundVideoListContextListener = null;
this.latestFeedMetadata = null;
this.nostrService = services.nostrService || nostrService;
this.r2Service = services.r2Service || r2Service;
this.feedEngine = services.feedEngine || createFeedEngine();
if (
this.feedEngine &&
typeof this.feedEngine.run !== "function" &&
typeof this.feedEngine.runFeed === "function"
) {
this.feedEngine.run = (...args) => this.feedEngine.runFeed(...args);
}
this.registerRecentFeed();
this.playbackService =
services.playbackService ||
@@ -4903,6 +4920,111 @@ class Application {
}
}
/**
* Register the default "recent" feed pipeline.
*/
registerRecentFeed() {
if (!this.feedEngine || typeof this.feedEngine.registerFeed !== "function") {
return null;
}
const existingDefinition =
typeof this.feedEngine.getFeedDefinition === "function"
? this.feedEngine.getFeedDefinition("recent")
: null;
if (existingDefinition) {
return existingDefinition;
}
try {
return this.feedEngine.registerFeed("recent", {
source: createActiveNostrSource({ service: this.nostrService }),
stages: [
createBlacklistFilterStage({
shouldIncludeVideo: (video, options) =>
this.nostrService.shouldIncludeVideo(video, options),
}),
createDedupeByRootStage({
dedupe: (videos) => this.dedupeVideosByRoot(videos),
}),
],
sorter: createChronologicalSorter(),
});
} catch (error) {
console.warn("[Application] Failed to register recent feed:", error);
return null;
}
}
buildRecentFeedRuntime() {
const blacklist =
this.blacklistedEventIds instanceof Set
? new Set(this.blacklistedEventIds)
: new Set();
return {
blacklistedEventIds: blacklist,
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
};
}
refreshRecentFeed({ reason, fallbackVideos } = {}) {
const runtime = this.buildRecentFeedRuntime();
const normalizedReason = typeof reason === "string" ? reason : undefined;
const fallback = Array.isArray(fallbackVideos) ? fallbackVideos : [];
if (!this.feedEngine || typeof this.feedEngine.run !== "function") {
const metadata = {
reason: normalizedReason,
engine: "unavailable",
};
this.latestFeedMetadata = metadata;
this.videosMap = this.nostrService.getVideosMap();
if (this.videoListView) {
this.videoListView.state.videosMap = this.videosMap;
}
this.renderVideoList({ videos: fallback, metadata });
return Promise.resolve({ videos: fallback, metadata });
}
return this.feedEngine
.run("recent", { runtime })
.then((result) => {
const videos = Array.isArray(result?.videos) ? result.videos : [];
const metadata = {
...(result?.metadata || {}),
};
if (normalizedReason) {
metadata.reason = normalizedReason;
}
this.latestFeedMetadata = metadata;
this.videosMap = this.nostrService.getVideosMap();
if (this.videoListView) {
this.videoListView.state.videosMap = this.videosMap;
}
const payload = { videos, metadata };
this.renderVideoList(payload);
return payload;
})
.catch((error) => {
console.error("[Application] Failed to run recent feed:", error);
const metadata = {
reason: normalizedReason || "error:recent-feed",
error: true,
};
this.latestFeedMetadata = metadata;
this.videosMap = this.nostrService.getVideosMap();
if (this.videoListView) {
this.videoListView.state.videosMap = this.videosMap;
}
const payload = { videos: fallback, metadata };
this.renderVideoList(payload);
return payload;
});
}
/**
* Subscribe to videos (older + new) and render them as they come in.
*/
@@ -4916,15 +5038,27 @@ class Application {
container.innerHTML = getSidebarLoadingMarkup("Fetching recent videos…");
}
let initialRefreshPromise = null;
const videos = await this.nostrService.loadVideos({
forceFetch,
blacklistedEventIds: this.blacklistedEventIds,
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
onVideos: (payload) => this.renderVideoList(payload),
onVideos: (payload, detail = {}) => {
const promise = this.refreshRecentFeed({
reason: detail?.reason,
fallbackVideos: payload,
});
if (!initialRefreshPromise) {
initialRefreshPromise = promise;
}
},
});
if (!Array.isArray(videos) || videos.length === 0) {
this.renderVideoList([]);
if (initialRefreshPromise) {
await initialRefreshPromise;
} else if (!Array.isArray(videos) || videos.length === 0) {
await this.refreshRecentFeed({ reason: "initial", fallbackVideos: [] });
}
this.videoSubscription = this.nostrService.getVideoSubscription() || null;
@@ -4945,11 +5079,7 @@ class Application {
return;
}
const all = this.nostrService.getFilteredActiveVideos({
blacklistedEventIds: this.blacklistedEventIds,
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
});
this.renderVideoList(all);
await this.refreshRecentFeed({ reason: "older-fetch" });
}
/**
@@ -5239,7 +5369,7 @@ class Application {
});
}
async renderVideoList(videos) {
async renderVideoList(payload) {
if (!this.videoListView) {
return;
}
@@ -5249,7 +5379,26 @@ class Application {
return;
}
this.videoListView.render(videos);
let videos = [];
let metadata = null;
if (Array.isArray(payload)) {
videos = payload;
} else if (payload && typeof payload === "object") {
if (Array.isArray(payload.videos)) {
videos = payload.videos;
}
if (payload.metadata && typeof payload.metadata === "object") {
metadata = { ...payload.metadata };
}
}
this.latestFeedMetadata = metadata;
if (this.videoListView) {
this.videoListView.state.feedMetadata = metadata;
}
this.videoListView.render(videos, metadata);
}
refreshVideoDiscussionCounts(videos = []) {
+10 -2
View File
@@ -54,6 +54,10 @@ export class VideoListView {
loadedThumbnails:
state.loadedThumbnails instanceof Map ? state.loadedThumbnails : new Map(),
videosMap: state.videosMap instanceof Map ? state.videosMap : new Map(),
feedMetadata:
state.feedMetadata && typeof state.feedMetadata === "object"
? { ...state.feedMetadata }
: null,
};
this.renderers = {
@@ -234,7 +238,7 @@ export class VideoListView {
this.handlers.blacklist = typeof handler === "function" ? handler : null;
}
render(videos) {
render(videos, metadata = null) {
if (!this.container) {
return [];
}
@@ -244,6 +248,10 @@ export class VideoListView {
this._lastRenderedVideoListElement = this.container;
}
const normalizedMetadata =
metadata && typeof metadata === "object" ? { ...metadata } : null;
this.state.feedMetadata = normalizedMetadata;
const source = Array.isArray(videos) ? videos : [];
const dedupedVideos = this.utils.dedupeVideos(source);
this.currentVideos = dedupedVideos.slice();
@@ -471,7 +479,7 @@ export class VideoListView {
this.currentVideos[index] = next;
this.state.videosMap.set(videoId, next);
this.render(this.currentVideos.slice());
this.render(this.currentVideos.slice(), this.state.feedMetadata);
return next;
}
+44
View File
@@ -171,8 +171,52 @@ async function testWatchHistoryHookIsolation() {
assert.equal(whyA.videoId, "x1");
}
async function testBlacklistOrderingWithRuntimeChanges() {
const engine = createFeedEngine();
const feedName = "runtime-order";
engine.registerFeed(feedName, {
source: async () => [
{ video: { id: "v1", created_at: 100 } },
{ video: { id: "v2", created_at: 200 } },
{ video: { id: "v3", created_at: 300 } },
],
stages: [
createBlacklistFilterStage({
shouldIncludeVideo(video, { blacklistedEventIds }) {
return !blacklistedEventIds.has(video.id);
},
}),
],
sorter: createChronologicalSorter(),
});
const baseline = await engine.runFeed(feedName, {
runtime: { blacklistedEventIds: new Set() },
});
assert.deepEqual(baseline.videos.map((video) => video.id), [
"v3",
"v2",
"v1",
]);
const withoutTop = await engine.runFeed(feedName, {
runtime: { blacklistedEventIds: new Set(["v3"]) },
});
assert.deepEqual(withoutTop.videos.map((video) => video.id), ["v2", "v1"]);
const withoutMiddle = await engine.runFeed(feedName, {
runtime: { blacklistedEventIds: new Set(["v2"]) },
});
assert.deepEqual(withoutMiddle.videos.map((video) => video.id), [
"v3",
"v1",
]);
}
await testDedupeOrdering();
await testBlacklistFiltering();
await testWatchHistoryHookIsolation();
await testBlacklistOrderingWithRuntimeChanges();
console.log("All feed engine tests passed");