diff --git a/docs/architecture/parental-lock.md b/docs/architecture/parental-lock.md index 50b664f77..6bc30c4f3 100644 --- a/docs/architecture/parental-lock.md +++ b/docs/architecture/parental-lock.md @@ -190,7 +190,9 @@ on either side. answered before every state patch, so a read issued under an older lock version is dropped instead of published. A lock change that overtakes the INITIAL hydration (the content is not initialized yet, so the reload - has nothing to re-read) sets a deferred-reload flag instead: the + has nothing to re-read) sets a deferred-reload flag instead — already + when `withholdCatalog()` empties the lists, before the category reload is + awaited: the hydration then publishes empty lists in place of the rows it read under the previous lock state (categories included), and the filtered reload of categories and content runs as soon as the hydration settles, on diff --git a/libs/portal/xtream/data-access/src/lib/stores/features/with-content.feature.reload.spec.ts b/libs/portal/xtream/data-access/src/lib/stores/features/with-content.feature.reload.spec.ts index ae581d853..e0ddbffd0 100644 --- a/libs/portal/xtream/data-access/src/lib/stores/features/with-content.feature.reload.spec.ts +++ b/libs/portal/xtream/data-access/src/lib/stores/features/with-content.feature.reload.spec.ts @@ -173,6 +173,37 @@ describe('withContent parental-lock reloads', () => { expect(store.serialStreams()).toEqual([]); }); + it('suppresses a hydration publication as soon as the catalog is withheld', async () => { + const live = createDeferred(); + let calls = 0; + let liveAfterHydrationPublish: unknown; + dataSource.getContent.mockImplementation(() => { + calls += 1; + if (calls === 1) { + return live.promise; + } + if (calls === 2) { + // The hydration published its live rows just before this + // (vod) read: capture what it left on screen. + liveAfterHydrationPublish = store.liveStreams(); + } + return Promise.resolve([{ xtream_id: calls }]); + }); + const initialization = store.initializeContent(); + await waitForCondition(() => calls === 1); + + // Relock: the catalog is withheld BEFORE any reload has run, then + // the hydration's pending (unlocked) live read resolves. + store.withholdCatalog(); + live.resolve([{ xtream_id: 1 }]); + await initialization; + + expect(liveAfterHydrationPublish).toEqual([]); + // The deferred reload refilled the list with fresh rows. + expect(store.liveStreams()).not.toEqual([{ xtream_id: 1 }]); + expect(store.liveStreams()).not.toEqual([]); + }); + it('empties the category lists when their reload fails', async () => { dataSource.getCategories.mockResolvedValue([{ category_id: 'x' }]); await store.reloadCategories(); diff --git a/libs/portal/xtream/data-access/src/lib/stores/features/with-content.feature.ts b/libs/portal/xtream/data-access/src/lib/stores/features/with-content.feature.ts index 78db6ae3c..d9cd79efd 100644 --- a/libs/portal/xtream/data-access/src/lib/stores/features/with-content.feature.ts +++ b/libs/portal/xtream/data-access/src/lib/stores/features/with-content.feature.ts @@ -1524,6 +1524,13 @@ export function withContent() { * refills the lists. */ withholdCatalog(): void { + // Before any reload is awaited: an initial hydration + // still in flight read its rows under the previous lock + // state, and must not publish them while the category + // reload runs. The deferred reload refills afterwards. + if (!store.isContentInitialized()) { + reloadAfterInitialization = true; + } patchState(store, { liveCategories: [], vodCategories: [],