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:
4grayandClaude Fable 5.1 committed 2026-09-26 13:23:10 +02:00
1 parent d2d9a56096
commit efece8628f
5 files changed
+69 -6

No files matched your search

@@ -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;
}
}