55
66import EventEmitter from '../../controllers/eventEmitter/eventEmitter'
77
8+ type RecurringTimeoutStartOptions = {
9+ timeout ?: number
10+ /**
11+ * Whether to run the function immediately upon starting,
12+ * instead of waiting for the first timeout interval.
13+ */
14+ runImmediately ?: boolean
15+ /**
16+ * Whether to allow the starting of a new function execution
17+ * even if the previous one is still running. There will still
18+ * be only one interval scheduled at a time.
19+ *
20+ * @example
21+ * - Execution 1 starts
22+ * - Execution 2 starts while Execution 1 is still running
23+ * - Execution 1 completes - a new execution is not scheduled. Simply
24+ * the promise of Execution 1 is resolved.
25+ * - Execution 2 completes - a new execution is scheduled.
26+ */
27+ allowOverlap ?: boolean
28+ }
29+
830export interface IRecurringTimeout {
9- start : ( options ?: { timeout ?: number ; runImmediately ?: boolean } ) => void
10- restart : ( options ?: { timeout ?: number ; runImmediately ?: boolean } ) => void
31+ start : ( options ?: RecurringTimeoutStartOptions ) => void
32+ restart : ( options ?: RecurringTimeoutStartOptions ) => void
1133 stop : ( ) => void
1234 updateTimeout : ( options : { timeout : number } ) => void
1335 running : boolean
@@ -42,7 +64,7 @@ export class RecurringTimeout implements IRecurringTimeout {
4264 promise : Promise < void > | undefined
4365
4466 // collapse multiple start/restart calls in the same tick
45- #pendingStart?: { timeout ?: number ; runImmediately ?: boolean }
67+ #pendingStart?: RecurringTimeoutStartOptions
4668
4769 startScheduled = false
4870
@@ -62,7 +84,7 @@ export class RecurringTimeout implements IRecurringTimeout {
6284 this . currentTimeout = timeout
6385 }
6486
65- start ( opts : { timeout ?: number ; runImmediately ?: boolean } = { } ) {
87+ start ( opts : RecurringTimeoutStartOptions = { } ) {
6688 this . #scheduleStart( opts )
6789 }
6890
@@ -71,43 +93,42 @@ export class RecurringTimeout implements IRecurringTimeout {
7193 this . #reset( )
7294 }
7395
74- restart ( opts : { timeout ?: number ; runImmediately ?: boolean } = { } ) {
96+ restart ( opts : RecurringTimeoutStartOptions = { } ) {
7597 this . #reset( )
7698 this . #scheduleStart( opts )
7799 }
78100
79101 async #loop( ) {
102+ this . fnExecutionsCount += 1
103+ const currentCount = this . fnExecutionsCount
104+
80105 try {
81106 this . promise = this . #fn( )
82- this . fnExecutionsCount += 1
83107 await this . promise
84108 } catch ( err : any ) {
85109 if ( ! this . promise ) return
86110 console . error ( 'Recurring task error:' , err )
87111 if ( this . #emitError)
88112 this . #emitError( { error : err , message : 'Recurring task failed' , level : 'minor' } )
89113 } finally {
90- if ( this . promise ) {
114+ // If fnExecutionsCount has changed, it means `restart` was called during the execution of fn,
115+ // so we shouldn't schedule the next loop here.
116+ if ( this . promise && this . fnExecutionsCount === currentCount ) {
91117 if ( this . running ) this . #timeoutId = setTimeout ( this . #loop. bind ( this ) , this . currentTimeout )
92118 this . promise = undefined
93119 }
94120 }
95121 }
96122
97- #scheduleStart(
98- opts : {
99- timeout ?: number
100- runImmediately ?: boolean
101- } = { }
102- ) {
123+ #scheduleStart( opts : RecurringTimeoutStartOptions = { } ) {
103124 if ( this . running ) return
104125 this . #pendingStart = opts // collect latest opts for this tick
105126 if ( this . startScheduled ) return
106127 this . startScheduled = true
107128
108129 queueMicrotask ( ( ) => {
109130 this . startScheduled = false
110- const { timeout : newTimeout , runImmediately } = this . #pendingStart || { }
131+ const { timeout : newTimeout , runImmediately, allowOverlap } = this . #pendingStart || { }
111132 this . #pendingStart = undefined
112133
113134 this . running = true
@@ -116,7 +137,8 @@ export class RecurringTimeout implements IRecurringTimeout {
116137
117138 if ( newTimeout ) this . updateTimeout ( { timeout : newTimeout } )
118139
119- if ( this . promise ) return // prevents multiple executions in one tick
140+ // Prevents starting a new loop if the previous one is still running
141+ if ( this . promise && ! allowOverlap ) return
120142
121143 if ( runImmediately ) {
122144 this . #loop( )
0 commit comments