mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 22:26:16 -08:00
fix(settings): restore the lock index when an emptying write fails and publish rollbacks after re-stamping
- Removing a playlist's last lock clears the SQLite index first; if the clear or the store write then fails, the index is re-stamped from the previous locks at once. Title matching and multi-source discovery query the worker directly and trust the index, so the stale flag alone did not protect them. - A rollback publishes its store revision only after every type is re-stamped, so a reload cannot read a later type through the attempted stamps. - docs: restore the index rules the earlier surfaces rewrite dropped from the contract, now in the Lock store lifetime section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5c3e675b41
commit
73ccdebb60
3 files changed
+99
-8
No files matched your search
@@ -141,6 +141,52 @@ describe('ParentalLockLockStore', () => {
|
||||
expect(storage.writeLocks).toHaveBeenLastCalledWith({});
|
||||
});
|
||||
|
||||
it('restores the cleared index when saving the emptied store fails', async () => {
|
||||
await store.load();
|
||||
await store.ensureReadable();
|
||||
setCategoryLocks.mockClear();
|
||||
storage.writeLocks.mockResolvedValueOnce(false);
|
||||
|
||||
await expect(store.setXtreamLocks('pl-1', 'live', [])).resolves.toBe(
|
||||
false
|
||||
);
|
||||
|
||||
// Cleared first, then put back from the previous locks.
|
||||
expect(setCategoryLocks.mock.calls).toEqual([
|
||||
['pl-1', 'live', []],
|
||||
['pl-1', 'live', [7]],
|
||||
]);
|
||||
expect(store.readable()).toBe(true);
|
||||
});
|
||||
|
||||
it('publishes a rollback only after every type is re-stamped', async () => {
|
||||
await store.load();
|
||||
await store.ensureReadable();
|
||||
const before = store.revision();
|
||||
setCategoryLocks.mockClear();
|
||||
const revisionsAtStamp: number[] = [];
|
||||
let call = 0;
|
||||
setCategoryLocks.mockImplementation(async () => {
|
||||
revisionsAtStamp.push(store.revision());
|
||||
call += 1;
|
||||
// live ok, movies fails, then the rollback re-stamps succeed
|
||||
return call !== 2;
|
||||
});
|
||||
|
||||
await expect(
|
||||
store.replacePlaylistLocks('pl-1', {
|
||||
xtream: [{ categoryType: 'movies', xtreamId: 3 }],
|
||||
stalker: [],
|
||||
m3u: [],
|
||||
})
|
||||
).resolves.toBe(false);
|
||||
|
||||
expect(revisionsAtStamp.every((revision) => revision === before)).toBe(
|
||||
true
|
||||
);
|
||||
expect(store.revision()).toBe(before + 1);
|
||||
});
|
||||
|
||||
it('does not drop the key when clearing the index fails', async () => {
|
||||
await store.load();
|
||||
await store.ensureReadable();
|
||||
|
||||
@@ -365,19 +365,20 @@ export class ParentalLockLockStore {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!(await this.stampXtreamLocks(
|
||||
(await this.stampXtreamLocks(
|
||||
playlistId,
|
||||
categoryTypes,
|
||||
normalizedNext
|
||||
))
|
||||
)) &&
|
||||
(await this.persistPlaylistLocks(playlistId, normalizedNext))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (await this.persistPlaylistLocks(playlistId, normalizedNext)) {
|
||||
return true;
|
||||
}
|
||||
this.markIndexStale(playlistId);
|
||||
this.revisionState.update((value) => value + 1);
|
||||
// The clear (partly) reached the index but the store still holds
|
||||
// the locks: put the index back at once — consumers that query
|
||||
// the worker directly (title matching, multi-source discovery)
|
||||
// trust `locked` and are not gated by the renderer's stale flag.
|
||||
await this.restoreIndex(playlistId, previous, categoryTypes);
|
||||
return false;
|
||||
}
|
||||
// The revision is published only once every type is stamped: a
|
||||
@@ -414,13 +415,30 @@ export class ParentalLockLockStore {
|
||||
previous: ParentalLockPlaylistLocks,
|
||||
categoryTypes: readonly ParentalLockXtreamCategoryType[]
|
||||
): Promise<void> {
|
||||
const restored = await this.persistPlaylistLocks(playlistId, previous);
|
||||
// Published once, after the re-stamp: a reload on an earlier
|
||||
// revision would read a later type through the attempted stamps.
|
||||
const restored = await this.persistPlaylistLocks(playlistId, previous, {
|
||||
publish: false,
|
||||
});
|
||||
const restamped =
|
||||
restored &&
|
||||
(await this.stampXtreamLocks(playlistId, categoryTypes));
|
||||
if (!restored || !restamped) {
|
||||
console.error('Failed to roll back the parental lock index.');
|
||||
this.markIndexStale(playlistId);
|
||||
}
|
||||
this.revisionState.update((value) => value + 1);
|
||||
}
|
||||
|
||||
/** Re-stamps the index from `locks`; marks it stale when that fails. */
|
||||
private async restoreIndex(
|
||||
playlistId: string,
|
||||
locks: ParentalLockPlaylistLocks,
|
||||
categoryTypes: readonly ParentalLockXtreamCategoryType[]
|
||||
): Promise<void> {
|
||||
if (!(await this.stampXtreamLocks(playlistId, categoryTypes, locks))) {
|
||||
console.error('Failed to restore the parental lock index.');
|
||||
this.markIndexStale(playlistId);
|
||||
this.revisionState.update((value) => value + 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user