fix(settings): apply single-row lock toggles inside the lock store's write queue

The right-click Lock/Unlock (portal categories and M3U groups) built the
new list before entering the queue, so two quick toggles shared one
snapshot and the second dropped the first. Lock writes now accept an edit
of the current list, evaluated inside the queue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-27 01:22:15 +02:00
1 parent 0f9c7a6364
commit 54f27fe656
11 files changed
+145 -56

No files matched your search

@@ -55,7 +55,7 @@ describe('WorkspaceCategoryLockActionService', () => {
);
});
it('adds and removes one Xtream category behind the PIN gate', async () => {
it('adds and removes one Xtream category as an edit of the current list', async () => {
const target = {
provider: 'xtreams' as const,
playlistId: 'p',
@@ -63,21 +63,24 @@ describe('WorkspaceCategoryLockActionService', () => {
item: { id: 2, xtream_id: 8 },
};
await expect(service.setLocked(target, true)).resolves.toBe(true);
expect(parentalLock.setXtreamLocks).toHaveBeenCalledWith(
'p',
'live',
[7, 8]
);
const [, type, edit] = parentalLock.setXtreamLocks.mock
.calls[0] as unknown as [
string,
string,
(current: number[]) => number[],
];
expect(type).toBe('live');
// Applied inside the store's queue to whatever is current THEN.
expect(edit([7])).toEqual([7, 8]);
expect(edit([7, 8])).toEqual([7, 8]);
await service.setLocked(
{ ...target, item: { id: 1, xtream_id: 7 } },
false
);
expect(parentalLock.setXtreamLocks).toHaveBeenLastCalledWith(
'p',
'live',
[]
);
const unlock = parentalLock.setXtreamLocks.mock
.calls[1][2] as unknown as (current: number[]) => number[];
expect(unlock([7, 8])).toEqual([8]);
});
it('toggles a Stalker genre and never the All pseudo-category', async () => {
@@ -91,10 +94,9 @@ describe('WorkspaceCategoryLockActionService', () => {
service.isLocked({ ...target, item: { category_id: '9' } })
).toBe(true);
await expect(service.setLocked(target, true)).resolves.toBe(true);
expect(parentalLock.setStalkerLocks).toHaveBeenCalledWith('p', 'itv', [
'9',
'5',
]);
const edit = parentalLock.setStalkerLocks.mock
.calls[0][2] as unknown as (current: string[]) => string[];
expect(edit(['9'])).toEqual(['9', '5']);
await expect(
service.setLocked({ ...target, item: { category_id: '*' } }, true)
).resolves.toBe(false);
@@ -89,16 +89,15 @@ export class WorkspaceCategoryLockActionService {
if (!type || id === null) {
return Promise.resolve(false);
}
const current = this.parentalLock.lockedXtreamIds(
target.playlistId,
type
);
// An edit, not a list: it is applied inside the lock store's
// write queue, so back-to-back toggles do not share a snapshot.
return this.parentalLock.setXtreamLocks(
target.playlistId,
type,
locked
? [...current, id]
: current.filter((entry) => entry !== id)
(current) =>
locked
? [...new Set([...current, id])]
: current.filter((entry) => entry !== id)
);
}
const type = toParentalLockStalkerCategoryType(target.section);
@@ -106,14 +105,13 @@ export class WorkspaceCategoryLockActionService {
if (!type || id === null) {
return Promise.resolve(false);
}
const current = this.parentalLock.lockedStalkerIds(
target.playlistId,
type
);
return this.parentalLock.setStalkerLocks(
target.playlistId,
type,
locked ? [...current, id] : current.filter((entry) => entry !== id)
(current) =>
locked
? [...new Set([...current, id])]
: current.filter((entry) => entry !== id)
);
}