From afd64ba1ce9f73e5c808c4de64a910b865b3fe79 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 23:15:56 +0000 Subject: [PATCH] Refactor feed loading logic into shared loadFeedVideos method - Introduce `loadFeedVideos(feedType, forceFetch)` in `js/app/feedCoordinator.js` to consolidate video loading logic. - Update `loadForYouVideos`, `loadKidsVideos`, `loadExploreVideos`, and `loadVideos` to use the shared method. - Parameterize feed-specific configuration (telemetry context, tags, loading messages, relay health checks) based on feed type. - Add `tests/app/feedCoordinator.test.mjs` to verify the new logic. Signed-off-by: Jules Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- js/app/feedCoordinator.js | 276 ++++++++++------------------- tests/app/feedCoordinator.test.mjs | 89 ++++++++++ 2 files changed, 183 insertions(+), 182 deletions(-) create mode 100644 tests/app/feedCoordinator.test.mjs diff --git a/js/app/feedCoordinator.js b/js/app/feedCoordinator.js index 06d6237d..8fa082e2 100644 --- a/js/app/feedCoordinator.js +++ b/js/app/feedCoordinator.js @@ -1085,6 +1085,96 @@ export function createFeedCoordinator(deps) { return true; }, + /** + * Unified loader for all feed types. + */ + async loadFeedVideos(feedType, forceFetch = false) { + devLogger.log(`Starting loadFeedVideos(${feedType})... (forceFetch =`, forceFetch, ")"); + this.setFeedTelemetryContext(feedType); + + let includeTags = false; + let loadingMessage = "Fetching videos\u2026"; + let refreshMethod = null; + let shouldCheckRelayHealth = false; + + switch (feedType) { + case "recent": + includeTags = true; + loadingMessage = "Fetching recent videos\u2026"; + refreshMethod = (opts) => this.refreshRecentFeed(opts); + shouldCheckRelayHealth = true; + break; + case FEED_TYPES.FOR_YOU: + includeTags = false; + loadingMessage = "Fetching for-you videos\u2026"; + refreshMethod = (opts) => this.refreshForYouFeed(opts); + shouldCheckRelayHealth = true; + break; + case FEED_TYPES.KIDS: + includeTags = true; + loadingMessage = "Fetching kids videos\u2026"; + refreshMethod = (opts) => this.refreshKidsFeed(opts); + shouldCheckRelayHealth = false; + break; + case FEED_TYPES.EXPLORE: + includeTags = false; + loadingMessage = "Fetching explore videos\u2026"; + refreshMethod = (opts) => this.refreshExploreFeed(opts); + shouldCheckRelayHealth = false; + break; + default: + devLogger.warn(`[Application] Unknown feed type for loadFeedVideos: ${feedType}`); + return; + } + + if (shouldCheckRelayHealth) { + this.checkRelayHealthWarning(); + } + + const container = this.mountVideoListView({ includeTags }); + const hasCachedVideos = + this.nostrService && + Array.isArray(this.nostrService.getFilteredActiveVideos()) && + this.nostrService.getFilteredActiveVideos().length > 0; + + if (!hasCachedVideos) { + if (this.videoListView && container) { + this.videoListView.showLoading(loadingMessage); + } else if (container) { + container.innerHTML = getSidebarLoadingMarkup(loadingMessage); + } + } + + let initialRefreshPromise = null; + + const videos = await this.nostrService.loadVideos({ + forceFetch, + blacklistedEventIds: this.blacklistedEventIds, + isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey), + onVideos: (payload, detail = {}) => { + const promise = refreshMethod({ + reason: detail?.reason, + fallbackVideos: payload, + }); + if (!initialRefreshPromise) { + initialRefreshPromise = promise; + } + }, + }); + + if (initialRefreshPromise) { + await initialRefreshPromise; + } else if (!Array.isArray(videos) || videos.length === 0) { + await refreshMethod({ reason: "initial", fallbackVideos: [] }); + } + + this.videoSubscription = this.nostrService.getVideoSubscription() || null; + this.videosMap = this.nostrService.getVideosMap(); + if (this.videoListView) { + this.videoListView.state.videosMap = this.videosMap; + } + }, + /** * Subscribe to videos (older + new) and render them as they come in. */ @@ -1102,52 +1192,7 @@ export function createFeedCoordinator(deps) { this.lastLoadVideosTime = now; this.loadVideosPromise = (async () => { - devLogger.log("Starting loadVideos... (forceFetch =", forceFetch, ")"); - this.setFeedTelemetryContext("recent"); - this.checkRelayHealthWarning(); - - const container = this.mountVideoListView(); - const hasCachedVideos = - this.nostrService && - Array.isArray(this.nostrService.getFilteredActiveVideos()) && - this.nostrService.getFilteredActiveVideos().length > 0; - - if (!hasCachedVideos) { - if (this.videoListView && container) { - this.videoListView.showLoading("Fetching recent videos\u2026"); - } else if (container) { - container.innerHTML = getSidebarLoadingMarkup("Fetching recent videos\u2026"); - } - } - - let initialRefreshPromise = null; - - const videos = await this.nostrService.loadVideos({ - forceFetch, - blacklistedEventIds: this.blacklistedEventIds, - isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey), - onVideos: (payload, detail = {}) => { - const promise = this.refreshRecentFeed({ - reason: detail?.reason, - fallbackVideos: payload, - }); - if (!initialRefreshPromise) { - initialRefreshPromise = promise; - } - }, - }); - - if (initialRefreshPromise) { - await initialRefreshPromise; - } else if (!Array.isArray(videos) || videos.length === 0) { - await this.refreshRecentFeed({ reason: "initial", fallbackVideos: [] }); - } - - this.videoSubscription = this.nostrService.getVideoSubscription() || null; - this.videosMap = this.nostrService.getVideosMap(); - if (this.videoListView) { - this.videoListView.state.videosMap = this.videosMap; - } + await this.loadFeedVideos("recent", forceFetch); })(); try { @@ -1158,148 +1203,15 @@ export function createFeedCoordinator(deps) { }, async loadForYouVideos(forceFetch = false) { - devLogger.log("Starting loadForYouVideos... (forceFetch =", forceFetch, ")"); - this.setFeedTelemetryContext(FEED_TYPES.FOR_YOU); - this.checkRelayHealthWarning(); - - const container = this.mountVideoListView({ includeTags: false }); - const hasCachedVideos = - this.nostrService && - Array.isArray(this.nostrService.getFilteredActiveVideos()) && - this.nostrService.getFilteredActiveVideos().length > 0; - - if (!hasCachedVideos) { - if (this.videoListView && container) { - this.videoListView.showLoading("Fetching for-you videos\u2026"); - } else if (container) { - container.innerHTML = getSidebarLoadingMarkup("Fetching for-you videos\u2026"); - } - } - - let initialRefreshPromise = null; - - const videos = await this.nostrService.loadVideos({ - forceFetch, - blacklistedEventIds: this.blacklistedEventIds, - isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey), - onVideos: (payload, detail = {}) => { - const promise = this.refreshForYouFeed({ - reason: detail?.reason, - fallbackVideos: payload, - }); - if (!initialRefreshPromise) { - initialRefreshPromise = promise; - } - }, - }); - - if (initialRefreshPromise) { - await initialRefreshPromise; - } else if (!Array.isArray(videos) || videos.length === 0) { - await this.refreshForYouFeed({ reason: "initial", fallbackVideos: [] }); - } - - this.videoSubscription = this.nostrService.getVideoSubscription() || null; - this.videosMap = this.nostrService.getVideosMap(); - if (this.videoListView) { - this.videoListView.state.videosMap = this.videosMap; - } + return this.loadFeedVideos(FEED_TYPES.FOR_YOU, forceFetch); }, async loadKidsVideos(forceFetch = false) { - devLogger.log("Starting loadKidsVideos... (forceFetch =", forceFetch, ")"); - this.setFeedTelemetryContext(FEED_TYPES.KIDS); - - const container = this.mountVideoListView({ includeTags: true }); - const hasCachedVideos = - this.nostrService && - Array.isArray(this.nostrService.getFilteredActiveVideos()) && - this.nostrService.getFilteredActiveVideos().length > 0; - - if (!hasCachedVideos) { - if (this.videoListView && container) { - this.videoListView.showLoading("Fetching kids videos\u2026"); - } else if (container) { - container.innerHTML = getSidebarLoadingMarkup("Fetching kids videos\u2026"); - } - } - - let initialRefreshPromise = null; - - const videos = await this.nostrService.loadVideos({ - forceFetch, - blacklistedEventIds: this.blacklistedEventIds, - isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey), - onVideos: (payload, detail = {}) => { - const promise = this.refreshKidsFeed({ - reason: detail?.reason, - fallbackVideos: payload, - }); - if (!initialRefreshPromise) { - initialRefreshPromise = promise; - } - }, - }); - - if (initialRefreshPromise) { - await initialRefreshPromise; - } else if (!Array.isArray(videos) || videos.length === 0) { - await this.refreshKidsFeed({ reason: "initial", fallbackVideos: [] }); - } - - this.videoSubscription = this.nostrService.getVideoSubscription() || null; - this.videosMap = this.nostrService.getVideosMap(); - if (this.videoListView) { - this.videoListView.state.videosMap = this.videosMap; - } + return this.loadFeedVideos(FEED_TYPES.KIDS, forceFetch); }, async loadExploreVideos(forceFetch = false) { - devLogger.log("Starting loadExploreVideos... (forceFetch =", forceFetch, ")"); - this.setFeedTelemetryContext(FEED_TYPES.EXPLORE); - - const container = this.mountVideoListView({ includeTags: false }); - const hasCachedVideos = - this.nostrService && - Array.isArray(this.nostrService.getFilteredActiveVideos()) && - this.nostrService.getFilteredActiveVideos().length > 0; - - if (!hasCachedVideos) { - if (this.videoListView && container) { - this.videoListView.showLoading("Fetching explore videos\u2026"); - } else if (container) { - container.innerHTML = getSidebarLoadingMarkup("Fetching explore videos\u2026"); - } - } - - let initialRefreshPromise = null; - - const videos = await this.nostrService.loadVideos({ - forceFetch, - blacklistedEventIds: this.blacklistedEventIds, - isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey), - onVideos: (payload, detail = {}) => { - const promise = this.refreshExploreFeed({ - reason: detail?.reason, - fallbackVideos: payload, - }); - if (!initialRefreshPromise) { - initialRefreshPromise = promise; - } - }, - }); - - if (initialRefreshPromise) { - await initialRefreshPromise; - } else if (!Array.isArray(videos) || videos.length === 0) { - await this.refreshExploreFeed({ reason: "initial", fallbackVideos: [] }); - } - - this.videoSubscription = this.nostrService.getVideoSubscription() || null; - this.videosMap = this.nostrService.getVideosMap(); - if (this.videoListView) { - this.videoListView.state.videosMap = this.videosMap; - } + return this.loadFeedVideos(FEED_TYPES.EXPLORE, forceFetch); }, async loadOlderVideos(lastTimestamp) { diff --git a/tests/app/feedCoordinator.test.mjs b/tests/app/feedCoordinator.test.mjs new file mode 100644 index 00000000..80f93cfd --- /dev/null +++ b/tests/app/feedCoordinator.test.mjs @@ -0,0 +1,89 @@ +import { test, mock } from 'node:test'; +import assert from 'node:assert/strict'; +import { createFeedCoordinator } from '../../js/app/feedCoordinator.js'; +import { FEED_TYPES } from '../../js/constants.js'; + +test('createFeedCoordinator - loadForYouVideos', async (t) => { + const mockDeps = { + devLogger: { log: mock.fn(), warn: mock.fn() }, + userLogger: { warn: mock.fn() }, + nostrClient: {}, + getSidebarLoadingMarkup: mock.fn(() => '
Loading...
'), + watchHistoryService: {}, + subscriptions: {}, + createActiveNostrSource: mock.fn(), + createBlacklistFilterStage: mock.fn(), + createDisinterestFilterStage: mock.fn(), + createDedupeByRootStage: mock.fn(), + createExploreDiversitySorter: mock.fn(), + createExploreScorerStage: mock.fn(), + createKidsAudienceFilterStage: mock.fn(), + createKidsScorerStage: mock.fn(), + createKidsScoreSorter: mock.fn(), + createModerationStage: mock.fn(), + createResolvePostedAtStage: mock.fn(), + createTagPreferenceFilterStage: mock.fn(), + createWatchHistorySuppressionStage: mock.fn(), + createChronologicalSorter: mock.fn(), + createSubscriptionAuthorsSource: mock.fn(), + registerWatchHistoryFeed: mock.fn(), + }; + + const coordinator = createFeedCoordinator(mockDeps); + + // Mock application context + const app = { + ...coordinator, + nostrService: { + getFilteredActiveVideos: mock.fn(() => []), // Simulate no cached videos + loadVideos: mock.fn(async ({ onVideos }) => { + // Simulate fetch completing with videos + onVideos([], { reason: 'test-reason' }); + return []; + }), + getVideoSubscription: mock.fn(() => 'mock-subscription'), + getVideosMap: mock.fn(() => new Map()), + }, + videoListView: { + showLoading: mock.fn(), + state: {}, + }, + mountVideoListView: mock.fn(() => ({ innerHTML: '' })), + checkRelayHealthWarning: mock.fn(), + setFeedTelemetryContext: mock.fn(), + isAuthorBlocked: mock.fn(() => false), + refreshForYouFeed: mock.fn(async () => {}), + refreshRecentFeed: mock.fn(async () => {}), + refreshKidsFeed: mock.fn(async () => {}), + refreshExploreFeed: mock.fn(async () => {}), + blacklistedEventIds: new Set(), + }; + + // Bind methods to app context + const loadForYouVideos = app.loadForYouVideos.bind(app); + + await t.test('loadForYouVideos executes successfully', async () => { + await loadForYouVideos(false); + + // Verify devLogger was called + assert.strictEqual(mockDeps.devLogger.log.mock.callCount() > 0, true, 'devLogger.log should be called'); + + // Verify telemetry context set + assert.strictEqual(app.setFeedTelemetryContext.mock.calls[0].arguments[0], FEED_TYPES.FOR_YOU, 'Should set telemetry context to for-you'); + + // Verify mountVideoListView called with includeTags: false + assert.deepStrictEqual(app.mountVideoListView.mock.calls[0].arguments[0], { includeTags: false }, 'Should mount list view with includeTags: false'); + + // Verify checkRelayHealthWarning called + assert.strictEqual(app.checkRelayHealthWarning.mock.callCount(), 1, 'Should check relay health warning'); + + // Verify nostrService.loadVideos called + assert.strictEqual(app.nostrService.loadVideos.mock.callCount(), 1, 'Should call nostrService.loadVideos'); + + // Verify refreshForYouFeed called via callback + assert.strictEqual(app.refreshForYouFeed.mock.callCount(), 1, 'Should call refreshForYouFeed via callback'); + + // Verify subscription updated + assert.strictEqual(app.videoSubscription, 'mock-subscription', 'Should update videoSubscription'); + }); +});