perf(m3u): stop cancelled refresh workers (#1268)

This commit is contained in:
4gray authored and GitHub committed 2026-07-26 09:25:51 +02:00
1 parent 7e8c2ccce1
commit f147d4fe37
28 files changed
+3205 -89

No files matched your search

@@ -0,0 +1,49 @@
import type {
ElectronBridgeApi,
PlaylistRefreshPayload,
} from '@iptvnator/shared/interfaces';
import { PlaylistRefreshService } from './playlist-refresh.service';
describe('PlaylistRefreshService', () => {
const originalElectron = window.electron;
const payload: PlaylistRefreshPayload = {
operationId: 'playlist-refresh-cancelled',
playlistId: 'playlist-1',
title: 'Large playlist',
url: 'http://127.0.0.1/large.m3u',
};
afterEach(() => {
Object.defineProperty(window, 'electron', {
configurable: true,
value: originalElectron,
writable: true,
});
});
it('creates a renderer-local AbortError from a cancellation result', async () => {
const unsubscribe = jest.fn();
const electron = {
onPlaylistRefreshEvent: jest.fn(() => unsubscribe),
refreshPlaylist: jest.fn().mockResolvedValue({
operationId: payload.operationId,
type: 'playlist-refresh-cancelled',
}),
} as unknown as ElectronBridgeApi;
Object.defineProperty(window, 'electron', {
configurable: true,
value: electron,
writable: true,
});
await expect(
new PlaylistRefreshService().refreshPlaylist(payload)
).rejects.toMatchObject({
message:
'Playlist refresh "playlist-refresh-cancelled" was cancelled',
name: 'AbortError',
});
expect(unsubscribe).toHaveBeenCalledTimes(1);
});
});
@@ -1,5 +1,6 @@
import { Injectable } from '@angular/core';
import {
isPlaylistRefreshCancelledResult,
Playlist,
PlaylistRefreshEvent,
PlaylistRefreshPayload,
@@ -30,7 +31,15 @@ export class PlaylistRefreshService {
});
try {
return await window.electron.refreshPlaylist(payload);
const result = await window.electron.refreshPlaylist(payload);
if (isPlaylistRefreshCancelledResult(result)) {
const error = new Error(
`Playlist refresh "${result.operationId}" was cancelled`
);
error.name = 'AbortError';
throw error;
}
return result;
} finally {
unsubscribe?.();
}
@@ -21,6 +21,7 @@ import {
XtreamBackupRecentlyViewedItem,
} from './playlist-backup.interface';
import {
PlaylistRefreshCancelledResult,
PlaylistRefreshEvent,
PlaylistRefreshPayload,
} from './playlist-refresh.interface';
@@ -671,7 +672,9 @@ export interface ElectronBridgeApi {
url: string,
method?: 'GET' | 'HEAD'
) => Promise<ElectronBridgeXtreamProbeResult>;
refreshPlaylist: (payload: PlaylistRefreshPayload) => Promise<Playlist>;
refreshPlaylist: (
payload: PlaylistRefreshPayload
) => Promise<Playlist | PlaylistRefreshCancelledResult>;
cancelPlaylistRefresh: (
operationId: string
) => Promise<ElectronBridgeResult>;
@@ -27,3 +27,25 @@ export interface PlaylistRefreshPayload {
url?: string;
trustedInsecureTlsHosts?: string[];
}
export const PLAYLIST_REFRESH_CANCELLED_RESULT_TYPE =
'playlist-refresh-cancelled' as const;
export interface PlaylistRefreshCancelledResult {
operationId: string;
type: typeof PLAYLIST_REFRESH_CANCELLED_RESULT_TYPE;
}
export function isPlaylistRefreshCancelledResult(
value: unknown
): value is PlaylistRefreshCancelledResult {
if (!value || typeof value !== 'object') {
return false;
}
const candidate = value as Record<string, unknown>;
return (
candidate['type'] === PLAYLIST_REFRESH_CANCELLED_RESULT_TYPE &&
typeof candidate['operationId'] === 'string'
);
}