fix(electron): isolate refresh performance correlation

This commit is contained in:
4gray committed 2026-07-26 22:37:52 +02:00
1 parent e3ce60a35e
commit f9e71a6d8d
25 files changed
+728 -402

No files matched your search

@@ -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;
}
);
}
@@ -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,
};
@@ -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(