From eb770b16d6b003b3df5ba53f0985cc6025c92ff4 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 27 Sep 2026 11:52:30 +0200 Subject: [PATCH] fix(settings): defer a stale Xtream hydration as soon as the catalog is withheld On Electron a relock that overtook the initial content hydration waited for the category reload before the content reload set the deferral flag; the older unlocked hydration could publish its streams in that window. withholdCatalog() now sets the flag itself, before anything is awaited. Co-Authored-By: Claude Opus 5.5 --- docs/architecture/parental-lock.md | 4 ++- .../with-content.feature.reload.spec.ts | 31 +++++++++++++++++++ .../stores/features/with-content.feature.ts | 7 +++++ 3 files changed, 41 insertions(+), 1 deletion(-) 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: [],