Skip to content

Commit 2649af8

Browse files
committed
add refreshSkipState
1 parent 454ef4f commit 2649af8

File tree

2 files changed

+105
-1
lines changed

2 files changed

+105
-1
lines changed

packages/rrweb/src/replay/index.ts

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import {
4141
type PlayerMachineState,
4242
type SpeedMachineState,
4343
} from './machine';
44-
import type { playerConfig, missingNodeMap } from '../types';
44+
import type { playerConfig, missingNodeMap, EventIndexCache } from '../types';
4545
import {
4646
EventType,
4747
IncrementalSource,
@@ -198,6 +198,9 @@ export class Replayer {
198198
// Similar to the reason for constructedStyleMutations.
199199
private adoptedStyleSheets: adoptedStyleSheetData[] = [];
200200

201+
// Cache for optimized event index lookups during playback.
202+
private eventIndexCache: EventIndexCache;
203+
201204
constructor(
202205
events: Array<eventWithTime | string>,
203206
config?: Partial<playerConfig>,
@@ -231,6 +234,12 @@ export class Replayer {
231234
this.applyEventsSynchronously = this.applyEventsSynchronously.bind(this);
232235
this.emitter.on(ReplayerEvents.Resize, this.handleResize as Handler);
233236

237+
this.eventIndexCache = {
238+
lastTime: -1,
239+
lastIndex: 0,
240+
maxDrift: 3000 // 3 second max drift from the current playback position.
241+
};
242+
234243
this.setupDom();
235244

236245
/**
@@ -633,6 +642,94 @@ export class Replayer {
633642
this.cache = createCache();
634643
}
635644

645+
public resetFastForward() {
646+
this.backToNormal();
647+
}
648+
649+
private binarySearchEventIndex(events: eventWithTime[], currentEventTime: number): number {
650+
let left = 0, right = events.length - 1;
651+
let result = -1;
652+
653+
while (left <= right) {
654+
const mid = Math.floor((left + right) / 2);
655+
if (events[mid].timestamp <= currentEventTime) {
656+
result = mid;
657+
left = mid + 1;
658+
} else {
659+
right = mid - 1;
660+
}
661+
}
662+
return result;
663+
}
664+
665+
private getCachedEventIndex(events: eventWithTime[], currentEventTime: number): number {
666+
const cache = this.eventIndexCache;
667+
if (cache.lastIndex < events.length) {
668+
const cachedEvent = events[cache.lastIndex];
669+
if (cachedEvent) {
670+
const eventTimeDiff = Math.abs(cachedEvent.timestamp - currentEventTime);
671+
if (eventTimeDiff <= cache.maxDrift) {
672+
return cache.lastIndex;
673+
}
674+
}
675+
}
676+
return -1;
677+
}
678+
679+
public refreshSkipState(): void {
680+
if (!this.config.skipInactive) {
681+
return;
682+
}
683+
684+
// Clear stale state
685+
this.nextUserInteractionEvent = null;
686+
687+
// Get current time and convert to event-relative time
688+
const currentTime = this.getCurrentTime();
689+
const events = this.service.state.context.events;
690+
const firstEvent = events[0];
691+
if (!firstEvent) {
692+
return;
693+
}
694+
const currentEventTime = firstEvent.timestamp + currentTime;
695+
696+
// Try cache first for nearby positions (O(1))
697+
let currentEventIndex = this.getCachedEventIndex(events, currentEventTime);
698+
if (currentEventIndex === -1) {
699+
// Cache miss - use binary search (O(log n))
700+
currentEventIndex = this.binarySearchEventIndex(events, currentEventTime);
701+
this.eventIndexCache.lastTime = currentEventTime;
702+
this.eventIndexCache.lastIndex = currentEventIndex;
703+
}
704+
705+
if (currentEventIndex === -1) {
706+
return;
707+
}
708+
709+
// Find next user interaction event starting from the current event index
710+
const currentEvent = events[currentEventIndex];
711+
const threshold = this.config.inactivePeriodThreshold * this.speedService.state.context.timer.speed;
712+
for (let i = currentEventIndex + 1; i < events.length; i++) {
713+
const event = events[i];
714+
if (this.isUserInteraction(event)) {
715+
const gapTime = event.timestamp - currentEvent.timestamp;
716+
717+
if (gapTime > threshold) {
718+
this.nextUserInteractionEvent = event;
719+
const payload = {
720+
speed: Math.min(
721+
Math.round(gapTime / SKIP_TIME_INTERVAL),
722+
this.config.maxSpeed
723+
)
724+
};
725+
this.speedService.send({ type: "FAST_FORWARD", payload });
726+
this.emitter.emit(ReplayerEvents.SkipStart, payload);
727+
}
728+
break;
729+
}
730+
}
731+
}
732+
636733
private setupDom() {
637734
this.wrapper = document.createElement('div');
638735
this.wrapper.classList.add('replayer-wrapper');

packages/rrweb/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,10 @@ export type CrossOriginIframeMessageEvent =
251251
MessageEvent<CrossOriginIframeMessageEventContent>;
252252

253253
export type ErrorHandler = (error: unknown) => void | boolean;
254+
255+
256+
export type EventIndexCache = {
257+
lastTime: number;
258+
lastIndex: number;
259+
maxDrift: number;
260+
};

0 commit comments

Comments
 (0)