diff --git a/libs/workspace/dashboard/data-access/src/lib/dashboard-recommendations.service.spec.ts b/libs/workspace/dashboard/data-access/src/lib/dashboard-recommendations.service.spec.ts index b2db2dd2e..10602e866 100644 --- a/libs/workspace/dashboard/data-access/src/lib/dashboard-recommendations.service.spec.ts +++ b/libs/workspace/dashboard/data-access/src/lib/dashboard-recommendations.service.spec.ts @@ -1044,4 +1044,36 @@ describe('DashboardRecommendationsService', () => { expect(service.seedTitles()).toEqual(['The Matrix']); }); + + it('derives the seed heading from the cards visible now', async () => { + recentVod = [ + { title: 'The Matrix', type: 'movie' }, + { title: 'Blade Runner', type: 'movie' }, + ]; + recentAll = [...recentVod]; + const second = ['Akira', 'Ghost in the Shell', 'Paprika']; + enrichMovie.mockImplementation(async (query: { title: string }) => ({ + recommendations: { + results: + query.title === 'The Matrix' + ? recTitles.map((t, i) => rec(100 + i, t)) + : second.map((t, i) => rec(300 + i, t)), + }, + })); + // The second seed's recommendations sit in category 8. + matchTitles.mockImplementation(async (titles: string[]) => + titles.map((title) => + match(title, second.includes(title) ? { categoryId: 8 } : {}) + ) + ); + // Locked at load: the second seed has no visible card yet. + withheldCategories.set(new Set([8])); + const service = createService(); + await service.load(); + expect(service.seedTitles()).toEqual(['The Matrix']); + + // Unlocked: its cards appear, and so does its heading. + withheldCategories.set(new Set()); + expect(service.seedTitles()).toEqual(['The Matrix', 'Blade Runner']); + }); }); diff --git a/libs/workspace/dashboard/data-access/src/lib/dashboard-recommendations.service.ts b/libs/workspace/dashboard/data-access/src/lib/dashboard-recommendations.service.ts index c19d5b8c1..46c87951c 100644 --- a/libs/workspace/dashboard/data-access/src/lib/dashboard-recommendations.service.ts +++ b/libs/workspace/dashboard/data-access/src/lib/dashboard-recommendations.service.ts @@ -70,7 +70,12 @@ export class DashboardRecommendationsService { /** The last load's candidates and ALL their catalog matches. */ private readonly loaded = signal(EMPTY_LOAD); - private readonly matchedSeedTitles = signal([]); + /** + * Every seed of the last load, most recent first. `seedTitles` keeps + * those that contribute a card NOW: a relock can hide some cards and let + * another seed's recommendations fill their places. + */ + private readonly seedOrder = signal([]); /** * The cards, built on READ from the matches the parental lock does not * withhold: a relock hides a card matched while unlocked, and a title @@ -89,9 +94,7 @@ export class DashboardRecommendationsService { /** Seeds that contributed at least one visible card, most recent first */ readonly seedTitles = computed(() => { const contributed = new Set(this.items().map((item) => item.seedTitle)); - return this.matchedSeedTitles().filter((title) => - contributed.has(title) - ); + return this.seedOrder().filter((title) => contributed.has(title)); }); readonly loading = signal(false); @@ -117,7 +120,7 @@ export class DashboardRecommendationsService { // The service outlives the dashboard (root-provided), so a // cleared watch history must clear the rail too. this.loaded.set(EMPTY_LOAD); - this.matchedSeedTitles.set([]); + this.seedOrder.set([]); this.loadedKey = null; return; } @@ -161,15 +164,8 @@ export class DashboardRecommendationsService { MAX_ITEMS ); if (matched.length >= MIN_RECOMMENDATION_MATCHES) { - const contributed = new Set( - matched.map((item) => item.seedTitle) - ); this.loaded.set(load); - this.matchedSeedTitles.set( - perSeed - .map((seed) => seed.seedTitle) - .filter((title) => contributed.has(title)) - ); + this.seedOrder.set(perSeed.map((seed) => seed.seedTitle)); // Latch only once EVERY seed answered. A seed that did // not resolve may have failed transiently, and latching // on its behalf would drop its recommendations for the @@ -193,7 +189,7 @@ export class DashboardRecommendationsService { // exact inputs (say, un-favoriting again) would // otherwise hit the equality guard and stay empty. this.loaded.set(EMPTY_LOAD); - this.matchedSeedTitles.set([]); + this.seedOrder.set([]); this.loadedKey = null; } } @@ -253,15 +249,11 @@ export class DashboardRecommendationsService { ); if (cards.length < MIN_RECOMMENDATION_MATCHES) { this.loaded.set(EMPTY_LOAD); - this.matchedSeedTitles.set([]); + this.seedOrder.set([]); return; } - const contributed = new Set(cards.map((item) => item.seedTitle)); this.loaded.set(kept); - this.matchedSeedTitles.set( - this.matchedSeedTitles().filter((title) => contributed.has(title)) - ); } /**