diff --git a/js/app/feedCoordinator.js b/js/app/feedCoordinator.js index 73138370..5281941d 100644 --- a/js/app/feedCoordinator.js +++ b/js/app/feedCoordinator.js @@ -1016,8 +1016,21 @@ export function createFeedCoordinator(deps) { const recommendationFeed = feedType === FEED_TYPES.FOR_YOU || feedType === FEED_TYPES.EXPLORE; + let exploreActivation = null; if (recommendationFeed && typeof this.ensureExploreDataService === "function") { - await this.ensureExploreDataService(); + // Recommendation indexes are an enhancement, not a prerequisite for + // showing cached videos. Activating the worker here used to leave a + // returning For You view stuck on its static loading markup whenever + // worker startup was slow or unavailable. + exploreActivation = Promise.resolve(this.ensureExploreDataService()).catch( + (error) => { + devLogger.warn( + `[Application] Recommendation data activation failed for ${feedType}:`, + error, + ); + return null; + }, + ); } else if (this.exploreDataService?.setActive) { this.exploreDataService.setActive(false); } @@ -1117,6 +1130,23 @@ export function createFeedCoordinator(deps) { if (this.videoListView) { this.videoListView.state.videosMap = this.videosMap; } + + // Re-score once the optional recommendation indexes are ready, but only + // if the user is still looking at this feed. The first render above is + // deliberately cache-first and must not wait for worker startup. + if (exploreActivation) { + void exploreActivation.then(() => { + if (!this.isFeedActive(feedType)) { + return; + } + return this.refreshFeed(feedType, { reason: "recommendation-data-ready" }); + }).catch((error) => { + devLogger.warn( + `[Application] Failed to refresh ${feedType} after recommendation data activation:`, + error, + ); + }); + } }, /** diff --git a/tests/app/feedCoordinator.test.mjs b/tests/app/feedCoordinator.test.mjs index be34d9fd..e8c989e1 100644 --- a/tests/app/feedCoordinator.test.mjs +++ b/tests/app/feedCoordinator.test.mjs @@ -85,3 +85,61 @@ test('createFeedCoordinator - loadForYouVideos', async (t) => { assert.strictEqual(app.videoSubscription, 'mock-subscription', 'Should update videoSubscription'); }); }); + +test("loadForYouVideos renders without waiting for recommendation worker activation", async () => { + let resolveActivation; + const activation = new Promise((resolve) => { + resolveActivation = resolve; + }); + let loadVideosStarted = false; + 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); + const app = { + ...coordinator, + nostrService: { + getFilteredActiveVideos: mock.fn(() => [{ id: "cached" }]), + loadVideos: mock.fn(async ({ onVideos }) => { + loadVideosStarted = true; + onVideos([{ id: "cached", title: "Cached", created_at: 1 }], { reason: "cache" }); + return [{ id: "cached", title: "Cached", created_at: 1 }]; + }), + getVideoSubscription: mock.fn(() => null), + getVideosMap: mock.fn(() => new Map()), + }, + videoListView: { state: {}, render: mock.fn() }, + mountVideoListView: mock.fn(() => ({ innerHTML: "" })), + checkRelayHealthWarning: mock.fn(), + setFeedTelemetryContext: mock.fn(), + isAuthorBlocked: mock.fn(() => false), + refreshFeed: mock.fn(async () => {}), + ensureExploreDataService: mock.fn(() => activation), + blacklistedEventIds: new Set(), + }; + + await app.loadForYouVideos(false); + assert.equal(loadVideosStarted, true); + resolveActivation(); +});