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 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-27 11:52:30 +02:00
1 parent 870e526f9c
commit eb770b16d6
3 files changed
+41 -1

No files matched your search

+3 -1
View File
@@ -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
@@ -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<unknown[]>();
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();
@@ -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: [],