1
1
import { EventEmitter , CustomEvent } from '@libp2p/interfaces/events' ;
2
2
import { simpleUid } from '@windingtree/contracts' ;
3
+ import { Storage } from '../storage/index.js' ;
3
4
import { backoffWithJitter } from '../utils/time.js' ;
4
5
import { createLogger } from '../utils/logger.js' ;
5
6
@@ -29,6 +30,54 @@ export interface JobHandler<T extends JobData = JobData> {
29
30
( data ?: T ) : Promise < boolean > ;
30
31
}
31
32
33
+ export interface JobHistoryInterface {
34
+ /** A history of all status changes for the job. */
35
+ statusChanges ?: { timestamp : number ; status : JobStatus } [ ] ;
36
+ /** A history of all errors for the job. */
37
+ errors ?: string [ ] ;
38
+ }
39
+
40
+ /**
41
+ * A class to manage the history of a job. This includes status changes and errors.
42
+ *
43
+ * @export
44
+ * @class JobHistory
45
+ */
46
+ export class JobHistory implements JobHistoryInterface {
47
+ /** A history of all status changes for the job. */
48
+ statusChanges : { timestamp : number ; status : JobStatus } [ ] ;
49
+ /** A history of all errors for the job. */
50
+ errors : string [ ] ;
51
+
52
+ /**
53
+ * Creates an instance of JobHistory.
54
+ * @memberof JobHistory
55
+ */
56
+ constructor ( config : JobHistoryInterface ) {
57
+ this . statusChanges = config . statusChanges ?? [ ] ;
58
+ this . errors = config . errors ?? [ ] ;
59
+ }
60
+
61
+ static getStatus ( source : JobHistory | JobHistoryInterface ) {
62
+ return source . statusChanges && source . statusChanges . length > 0
63
+ ? source . statusChanges [ source . statusChanges . length - 1 ] . status
64
+ : JobStatus . Pending ;
65
+ }
66
+
67
+ /**
68
+ * Returns class as object
69
+ *
70
+ * @returns
71
+ * @memberof JobHistory
72
+ */
73
+ toJSON ( ) : JobHistoryInterface {
74
+ return {
75
+ statusChanges : this . statusChanges ,
76
+ errors : this . errors ,
77
+ } ;
78
+ }
79
+ }
80
+
32
81
/**
33
82
* Configuration object for a job.
34
83
*/
@@ -51,24 +100,8 @@ export interface JobConfig<T extends JobData = JobData> {
51
100
retries ?: number ;
52
101
/** Retries delay */
53
102
retriesDelay ?: number ;
54
- }
55
-
56
- /**
57
- * A class to manage the history of a job. This includes status changes and errors.
58
- *
59
- * @export
60
- * @class JobHistory
61
- */
62
- export class JobHistory {
63
- /** A history of all status changes for the job. */
64
- statusChanges : { timestamp : Date ; status : JobStatus } [ ] ;
65
- /** A history of all errors for the job. */
66
- errors : Error [ ] ;
67
-
68
- constructor ( ) {
69
- this . statusChanges = [ ] ;
70
- this . errors = [ ] ;
71
- }
103
+ /** The history of the job */
104
+ history ?: JobHistoryInterface ;
72
105
}
73
106
74
107
/**
@@ -109,17 +142,16 @@ export class Job<T extends JobData = JobData> {
109
142
*/
110
143
constructor ( config : JobConfig < T > ) {
111
144
this . id = simpleUid ( ) ;
112
- this . history = new JobHistory ( ) ;
113
145
this . handlerName = config . handlerName ;
114
146
this . data = config . data ;
115
147
this . expire = config . expire ;
116
- this . status = JobStatus . Pending ;
117
148
this . isRecurrent = config . isRecurrent ?? false ;
118
149
this . recurrenceInterval = config . recurrenceInterval ?? 0 ;
119
150
this . maxRecurrences = config . maxRecurrences ?? 0 ;
120
151
this . maxRetries = config . maxRetries ?? 0 ;
121
152
this . retries = config . retries ?? 0 ;
122
153
this . retriesDelay = config . retriesDelay ?? 0 ;
154
+ this . history = new JobHistory ( config . history ?? { } ) ;
123
155
}
124
156
125
157
/**
@@ -129,7 +161,7 @@ export class Job<T extends JobData = JobData> {
129
161
*/
130
162
set status ( newStatus : JobStatus ) {
131
163
this . history . statusChanges . push ( {
132
- timestamp : new Date ( ) ,
164
+ timestamp : Date . now ( ) ,
133
165
status : newStatus ,
134
166
} ) ;
135
167
logger . trace ( `Job #${ this . id } status changed to: ${ this . status } ` ) ;
@@ -141,8 +173,7 @@ export class Job<T extends JobData = JobData> {
141
173
* @memberof Job
142
174
*/
143
175
get status ( ) {
144
- return this . history . statusChanges [ this . history . statusChanges . length - 1 ]
145
- . status ;
176
+ return JobHistory . getStatus ( this . history ) ;
146
177
}
147
178
148
179
/**
@@ -182,6 +213,27 @@ export class Job<T extends JobData = JobData> {
182
213
) ;
183
214
}
184
215
216
+ /**
217
+ * Returns Job as config object
218
+ *
219
+ * @returns {JobConfig<T> }
220
+ * @memberof Job
221
+ */
222
+ toJSON ( ) : JobConfig < T > {
223
+ return {
224
+ handlerName : this . handlerName ,
225
+ data : this . data ,
226
+ expire : this . expire ,
227
+ isRecurrent : this . isRecurrent ,
228
+ recurrenceInterval : this . recurrenceInterval ,
229
+ maxRecurrences : this . maxRecurrences ,
230
+ maxRetries : this . maxRetries ,
231
+ retries : this . retries ,
232
+ retriesDelay : this . retriesDelay ,
233
+ history : this . history . toJSON ( ) ,
234
+ } ;
235
+ }
236
+
185
237
/**
186
238
* Executes the job using the provided handler.
187
239
*
@@ -249,6 +301,11 @@ export class JobHandlerRegistry {
249
301
* @interface QueueOptions
250
302
*/
251
303
export interface QueueOptions {
304
+ /** Queue storage object */
305
+ storage ?: Storage ;
306
+ /** Name of the key that is used for storing jobs Ids */
307
+ idsKeyName ?: string ;
308
+ /** The maximum number of jobs that can be concurrently active. */
252
309
concurrencyLimit ?: number ;
253
310
}
254
311
@@ -288,6 +345,10 @@ export interface QueueEvents<T extends JobData = JobData> {
288
345
* @extends {EventEmitter<QueueEvents> }
289
346
*/
290
347
export class Queue extends EventEmitter < QueueEvents > {
348
+ /** Queue storage object */
349
+ storage ?: Storage ;
350
+ /** Name of the key that is used for storing jobs Ids */
351
+ idsKeyName : string ;
291
352
/** The maximum number of jobs that can be concurrently active. */
292
353
concurrencyLimit : number ;
293
354
/** The list of all jobs in the queue. */
@@ -301,11 +362,134 @@ export class Queue extends EventEmitter<QueueEvents> {
301
362
* @param {QueueOptions } { concurrencyLimit }
302
363
* @memberof Queue
303
364
*/
304
- constructor ( { concurrencyLimit } : QueueOptions ) {
365
+ constructor ( { storage , idsKeyName , concurrencyLimit } : QueueOptions ) {
305
366
super ( ) ;
367
+ ( this . storage = storage ) , ( this . idsKeyName = idsKeyName ?? 'jobsIds' ) ;
306
368
this . concurrencyLimit = concurrencyLimit ?? 5 ;
307
369
this . jobs = [ ] ;
308
370
this . handlers = new JobHandlerRegistry ( ) ;
371
+ void this . storageUp ( ) ;
372
+ }
373
+
374
+ /**
375
+ * Restores saved jobs from the storage
376
+ *
377
+ * @protected
378
+ * @returns
379
+ * @memberof Queue
380
+ */
381
+ protected async storageUp ( ) {
382
+ try {
383
+ // Ignore storage features if not set up
384
+ if ( ! this . storage ) {
385
+ return ;
386
+ }
387
+
388
+ const jobsIds = await this . storage . get < string [ ] > ( this . idsKeyName ) ;
389
+
390
+ if ( jobsIds ) {
391
+ for ( const id of jobsIds ) {
392
+ try {
393
+ const jobConfig = await this . storage . get < JobConfig > ( id ) ;
394
+
395
+ if ( ! jobConfig ) {
396
+ throw new Error ( `Unable to get job #${ id } from storage` ) ;
397
+ }
398
+
399
+ // Only Pending jobs must be restored
400
+ if (
401
+ jobConfig . history &&
402
+ JobHistory . getStatus ( jobConfig . history ) === JobStatus . Pending
403
+ ) {
404
+ this . add ( jobConfig ) ;
405
+ }
406
+ } catch ( error ) {
407
+ logger . error ( error ) ;
408
+ }
409
+ }
410
+ } else {
411
+ logger . trace ( 'Jobs Ids not found in the storage' ) ;
412
+ }
413
+ } catch ( error ) {
414
+ logger . error ( 'storageUp error:' , error ) ;
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Stores all pending jobs to the storage
420
+ *
421
+ * @protected
422
+ * @returns
423
+ * @memberof Queue
424
+ */
425
+ protected async storageDown ( ) {
426
+ try {
427
+ // Ignore storage features if not set up
428
+ if ( ! this . storage ) {
429
+ return ;
430
+ }
431
+
432
+ const pendingJobs = this . jobs . filter ( ( job ) => job . executable ) ;
433
+
434
+ const { ids, configs } = pendingJobs . reduce < {
435
+ ids : string [ ] ;
436
+ configs : JobConfig [ ] ;
437
+ } > (
438
+ ( a , v ) => {
439
+ a . ids . push ( v . id ) ;
440
+ a . configs . push ( v . toJSON ( ) ) ;
441
+ return a ;
442
+ } ,
443
+ {
444
+ ids : [ ] ,
445
+ configs : [ ] ,
446
+ } ,
447
+ ) ;
448
+
449
+ const jobsIds = new Set (
450
+ ( await this . storage . get < string [ ] > ( this . idsKeyName ) ) ?? [ ] ,
451
+ ) ;
452
+
453
+ for ( let i = 0 ; i < ids . length ; i ++ ) {
454
+ try {
455
+ jobsIds . add ( ids [ i ] ) ;
456
+ await this . storage . set ( ids [ i ] , configs [ i ] ) ;
457
+ } catch ( error ) {
458
+ logger . error ( `Job #${ ids [ i ] } save error:` , error ) ;
459
+ }
460
+ }
461
+
462
+ await this . storage . set ( this . idsKeyName , Array . from ( jobsIds ) ) ;
463
+ } catch ( error ) {
464
+ logger . error ( 'storageDown error:' , error ) ;
465
+ }
466
+ }
467
+
468
+ /**
469
+ * Updated saved job on storage
470
+ *
471
+ * @protected
472
+ * @param {string } id
473
+ * @param {Job } job
474
+ * @returns
475
+ * @memberof Queue
476
+ */
477
+ protected async storageUpdate ( id : string , job : Job ) {
478
+ try {
479
+ // Ignore storage features if not set up
480
+ if ( ! this . storage ) {
481
+ return ;
482
+ }
483
+
484
+ const jobsIds = new Set (
485
+ ( await this . storage . get < string [ ] > ( this . idsKeyName ) ) ?? [ ] ,
486
+ ) ;
487
+ jobsIds . add ( id ) ;
488
+ await this . storage . set ( id , job . toJSON ( ) ) ;
489
+ await this . storage . set ( this . idsKeyName , Array . from ( jobsIds ) ) ;
490
+ } catch ( error ) {
491
+ logger . error ( 'storageDown error:' , error ) ;
492
+ }
309
493
}
310
494
311
495
/**
@@ -323,6 +507,7 @@ export class Queue extends EventEmitter<QueueEvents> {
323
507
detail : job ,
324
508
} ) ,
325
509
) ;
510
+ void this . storageUpdate ( job . id , job ) ;
326
511
}
327
512
328
513
/**
@@ -395,7 +580,7 @@ export class Queue extends EventEmitter<QueueEvents> {
395
580
}
396
581
} catch ( error ) {
397
582
logger . error ( `Job #${ job . id } is errored` , error ) ;
398
- job . history . errors . push ( error as Error ) ;
583
+ job . history . errors . push ( String ( error ) ) ;
399
584
400
585
if ( job . maxRetries > 0 && job . retries < job . maxRetries ) {
401
586
// If the job hasn't reached the maximum number of retries, retry it
@@ -457,19 +642,50 @@ export class Queue extends EventEmitter<QueueEvents> {
457
642
const job = new Job < T > ( config ) ;
458
643
this . jobs . push ( job ) ;
459
644
logger . trace ( 'Job added:' , job ) ;
645
+ void this . storageUpdate ( job . id , job ) ;
460
646
void this . start ( ) ;
461
647
return job . id ;
462
648
}
463
649
464
650
/**
465
- * Returns a job from the queue by its ID.
651
+ * Returns a job from the queue by its ID. Uses local in-memory source
466
652
*
467
653
* @param {string } id
468
654
* @returns {(Job | undefined) } The job if found, otherwise undefined.
469
655
* @memberof Queue
470
656
*/
471
- get ( id : string ) : Job | undefined {
472
- return this . jobs . find ( ( job ) => job . id === id ) ;
657
+ getLocal < T extends JobData = JobData > ( id : string ) : Job < T > | undefined {
658
+ const localJob = this . jobs . find ( ( job ) => job . id === id ) as Job < T > ;
659
+
660
+ if ( localJob ) {
661
+ return localJob ;
662
+ }
663
+
664
+ return ;
665
+ }
666
+
667
+ /**
668
+ * Returns a job config from the queue by its ID. Uses both local and storage search
669
+ *
670
+ * @param {string } id
671
+ * @returns {Promise<JobConfig | undefined> } The job if found, otherwise undefined.
672
+ * @memberof Queue
673
+ */
674
+ async get < T extends JobData = JobData > (
675
+ id : string ,
676
+ ) : Promise < JobConfig < T > | undefined > {
677
+ const localJob = this . getLocal < T > ( id ) ;
678
+
679
+ if ( localJob ) {
680
+ return localJob . toJSON ( ) ;
681
+ }
682
+
683
+ if ( ! this . storage ) {
684
+ return ;
685
+ }
686
+
687
+ // If job not found locally we will try to find on storage
688
+ return await this . storage . get < JobConfig < T > > ( id ) ;
473
689
}
474
690
475
691
/**
@@ -513,4 +729,13 @@ export class Queue extends EventEmitter<QueueEvents> {
513
729
514
730
return isDeleted ;
515
731
}
732
+
733
+ /**
734
+ * Graceful queue stop
735
+ *
736
+ * @memberof Queue
737
+ */
738
+ async stop ( ) {
739
+ await this . storageDown ( ) ;
740
+ }
516
741
}
0 commit comments