mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 21:56:18 -08:00
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:
1 parent
a030e1af2f
commit
0f9c7a6364
9 files changed
+212
-35
No files matched your search
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user