From fb381a12548554fa038a00685c4ddaf8ed038250 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] Ignore firstFullSnapshot once only after initial 'poster' build (#608) * Encountered a bug where firstFullSnapshot was played twice because timer was immediately started and reached the snapshot before the setTimeout returned * Ignoring a FullSnapshot needs to be a one-time only thing, as otherwise we'll ignore it after scrubbing (restarting play head at a particular time). This is a problem if mutations have altered the player state, and we try to replay those mutations, so we e.g. try to remove an element that has already been removed because we haven't reset the FullSnapshot state * Some `npm run typings` related fixups --- src/replay/index.ts | 23 +++++++++++++++-------- typings/replay/index.d.ts | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/replay/index.ts b/src/replay/index.ts index 0e5aec9b..b411acf6 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -89,8 +89,8 @@ export class Replayer { private imageMap: Map = new Map(); private mirror: Mirror = createMirror(); - /** The first time the player is playing. */ - private firstPlayedEvent: eventWithTime | null = null; + + private firstFullSnapshot: eventWithTime | true | null = null; private newDocumentQueue: addedNodeMutation[] = []; @@ -145,7 +145,7 @@ export class Replayer { } }); this.emitter.on(ReplayerEvents.PlayBack, () => { - this.firstPlayedEvent = null; + this.firstFullSnapshot = null; this.mirror.reset(); }); @@ -207,10 +207,11 @@ export class Replayer { if (firstFullsnapshot) { setTimeout(() => { // when something has been played, there is no need to rebuild poster - if (this.firstPlayedEvent) { + if (this.firstFullSnapshot) { + // true if any other fullSnapshot has been executed by Timer already return; } - this.firstPlayedEvent = firstFullsnapshot; + this.firstFullSnapshot = firstFullsnapshot; this.rebuildFullSnapshot( firstFullsnapshot as fullSnapshotEvent & { timestamp: number }, ); @@ -429,9 +430,15 @@ export class Replayer { break; case EventType.FullSnapshot: castFn = () => { - // Don't build a full snapshot during the first play through since we've already built it when the player was mounted. - if (this.firstPlayedEvent && this.firstPlayedEvent === event) { - return; + if (this.firstFullSnapshot) { + if (this.firstFullSnapshot === event) { + // we've already built this exact FullSnapshot when the player was mounted, and haven't built any other FullSnapshot since + this.firstFullSnapshot = true; // forget as we might need to re-execute this FullSnapshot later e.g. to rebuild after scrubbing + return; + } + } else { + // Timer (requestAnimationFrame) can be faster than setTimeout(..., 1) + this.firstFullSnapshot = true; } this.rebuildFullSnapshot(event, isSync); this.iframe.contentWindow!.scrollTo(event.data.initialOffset); diff --git a/typings/replay/index.d.ts b/typings/replay/index.d.ts index fc930209..0f021d32 100644 --- a/typings/replay/index.d.ts +++ b/typings/replay/index.d.ts @@ -20,7 +20,7 @@ export declare class Replayer { private elementStateMap; private imageMap; private mirror; - private firstPlayedEvent; + private firstFullSnapshot; private newDocumentQueue; constructor(events: Array, config?: Partial); on(event: string, handler: Handler): this;