fix(settings): serialize lock-store writes and drop a deleted playlist's locks

- Lock-store mutations run through one write queue: each rewrites the
  whole persisted store, so overlapping edits could otherwise snapshot
  the same store and the later write would drop the earlier edit.
- Deleting a playlist removes its locks through the PLAYLIST_DELETE_CLEANUP
  hook; "Remove all playlists" clears the lock store once the deletion
  has succeeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-27 00:14:27 +02:00
1 parent a030e1af2f
commit 0f9c7a6364
9 files changed
+212 -35

No files matched your search

+5 -1
View File
@@ -33,7 +33,10 @@ import {
} from '@iptvnator/portal/shared/util';
import { STALKER_PLAYLIST_CONNECTION_EDITOR } from '@iptvnator/playlist/shared/ui';
import { provideXtreamDataSource } from '@iptvnator/portal/xtream/data-access';
import { DataService } from '@iptvnator/services';
import {
provideParentalLockPlaylistCleanup,
DataService,
} from '@iptvnator/services';
import { dbConfig } from '@iptvnator/shared/interfaces';
import { AppConfig } from '../environments/environment';
import { routes } from './app.routes';
@@ -162,6 +165,7 @@ export const appConfig: ApplicationConfig = {
},
...provideWorkspaceShellActions(),
...provideParentalLockPrompt(),
provideParentalLockPlaylistCleanup(),
...provideXtreamDataSource(),
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
@@ -6,7 +6,11 @@ import {
selectAllPlaylistsMeta,
selectIsEpgAvailable,
} from '@iptvnator/m3u-state';
import { DatabaseService, PlaylistsService } from '@iptvnator/services';
import {
ParentalLockService,
DatabaseService,
PlaylistsService,
} from '@iptvnator/services';
import { PlaylistMeta } from '@iptvnator/shared/interfaces';
import { Store } from '@ngrx/store';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
@@ -59,6 +63,9 @@ describe('SettingsPlaylistResetFacade', () => {
MockProvider(PlaylistsService, {
removeAll: jest.fn().mockReturnValue(of(undefined)),
}),
MockProvider(ParentalLockService, {
clearAllLocks: jest.fn().mockResolvedValue(true),
}),
provideMockStore({
selectors: [
{ selector: selectAllPlaylistsMeta, value: [] },
@@ -218,6 +225,9 @@ describe('SettingsPlaylistResetFacade', () => {
expect(playlistsService.removeAll).toHaveBeenCalled();
expect(databaseService.deleteAllPlaylists).not.toHaveBeenCalled();
expect(
TestBed.inject(ParentalLockService).clearAllLocks
).toHaveBeenCalled();
expect(dispatchSpy).toHaveBeenCalledWith(
PlaylistActions.removeAllPlaylists()
);
@@ -235,6 +245,10 @@ describe('SettingsPlaylistResetFacade', () => {
await new Promise<void>((resolve) => setTimeout(resolve, 0));
expect(facade.isRemovingAllPlaylists()).toBe(false);
// Nothing was deleted, so the locks stay.
expect(
TestBed.inject(ParentalLockService).clearAllLocks
).not.toHaveBeenCalled();
expect(snackBar.open).toHaveBeenCalledWith(
'SETTINGS.PLAYLISTS_REMOVE_FAILED',
undefined,
@@ -5,6 +5,7 @@ import { TranslateService } from '@ngx-translate/core';
import { firstValueFrom, take } from 'rxjs';
import { PlaylistActions, selectAllPlaylistsMeta } from '@iptvnator/m3u-state';
import {
ParentalLockService,
DatabaseService,
DbOperationEvent,
PlaylistsService,
@@ -24,6 +25,7 @@ import { SettingsSnackbarService } from './settings-snackbar.service';
@Injectable()
export class SettingsPlaylistResetFacade {
private readonly databaseService = inject(DatabaseService);
private readonly parentalLock = inject(ParentalLockService);
private readonly matDialog = inject(MatDialog);
private readonly playlistsService = inject(PlaylistsService);
private readonly runtime = inject(RuntimeCapabilitiesService);
@@ -107,6 +109,12 @@ export class SettingsPlaylistResetFacade {
if (!deleted) {
throw new Error('Delete all playlists returned success=false');
}
// The playlists are gone; their locks must not linger in the
// lock store (the deletion itself already succeeded, so a failed
// clear is only logged).
if (!(await this.parentalLock.clearAllLocks())) {
console.warn('Clearing the parental locks failed.');
}
this.store.dispatch(PlaylistActions.removeAllPlaylists());
this.settingsSnackbar.open(