fix(portals): preserve Xtream restore retry state

This commit is contained in:
4gray committed 2026-07-30 00:58:38 +02:00
1 parent b0598f8f70
commit 83fb06e79a
24 files changed
+913 -115

No files matched your search

@@ -4,6 +4,7 @@ import {
selectAllPlaylistsMeta,
selectIsEpgAvailable,
} from '@iptvnator/m3u-state';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import { PlaylistBackupService } from '@iptvnator/services';
import { provideMockStore } from '@ngrx/store/testing';
import { TranslateModule } from '@ngx-translate/core';
@@ -36,6 +37,9 @@ describe('SettingsBackupFacade', () => {
.mockResolvedValue(BACKUP_EXPORT_RESULT),
importBackup: jest.fn(),
}),
MockProvider(XtreamStore, {
reconcilePendingRestoreBlock: jest.fn(),
}),
provideMockStore({
selectors: [
{ selector: selectAllPlaylistsMeta, value: [] },
@@ -154,4 +158,46 @@ describe('SettingsBackupFacade', () => {
expect.objectContaining({ panelClass: ['settings-snackbar'] })
);
});
it('reconciles a pending Xtream restore before completing an import', async () => {
configure();
const input = document.createElement('input');
const file = {
text: jest.fn().mockResolvedValue('{}'),
} as unknown as File;
Object.defineProperty(input, 'files', { value: [file] });
const addEventListener = jest.spyOn(input, 'addEventListener');
const createElement = jest
.spyOn(document, 'createElement')
.mockReturnValue(input);
jest.spyOn(input, 'click').mockImplementation();
const summary = {
imported: 0,
merged: 0,
skipped: 0,
failed: 1,
errors: ['pending restore state could not be consumed'],
};
(playlistBackupService.importBackup as jest.Mock).mockResolvedValue(
summary
);
const onImported = jest.fn();
const xtreamStore = TestBed.inject(XtreamStore);
facade.importData(onImported);
const changeListener = addEventListener.mock.calls.find(
([type]) => type === 'change'
)?.[1] as (event: Event) => Promise<void>;
await changeListener({ target: input } as unknown as Event);
expect(xtreamStore.reconcilePendingRestoreBlock).toHaveBeenCalledTimes(
1
);
expect(onImported).toHaveBeenCalledTimes(1);
expect(
(xtreamStore.reconcilePendingRestoreBlock as jest.Mock).mock
.invocationCallOrder[0]
).toBeLessThan(onImported.mock.invocationCallOrder[0]);
createElement.mockRestore();
});
});
@@ -1,7 +1,8 @@
import { inject, Injectable, signal } from '@angular/core';
import { inject, Injectable, Injector, signal } from '@angular/core';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { PlaylistActions } from '@iptvnator/m3u-state';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import {
PlaylistBackupImportSummary,
PlaylistBackupService,
@@ -16,6 +17,7 @@ export class SettingsBackupFacade {
private readonly settingsSnackbar = inject(SettingsSnackbarService);
private readonly store = inject(Store);
private readonly translate = inject(TranslateService);
private readonly injector = inject(Injector);
readonly isExportingData = signal(false);
@@ -79,6 +81,9 @@ export class SettingsBackupFacade {
const summary = await this.playlistBackupService.importBackup(
await file.text()
);
this.injector
.get(XtreamStore, null)
?.reconcilePendingRestoreBlock();
if (summary.imported > 0 || summary.merged > 0) {
this.store.dispatch(PlaylistActions.removeAllPlaylists());