mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(electron): isolate refresh performance correlation
This commit is contained in:
1 parent
e3ce60a35e
commit
f9e71a6d8d
25 files changed
+728
-402
No files matched your search
@@ -166,6 +166,7 @@ async function runIteration(
|
||||
],
|
||||
env: {
|
||||
IPTVNATOR_DB_WORKER_BATCH_DELAY_MS: '0',
|
||||
IPTVNATOR_PERF_CAPTURE: '1',
|
||||
IPTVNATOR_PERF_WORKER_PROFILING: '1',
|
||||
IPTVNATOR_TRACE_RENDERER_CONSOLE:
|
||||
process.env['IPTVNATOR_TRACE_RENDERER_CONSOLE'] ?? '0',
|
||||
|
||||
@@ -224,3 +224,15 @@ test('the cancellation benchmark command is pinned to its Playwright test file',
|
||||
'pnpm exec playwright test --config=playwright.performance.config.ts src/m3u-refresh-cancellation.performance.ts'
|
||||
);
|
||||
});
|
||||
|
||||
test('the cancellation benchmark enables preload performance capture', () => {
|
||||
const source = readFileSync(
|
||||
join(
|
||||
workspaceRoot,
|
||||
'apps/electron-backend-e2e/src/performance/m3u-refresh-cancellation.benchmark.ts'
|
||||
),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
assert.match(source, /IPTVNATOR_PERF_CAPTURE:\s*'1'/);
|
||||
});
|
||||
@@ -77,12 +77,12 @@ describe('main preload performance marker contract', () => {
|
||||
await expect(api.refreshPlaylist(payload)).resolves.toBe(
|
||||
refreshedPlaylist
|
||||
);
|
||||
await expect(api.dbGetAppPlaylist(payload.playlistId)).resolves.toBe(
|
||||
storedPlaylist
|
||||
);
|
||||
await expect(api.dbUpsertAppPlaylist(upsertPlaylist)).resolves.toBe(
|
||||
upsertResult
|
||||
);
|
||||
await expect(
|
||||
api.dbGetAppPlaylist(payload.playlistId, payload.operationId)
|
||||
).resolves.toBe(storedPlaylist);
|
||||
await expect(
|
||||
api.dbUpsertAppPlaylist(upsertPlaylist, payload.operationId)
|
||||
).resolves.toBe(upsertResult);
|
||||
|
||||
expect(harness.ipcRenderer.invoke.mock.calls).toEqual([
|
||||
['PLAYLIST:REFRESH', payload],
|
||||
@@ -289,12 +289,15 @@ describe('main preload performance marker contract', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps results unchanged when marker delivery throws', async () => {
|
||||
it('poisons correlation after the first marker delivery failure', async () => {
|
||||
const payload = createRefreshPayload();
|
||||
const result = createPlaylist(payload.playlistId);
|
||||
await loadPerformancePreload();
|
||||
harness.ipcRenderer.invoke.mockResolvedValueOnce(result);
|
||||
harness.ipcRenderer.send.mockImplementation((channel: string) => {
|
||||
harness.ipcRenderer.invoke
|
||||
.mockResolvedValueOnce(result)
|
||||
.mockResolvedValueOnce(result)
|
||||
.mockResolvedValueOnce({ success: true });
|
||||
harness.ipcRenderer.send.mockImplementationOnce((channel: string) => {
|
||||
if (channel === PRELOAD_PERFORMANCE_MARKER_CHANNEL) {
|
||||
throw new Error('marker-send-failed');
|
||||
}
|
||||
@@ -303,11 +306,33 @@ describe('main preload performance marker contract', () => {
|
||||
await expect(harness.getApi().refreshPlaylist(payload)).resolves.toBe(
|
||||
result
|
||||
);
|
||||
await expect(
|
||||
harness
|
||||
.getApi()
|
||||
.dbGetAppPlaylist(payload.playlistId, payload.operationId)
|
||||
).resolves.toBe(result);
|
||||
await expect(
|
||||
harness.getApi().dbUpsertAppPlaylist(result, payload.operationId)
|
||||
).resolves.toEqual({ success: true });
|
||||
expect(
|
||||
harness.ipcRenderer.send.mock.calls.filter(
|
||||
([channel]) => channel === PRELOAD_PERFORMANCE_MARKER_CHANNEL
|
||||
)
|
||||
).toHaveLength(2);
|
||||
).toHaveLength(1);
|
||||
expect(harness.getMarkers()).toHaveLength(1);
|
||||
expect(harness.getMarkers()[0]).toMatchObject({
|
||||
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
|
||||
phase: 'start',
|
||||
});
|
||||
expect(
|
||||
harness
|
||||
.getMarkers()
|
||||
.some(
|
||||
({ correlationState }) =>
|
||||
correlationState ===
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
PRELOAD_PERFORMANCE_MARKER_CHANNEL,
|
||||
PRELOAD_PERFORMANCE_METHOD,
|
||||
type Settings,
|
||||
Theme,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { redactSensitiveData } from '@iptvnator/shared/logging';
|
||||
import { DEBUG_TRACE_EVENT_CHANNEL } from '../services/debug-trace';
|
||||
import {
|
||||
createPreloadPerformanceHarness,
|
||||
@@ -20,6 +22,7 @@ describe('main preload performance marker gates', () => {
|
||||
|
||||
afterEach(() => {
|
||||
harness.cleanup();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each([undefined, '', '0', 'false', 'no', 'off'])(
|
||||
@@ -146,4 +149,99 @@ describe('main preload performance marker gates', () => {
|
||||
);
|
||||
expect(harness.getMarkers()).toEqual([]);
|
||||
});
|
||||
|
||||
it('preserves the full legacy trace payload for non-target success', async () => {
|
||||
const settings = {
|
||||
password: 'sentinel-argument-secret',
|
||||
theme: Theme.DarkTheme,
|
||||
} as Partial<Settings>;
|
||||
const result = {
|
||||
password: 'sentinel-result-secret',
|
||||
success: true,
|
||||
};
|
||||
await harness.load({
|
||||
[PRELOAD_ENVIRONMENT.TRACE_IPC]: '1',
|
||||
});
|
||||
jest.spyOn(globalThis.performance, 'now')
|
||||
.mockReturnValueOnce(10)
|
||||
.mockReturnValueOnce(12.34);
|
||||
harness.ipcRenderer.invoke.mockResolvedValueOnce(result);
|
||||
|
||||
await expect(harness.getApi().updateSettings(settings)).resolves.toBe(
|
||||
result
|
||||
);
|
||||
|
||||
const traces = harness.getSentPayloads<Record<string, unknown>>(
|
||||
DEBUG_TRACE_EVENT_CHANNEL
|
||||
);
|
||||
expect(traces).toEqual([
|
||||
{
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
password: 'sentinel-argument-secret',
|
||||
theme: Theme.DarkTheme,
|
||||
},
|
||||
],
|
||||
length: 1,
|
||||
type: 'array',
|
||||
},
|
||||
method: 'updateSettings',
|
||||
phase: 'start',
|
||||
},
|
||||
{
|
||||
durationMs: 2.3,
|
||||
method: 'updateSettings',
|
||||
phase: 'success',
|
||||
result,
|
||||
},
|
||||
]);
|
||||
const redacted = JSON.stringify(redactSensitiveData(traces));
|
||||
expect(redacted).not.toContain('sentinel-argument-secret');
|
||||
expect(redacted).not.toContain('sentinel-result-secret');
|
||||
expect(harness.getMarkers()).toEqual([]);
|
||||
});
|
||||
|
||||
it('preserves the full legacy trace payload for non-target errors', async () => {
|
||||
const rejection = new Error('token=sentinel-error-secret');
|
||||
await harness.load({
|
||||
[PRELOAD_ENVIRONMENT.TRACE_IPC]: '1',
|
||||
});
|
||||
jest.spyOn(globalThis.performance, 'now')
|
||||
.mockReturnValueOnce(20)
|
||||
.mockReturnValueOnce(23.26);
|
||||
harness.ipcRenderer.invoke.mockRejectedValueOnce(rejection);
|
||||
|
||||
await expect(
|
||||
harness.getApi().updateSettings({ theme: Theme.LightTheme })
|
||||
).rejects.toBe(rejection);
|
||||
|
||||
const traces = harness.getSentPayloads<Record<string, unknown>>(
|
||||
DEBUG_TRACE_EVENT_CHANNEL
|
||||
);
|
||||
expect(traces).toEqual([
|
||||
{
|
||||
args: {
|
||||
items: [{ theme: Theme.LightTheme }],
|
||||
length: 1,
|
||||
type: 'array',
|
||||
},
|
||||
method: 'updateSettings',
|
||||
phase: 'start',
|
||||
},
|
||||
{
|
||||
durationMs: 3.3,
|
||||
error: {
|
||||
message: rejection.message,
|
||||
name: 'Error',
|
||||
},
|
||||
method: 'updateSettings',
|
||||
phase: 'error',
|
||||
},
|
||||
]);
|
||||
expect(JSON.stringify(redactSensitiveData(traces))).not.toContain(
|
||||
'sentinel-error-secret'
|
||||
);
|
||||
expect(harness.getMarkers()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE,
|
||||
PRELOAD_PERFORMANCE_METHOD,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
createPlaylist,
|
||||
createPreloadPerformanceHarness,
|
||||
createRefreshPayload,
|
||||
PRELOAD_ENVIRONMENT,
|
||||
type PreloadPerformanceHarness,
|
||||
} from './main.preload.performance.test-helpers';
|
||||
|
||||
describe('main preload performance marker identities', () => {
|
||||
let harness: PreloadPerformanceHarness;
|
||||
|
||||
beforeEach(async () => {
|
||||
harness = createPreloadPerformanceHarness();
|
||||
await harness.load({
|
||||
[PRELOAD_ENVIRONMENT.PERF_CAPTURE]: '1',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
harness.cleanup();
|
||||
});
|
||||
|
||||
it('completes only for database calls carrying the refresh operation ID', async () => {
|
||||
const payload = createRefreshPayload();
|
||||
const playlist = createPlaylist(payload.playlistId);
|
||||
harness.ipcRenderer.invoke.mockResolvedValue(playlist);
|
||||
const api = harness.getApi();
|
||||
|
||||
await api.refreshPlaylist(payload);
|
||||
await api.dbGetAppPlaylist(payload.playlistId);
|
||||
await api.dbUpsertAppPlaylist(playlist);
|
||||
await api.dbGetAppPlaylist(payload.playlistId, 'other-operation');
|
||||
await api.dbUpsertAppPlaylist(playlist, 'other-operation');
|
||||
|
||||
expect(
|
||||
harness
|
||||
.getMarkers()
|
||||
.slice(2)
|
||||
.every(
|
||||
({ correlationState }) =>
|
||||
correlationState ===
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
harness
|
||||
.getMarkers()
|
||||
.some(
|
||||
({ correlationState }) =>
|
||||
correlationState ===
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE
|
||||
)
|
||||
).toBe(false);
|
||||
|
||||
await api.dbGetAppPlaylist(payload.playlistId, payload.operationId);
|
||||
await api.dbUpsertAppPlaylist(playlist, payload.operationId);
|
||||
|
||||
const markers = harness.getMarkers();
|
||||
expect(markers.at(-1)).toMatchObject({
|
||||
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
|
||||
operationId: payload.operationId,
|
||||
playlistId: payload.playlistId,
|
||||
});
|
||||
expect(harness.ipcRenderer.invoke.mock.calls).toEqual([
|
||||
['PLAYLIST:REFRESH', payload],
|
||||
['DB_GET_APP_PLAYLIST', payload.playlistId],
|
||||
['DB_UPSERT_APP_PLAYLIST', playlist],
|
||||
['DB_GET_APP_PLAYLIST', payload.playlistId],
|
||||
['DB_UPSERT_APP_PLAYLIST', playlist],
|
||||
['DB_GET_APP_PLAYLIST', payload.playlistId],
|
||||
['DB_UPSERT_APP_PLAYLIST', playlist],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -618,14 +618,14 @@ const electronApi: ElectronBridgeApi = {
|
||||
ipcRenderer.invoke('DB_CREATE_PLAYLIST', playlist),
|
||||
dbGetPlaylist: (playlistId: string) =>
|
||||
ipcRenderer.invoke('DB_GET_PLAYLIST', playlistId),
|
||||
dbUpsertAppPlaylist: (playlist: Playlist) =>
|
||||
dbUpsertAppPlaylist: (playlist: Playlist, _operationId?: string) =>
|
||||
ipcRenderer.invoke('DB_UPSERT_APP_PLAYLIST', playlist),
|
||||
dbUpsertAppPlaylists: (playlists: Playlist[]) =>
|
||||
ipcRenderer.invoke('DB_UPSERT_APP_PLAYLISTS', playlists),
|
||||
dbGetAppPlaylists: () => ipcRenderer.invoke('DB_GET_APP_PLAYLISTS'),
|
||||
dbGetAppPlaylistMetas: () =>
|
||||
ipcRenderer.invoke('DB_GET_APP_PLAYLIST_METAS'),
|
||||
dbGetAppPlaylist: (playlistId: string) =>
|
||||
dbGetAppPlaylist: (playlistId: string, _operationId?: string) =>
|
||||
ipcRenderer.invoke('DB_GET_APP_PLAYLIST', playlistId),
|
||||
dbGetAppPlaylistFavoriteChannels: (playlistId: string) =>
|
||||
ipcRenderer.invoke('DB_GET_APP_PLAYLIST_FAVORITE_CHANNELS', playlistId),
|
||||
|
||||
@@ -96,13 +96,13 @@ function extractIdentifiers(
|
||||
|
||||
if (method === PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST) {
|
||||
return {
|
||||
operationId: null,
|
||||
operationId: normalizePreloadPerformanceIdentifier(args[1]),
|
||||
playlistId: normalizePreloadPerformanceIdentifier(args[0]),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
operationId: null,
|
||||
operationId: normalizePreloadPerformanceIdentifier(args[1]),
|
||||
playlistId: normalizePreloadPerformanceIdentifier(
|
||||
readProperty(args[0], '_id')
|
||||
),
|
||||
@@ -115,6 +115,7 @@ export function createPreloadPerformanceCapture(
|
||||
): PreloadPerformanceCapture {
|
||||
let correlationState: PreloadPerformanceCorrelationState =
|
||||
createPreloadPerformanceCorrelationState();
|
||||
let markerDeliveryFailed = false;
|
||||
let lastSourceEpochMs = 0;
|
||||
let nextIpcCallId = 1;
|
||||
|
||||
@@ -123,6 +124,10 @@ export function createPreloadPerformanceCapture(
|
||||
phase: PreloadPerformancePhase,
|
||||
refreshCancelled = false
|
||||
): void {
|
||||
if (markerDeliveryFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const transition = advancePreloadPerformanceCorrelation(
|
||||
correlationState,
|
||||
@@ -132,17 +137,18 @@ export function createPreloadPerformanceCapture(
|
||||
refreshCancelled,
|
||||
}
|
||||
);
|
||||
correlationState = transition.state;
|
||||
const sourceEpochMs = Math.max(
|
||||
readHighResolutionEpochMs(),
|
||||
lastSourceEpochMs
|
||||
);
|
||||
lastSourceEpochMs = sourceEpochMs;
|
||||
sendMarker(PRELOAD_PERFORMANCE_MARKER_CHANNEL, {
|
||||
...transition.marker,
|
||||
sourceEpochMs,
|
||||
});
|
||||
correlationState = transition.state;
|
||||
lastSourceEpochMs = sourceEpochMs;
|
||||
} catch {
|
||||
markerDeliveryFailed = true;
|
||||
// Performance instrumentation must never affect the bridge call.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,25 @@ import {
|
||||
type PreloadPerformanceSequence,
|
||||
} from './preload-performance-correlation.model';
|
||||
|
||||
function eventForRememberedCall(
|
||||
event: PreloadPerformanceCorrelationEvent,
|
||||
call: PreloadPerformanceCall
|
||||
): PreloadPerformanceCorrelationEvent {
|
||||
return {
|
||||
...event,
|
||||
method: call.method,
|
||||
operationId: call.operationId,
|
||||
playlistId: call.playlistId,
|
||||
};
|
||||
}
|
||||
|
||||
function invalidCompletion(
|
||||
calls: Map<number, PreloadPerformanceCall>,
|
||||
sequences: Map<string, PreloadPerformanceSequence>,
|
||||
event: PreloadPerformanceCorrelationEvent,
|
||||
call: PreloadPerformanceCall
|
||||
): PreloadPerformanceMarkerMetadata {
|
||||
const rememberedEvent = eventForRememberedCall(event, call);
|
||||
const invalidSequence =
|
||||
call.playlistId === null
|
||||
? undefined
|
||||
@@ -36,11 +49,12 @@ function invalidCompletion(
|
||||
call.playlistId
|
||||
);
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
event,
|
||||
rememberedEvent,
|
||||
call.playlistId,
|
||||
invalidSequence?.operationId ?? call.operationId,
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
invalidSequence?.invalidReason ??
|
||||
call.invalidReason ??
|
||||
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
|
||||
);
|
||||
}
|
||||
@@ -49,26 +63,28 @@ function completeCorrelatedCall(
|
||||
calls: Map<number, PreloadPerformanceCall>,
|
||||
sequences: Map<string, PreloadPerformanceSequence>,
|
||||
event: PreloadPerformanceCorrelationEvent,
|
||||
call: PreloadPerformanceCall & { playlistId: string }
|
||||
call: PreloadPerformanceCall & {
|
||||
operationId: string;
|
||||
playlistId: string;
|
||||
}
|
||||
): PreloadPerformanceMarkerMetadata {
|
||||
const sequence = sequences.get(call.playlistId);
|
||||
const expectedStage =
|
||||
event.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST
|
||||
call.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST
|
||||
? PRELOAD_PERFORMANCE_SEQUENCE_STAGE.REFRESH_STARTED
|
||||
: event.method === PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST
|
||||
: call.method === PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST
|
||||
? PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_GET_STARTED
|
||||
: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_UPSERT_STARTED;
|
||||
if (
|
||||
!sequence ||
|
||||
sequence.stage !== expectedStage ||
|
||||
sequence.activeCallId !== event.ipcCallId ||
|
||||
sequence.operationId !== call.operationId
|
||||
sequence.operationId !== call.operationId ||
|
||||
sequence.stage !== expectedStage
|
||||
) {
|
||||
return invalidCompletion(calls, sequences, event, call);
|
||||
}
|
||||
|
||||
if (event.phase === 'error') {
|
||||
const invalidSequence = invalidatePreloadPerformanceSequence(
|
||||
invalidatePreloadPerformanceSequence(
|
||||
calls,
|
||||
sequences,
|
||||
call.playlistId,
|
||||
@@ -83,54 +99,51 @@ function completeCorrelatedCall(
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
event,
|
||||
call.playlistId,
|
||||
sequence.operationId,
|
||||
call.operationId,
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
invalidSequence?.invalidReason ??
|
||||
PRELOAD_PERFORMANCE_INVALID_REASON.IPC_ERROR
|
||||
PRELOAD_PERFORMANCE_INVALID_REASON.IPC_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
event.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST &&
|
||||
call.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST &&
|
||||
event.refreshCancelled === true
|
||||
) {
|
||||
sequences.delete(call.playlistId);
|
||||
calls.delete(event.ipcCallId);
|
||||
sequences.delete(call.playlistId);
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
event,
|
||||
call.playlistId,
|
||||
sequence.operationId,
|
||||
call.operationId,
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
PRELOAD_PERFORMANCE_INVALID_REASON.REFRESH_CANCELLED
|
||||
);
|
||||
}
|
||||
|
||||
calls.delete(event.ipcCallId);
|
||||
if (event.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST) {
|
||||
if (call.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST) {
|
||||
sequences.set(call.playlistId, {
|
||||
...sequence,
|
||||
activeCallId: null,
|
||||
stage: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.REFRESH_SUCCEEDED,
|
||||
});
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
event,
|
||||
call.playlistId,
|
||||
sequence.operationId,
|
||||
call.operationId,
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
if (event.method === PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST) {
|
||||
if (call.method === PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST) {
|
||||
sequences.set(call.playlistId, {
|
||||
...sequence,
|
||||
activeCallId: null,
|
||||
stage: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_GET_SUCCEEDED,
|
||||
});
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
event,
|
||||
call.playlistId,
|
||||
sequence.operationId,
|
||||
call.operationId,
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
|
||||
null
|
||||
);
|
||||
@@ -140,7 +153,7 @@ function completeCorrelatedCall(
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
event,
|
||||
call.playlistId,
|
||||
sequence.operationId,
|
||||
call.operationId,
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE,
|
||||
null
|
||||
);
|
||||
@@ -155,50 +168,33 @@ export function completePreloadPerformanceCall(
|
||||
): PreloadPerformanceMarkerMetadata {
|
||||
const call = calls.get(event.ipcCallId);
|
||||
if (!call) {
|
||||
const sequence = playlistId ? sequences.get(playlistId) : undefined;
|
||||
const invalidSequence = playlistId
|
||||
? invalidatePreloadPerformanceSequence(
|
||||
calls,
|
||||
sequences,
|
||||
playlistId,
|
||||
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
|
||||
)
|
||||
: undefined;
|
||||
const invalidReason =
|
||||
invalidSequence?.invalidReason ??
|
||||
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER;
|
||||
closeInvalidPreloadPerformanceSequenceWhenSettled(
|
||||
calls,
|
||||
sequences,
|
||||
playlistId
|
||||
);
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
event,
|
||||
playlistId,
|
||||
invalidSequence?.operationId ?? sequence?.operationId ?? null,
|
||||
operationId,
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
invalidReason
|
||||
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
|
||||
);
|
||||
}
|
||||
|
||||
const identityMatches =
|
||||
call.method === event.method &&
|
||||
call.playlistId === playlistId &&
|
||||
(event.method !== PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST ||
|
||||
call.expectedOperationId === operationId);
|
||||
call.operationId === operationId &&
|
||||
call.playlistId === playlistId;
|
||||
if (!identityMatches) {
|
||||
return invalidCompletion(calls, sequences, event, call);
|
||||
}
|
||||
|
||||
const rememberedEvent = eventForRememberedCall(event, call);
|
||||
if (
|
||||
call.correlationState ===
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED
|
||||
) {
|
||||
calls.delete(event.ipcCallId);
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
event,
|
||||
rememberedEvent,
|
||||
call.playlistId,
|
||||
null,
|
||||
call.operationId,
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
|
||||
null
|
||||
);
|
||||
@@ -214,7 +210,7 @@ export function completePreloadPerformanceCall(
|
||||
call.playlistId
|
||||
);
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
event,
|
||||
rememberedEvent,
|
||||
call.playlistId,
|
||||
call.operationId,
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
@@ -223,13 +219,13 @@ export function completePreloadPerformanceCall(
|
||||
);
|
||||
}
|
||||
|
||||
if (call.playlistId === null) {
|
||||
return invalidCompletion(calls, sequences, event, call);
|
||||
}
|
||||
return completeCorrelatedCall(
|
||||
calls,
|
||||
sequences,
|
||||
event,
|
||||
call as PreloadPerformanceCall & { playlistId: string }
|
||||
rememberedEvent,
|
||||
call as PreloadPerformanceCall & {
|
||||
operationId: string;
|
||||
playlistId: string;
|
||||
}
|
||||
);
|
||||
}
|
||||
+7
-202
@@ -8,210 +8,14 @@ import {
|
||||
OPERATION_ONE,
|
||||
OPERATION_TWO,
|
||||
PLAYLIST_ONE,
|
||||
PLAYLIST_TWO,
|
||||
type CorrelationHarness,
|
||||
} from './preload-performance-correlation.test-helpers';
|
||||
|
||||
describe('preload performance marker correlation completions', () => {
|
||||
let advance: CorrelationHarness['advance'];
|
||||
let reset: CorrelationHarness['reset'];
|
||||
|
||||
beforeEach(() => {
|
||||
({ advance, reset } = createCorrelationHarness());
|
||||
});
|
||||
|
||||
it.each(['success', 'error'] as const)(
|
||||
'rejects a %s completion with the wrong call ID',
|
||||
(phase) => {
|
||||
advance({
|
||||
ipcCallId: 1,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
|
||||
const mismatchedCompletion = advance({
|
||||
ipcCallId: 99,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
phase,
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
const originalCompletion = advance({
|
||||
ipcCallId: 1,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'success',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
|
||||
expect(mismatchedCompletion).toMatchObject({
|
||||
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
|
||||
});
|
||||
expect(originalCompletion).toMatchObject({
|
||||
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it('rejects a refresh completion whose playlist or operation ID changed', () => {
|
||||
advance({
|
||||
ipcCallId: 1,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
|
||||
const changedOperation = advance({
|
||||
ipcCallId: 1,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
|
||||
operationId: OPERATION_TWO,
|
||||
phase: 'success',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
|
||||
expect(changedOperation).toMatchObject({
|
||||
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
|
||||
});
|
||||
|
||||
reset();
|
||||
advance({
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
|
||||
const changedPlaylist = advance({
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'success',
|
||||
playlistId: PLAYLIST_TWO,
|
||||
});
|
||||
|
||||
expect(changedPlaylist).toMatchObject({
|
||||
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a completion whose method differs from the started call', () => {
|
||||
advance({
|
||||
ipcCallId: 1,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
|
||||
const wrongMethod = advance({
|
||||
ipcCallId: 1,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
phase: 'success',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
const restart = advance({
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
|
||||
operationId: OPERATION_TWO,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
|
||||
expect(wrongMethod).toMatchObject({
|
||||
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
|
||||
});
|
||||
expect(restart.correlationState).toBe(
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a duplicate completion instead of advancing the chain twice', () => {
|
||||
const refresh = {
|
||||
ipcCallId: 1,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
playlistId: PLAYLIST_ONE,
|
||||
} as const;
|
||||
advance({ ...refresh, phase: 'start' });
|
||||
advance({ ...refresh, phase: 'success' });
|
||||
|
||||
const duplicate = advance({ ...refresh, phase: 'success' });
|
||||
const laterGet = advance({
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
|
||||
expect(duplicate).toMatchObject({
|
||||
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
|
||||
});
|
||||
expect(laterGet).toMatchObject({
|
||||
correlationState:
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
|
||||
invalidReason: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a database completion for a different playlist', () => {
|
||||
advance({
|
||||
ipcCallId: 1,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
advance({
|
||||
ipcCallId: 1,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'success',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
advance({
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
|
||||
const changedPlaylist = advance({
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
phase: 'success',
|
||||
playlistId: PLAYLIST_TWO,
|
||||
});
|
||||
const laterUpsert = advance({
|
||||
ipcCallId: 3,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
|
||||
expect(changedPlaylist).toMatchObject({
|
||||
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
|
||||
});
|
||||
expect(laterUpsert).toMatchObject({
|
||||
correlationState:
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
|
||||
invalidReason: null,
|
||||
});
|
||||
({ advance } = createCorrelationHarness());
|
||||
});
|
||||
|
||||
it('invalidates a correlated sequence when a target call errors', () => {
|
||||
@@ -232,7 +36,7 @@ describe('preload performance marker correlation completions', () => {
|
||||
advance({
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
@@ -240,14 +44,14 @@ describe('preload performance marker correlation completions', () => {
|
||||
const error = advance({
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'error',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
const laterUpsert = advance({
|
||||
ipcCallId: 3,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
@@ -268,6 +72,7 @@ describe('preload performance marker correlation completions', () => {
|
||||
correlationState:
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
|
||||
invalidReason: null,
|
||||
operationId: OPERATION_ONE,
|
||||
});
|
||||
expect(restart.correlationState).toBe(
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED
|
||||
@@ -294,7 +99,7 @@ describe('preload performance marker correlation completions', () => {
|
||||
const laterGet = advance({
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
@@ -308,7 +113,7 @@ describe('preload performance marker correlation completions', () => {
|
||||
correlationState:
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
|
||||
invalidReason: null,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
import {
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE,
|
||||
PRELOAD_PERFORMANCE_INVALID_REASON,
|
||||
PRELOAD_PERFORMANCE_METHOD,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
createCorrelationHarness,
|
||||
OPERATION_ONE,
|
||||
OPERATION_TWO,
|
||||
PLAYLIST_ONE,
|
||||
PLAYLIST_TWO,
|
||||
} from './preload-performance-correlation.test-helpers';
|
||||
|
||||
describe('preload performance correlation identities', () => {
|
||||
it('advances only database calls tagged with the refresh operation', () => {
|
||||
const { advance } = createCorrelationHarness();
|
||||
advance({
|
||||
ipcCallId: 1,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
advance({
|
||||
ipcCallId: 1,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'success',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
|
||||
const unrelated = [
|
||||
advance({
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
}),
|
||||
advance({
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
phase: 'success',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
}),
|
||||
advance({
|
||||
ipcCallId: 3,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
|
||||
operationId: OPERATION_TWO,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
}),
|
||||
advance({
|
||||
ipcCallId: 3,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
|
||||
operationId: OPERATION_TWO,
|
||||
phase: 'success',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
}),
|
||||
];
|
||||
|
||||
expect(
|
||||
unrelated.every(
|
||||
({ correlationState }) =>
|
||||
correlationState ===
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
for (const event of [
|
||||
{
|
||||
ipcCallId: 4,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
phase: 'start',
|
||||
},
|
||||
{
|
||||
ipcCallId: 4,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
phase: 'success',
|
||||
},
|
||||
{
|
||||
ipcCallId: 5,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
|
||||
phase: 'start',
|
||||
},
|
||||
] as const) {
|
||||
advance({
|
||||
...event,
|
||||
operationId: OPERATION_ONE,
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
}
|
||||
const complete = advance({
|
||||
ipcCallId: 5,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'success',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
|
||||
expect(complete).toMatchObject({
|
||||
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE,
|
||||
operationId: OPERATION_ONE,
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: 'operation',
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: OPERATION_TWO,
|
||||
playlistId: PLAYLIST_ONE,
|
||||
},
|
||||
{
|
||||
label: 'playlist',
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
playlistId: PLAYLIST_TWO,
|
||||
},
|
||||
{
|
||||
label: 'method',
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
playlistId: PLAYLIST_ONE,
|
||||
},
|
||||
])(
|
||||
'fails closed when the completion changes the started $label',
|
||||
({ method, operationId, playlistId }) => {
|
||||
const { advance } = createCorrelationHarness();
|
||||
advance({
|
||||
ipcCallId: 1,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
advance({
|
||||
ipcCallId: 1,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'success',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
advance({
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
|
||||
const completion = advance({
|
||||
ipcCallId: 2,
|
||||
method,
|
||||
operationId,
|
||||
phase: 'success',
|
||||
playlistId,
|
||||
});
|
||||
const laterUpsert = advance({
|
||||
ipcCallId: 3,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
|
||||
expect(completion).toMatchObject({
|
||||
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: OPERATION_ONE,
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
expect(laterUpsert).toMatchObject({
|
||||
correlationState:
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
|
||||
invalidReason: null,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -23,7 +23,6 @@ export type PreloadPerformanceSequenceStage =
|
||||
(typeof PRELOAD_PERFORMANCE_SEQUENCE_STAGE)[keyof typeof PRELOAD_PERFORMANCE_SEQUENCE_STAGE];
|
||||
|
||||
export interface PreloadPerformanceSequence {
|
||||
activeCallId: number | null;
|
||||
invalidReason: PreloadPerformanceInvalidReason | null;
|
||||
operationId: string;
|
||||
stage: PreloadPerformanceSequenceStage;
|
||||
@@ -31,7 +30,6 @@ export interface PreloadPerformanceSequence {
|
||||
|
||||
export interface PreloadPerformanceCall {
|
||||
correlationState: PreloadPerformanceCorrelationStateName;
|
||||
expectedOperationId: string | null;
|
||||
invalidReason: PreloadPerformanceInvalidReason | null;
|
||||
method: PreloadPerformanceMethod;
|
||||
operationId: string | null;
|
||||
@@ -100,12 +98,10 @@ export function createInvalidPreloadPerformanceCall(
|
||||
event: PreloadPerformanceCorrelationEvent,
|
||||
playlistId: string | null,
|
||||
operationId: string | null,
|
||||
invalidReason: PreloadPerformanceInvalidReason,
|
||||
expectedOperationId = operationId
|
||||
invalidReason: PreloadPerformanceInvalidReason
|
||||
): PreloadPerformanceCall {
|
||||
return {
|
||||
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
expectedOperationId,
|
||||
invalidReason,
|
||||
method: event.method,
|
||||
operationId,
|
||||
@@ -130,7 +126,6 @@ export function invalidatePreloadPerformanceSequence(
|
||||
: reason;
|
||||
const invalidSequence: PreloadPerformanceSequence = {
|
||||
...sequence,
|
||||
activeCallId: null,
|
||||
invalidReason,
|
||||
stage: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.INVALID,
|
||||
};
|
||||
|
||||
+14
-14
@@ -39,28 +39,28 @@ describe('preload performance marker correlation sequences', () => {
|
||||
const getStart = advance({
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
const getSuccess = advance({
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'success',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
const upsertStart = advance({
|
||||
ipcCallId: 3,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
const upsertSuccess = advance({
|
||||
ipcCallId: 3,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'success',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
@@ -104,7 +104,7 @@ describe('preload performance marker correlation sequences', () => {
|
||||
const wrongOrder = advance({
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
@@ -133,21 +133,21 @@ describe('preload performance marker correlation sequences', () => {
|
||||
advance({
|
||||
ipcCallId: 4,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
const duplicate = advance({
|
||||
ipcCallId: 5,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
const originalCompletion = advance({
|
||||
ipcCallId: 4,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'success',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
@@ -253,7 +253,7 @@ describe('preload performance marker correlation sequences', () => {
|
||||
advance({
|
||||
ipcCallId: 3,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
});
|
||||
@@ -261,7 +261,7 @@ describe('preload performance marker correlation sequences', () => {
|
||||
const playlistTwoGet = advance({
|
||||
ipcCallId: 4,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_TWO,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_TWO,
|
||||
});
|
||||
@@ -293,28 +293,28 @@ describe('preload performance marker correlation sequences', () => {
|
||||
{
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
},
|
||||
{
|
||||
ipcCallId: 2,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'success',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
},
|
||||
{
|
||||
ipcCallId: 3,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'start',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
},
|
||||
{
|
||||
ipcCallId: 3,
|
||||
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
|
||||
operationId: null,
|
||||
operationId: OPERATION_ONE,
|
||||
phase: 'success',
|
||||
playlistId: PLAYLIST_ONE,
|
||||
},
|
||||
|
||||
@@ -14,6 +14,28 @@ import {
|
||||
type PreloadPerformanceSequence,
|
||||
} from './preload-performance-correlation.model';
|
||||
|
||||
function rememberUncorrelatedCall(
|
||||
calls: Map<number, PreloadPerformanceCall>,
|
||||
event: PreloadPerformanceCorrelationEvent,
|
||||
playlistId: string,
|
||||
operationId: string | null
|
||||
): PreloadPerformanceMarkerMetadata {
|
||||
calls.set(event.ipcCallId, {
|
||||
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
|
||||
invalidReason: null,
|
||||
method: event.method,
|
||||
operationId,
|
||||
playlistId,
|
||||
});
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
event,
|
||||
playlistId,
|
||||
operationId,
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function startRefresh(
|
||||
calls: Map<number, PreloadPerformanceCall>,
|
||||
sequences: Map<string, PreloadPerformanceSequence>,
|
||||
@@ -21,28 +43,27 @@ function startRefresh(
|
||||
playlistId: string,
|
||||
operationId: string
|
||||
): PreloadPerformanceMarkerMetadata {
|
||||
const sequence = sequences.get(playlistId);
|
||||
if (sequence) {
|
||||
const existing = sequences.get(playlistId);
|
||||
if (existing) {
|
||||
const reason =
|
||||
existing.stage === PRELOAD_PERFORMANCE_SEQUENCE_STAGE.INVALID
|
||||
? (existing.invalidReason ??
|
||||
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER)
|
||||
: PRELOAD_PERFORMANCE_INVALID_REASON.CONCURRENT_REFRESH;
|
||||
const invalidSequence = invalidatePreloadPerformanceSequence(
|
||||
calls,
|
||||
sequences,
|
||||
playlistId,
|
||||
sequence.stage === PRELOAD_PERFORMANCE_SEQUENCE_STAGE.INVALID
|
||||
? (sequence.invalidReason ??
|
||||
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER)
|
||||
: PRELOAD_PERFORMANCE_INVALID_REASON.CONCURRENT_REFRESH
|
||||
reason
|
||||
);
|
||||
const invalidReason =
|
||||
invalidSequence?.invalidReason ??
|
||||
PRELOAD_PERFORMANCE_INVALID_REASON.CONCURRENT_REFRESH;
|
||||
const invalidReason = invalidSequence?.invalidReason ?? reason;
|
||||
calls.set(
|
||||
event.ipcCallId,
|
||||
createInvalidPreloadPerformanceCall(
|
||||
event,
|
||||
playlistId,
|
||||
operationId,
|
||||
invalidReason,
|
||||
operationId
|
||||
invalidReason
|
||||
)
|
||||
);
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
@@ -55,14 +76,12 @@ function startRefresh(
|
||||
}
|
||||
|
||||
sequences.set(playlistId, {
|
||||
activeCallId: event.ipcCallId,
|
||||
invalidReason: null,
|
||||
operationId,
|
||||
stage: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.REFRESH_STARTED,
|
||||
});
|
||||
calls.set(event.ipcCallId, {
|
||||
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
|
||||
expectedOperationId: operationId,
|
||||
invalidReason: null,
|
||||
method: event.method,
|
||||
operationId,
|
||||
@@ -81,26 +100,16 @@ function startDatabaseCall(
|
||||
calls: Map<number, PreloadPerformanceCall>,
|
||||
sequences: Map<string, PreloadPerformanceSequence>,
|
||||
event: PreloadPerformanceCorrelationEvent,
|
||||
playlistId: string
|
||||
playlistId: string,
|
||||
operationId: string | null
|
||||
): PreloadPerformanceMarkerMetadata {
|
||||
if (operationId === null) {
|
||||
return rememberUncorrelatedCall(calls, event, playlistId, operationId);
|
||||
}
|
||||
|
||||
const sequence = sequences.get(playlistId);
|
||||
if (!sequence) {
|
||||
calls.set(event.ipcCallId, {
|
||||
correlationState:
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
|
||||
expectedOperationId: null,
|
||||
invalidReason: null,
|
||||
method: event.method,
|
||||
operationId: null,
|
||||
playlistId,
|
||||
});
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
event,
|
||||
playlistId,
|
||||
null,
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
|
||||
null
|
||||
);
|
||||
if (!sequence || sequence.operationId !== operationId) {
|
||||
return rememberUncorrelatedCall(calls, event, playlistId, operationId);
|
||||
}
|
||||
|
||||
if (sequence.stage === PRELOAD_PERFORMANCE_SEQUENCE_STAGE.INVALID) {
|
||||
@@ -112,15 +121,14 @@ function startDatabaseCall(
|
||||
createInvalidPreloadPerformanceCall(
|
||||
event,
|
||||
playlistId,
|
||||
sequence.operationId,
|
||||
invalidReason,
|
||||
null
|
||||
operationId,
|
||||
invalidReason
|
||||
)
|
||||
);
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
event,
|
||||
playlistId,
|
||||
sequence.operationId,
|
||||
operationId,
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
invalidReason
|
||||
);
|
||||
@@ -134,6 +142,7 @@ function startDatabaseCall(
|
||||
const startedStage = isGet
|
||||
? PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_GET_STARTED
|
||||
: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_UPSERT_STARTED;
|
||||
|
||||
if (sequence.stage !== expectedStage) {
|
||||
const invalidSequence = invalidatePreloadPerformanceSequence(
|
||||
calls,
|
||||
@@ -149,15 +158,14 @@ function startDatabaseCall(
|
||||
createInvalidPreloadPerformanceCall(
|
||||
event,
|
||||
playlistId,
|
||||
sequence.operationId,
|
||||
invalidReason,
|
||||
null
|
||||
operationId,
|
||||
invalidReason
|
||||
)
|
||||
);
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
event,
|
||||
playlistId,
|
||||
sequence.operationId,
|
||||
operationId,
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
invalidReason
|
||||
);
|
||||
@@ -165,21 +173,19 @@ function startDatabaseCall(
|
||||
|
||||
sequences.set(playlistId, {
|
||||
...sequence,
|
||||
activeCallId: event.ipcCallId,
|
||||
stage: startedStage,
|
||||
});
|
||||
calls.set(event.ipcCallId, {
|
||||
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
|
||||
expectedOperationId: null,
|
||||
invalidReason: null,
|
||||
method: event.method,
|
||||
operationId: sequence.operationId,
|
||||
operationId,
|
||||
playlistId,
|
||||
});
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
event,
|
||||
playlistId,
|
||||
sequence.operationId,
|
||||
operationId,
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
|
||||
null
|
||||
);
|
||||
@@ -211,8 +217,7 @@ export function startPreloadPerformanceCall(
|
||||
event,
|
||||
playlistId,
|
||||
null,
|
||||
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER,
|
||||
operationId
|
||||
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER
|
||||
)
|
||||
);
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
@@ -224,28 +229,7 @@ export function startPreloadPerformanceCall(
|
||||
);
|
||||
}
|
||||
|
||||
const existingCall = calls.get(event.ipcCallId);
|
||||
if (existingCall) {
|
||||
const sequence =
|
||||
existingCall.playlistId === null
|
||||
? undefined
|
||||
: invalidatePreloadPerformanceSequence(
|
||||
calls,
|
||||
sequences,
|
||||
existingCall.playlistId,
|
||||
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
|
||||
);
|
||||
return createPreloadPerformanceMarkerMetadata(
|
||||
event,
|
||||
playlistId,
|
||||
sequence?.operationId ?? null,
|
||||
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
|
||||
sequence?.invalidReason ??
|
||||
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
|
||||
);
|
||||
}
|
||||
|
||||
return event.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST
|
||||
? startRefresh(calls, sequences, event, playlistId, operationId)
|
||||
: startDatabaseCall(calls, sequences, event, playlistId);
|
||||
: startDatabaseCall(calls, sequences, event, playlistId, operationId);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PRELOAD_PERFORMANCE_METHOD } from '@iptvnator/shared/interfaces';
|
||||
import { completePreloadPerformanceCall } from './preload-performance-correlation.complete';
|
||||
import {
|
||||
normalizePreloadPerformanceIdentifier,
|
||||
@@ -29,10 +28,9 @@ export function advancePreloadPerformanceCorrelation(
|
||||
const calls = new Map(state.calls);
|
||||
const sequences = new Map(state.sequences);
|
||||
const playlistId = normalizePreloadPerformanceIdentifier(event.playlistId);
|
||||
const operationId =
|
||||
event.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST
|
||||
? normalizePreloadPerformanceIdentifier(event.operationId)
|
||||
: null;
|
||||
const operationId = normalizePreloadPerformanceIdentifier(
|
||||
event.operationId
|
||||
);
|
||||
const marker =
|
||||
event.phase === 'start'
|
||||
? startPreloadPerformanceCall(
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { createActionGroup, emptyProps, props } from '@ngrx/store';
|
||||
import { Channel, EpgProgram, Playlist, PlaylistMeta } from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
Channel,
|
||||
EpgProgram,
|
||||
Playlist,
|
||||
PlaylistMeta,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
|
||||
export const PlaylistActions = createActionGroup({
|
||||
source: 'Playlists',
|
||||
@@ -11,6 +16,8 @@ export const PlaylistActions = createActionGroup({
|
||||
'Remove Playlist': props<{ playlistId: string }>(),
|
||||
'Update Playlist Meta': props<{ playlist: PlaylistMeta }>(),
|
||||
'Update Playlist': props<{
|
||||
/** Instrumentation-only; never persisted or sent to main/worker IPC. */
|
||||
operationId?: string;
|
||||
playlist: Playlist;
|
||||
playlistId: string;
|
||||
refreshEpg?: boolean;
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
} from './external-player-payload.util';
|
||||
import { resolvePlaylistScopedEpgFetchPlan } from './playlist-scoped-epg-fetch.util';
|
||||
import { resolveActiveEpgProgramAction } from './resolve-active-epg-program.util';
|
||||
import { persistPlaylistUpdate } from './playlist-update.effect-handler';
|
||||
|
||||
@Injectable({ providedIn: 'any' })
|
||||
export class PlaylistEffects {
|
||||
@@ -305,10 +306,7 @@ export class PlaylistEffects {
|
||||
return this.actions$.pipe(
|
||||
ofType(PlaylistActions.updatePlaylist),
|
||||
switchMap((action) =>
|
||||
this.playlistsService.updatePlaylist(action.playlistId, {
|
||||
...action.playlist,
|
||||
_id: action.playlistId,
|
||||
}).pipe(
|
||||
persistPlaylistUpdate(this.playlistsService, action).pipe(
|
||||
tap(() => {
|
||||
this.fetchPlaylistScopedEpg(action.playlist, {
|
||||
force: action.refreshEpg === true,
|
||||
@@ -353,12 +351,14 @@ export class PlaylistEffects {
|
||||
if ('isTemporary' in action && action.isTemporary) {
|
||||
return EMPTY;
|
||||
}
|
||||
return this.playlistsService.addPlaylist(action.playlist).pipe(
|
||||
tap(() => {
|
||||
this.fetchPlaylistScopedEpg(action.playlist);
|
||||
this.navigateToPlaylist(action.playlist);
|
||||
})
|
||||
);
|
||||
return this.playlistsService
|
||||
.addPlaylist(action.playlist)
|
||||
.pipe(
|
||||
tap(() => {
|
||||
this.fetchPlaylistScopedEpg(action.playlist);
|
||||
this.navigateToPlaylist(action.playlist);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
@@ -370,17 +370,21 @@ export class PlaylistEffects {
|
||||
return this.actions$.pipe(
|
||||
ofType(PlaylistActions.updatePlaylistMeta),
|
||||
switchMap((action) =>
|
||||
this.playlistsService.updatePlaylistMeta(action.playlist).pipe(
|
||||
tap(() => {
|
||||
if (
|
||||
this.hasPlaylistScopedEpgSourceChange(
|
||||
action.playlist
|
||||
)
|
||||
) {
|
||||
this.fetchPlaylistScopedEpg(action.playlist);
|
||||
}
|
||||
})
|
||||
)
|
||||
this.playlistsService
|
||||
.updatePlaylistMeta(action.playlist)
|
||||
.pipe(
|
||||
tap(() => {
|
||||
if (
|
||||
this.hasPlaylistScopedEpgSourceChange(
|
||||
action.playlist
|
||||
)
|
||||
) {
|
||||
this.fetchPlaylistScopedEpg(
|
||||
action.playlist
|
||||
);
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Playlist } from '@iptvnator/shared/interfaces';
|
||||
|
||||
interface PlaylistUpdateAction {
|
||||
operationId?: string;
|
||||
playlist: Playlist;
|
||||
playlistId: string;
|
||||
}
|
||||
|
||||
interface PlaylistUpdater<TResult> {
|
||||
updatePlaylist: (
|
||||
playlistId: string,
|
||||
playlist: Playlist,
|
||||
operationId?: string
|
||||
) => TResult;
|
||||
}
|
||||
|
||||
export function persistPlaylistUpdate<TResult>(
|
||||
playlistsService: PlaylistUpdater<TResult>,
|
||||
action: PlaylistUpdateAction
|
||||
): TResult {
|
||||
return playlistsService.updatePlaylist(
|
||||
action.playlistId,
|
||||
{
|
||||
...action.playlist,
|
||||
_id: action.playlistId,
|
||||
},
|
||||
action.operationId
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { type Playlist } from '@iptvnator/shared/interfaces';
|
||||
import { of } from 'rxjs';
|
||||
import { PlaylistActions } from './actions';
|
||||
import { persistPlaylistUpdate } from './playlist-update.effect-handler';
|
||||
|
||||
describe('PlaylistEffects updatePlaylist', () => {
|
||||
it('forwards the refresh operation ID to playlist persistence', () => {
|
||||
const playlistsService = {
|
||||
updatePlaylist: jest.fn(() => of(undefined)),
|
||||
};
|
||||
const playlist = {
|
||||
_id: 'playlist-1',
|
||||
playlist: { items: [] },
|
||||
} as unknown as Playlist;
|
||||
const action = PlaylistActions.updatePlaylist({
|
||||
playlist,
|
||||
playlistId: playlist._id,
|
||||
refreshEpg: true,
|
||||
operationId: 'playlist-refresh-operation',
|
||||
});
|
||||
|
||||
persistPlaylistUpdate(playlistsService, action);
|
||||
|
||||
expect(playlistsService.updatePlaylist).toHaveBeenCalledWith(
|
||||
playlist._id,
|
||||
playlist,
|
||||
'playlist-refresh-operation'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -630,6 +630,7 @@ describe('PlaylistRefreshActionService', () => {
|
||||
playlist: refreshedPlaylist,
|
||||
playlistId: item._id,
|
||||
refreshEpg: true,
|
||||
operationId: 'playlist-refresh-op',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -205,12 +205,11 @@ export class PlaylistRefreshActionService {
|
||||
}
|
||||
|
||||
try {
|
||||
const operationId =
|
||||
this.databaseService.createOperationId('playlist-refresh');
|
||||
const refreshedPlaylist =
|
||||
await this.playlistRefreshService.refreshPlaylist({
|
||||
operationId:
|
||||
this.databaseService.createOperationId(
|
||||
'playlist-refresh'
|
||||
),
|
||||
operationId,
|
||||
playlistId: item._id,
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
@@ -228,6 +227,7 @@ export class PlaylistRefreshActionService {
|
||||
},
|
||||
playlistId: item._id,
|
||||
refreshEpg: true,
|
||||
operationId,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -549,5 +549,15 @@ describe('RecentPlaylistsComponent busy state', () => {
|
||||
expect(dataService.sendIpcEvent).not.toHaveBeenCalled();
|
||||
|
||||
await Promise.resolve();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
PlaylistActions.updatePlaylist({
|
||||
playlist: expect.objectContaining({
|
||||
_id: item._id,
|
||||
}),
|
||||
playlistId: item._id,
|
||||
refreshEpg: true,
|
||||
operationId: 'playlist-refresh-op',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -463,6 +463,7 @@ export class RecentPlaylistsComponent {
|
||||
},
|
||||
playlistId: item._id,
|
||||
refreshEpg: true,
|
||||
operationId,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -1336,6 +1336,37 @@ describe('PlaylistsService', () => {
|
||||
expect(electron.dbUpsertAppPlaylist).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('forwards refresh instrumentation only to its SQLite read and write', async () => {
|
||||
const { electron } = createStatefulElectronStore(
|
||||
createBasePlaylist('playlist-instrumented-refresh')
|
||||
);
|
||||
testWindow.electron = electron;
|
||||
const service = createService();
|
||||
const operationId = 'playlist-refresh-operation';
|
||||
|
||||
await firstValueFrom(
|
||||
service.updatePlaylist(
|
||||
'playlist-instrumented-refresh',
|
||||
{
|
||||
_id: 'playlist-instrumented-refresh',
|
||||
playlist: { items: [] },
|
||||
} as Playlist,
|
||||
operationId
|
||||
)
|
||||
);
|
||||
|
||||
expect(electron.dbGetAppPlaylist).toHaveBeenCalledWith(
|
||||
'playlist-instrumented-refresh',
|
||||
operationId
|
||||
);
|
||||
expect(electron.dbUpsertAppPlaylist).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
_id: 'playlist-instrumented-refresh',
|
||||
}),
|
||||
operationId
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps both favorites when two rapid favorite adds overlap (SQLite)', async () => {
|
||||
const { store, electron } = createStatefulElectronStore(
|
||||
createBasePlaylist('portal-race-two-favorites')
|
||||
|
||||
@@ -52,7 +52,10 @@ type PlaylistRawItem = {
|
||||
type PlaylistStorageElectronApi = {
|
||||
dbDeleteAllPlaylists: () => Promise<unknown>;
|
||||
dbDeletePlaylist: (playlistId: string) => Promise<unknown>;
|
||||
dbGetAppPlaylist: (playlistId: string) => Promise<Playlist | null>;
|
||||
dbGetAppPlaylist: (
|
||||
playlistId: string,
|
||||
operationId?: string
|
||||
) => Promise<Playlist | null>;
|
||||
dbGetAppPlaylistFavoriteChannels?: (
|
||||
playlistId: string
|
||||
) => Promise<M3uFavoriteChannel[]>;
|
||||
@@ -60,7 +63,10 @@ type PlaylistStorageElectronApi = {
|
||||
dbGetAppPlaylists: () => Promise<Playlist[]>;
|
||||
dbGetAppState: (key: string) => Promise<string | null>;
|
||||
dbSetAppState: (key: string, value: string) => Promise<unknown>;
|
||||
dbUpsertAppPlaylist: (playlist: Playlist) => Promise<unknown>;
|
||||
dbUpsertAppPlaylist: (
|
||||
playlist: Playlist,
|
||||
operationId?: string
|
||||
) => Promise<unknown>;
|
||||
dbUpsertAppPlaylists: (playlists: Playlist[]) => Promise<unknown>;
|
||||
};
|
||||
|
||||
@@ -362,14 +368,18 @@ export class PlaylistsService {
|
||||
}
|
||||
}
|
||||
|
||||
private upsertSqlitePlaylist(playlist: Playlist) {
|
||||
private upsertSqlitePlaylist(playlist: Playlist, operationId?: string) {
|
||||
return this.runOnSqlite(async () => {
|
||||
const electron = this.electronApi;
|
||||
if (!electron) {
|
||||
return playlist;
|
||||
}
|
||||
|
||||
await electron.dbUpsertAppPlaylist(playlist);
|
||||
if (operationId === undefined) {
|
||||
await electron.dbUpsertAppPlaylist(playlist);
|
||||
} else {
|
||||
await electron.dbUpsertAppPlaylist(playlist, operationId);
|
||||
}
|
||||
return playlist;
|
||||
});
|
||||
}
|
||||
@@ -411,9 +421,14 @@ export class PlaylistsService {
|
||||
});
|
||||
}
|
||||
|
||||
private persistPlaylistMutation(nextPlaylist: Playlist) {
|
||||
private persistPlaylistMutation(
|
||||
nextPlaylist: Playlist,
|
||||
operationId?: string
|
||||
) {
|
||||
if (this.isElectronStorageAvailable) {
|
||||
return firstValueFrom(this.upsertSqlitePlaylist(nextPlaylist));
|
||||
return firstValueFrom(
|
||||
this.upsertSqlitePlaylist(nextPlaylist, operationId)
|
||||
);
|
||||
}
|
||||
|
||||
return firstValueFrom(
|
||||
@@ -554,10 +569,14 @@ export class PlaylistsService {
|
||||
};
|
||||
}
|
||||
|
||||
updatePlaylist(playlistId: string, updatedPlaylist: Playlist) {
|
||||
updatePlaylist(
|
||||
playlistId: string,
|
||||
updatedPlaylist: Playlist,
|
||||
operationId?: string
|
||||
) {
|
||||
return this.serializePlaylistWrite(playlistId, async () => {
|
||||
const currentPlaylist = await firstValueFrom(
|
||||
this.getPlaylistById(playlistId)
|
||||
this.getPlaylistById(playlistId, operationId)
|
||||
);
|
||||
const mergedPlaylist = this.mergeRefreshedPlaylist(
|
||||
currentPlaylist,
|
||||
@@ -565,17 +584,21 @@ export class PlaylistsService {
|
||||
playlistId
|
||||
);
|
||||
|
||||
return this.persistPlaylistMutation(mergedPlaylist);
|
||||
return this.persistPlaylistMutation(mergedPlaylist, operationId);
|
||||
});
|
||||
}
|
||||
|
||||
getPlaylistById(id: string) {
|
||||
getPlaylistById(id: string, operationId?: string) {
|
||||
if (this.isElectronStorageAvailable) {
|
||||
return this.runOnSqlite(async () => {
|
||||
const electron = this.electronApi;
|
||||
const playlist = electron
|
||||
? await electron.dbGetAppPlaylist(id)
|
||||
: null;
|
||||
let playlist: Playlist | null = null;
|
||||
if (electron) {
|
||||
playlist =
|
||||
operationId === undefined
|
||||
? await electron.dbGetAppPlaylist(id)
|
||||
: await electron.dbGetAppPlaylist(id, operationId);
|
||||
}
|
||||
return playlist
|
||||
? this.createSqliteFallbackPlaylist(playlist as Playlist)
|
||||
: (undefined as unknown as Playlist);
|
||||
|
||||
@@ -684,13 +684,21 @@ export interface ElectronBridgeApi {
|
||||
dbGetPlaylist: (
|
||||
playlistId: string
|
||||
) => Promise<ElectronBridgePlaylistRow | null>;
|
||||
dbUpsertAppPlaylist: (playlist: Playlist) => Promise<ElectronBridgeResult>;
|
||||
dbUpsertAppPlaylist: (
|
||||
playlist: Playlist,
|
||||
/** Instrumentation-only; never persisted or sent to main/worker IPC. */
|
||||
operationId?: string
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbUpsertAppPlaylists: (
|
||||
playlists: Playlist[]
|
||||
) => Promise<ElectronBridgeCountResult>;
|
||||
dbGetAppPlaylists: () => Promise<Playlist[]>;
|
||||
dbGetAppPlaylistMetas: () => Promise<Playlist[]>;
|
||||
dbGetAppPlaylist: (playlistId: string) => Promise<Playlist | null>;
|
||||
dbGetAppPlaylist: (
|
||||
playlistId: string,
|
||||
/** Instrumentation-only; never persisted or sent to main/worker IPC. */
|
||||
operationId?: string
|
||||
) => Promise<Playlist | null>;
|
||||
dbGetAppPlaylistFavoriteChannels: (
|
||||
playlistId: string
|
||||
) => Promise<M3uFavoriteChannel[]>;
|
||||
|
||||
Reference in new issue
Block a user