@@ -7,79 +7,127 @@ interface CronJob {
77 handler : ( ) => Promise < void >
88}
99
10- // Manages scheduled jobs across multiple tabs
1110export class Cron {
1211 private jobs : Map < string , CronJob > = new Map ( )
1312 private checkInterval ?: ReturnType < typeof setInterval >
1413 private readonly STORAGE_KEY = 'sequence-cron-jobs'
14+ private isStopping : boolean = false
15+ private currentCheckJobsPromise : Promise < void > = Promise . resolve ( )
1516
1617 constructor ( private readonly shared : Shared ) {
1718 this . start ( )
1819 }
1920
2021 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+ } )
2452 }
2553
26- // Register a new job with a unique ID and interval in milliseconds
2754 registerJob ( id : string , interval : number , handler : ( ) => Promise < void > ) {
2855 if ( this . jobs . has ( id ) ) {
2956 throw new Error ( `Job with ID ${ id } already exists` )
3057 }
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 }
3959 this . jobs . set ( id , job )
40- this . syncWithStorage ( )
60+ // No syncWithStorage needed here, it happens in checkJobs
4161 }
4262
43- // Unregister a job by ID
4463 unregisterJob ( id : string ) {
45- if ( this . jobs . delete ( id ) ) {
46- this . syncWithStorage ( )
47- }
64+ this . jobs . delete ( id )
4865 }
4966
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+ }
69107 }
70108 }
71- }
72109
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+ }
75121 }
76122
77123 private async getStorageState ( ) : Promise < Map < string , { lastRun : number } > > {
124+ if ( this . isStopping ) return new Map ( )
78125 const state = localStorage . getItem ( this . STORAGE_KEY )
79126 return new Map ( state ? JSON . parse ( state ) : [ ] )
80127 }
81128
82129 private async syncWithStorage ( ) {
130+ if ( this . isStopping ) return
83131 const state = Array . from ( this . jobs . entries ( ) ) . map ( ( [ id , job ] ) => [ id , { lastRun : job . lastRun } ] )
84132 localStorage . setItem ( this . STORAGE_KEY , JSON . stringify ( state ) )
85133 }
0 commit comments