From cfe59cb4b42c7ec14b35f88d76889e514a70a0cf Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] Sort events upon creation, and keep ordered (#411) * Sort events at start, as otherwise we risk misidentifying the last event * Keep inserted events in the correct order, ensuring we don't misidentify the last event - e.g. network conditions mean that 'live' events come in non-sequentially - or so that adding custom events to an existing event works * Ensure we maintain original ordering while inserting a new event which has an identical timestamp to an existing event. This came up with a series of mutations which had the same timestamp but needed to be applied in the correct order * Fast track the common case of a new event being added which occurs after all prior events --- src/replay/index.ts | 4 ++++ src/replay/machine.ts | 24 +++++++++++++++++++++++- src/replay/timer.ts | 1 - 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/replay/index.ts b/src/replay/index.ts index 8cdada96..7af8bf63 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -129,6 +129,8 @@ export class Replayer { events: Array, config?: Partial, ) { + events.sort((a1, a2) => a1.timestamp - a2.timestamp); + if (!config?.liveMode && events.length < 2) { throw new Error('Replayer need at least 2 events.'); } @@ -520,6 +522,8 @@ export class Replayer { castFn(); } this.service.send({ type: 'CAST_EVENT', payload: { event } }); + + // events are kept sorted by timestamp, check if this is the last event if ( event === this.service.state.context.events[ diff --git a/src/replay/machine.ts b/src/replay/machine.ts index 86459e6a..d2e3313f 100644 --- a/src/replay/machine.ts +++ b/src/replay/machine.ts @@ -237,7 +237,29 @@ export function createPlayerService( if (machineEvent.type === 'ADD_EVENT') { const { event } = machineEvent.payload; addDelay(event, baselineTime); - events.push(event); + + let end = events.length - 1; + if (events[end].timestamp <= event.timestamp) { + // fast track + events.push(event); + } else { + let insertion_index = -1; + let start = 0; + while (start <= end) { + let mid = Math.floor((start + end) / 2); + if (events[mid].timestamp <= event.timestamp) { + start = mid + 1; + } else { + end = mid - 1; + } + } + if (insertion_index === -1) { + insertion_index = start; + } + events.splice(insertion_index, 0, event); + } + + const isSync = event.timestamp < baselineTime; const castFn = getCastFn(event, isSync); if (isSync) { diff --git a/src/replay/timer.ts b/src/replay/timer.ts index e5eefa04..2d2d7c23 100644 --- a/src/replay/timer.ts +++ b/src/replay/timer.ts @@ -34,7 +34,6 @@ export class Timer { } public start() { - this.actions.sort((a1, a2) => a1.delay - a2.delay); this.timeOffset = 0; let lastTimestamp = performance.now(); const { actions } = this;