@boringnode/queue 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import {
2
- parse
3
- } from "./chunk-PBGPIFI5.js";
4
- import {
2
+ Job,
3
+ JobBatchDispatcher,
4
+ JobDispatcher,
5
5
  Locator,
6
6
  QueueManager,
7
+ ScheduleBuilder,
7
8
  debug_default
8
- } from "./chunk-LI2ZMCNO.js";
9
+ } from "./chunk-H6WOFLPJ.js";
9
10
  import {
10
11
  DEFAULT_ERROR_RETRY_DELAY,
11
12
  DEFAULT_IDLE_DELAY,
@@ -13,692 +14,16 @@ import {
13
14
  DEFAULT_STALLED_INTERVAL,
14
15
  DEFAULT_STALLED_THRESHOLD,
15
16
  E_INVALID_BASE_DELAY,
16
- E_INVALID_CRON_EXPRESSION,
17
17
  E_INVALID_MAX_DELAY,
18
18
  E_INVALID_MULTIPLIER,
19
- E_INVALID_SCHEDULE_CONFIG,
20
19
  E_JOB_MAX_ATTEMPTS_REACHED,
21
20
  E_JOB_TIMEOUT,
22
- exceptions_exports
23
- } from "./chunk-SMOKFZ46.js";
24
-
25
- // src/job_dispatcher.ts
26
- import { randomUUID } from "crypto";
27
- var JobDispatcher = class {
28
- #name;
29
- #payload;
30
- #queue = "default";
31
- #adapter;
32
- #delay;
33
- #priority;
34
- #groupId;
35
- /**
36
- * Create a new job dispatcher.
37
- *
38
- * @param name - The job class name (used to locate the class at runtime)
39
- * @param payload - The data to pass to the job
40
- */
41
- constructor(name, payload) {
42
- this.#name = name;
43
- this.#payload = payload;
44
- }
45
- /**
46
- * Set the target queue for this job.
47
- *
48
- * @param queue - Queue name (default: 'default')
49
- * @returns This dispatcher for chaining
50
- *
51
- * @example
52
- * ```typescript
53
- * await SendEmailJob.dispatch(payload).toQueue('emails')
54
- * ```
55
- */
56
- toQueue(queue) {
57
- this.#queue = queue;
58
- return this;
59
- }
60
- /**
61
- * Delay the job execution.
62
- *
63
- * The job will be stored in a delayed state and moved to pending
64
- * after the delay expires.
65
- *
66
- * @param delay - Delay as milliseconds or duration string ('5s', '1h', '7d')
67
- * @returns This dispatcher for chaining
68
- *
69
- * @example
70
- * ```typescript
71
- * // Send reminder in 24 hours
72
- * await ReminderJob.dispatch(payload).in('24h')
73
- *
74
- * // Process in 5 minutes
75
- * await CleanupJob.dispatch(payload).in('5m')
76
- * ```
77
- */
78
- in(delay) {
79
- this.#delay = delay;
80
- return this;
81
- }
82
- /**
83
- * Set the job priority.
84
- *
85
- * Lower numbers = higher priority. Jobs with lower priority values
86
- * are processed before jobs with higher values.
87
- *
88
- * @param priority - Priority level (1-10, default: 5)
89
- * @returns This dispatcher for chaining
90
- *
91
- * @example
92
- * ```typescript
93
- * // High priority job
94
- * await UrgentJob.dispatch(payload).priority(1)
95
- *
96
- * // Low priority job
97
- * await BackgroundJob.dispatch(payload).priority(10)
98
- * ```
99
- */
100
- priority(priority) {
101
- this.#priority = priority;
102
- return this;
103
- }
104
- /**
105
- * Assign this job to a group.
106
- *
107
- * Jobs with the same groupId can be filtered and displayed together
108
- * in monitoring UIs. Useful for batch operations like newsletters
109
- * or bulk exports.
110
- *
111
- * @param groupId - Group identifier
112
- * @returns This dispatcher for chaining
113
- *
114
- * @example
115
- * ```typescript
116
- * // Group newsletter jobs together
117
- * await SendEmailJob.dispatch({ to: 'user@example.com' })
118
- * .group('newsletter-jan-2025')
119
- * .run()
120
- * ```
121
- */
122
- group(groupId) {
123
- this.#groupId = groupId;
124
- return this;
125
- }
126
- /**
127
- * Use a specific adapter for this job.
128
- *
129
- * @param adapter - Adapter name or factory function
130
- * @returns This dispatcher for chaining
131
- *
132
- * @example
133
- * ```typescript
134
- * // Use named adapter
135
- * await Job.dispatch(payload).with('redis')
136
- *
137
- * // Use custom adapter instance
138
- * await Job.dispatch(payload).with(() => new CustomAdapter())
139
- * ```
140
- */
141
- with(adapter) {
142
- this.#adapter = adapter;
143
- return this;
144
- }
145
- /**
146
- * Dispatch the job to the queue.
147
- *
148
- * @returns A DispatchResult containing the jobId
149
- *
150
- * @example
151
- * ```typescript
152
- * const { jobId } = await SendEmailJob.dispatch(payload).run()
153
- * console.log(`Dispatched job: ${jobId}`)
154
- * ```
155
- */
156
- async run() {
157
- const id = randomUUID();
158
- debug_default("dispatching job %s with id %s using payload %s", this.#name, id, this.#payload);
159
- const adapter = this.#getAdapterInstance();
160
- const payload = {
161
- id,
162
- name: this.#name,
163
- payload: this.#payload,
164
- attempts: 0,
165
- priority: this.#priority,
166
- groupId: this.#groupId
167
- };
168
- if (this.#delay) {
169
- const parsedDelay = parse(this.#delay);
170
- await adapter.pushLaterOn(this.#queue, payload, parsedDelay);
171
- } else {
172
- await adapter.pushOn(this.#queue, payload);
173
- }
174
- return {
175
- jobId: id
176
- };
177
- }
178
- /**
179
- * Thenable implementation for auto-dispatch when awaited.
180
- *
181
- * Allows `await Job.dispatch(payload)` without explicit `.run()`.
182
- *
183
- * @param onFulfilled - Success callback
184
- * @param onRejected - Error callback
185
- * @returns Promise resolving to the DispatchResult
186
- */
187
- then(onFulfilled, onRejected) {
188
- return this.run().then(onFulfilled, onRejected);
189
- }
190
- #getAdapterInstance() {
191
- if (!this.#adapter) {
192
- return QueueManager.use();
193
- }
194
- if (typeof this.#adapter === "string") {
195
- return QueueManager.use(this.#adapter);
196
- }
197
- return this.#adapter();
198
- }
199
- };
200
-
201
- // src/job_batch_dispatcher.ts
202
- import { randomUUID as randomUUID2 } from "crypto";
203
- var JobBatchDispatcher = class {
204
- #name;
205
- #payloads;
206
- #queue = "default";
207
- #adapter;
208
- #priority;
209
- #groupId;
210
- /**
211
- * Create a new batch job dispatcher.
212
- *
213
- * @param name - The job class name (used to locate the class at runtime)
214
- * @param payloads - Array of data to pass to each job
215
- */
216
- constructor(name, payloads) {
217
- this.#name = name;
218
- this.#payloads = payloads;
219
- }
220
- /**
221
- * Set the target queue for all jobs.
222
- *
223
- * @param queue - Queue name (default: 'default')
224
- * @returns This dispatcher for chaining
225
- *
226
- * @example
227
- * ```typescript
228
- * await SendEmailJob.dispatchMany(payloads).toQueue('emails')
229
- * ```
230
- */
231
- toQueue(queue) {
232
- this.#queue = queue;
233
- return this;
234
- }
235
- /**
236
- * Set the priority for all jobs.
237
- *
238
- * Lower numbers = higher priority. Jobs with lower priority values
239
- * are processed before jobs with higher values.
240
- *
241
- * @param priority - Priority level (1-10, default: 5)
242
- * @returns This dispatcher for chaining
243
- *
244
- * @example
245
- * ```typescript
246
- * await UrgentJob.dispatchMany(payloads).priority(1)
247
- * ```
248
- */
249
- priority(priority) {
250
- this.#priority = priority;
251
- return this;
252
- }
253
- /**
254
- * Assign all jobs to a group.
255
- *
256
- * Jobs with the same groupId can be filtered and displayed together
257
- * in monitoring UIs. Useful for batch operations like newsletters
258
- * or bulk exports.
259
- *
260
- * @param groupId - Group identifier
261
- * @returns This dispatcher for chaining
262
- *
263
- * @example
264
- * ```typescript
265
- * await SendEmailJob.dispatchMany(recipients)
266
- * .group('newsletter-jan-2025')
267
- * .run()
268
- * ```
269
- */
270
- group(groupId) {
271
- this.#groupId = groupId;
272
- return this;
273
- }
274
- /**
275
- * Use a specific adapter for these jobs.
276
- *
277
- * @param adapter - Adapter name or factory function
278
- * @returns This dispatcher for chaining
279
- *
280
- * @example
281
- * ```typescript
282
- * await Job.dispatchMany(payloads).with('redis')
283
- * ```
284
- */
285
- with(adapter) {
286
- this.#adapter = adapter;
287
- return this;
288
- }
289
- /**
290
- * Dispatch all jobs to the queue.
291
- *
292
- * @returns A DispatchManyResult containing all jobIds
293
- *
294
- * @example
295
- * ```typescript
296
- * const { jobIds } = await SendEmailJob.dispatchMany(payloads).run()
297
- * console.log(`Dispatched ${jobIds.length} jobs`)
298
- * ```
299
- */
300
- async run() {
301
- debug_default("dispatching %d jobs of type %s", this.#payloads.length, this.#name);
302
- const adapter = this.#getAdapterInstance();
303
- const jobs = this.#payloads.map((payload) => ({
304
- id: randomUUID2(),
305
- name: this.#name,
306
- payload,
307
- attempts: 0,
308
- priority: this.#priority,
309
- groupId: this.#groupId
310
- }));
311
- await adapter.pushManyOn(this.#queue, jobs);
312
- return {
313
- jobIds: jobs.map((job) => job.id)
314
- };
315
- }
316
- /**
317
- * Thenable implementation for auto-dispatch when awaited.
318
- *
319
- * Allows `await Job.dispatchMany(payloads)` without explicit `.run()`.
320
- *
321
- * @param onFulfilled - Success callback
322
- * @param onRejected - Error callback
323
- * @returns Promise resolving to the DispatchManyResult
324
- */
325
- then(onFulfilled, onRejected) {
326
- return this.run().then(onFulfilled, onRejected);
327
- }
328
- #getAdapterInstance() {
329
- if (!this.#adapter) {
330
- return QueueManager.use();
331
- }
332
- if (typeof this.#adapter === "string") {
333
- return QueueManager.use(this.#adapter);
334
- }
335
- return this.#adapter();
336
- }
337
- };
338
-
339
- // src/schedule_builder.ts
340
- import { CronExpressionParser } from "cron-parser";
341
- var ScheduleBuilder = class {
342
- #name;
343
- #payload;
344
- #id;
345
- #cronExpression;
346
- #everyMs;
347
- #timezone = "UTC";
348
- #from;
349
- #to;
350
- #limit;
351
- constructor(name, payload) {
352
- this.#name = name;
353
- this.#payload = payload;
354
- }
355
- /**
356
- * Set a custom schedule ID.
357
- * If not specified, defaults to the job name.
358
- * If a schedule with this ID exists, it will be updated (upsert).
359
- */
360
- id(scheduleId) {
361
- this.#id = scheduleId;
362
- return this;
363
- }
364
- /**
365
- * Set a cron expression for the schedule.
366
- * Mutually exclusive with `every()`.
367
- */
368
- cron(expression) {
369
- this.#cronExpression = expression;
370
- return this;
371
- }
372
- /**
373
- * Set a repeating interval for the schedule.
374
- * Mutually exclusive with `cron()`.
375
- */
376
- every(interval) {
377
- this.#everyMs = parse(interval);
378
- return this;
379
- }
380
- /**
381
- * Set the timezone for cron evaluation.
382
- * @default 'UTC'
383
- */
384
- timezone(tz) {
385
- this.#timezone = tz;
386
- return this;
387
- }
388
- /**
389
- * Set the start boundary for the schedule.
390
- * No jobs will be dispatched before this date.
391
- */
392
- from(date) {
393
- this.#from = date;
394
- return this;
395
- }
396
- /**
397
- * Set the end boundary for the schedule.
398
- * No jobs will be dispatched after this date.
399
- */
400
- to(date) {
401
- this.#to = date;
402
- return this;
403
- }
404
- /**
405
- * Set both start and end boundaries for the schedule.
406
- * Shorthand for `.from(start).to(end)`.
407
- */
408
- between(from, to) {
409
- return this.from(from).to(to);
410
- }
411
- /**
412
- * Set the maximum number of runs for this schedule.
413
- */
414
- limit(maxRuns) {
415
- this.#limit = maxRuns;
416
- return this;
417
- }
418
- /**
419
- * Create the schedule and return the schedule ID.
420
- */
421
- async run() {
422
- if (!this.#cronExpression && !this.#everyMs) {
423
- throw new E_INVALID_SCHEDULE_CONFIG([
424
- "Schedule must have either a cron expression or an interval"
425
- ]);
426
- }
427
- if (this.#cronExpression && this.#everyMs) {
428
- throw new E_INVALID_SCHEDULE_CONFIG([
429
- "Schedule cannot have both a cron expression and an interval"
430
- ]);
431
- }
432
- if (this.#cronExpression) {
433
- try {
434
- CronExpressionParser.parse(this.#cronExpression, { tz: this.#timezone });
435
- } catch (error) {
436
- throw new E_INVALID_CRON_EXPRESSION([this.#cronExpression, error.message]);
437
- }
438
- }
439
- const config = {
440
- id: this.#id ?? this.#name,
441
- name: this.#name,
442
- payload: this.#payload,
443
- cronExpression: this.#cronExpression,
444
- everyMs: this.#everyMs,
445
- timezone: this.#timezone,
446
- from: this.#from,
447
- to: this.#to,
448
- limit: this.#limit
449
- };
450
- const adapter = QueueManager.use();
451
- const scheduleId = await adapter.createSchedule(config);
452
- const nextRunAt = this.#calculateNextRunAt();
453
- await adapter.updateSchedule(scheduleId, { nextRunAt });
454
- return { scheduleId };
455
- }
456
- /**
457
- * Calculate the next run time based on cron or interval.
458
- */
459
- #calculateNextRunAt() {
460
- const now = /* @__PURE__ */ new Date();
461
- let nextRun;
462
- if (this.#cronExpression) {
463
- const cron = CronExpressionParser.parse(this.#cronExpression, {
464
- currentDate: now,
465
- tz: this.#timezone
466
- });
467
- nextRun = cron.next().toDate();
468
- } else {
469
- nextRun = new Date(now.getTime() + this.#everyMs);
470
- }
471
- if (this.#from && nextRun < this.#from) {
472
- if (this.#cronExpression) {
473
- const cron = CronExpressionParser.parse(this.#cronExpression, {
474
- currentDate: this.#from,
475
- tz: this.#timezone
476
- });
477
- nextRun = cron.next().toDate();
478
- } else {
479
- nextRun = this.#from;
480
- }
481
- }
482
- return nextRun;
483
- }
484
- /**
485
- * Implement PromiseLike to allow `await builder.every('5m')` syntax.
486
- */
487
- then(onfulfilled, onrejected) {
488
- return this.run().then(onfulfilled, onrejected);
489
- }
490
- };
491
-
492
- // src/job.ts
493
- var Job = class {
494
- #payload;
495
- #context;
496
- #signal;
497
- /**
498
- * Static options for this job class.
499
- *
500
- * Override this property in subclasses to configure job behavior
501
- * such as queue name, retry policy, timeout, and more.
502
- *
503
- * @example
504
- * ```typescript
505
- * class SendEmailJob extends Job<SendEmailPayload> {
506
- * static options = {
507
- * queue: 'emails',
508
- * maxRetries: 3,
509
- * timeout: '30s',
510
- * }
511
- * }
512
- * ```
513
- */
514
- static options = {};
515
- /**
516
- * The payload data passed to this job instance.
517
- *
518
- * Contains the data provided when the job was dispatched.
519
- * Available after the job has been hydrated by the worker.
520
- *
521
- * @example
522
- * ```typescript
523
- * async execute() {
524
- * const { to, subject, body } = this.payload
525
- * await sendEmail(to, subject, body)
526
- * }
527
- * ```
528
- */
529
- get payload() {
530
- return this.#payload;
531
- }
532
- /**
533
- * Context information for the current job execution.
534
- *
535
- * Provides metadata such as job ID, current attempt number,
536
- * queue name, priority, and timing information.
537
- *
538
- * @example
539
- * ```typescript
540
- * async execute() {
541
- * if (this.context.attempt > 1) {
542
- * console.log(`Retry attempt ${this.context.attempt}`)
543
- * }
544
- * console.log(`Processing job ${this.context.jobId} on queue ${this.context.queue}`)
545
- * }
546
- * ```
547
- */
548
- get context() {
549
- return this.#context;
550
- }
551
- /**
552
- * The abort signal for timeout handling.
553
- *
554
- * Check `signal.aborted` in long-running operations to handle timeouts gracefully.
555
- *
556
- * @example
557
- * ```typescript
558
- * async execute() {
559
- * for (const item of this.payload.items) {
560
- * if (this.signal?.aborted) {
561
- * throw new Error('Job timed out')
562
- * }
563
- * await processItem(item)
564
- * }
565
- * }
566
- * ```
567
- */
568
- get signal() {
569
- return this.#signal;
570
- }
571
- /**
572
- * Hydrate the job with payload, context, and optional abort signal.
573
- *
574
- * This method is called by the worker after instantiation to provide
575
- * the job's runtime data. It should not be called directly by user code.
576
- *
577
- * @param payload - The data to be processed by this job
578
- * @param context - The job execution context
579
- * @param signal - Optional abort signal for timeout handling
580
- *
581
- * @internal
582
- */
583
- $hydrate(payload, context, signal) {
584
- this.#payload = payload;
585
- this.#context = Object.freeze(context);
586
- this.#signal = signal;
587
- }
588
- /**
589
- * Dispatch this job to the queue.
590
- *
591
- * Returns a JobDispatcher for fluent configuration before dispatching.
592
- * The job is not actually dispatched until `.run()` is called or the
593
- * dispatcher is awaited.
594
- *
595
- * @param payload - The data to pass to the job
596
- * @returns A JobDispatcher for fluent configuration
597
- *
598
- * @example
599
- * ```typescript
600
- * // Simple dispatch
601
- * await SendEmailJob.dispatch({ to: 'user@example.com', subject: 'Hello' })
602
- *
603
- * // With options
604
- * await SendEmailJob.dispatch({ to: 'user@example.com' })
605
- * .toQueue('high-priority')
606
- * .priority(1)
607
- * .in('5m')
608
- * .run()
609
- * ```
610
- */
611
- static dispatch(payload) {
612
- const options = this.options || {};
613
- const jobName = options.name || this.name;
614
- const dispatcher = new JobDispatcher(jobName, payload);
615
- if (options.queue) {
616
- dispatcher.toQueue(options.queue);
617
- }
618
- if (options.adapter) {
619
- dispatcher.with(options.adapter);
620
- }
621
- if (options.priority !== void 0) {
622
- dispatcher.priority(options.priority);
623
- }
624
- return dispatcher;
625
- }
626
- /**
627
- * Dispatch multiple jobs to the queue in a single batch.
628
- *
629
- * Returns a JobBatchDispatcher for fluent configuration before dispatching.
630
- * The jobs are not actually dispatched until `.run()` is called or the
631
- * dispatcher is awaited.
632
- *
633
- * This is more efficient than calling `dispatch()` multiple times as it
634
- * uses batched operations (e.g., Redis pipeline, SQL batch insert).
635
- *
636
- * @param payloads - Array of data to pass to each job
637
- * @returns A JobBatchDispatcher for fluent configuration
638
- *
639
- * @example
640
- * ```typescript
641
- * // Batch dispatch for newsletter
642
- * const { jobIds } = await SendEmailJob.dispatchMany([
643
- * { to: 'user1@example.com', subject: 'Newsletter' },
644
- * { to: 'user2@example.com', subject: 'Newsletter' },
645
- * ])
646
- * .group('newsletter-jan-2025')
647
- * .toQueue('emails')
648
- * .run()
649
- *
650
- * console.log(`Dispatched ${jobIds.length} jobs`)
651
- * ```
652
- */
653
- static dispatchMany(payloads) {
654
- const options = this.options || {};
655
- const jobName = options.name || this.name;
656
- const dispatcher = new JobBatchDispatcher(jobName, payloads);
657
- if (options.queue) {
658
- dispatcher.toQueue(options.queue);
659
- }
660
- if (options.adapter) {
661
- dispatcher.with(options.adapter);
662
- }
663
- if (options.priority !== void 0) {
664
- dispatcher.priority(options.priority);
665
- }
666
- return dispatcher;
667
- }
668
- /**
669
- * Create a schedule for this job.
670
- *
671
- * Returns a ScheduleBuilder for fluent configuration before creating the schedule.
672
- * The schedule is not actually created until `.run()` is called or the
673
- * builder is awaited.
674
- *
675
- * @param payload - The data to pass to the job on each run
676
- * @returns A ScheduleBuilder for fluent configuration
677
- *
678
- * @example
679
- * ```typescript
680
- * // Cron schedule
681
- * await CleanupJob.schedule({ days: 30 })
682
- * .id('cleanup-daily')
683
- * .cron('0 0 * * *')
684
- * .timezone('Europe/Paris')
685
- * .run()
686
- *
687
- * // Interval schedule
688
- * await SyncJob.schedule({ source: 'api' })
689
- * .every('5m')
690
- * .run()
691
- * ```
692
- */
693
- static schedule(payload) {
694
- const options = this.options || {};
695
- const jobName = options.name || this.name;
696
- return new ScheduleBuilder(jobName, payload);
697
- }
698
- };
21
+ exceptions_exports,
22
+ parse
23
+ } from "./chunk-3BIR4IQD.js";
699
24
 
700
25
  // src/worker.ts
701
- import { randomUUID as randomUUID3 } from "crypto";
26
+ import { randomUUID } from "crypto";
702
27
  import { setTimeout } from "timers/promises";
703
28
 
704
29
  // src/job_pool.ts
@@ -804,7 +129,7 @@ var Worker = class {
804
129
  */
805
130
  constructor(config) {
806
131
  this.#config = config;
807
- this.#id = randomUUID3();
132
+ this.#id = randomUUID();
808
133
  this.#idleDelay = parse(config.worker?.idleDelay ?? DEFAULT_IDLE_DELAY);
809
134
  this.#stalledInterval = parse(config.worker?.stalledInterval ?? DEFAULT_STALLED_INTERVAL);
810
135
  this.#stalledThreshold = parse(config.worker?.stalledThreshold ?? DEFAULT_STALLED_THRESHOLD);
@@ -1020,7 +345,7 @@ var Worker = class {
1020
345
  mergedConfig.maxRetries
1021
346
  );
1022
347
  await this.#adapter.failJob(job.id, queue, e, retention.removeOnFail);
1023
- const exception = new E_JOB_MAX_ATTEMPTS_REACHED([job.name]);
348
+ const exception = new E_JOB_MAX_ATTEMPTS_REACHED([job.name], { cause: e });
1024
349
  await instance.failed?.(exception);
1025
350
  return;
1026
351
  }
@@ -1152,7 +477,7 @@ var Worker = class {
1152
477
  const JobClass = Locator.get(schedule.name);
1153
478
  const queue = JobClass?.options?.queue ?? "default";
1154
479
  await this.#adapter.pushOn(queue, {
1155
- id: randomUUID3(),
480
+ id: randomUUID(),
1156
481
  name: schedule.name,
1157
482
  payload: schedule.payload,
1158
483
  attempts: 0,