mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(settings): retire in-flight searches on relock clearing and publish lock revisions after the stamps
- clearSearchResults() advances the search request version, so a search issued under the previous lock state cannot republish what a relock just cleared. - A lock write publishes its store revision only once every touched type is stamped, so a reload triggered by it cannot read a later type through its old stamps. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
d2d9a56096
commit
efece8628f
5 files changed
+69
-6
No files matched your search
@@ -177,7 +177,8 @@ on either side.
|
||||
the lock store already names its category (the pre-reload category list
|
||||
maps Electron's row id to the provider id), then `withholdCatalog()`
|
||||
empties every catalog list and `clearSearchResults()` the stored search
|
||||
before the filtered reads refill them; both reloads take a publish guard
|
||||
(retiring a search still in flight, which was issued under the previous
|
||||
lock state) before the filtered reads refill them; both reloads take a publish guard
|
||||
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
|
||||
@@ -305,7 +306,9 @@ on either side.
|
||||
backup restore stamps three types, and the ones before the failing type
|
||||
already carry the new locks; a category must not be recorded and shown
|
||||
as locked while Electron reads, which filter by the index alone, still
|
||||
serve it); if the rollback write or its re-stamp fails too, the playlist is
|
||||
serve it), and the store revision consumers reload on is published only
|
||||
once every touched type is stamped, so a reload cannot read a later type
|
||||
through its old stamps; if the rollback write or its re-stamp fails too, the playlist is
|
||||
re-stamped on the next store access, and every launch re-derives the
|
||||
index from the store for each playlist that has locks — which is why a
|
||||
write that removes a playlist's LAST lock clears the index first and
|
||||
|
||||
@@ -161,6 +161,18 @@ describe('withSearch refreshSearchResults', () => {
|
||||
expect(store.searchResults()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('retires a search still in flight when the results are cleared', async () => {
|
||||
const pending = createDeferred<unknown[]>();
|
||||
searchContent.mockReturnValueOnce(pending.promise);
|
||||
const running = store.searchContent('news', ['live']);
|
||||
|
||||
store.clearSearchResults();
|
||||
pending.resolve([{ xtream_id: 1 }]);
|
||||
await running;
|
||||
|
||||
expect(store.searchResults()).toEqual([]);
|
||||
});
|
||||
|
||||
it('does nothing without a previous search or after a reset', async () => {
|
||||
await store.refreshSearchResults();
|
||||
expect(searchContent).not.toHaveBeenCalled();
|
||||
|
||||
@@ -161,8 +161,14 @@ export function withSearch() {
|
||||
}
|
||||
},
|
||||
|
||||
/** Drops the stored results; term, filters and the last search stay. */
|
||||
/**
|
||||
* Drops the stored results; term, filters and the last search
|
||||
* stay. Also retires a search still in flight: it was issued
|
||||
* under the previous lock state and must not republish what
|
||||
* was just cleared.
|
||||
*/
|
||||
clearSearchResults(): void {
|
||||
searchRequestVersion++;
|
||||
patchState(store, { searchResults: [] });
|
||||
},
|
||||
|
||||
|
||||
@@ -155,6 +155,36 @@ describe('ParentalLockLockStore', () => {
|
||||
expect(store.lockedXtreamIds('pl-1', 'live')).toEqual([7]);
|
||||
});
|
||||
|
||||
it('publishes the revision only after every type is stamped', async () => {
|
||||
await store.load();
|
||||
await store.ensureReadable();
|
||||
const before = store.revision();
|
||||
let resolveStamp: (ok: boolean) => void = () => undefined;
|
||||
setCategoryLocks.mockImplementation(
|
||||
() => new Promise<boolean>((resolve) => (resolveStamp = resolve))
|
||||
);
|
||||
|
||||
const writing = store.replacePlaylistLocks('pl-1', {
|
||||
xtream: [{ categoryType: 'movies', xtreamId: 3 }],
|
||||
stalker: [],
|
||||
m3u: [],
|
||||
});
|
||||
for (
|
||||
let i = 0;
|
||||
i < 50 && storage.writeLocks.mock.calls.length === 0;
|
||||
i += 1
|
||||
) {
|
||||
await Promise.resolve();
|
||||
}
|
||||
expect(storage.writeLocks).toHaveBeenCalled();
|
||||
expect(store.revision()).toBe(before);
|
||||
|
||||
setCategoryLocks.mockResolvedValue(true);
|
||||
resolveStamp(true);
|
||||
await expect(writing).resolves.toBe(true);
|
||||
expect(store.revision()).toBe(before + 1);
|
||||
});
|
||||
|
||||
it('rolls the store back when the index re-stamp fails', async () => {
|
||||
await store.load();
|
||||
setCategoryLocks.mockResolvedValue(false);
|
||||
|
||||
@@ -285,10 +285,19 @@ export class ParentalLockLockStore {
|
||||
this.revisionState.update((value) => value + 1);
|
||||
return false;
|
||||
}
|
||||
if (!(await this.persistPlaylistLocks(playlistId, next))) {
|
||||
// The revision is published only once every type is stamped: a
|
||||
// consumer reloading on the revision while the stamps are still
|
||||
// running would read a later type through its old stamps, and a
|
||||
// successful stamp emits nothing afterwards to reload it.
|
||||
if (
|
||||
!(await this.persistPlaylistLocks(playlistId, next, {
|
||||
publish: false,
|
||||
}))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (await this.stampXtreamLocks(playlistId, categoryTypes)) {
|
||||
this.revisionState.update((value) => value + 1);
|
||||
return true;
|
||||
}
|
||||
await this.rollBack(playlistId, previous, categoryTypes);
|
||||
@@ -347,7 +356,8 @@ export class ParentalLockLockStore {
|
||||
|
||||
private async persistPlaylistLocks(
|
||||
playlistId: string,
|
||||
locks: ParentalLockPlaylistLocks
|
||||
locks: ParentalLockPlaylistLocks,
|
||||
options: { publish?: boolean } = {}
|
||||
): Promise<boolean> {
|
||||
if (!(await this.ensureReadable())) {
|
||||
return false;
|
||||
@@ -363,7 +373,9 @@ export class ParentalLockLockStore {
|
||||
return false;
|
||||
}
|
||||
this.locks.set(next);
|
||||
this.revisionState.update((value) => value + 1);
|
||||
if (options.publish !== false) {
|
||||
this.revisionState.update((value) => value + 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user