@@ -7,79 +7,127 @@ interface CronJob {
7
7
handler : ( ) => Promise < void >
8
8
}
9
9
10
- // Manages scheduled jobs across multiple tabs
11
10
export class Cron {
12
11
private jobs : Map < string , CronJob > = new Map ( )
13
12
private checkInterval ?: ReturnType < typeof setInterval >
14
13
private readonly STORAGE_KEY = 'sequence-cron-jobs'
14
+ private isStopping : boolean = false
15
+ private currentCheckJobsPromise : Promise < void > = Promise . resolve ( )
15
16
16
17
constructor ( private readonly shared : Shared ) {
17
18
this . start ( )
18
19
}
19
20
20
21
private start ( ) {
21
- // Check every minute
22
- this . checkInterval = setInterval ( ( ) => this . checkJobs ( ) , 60 * 1000 )
23
- this . checkJobs ( )
22
+ if ( this . isStopping ) return
23
+ this . executeCheckJobsChain ( )
24
+ this . checkInterval = setInterval ( ( ) => this . executeCheckJobsChain ( ) , 60 * 1000 )
25
+ }
26
+
27
+ // Wraps checkJobs to chain executions and manage currentCheckJobsPromise
28
+ private executeCheckJobsChain ( ) : void {
29
+ this . currentCheckJobsPromise = this . currentCheckJobsPromise
30
+ . catch ( ( ) => { } ) // Ignore errors from previous chain link for sequencing
31
+ . then ( ( ) => {
32
+ if ( ! this . isStopping ) {
33
+ return this . checkJobs ( )
34
+ }
35
+ return Promise . resolve ( )
36
+ } )
37
+ }
38
+
39
+ public async stop ( ) : Promise < void > {
40
+ this . isStopping = true
41
+
42
+ if ( this . checkInterval ) {
43
+ clearInterval ( this . checkInterval )
44
+ this . checkInterval = undefined
45
+ this . shared . modules . logger . log ( 'Cron: Interval cleared.' )
46
+ }
47
+
48
+ // Wait for the promise of the last (or current) checkJobs execution
49
+ await this . currentCheckJobsPromise . catch ( ( err ) => {
50
+ console . error ( 'Cron: Error during currentCheckJobsPromise settlement in stop():' , err )
51
+ } )
24
52
}
25
53
26
- // Register a new job with a unique ID and interval in milliseconds
27
54
registerJob ( id : string , interval : number , handler : ( ) => Promise < void > ) {
28
55
if ( this . jobs . has ( id ) ) {
29
56
throw new Error ( `Job with ID ${ id } already exists` )
30
57
}
31
-
32
- const job : CronJob = {
33
- id,
34
- interval,
35
- lastRun : 0 ,
36
- handler,
37
- }
38
-
58
+ const job : CronJob = { id, interval, lastRun : 0 , handler }
39
59
this . jobs . set ( id , job )
40
- this . syncWithStorage ( )
60
+ // No syncWithStorage needed here, it happens in checkJobs
41
61
}
42
62
43
- // Unregister a job by ID
44
63
unregisterJob ( id : string ) {
45
- if ( this . jobs . delete ( id ) ) {
46
- this . syncWithStorage ( )
47
- }
64
+ this . jobs . delete ( id )
48
65
}
49
66
50
- private async checkJobs ( ) {
51
- await navigator . locks . request ( 'sequence-cron-jobs' , async ( lock : Lock | null ) => {
52
- if ( ! lock ) return
53
-
54
- const now = Date . now ( )
55
- const storage = await this . getStorageState ( )
56
-
57
- for ( const [ id , job ] of this . jobs ) {
58
- const lastRun = storage . get ( id ) ?. lastRun ?? job . lastRun
59
- const timeSinceLastRun = now - lastRun
60
-
61
- if ( timeSinceLastRun >= job . interval ) {
62
- try {
63
- await job . handler ( )
64
- job . lastRun = now
65
- storage . set ( id , { lastRun : now } )
66
- } catch ( error ) {
67
- console . error ( `Cron job ${ id } failed:` , error )
68
- // Continue with other jobs even if this one failed
67
+ private async checkJobs ( ) : Promise < void > {
68
+ if ( this . isStopping ) {
69
+ return
70
+ }
71
+
72
+ try {
73
+ await navigator . locks . request ( 'sequence-cron-jobs' , async ( lock : Lock | null ) => {
74
+ if ( this . isStopping ) {
75
+ return
76
+ }
77
+ if ( ! lock ) {
78
+ return
79
+ }
80
+
81
+ const now = Date . now ( )
82
+ const storage = await this . getStorageState ( )
83
+
84
+ for ( const [ id , job ] of this . jobs ) {
85
+ if ( this . isStopping ) {
86
+ break
87
+ }
88
+
89
+ const lastRun = storage . get ( id ) ?. lastRun ?? job . lastRun
90
+ const timeSinceLastRun = now - lastRun
91
+
92
+ if ( timeSinceLastRun >= job . interval ) {
93
+ try {
94
+ await job . handler ( )
95
+ if ( ! this . isStopping ) {
96
+ job . lastRun = now
97
+ storage . set ( id , { lastRun : now } )
98
+ } else {
99
+ }
100
+ } catch ( error ) {
101
+ if ( error instanceof DOMException && error . name === 'AbortError' ) {
102
+ this . shared . modules . logger . log ( `Cron: Job ${ id } was aborted.` )
103
+ } else {
104
+ console . error ( `Cron job ${ id } failed:` , error )
105
+ }
106
+ }
69
107
}
70
108
}
71
- }
72
109
73
- await this . syncWithStorage ( )
74
- } )
110
+ if ( ! this . isStopping ) {
111
+ await this . syncWithStorage ( )
112
+ }
113
+ } )
114
+ } catch ( error ) {
115
+ if ( error instanceof DOMException && error . name === 'AbortError' ) {
116
+ this . shared . modules . logger . log ( 'Cron: navigator.locks.request was aborted.' )
117
+ } else {
118
+ console . error ( 'Cron: Error in navigator.locks.request:' , error )
119
+ }
120
+ }
75
121
}
76
122
77
123
private async getStorageState ( ) : Promise < Map < string , { lastRun : number } > > {
124
+ if ( this . isStopping ) return new Map ( )
78
125
const state = localStorage . getItem ( this . STORAGE_KEY )
79
126
return new Map ( state ? JSON . parse ( state ) : [ ] )
80
127
}
81
128
82
129
private async syncWithStorage ( ) {
130
+ if ( this . isStopping ) return
83
131
const state = Array . from ( this . jobs . entries ( ) ) . map ( ( [ id , job ] ) => [ id , { lastRun : job . lastRun } ] )
84
132
localStorage . setItem ( this . STORAGE_KEY , JSON . stringify ( state ) )
85
133
}
0 commit comments