From 7e5d1c7d6373d94f0c48c272eddf2be22a71ca1c Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 8 Feb 2026 14:48:19 -0500 Subject: [PATCH] Guard watch-history renderer feed fallback path --- js/historyView.js | 48 ++++++++++++++++++---------- tests/watch-history.test.mjs | 61 ++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 16 deletions(-) diff --git a/js/historyView.js b/js/historyView.js index 5f9f6618..4adda743 100644 --- a/js/historyView.js +++ b/js/historyView.js @@ -11,7 +11,7 @@ import { WATCH_HISTORY_BATCH_PAGE_SIZE } from "./config.js"; import { getApplication } from "./applicationContext.js"; -import { userLogger } from "./utils/logger.js"; +import { devLogger, userLogger } from "./utils/logger.js"; import { normalizeVideoModerationContext, applyModerationContextDatasets, @@ -1391,6 +1391,7 @@ export function buildHistoryCard({ item, video, profile, variant }) { } export function createWatchHistoryRenderer(config = {}) { + let hasLoggedFeedRegistrationFallback = false; const { container = typeof document !== "undefined" ? document : null, fetchHistory = async ( @@ -1400,25 +1401,40 @@ export function createWatchHistoryRenderer(config = {}) { const app = getAppInstance(); const engine = app?.feedEngine; if (engine && typeof engine.run === "function") { + let hasFeedDefinition = + typeof engine.getFeedDefinition === "function" + ? Boolean(engine.getFeedDefinition("watch-history")) + : true; // Ensure the watch-history feed is registered. It may not be if the // feed engine was ready but registration was silently skipped during // bootstrap (e.g. timing edge cases). - if ( - typeof engine.getFeedDefinition === "function" && - !engine.getFeedDefinition("watch-history") && - typeof app.registerWatchHistoryFeed === "function" - ) { - app.registerWatchHistoryFeed(); + if (!hasFeedDefinition && typeof app.registerWatchHistoryFeed === "function") { + try { + app.registerWatchHistoryFeed(); + } catch (registerError) { + // Fall through to service fallback if registration fails. + } + hasFeedDefinition = + typeof engine.getFeedDefinition === "function" + ? Boolean(engine.getFeedDefinition("watch-history")) + : hasFeedDefinition; } - try { - const runtime = buildWatchHistoryFeedRuntime({ - actor: actorInput, - cursor, - forceRefresh, - }); - return await engine.run("watch-history", { runtime }); - } catch (feedError) { - // Fall through to service fallback if feed is still not registered + if (hasFeedDefinition) { + try { + const runtime = buildWatchHistoryFeedRuntime({ + actor: actorInput, + cursor, + forceRefresh, + }); + return await engine.run("watch-history", { runtime }); + } catch (feedError) { + // Fall through to service fallback if feed is still not registered + } + } else if (!hasLoggedFeedRegistrationFallback) { + hasLoggedFeedRegistrationFallback = true; + devLogger.info( + "[historyView] watch-history feed unavailable after registration attempt; using service fallback.", + ); } } const items = await watchHistoryService.loadLatest(actorInput, { diff --git a/tests/watch-history.test.mjs b/tests/watch-history.test.mjs index 695ea559..ddeda96f 100644 --- a/tests/watch-history.test.mjs +++ b/tests/watch-history.test.mjs @@ -2392,6 +2392,66 @@ async function testWatchHistoryLocalFallbackWhenDisabled() { } } +async function testWatchHistoryRendererFeedRegistrationFallback() { + console.log("Running watch history renderer feed registration fallback test..."); + + const actor = "watch-history-renderer-fallback"; + const originalApp = getApplication(); + const originalLoadLatest = watchHistoryService.loadLatest; + const originalDocument = globalThis.document; + const originalHTMLElement = globalThis.HTMLElement; + + let runCalled = false; + let loadLatestCalls = 0; + + try { + globalThis.document = { querySelector: () => null }; + globalThis.HTMLElement = class HTMLElement {}; + + watchHistoryService.loadLatest = async (actorInput) => { + loadLatestCalls += 1; + assert.equal(actorInput, actor, "fallback loader should target provided actor"); + return [{ type: "e", value: "service-fallback-entry", watchedAt: 1_700_700_000 }]; + }; + + setApplication({ + feedEngine: { + run() { + runCalled = true; + throw new Error("not registered"); + }, + getFeedDefinition() { + return null; + }, + }, + registerWatchHistoryFeed() { + throw new Error("registration failed"); + }, + isAuthorBlocked: () => false, + getHashtagPreferences: () => ({ interests: [], disinterests: [] }), + }); + + const renderer = createWatchHistoryRenderer({ + container: { querySelector: () => null }, + getActor: async () => actor, + }); + + await renderer.init({ actor, force: true }); + const state = renderer.getState(); + + assert.equal(runCalled, false, "renderer should not run unregistered feed"); + assert.equal(loadLatestCalls, 1, "renderer should use service fallback once"); + assert.equal(state.lastError, null, "fallback path should avoid surfacing feed registration errors"); + assert.equal(state.items.length, 1, "fallback path should populate items from service"); + assert.equal(state.items[0]?.pointer?.value, "service-fallback-entry"); + } finally { + watchHistoryService.loadLatest = originalLoadLatest; + globalThis.document = originalDocument; + globalThis.HTMLElement = originalHTMLElement; + setApplication(originalApp || null); + } +} + async function testWatchHistorySyncEnabledForLoggedInUsers() { console.log("Running watch history logged-in sync override test..."); @@ -2636,6 +2696,7 @@ await testWatchHistoryStaleCacheRefresh(); await testWatchHistoryLocalFallbackWhenDisabled(); await testWatchHistorySyncEnabledForLoggedInUsers(); await testWatchHistoryAppLoginFallback(); +await testWatchHistoryRendererFeedRegistrationFallback(); await testNormalizeActorKeyShortCircuit(); // await testNormalizeActorKeyManualFallback(); // Flaky in env without robust nostr-tools