@@ -41,7 +41,7 @@ import {
41
41
type PlayerMachineState ,
42
42
type SpeedMachineState ,
43
43
} from './machine' ;
44
- import type { playerConfig , missingNodeMap } from '../types' ;
44
+ import type { playerConfig , missingNodeMap , EventIndexCache } from '../types' ;
45
45
import {
46
46
EventType ,
47
47
IncrementalSource ,
@@ -198,6 +198,9 @@ export class Replayer {
198
198
// Similar to the reason for constructedStyleMutations.
199
199
private adoptedStyleSheets : adoptedStyleSheetData [ ] = [ ] ;
200
200
201
+ // Cache for optimized event index lookups during playback.
202
+ private eventIndexCache : EventIndexCache ;
203
+
201
204
constructor (
202
205
events : Array < eventWithTime | string > ,
203
206
config ?: Partial < playerConfig > ,
@@ -231,6 +234,12 @@ export class Replayer {
231
234
this . applyEventsSynchronously = this . applyEventsSynchronously . bind ( this ) ;
232
235
this . emitter . on ( ReplayerEvents . Resize , this . handleResize as Handler ) ;
233
236
237
+ this . eventIndexCache = {
238
+ lastTime : - 1 ,
239
+ lastIndex : 0 ,
240
+ maxDrift : 3000 // 3 second max drift from the current playback position.
241
+ } ;
242
+
234
243
this . setupDom ( ) ;
235
244
236
245
/**
@@ -633,6 +642,94 @@ export class Replayer {
633
642
this . cache = createCache ( ) ;
634
643
}
635
644
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
+
636
733
private setupDom ( ) {
637
734
this . wrapper = document . createElement ( 'div' ) ;
638
735
this . wrapper . classList . add ( 'replayer-wrapper' ) ;
0 commit comments