mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(migration): show startup preparation and recover source read failures (#1568)
* fix(migration): show startup preparation and recover source read failures * fix(migration): keep inventory reload failures recoverable * test(migration): type the deferred recovery hook * fix(migration): reconcile failed EPG cleanup before recovery * fix(migration): await settings before inventory readiness * style(migration): add a subtle theme-aware startup watermark
This commit is contained in:
1 parent
97b0264dee
commit
9f950a1530
37 files changed
+1024
-14
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { Injector } from '@angular/core';
|
||||
import { firstValueFrom, of, Subject } from 'rxjs';
|
||||
import { firstValueFrom, of, Subject, throwError } from 'rxjs';
|
||||
import { EpgSourceSettingsService } from './epg-source-settings.service';
|
||||
import { PlaylistsService } from './playlists.service';
|
||||
|
||||
@@ -109,4 +109,70 @@ describe('EPG source settings synchronization', () => {
|
||||
]);
|
||||
expect(service.retainCurrentSources(['a', 'b'], 0)).toEqual(['b']);
|
||||
});
|
||||
it('retries failed inventory ownership before pruning, using the committed URLs', async () => {
|
||||
const getAllPlaylists = jest
|
||||
.fn()
|
||||
.mockReturnValue(throwError(() => new Error('disk')));
|
||||
const reconcileEpgSources = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ success: true });
|
||||
window.electron = {
|
||||
reconcileEpgSources,
|
||||
} as unknown as typeof window.electron;
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
EpgSourceSettingsService,
|
||||
{ provide: PlaylistsService, useValue: { getAllPlaylists } },
|
||||
],
|
||||
});
|
||||
const service = injector.get(EpgSourceSettingsService);
|
||||
// No known committed settings means no permission to prune defaults.
|
||||
await service.retryFailedReconciliation();
|
||||
expect(getAllPlaylists).not.toHaveBeenCalled();
|
||||
await expect(
|
||||
service.synchronize([' current ', 'current'])
|
||||
).rejects.toThrow();
|
||||
expect(reconcileEpgSources).not.toHaveBeenCalled();
|
||||
getAllPlaylists.mockReturnValue(of([]));
|
||||
await service.retryFailedReconciliation();
|
||||
expect(reconcileEpgSources).toHaveBeenCalledWith(['current']);
|
||||
await service.retryFailedReconciliation();
|
||||
expect(reconcileEpgSources).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('waits for a newer committed synchronization without replaying an older failed set', async () => {
|
||||
let finish!: (result: { success: boolean }) => void;
|
||||
const reconcileEpgSources = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ success: false })
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
finish = resolve;
|
||||
})
|
||||
);
|
||||
window.electron = {
|
||||
reconcileEpgSources,
|
||||
} as unknown as typeof window.electron;
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
EpgSourceSettingsService,
|
||||
{
|
||||
provide: PlaylistsService,
|
||||
useValue: { getAllPlaylists: () => of([]) },
|
||||
},
|
||||
],
|
||||
});
|
||||
const service = injector.get(EpgSourceSettingsService);
|
||||
await expect(service.synchronize(['old'])).rejects.toThrow();
|
||||
const recovery = service.retryFailedReconciliation();
|
||||
const latest = service.synchronize(['new']);
|
||||
await Promise.resolve();
|
||||
finish({ success: true });
|
||||
await Promise.all([latest, recovery]);
|
||||
expect(reconcileEpgSources.mock.calls).toEqual([[['old']], [['new']]]);
|
||||
expect(service.retainCurrentSources(['old', 'new'], 0)).toEqual([
|
||||
'new',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -42,6 +42,7 @@ export class EpgSourceSettingsService {
|
||||
private readonly injector = inject(Injector);
|
||||
private activeUrls = new Set<string>();
|
||||
private reconciliation: Promise<void> | undefined;
|
||||
private failedReconciliation: { urls: string[] } | undefined;
|
||||
readonly revision = signal(0);
|
||||
readonly changed$ = new Subject<void>();
|
||||
|
||||
@@ -66,6 +67,18 @@ export class EpgSourceSettingsService {
|
||||
}
|
||||
}
|
||||
|
||||
/** A recovered inventory must finish any previously authorized cleanup. */
|
||||
async retryFailedReconciliation(): Promise<void> {
|
||||
// A save can start between the wait resolving and this continuation.
|
||||
// Recheck ownership before scheduling a retry of the failed URL set.
|
||||
do {
|
||||
await this.waitForReconciliation();
|
||||
} while (this.reconciliation);
|
||||
if (this.failedReconciliation) {
|
||||
await this.synchronize(this.failedReconciliation.urls);
|
||||
}
|
||||
}
|
||||
|
||||
async synchronize(urls: string[] | string | undefined): Promise<void> {
|
||||
if (
|
||||
typeof window === 'undefined' ||
|
||||
@@ -108,7 +121,9 @@ export class EpgSourceSettingsService {
|
||||
await window.electron.reconcileEpgSources(normalized);
|
||||
if (!result.success)
|
||||
throw new Error('EPG source reconciliation failed');
|
||||
this.failedReconciliation = undefined;
|
||||
} catch {
|
||||
this.failedReconciliation = { urls: normalized };
|
||||
throw new EpgSourceReconciliationError();
|
||||
} finally {
|
||||
this.revision.update((revision) => revision + 1);
|
||||
|
||||
Reference in new issue
Block a user