@boringnode/queue 0.3.1 → 0.3.2

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.
@@ -0,0 +1,1550 @@
1
+ import {
2
+ DEFAULT_PRIORITY,
3
+ E_ADAPTER_INIT_ERROR,
4
+ E_CONFIGURATION_ERROR,
5
+ E_INVALID_CRON_EXPRESSION,
6
+ E_INVALID_SCHEDULE_CONFIG,
7
+ E_JOB_NOT_FOUND,
8
+ E_QUEUE_NOT_INITIALIZED,
9
+ parse
10
+ } from "./chunk-3BIR4IQD.js";
11
+
12
+ // src/drivers/fake_adapter.ts
13
+ import assert from "assert/strict";
14
+ import { randomUUID as randomUUID3 } from "crypto";
15
+ import { isDeepStrictEqual } from "util";
16
+ import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
17
+
18
+ // src/debug.ts
19
+ import { debuglog } from "util";
20
+ var debug_default = debuglog("boringnode:queue");
21
+
22
+ // src/job_dispatcher.ts
23
+ import { randomUUID } from "crypto";
24
+
25
+ // src/locator.ts
26
+ import { glob } from "fs/promises";
27
+ import { resolve } from "path";
28
+ var LocatorSingleton = class {
29
+ #registry = /* @__PURE__ */ new Map();
30
+ /**
31
+ * Register a job class with a given name.
32
+ *
33
+ * @param name - The job name (usually the class name)
34
+ * @param JobClass - The job class constructor
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * Locator.register('SendEmailJob', SendEmailJob)
39
+ * ```
40
+ */
41
+ register(name, JobClass) {
42
+ debug_default("registering job: %s", name);
43
+ this.#registry.set(name, JobClass);
44
+ }
45
+ /**
46
+ * Auto-register job classes from files matching glob patterns.
47
+ *
48
+ * Each file should have a default export that is a Job class.
49
+ * The class name is used as the registration name.
50
+ *
51
+ * @param patterns - Glob patterns to match job files
52
+ * @returns Number of jobs successfully registered
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * const count = await Locator.registerFromGlob([
57
+ * './jobs/**\/*.js',
58
+ * './app/jobs/**\/*.ts'
59
+ * ])
60
+ * console.log(`Registered ${count} jobs`)
61
+ * ```
62
+ */
63
+ async registerFromGlob(patterns) {
64
+ let registered = 0;
65
+ for (const pattern of patterns) {
66
+ debug_default("registering jobs from glob pattern: %s", pattern);
67
+ for await (const file of glob(pattern)) {
68
+ debug_default("found job file: %s", file);
69
+ try {
70
+ const absolutePath = resolve(file);
71
+ const module = await import(`file://${absolutePath}`);
72
+ const JobClass = module.default;
73
+ if (JobClass && typeof JobClass === "function") {
74
+ const jobName = JobClass.options?.name || JobClass.name;
75
+ this.register(jobName, JobClass);
76
+ registered++;
77
+ }
78
+ } catch (error) {
79
+ console.warn(`Failed to load job from ${file}:`, error);
80
+ }
81
+ }
82
+ }
83
+ return registered;
84
+ }
85
+ /**
86
+ * Get a job class by name.
87
+ *
88
+ * @param name - The job name to look up
89
+ * @returns The job class, or undefined if not found
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * const JobClass = Locator.get('SendEmailJob')
94
+ * if (JobClass) {
95
+ * const instance = new JobClass(payload)
96
+ * }
97
+ * ```
98
+ */
99
+ get(name) {
100
+ return this.#registry.get(name);
101
+ }
102
+ /**
103
+ * Get a job class by name, throwing if not found.
104
+ *
105
+ * @param name - The job name to look up
106
+ * @returns The job class
107
+ * @throws {E_JOB_NOT_FOUND} If the job is not registered
108
+ *
109
+ * @example
110
+ * ```typescript
111
+ * const JobClass = Locator.getOrThrow('SendEmailJob')
112
+ * const instance = new JobClass(payload)
113
+ * ```
114
+ */
115
+ getOrThrow(name) {
116
+ const JobClass = this.get(name);
117
+ if (!JobClass) {
118
+ throw new E_JOB_NOT_FOUND([name]);
119
+ }
120
+ return JobClass;
121
+ }
122
+ /**
123
+ * Remove all registered jobs.
124
+ *
125
+ * Primarily useful for testing.
126
+ */
127
+ clear() {
128
+ this.#registry.clear();
129
+ }
130
+ };
131
+ var Locator = new LocatorSingleton();
132
+
133
+ // src/logger.ts
134
+ var ConsoleLogger = class _ConsoleLogger {
135
+ #prefix;
136
+ constructor(prefix = "queue") {
137
+ this.#prefix = prefix;
138
+ }
139
+ #format(level, msgOrObj, msg) {
140
+ const prefix = `[${this.#prefix}] ${level}:`;
141
+ if (typeof msgOrObj === "object") {
142
+ return [`${prefix} ${msg}`, msgOrObj];
143
+ }
144
+ return [`${prefix} ${msgOrObj}`];
145
+ }
146
+ trace(msgOrObj, msg) {
147
+ const [message, obj] = this.#format("TRACE", msgOrObj, msg);
148
+ if (obj) {
149
+ return console.log(message, obj);
150
+ }
151
+ console.log(message);
152
+ }
153
+ debug(msgOrObj, msg) {
154
+ const [message, obj] = this.#format("DEBUG", msgOrObj, msg);
155
+ if (obj) {
156
+ return console.log(message, obj);
157
+ }
158
+ console.log(message);
159
+ }
160
+ info(msgOrObj, msg) {
161
+ const [message, obj] = this.#format("INFO", msgOrObj, msg);
162
+ if (obj) {
163
+ return console.log(message, obj);
164
+ }
165
+ console.log(message);
166
+ }
167
+ warn(msgOrObj, msg) {
168
+ const [message, obj] = this.#format("WARN", msgOrObj, msg);
169
+ if (obj) {
170
+ return console.warn(message, obj);
171
+ }
172
+ console.warn(message);
173
+ }
174
+ error(msgOrObj, msg) {
175
+ const [message, obj] = this.#format("ERROR", msgOrObj, msg);
176
+ if (obj) {
177
+ return console.error(message, obj);
178
+ }
179
+ console.error(message);
180
+ }
181
+ child(obj) {
182
+ const childPrefix = obj.pkg ? String(obj.pkg) : this.#prefix;
183
+ return new _ConsoleLogger(childPrefix);
184
+ }
185
+ };
186
+ var consoleLogger = new ConsoleLogger();
187
+
188
+ // src/queue_manager.ts
189
+ var QueueManagerSingleton = class {
190
+ #initialized = false;
191
+ #defaultAdapter;
192
+ #adapters = {};
193
+ #adapterInstances = /* @__PURE__ */ new Map();
194
+ #globalRetryConfig;
195
+ #globalJobOptions;
196
+ #queueConfigs = /* @__PURE__ */ new Map();
197
+ #logger = consoleLogger;
198
+ #jobFactory;
199
+ #fakeState;
200
+ /**
201
+ * Initialize the queue system with the given configuration.
202
+ *
203
+ * This must be called before using the queue system. It:
204
+ * - Validates the configuration
205
+ * - Registers adapters
206
+ * - Auto-discovers and registers job classes from `locations`
207
+ *
208
+ * @param config - The queue configuration
209
+ * @returns This instance for chaining
210
+ * @throws {E_CONFIGURATION_ERROR} If the configuration is invalid
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * await QueueManager.init({
215
+ * default: 'redis',
216
+ * adapters: {
217
+ * redis: redis(),
218
+ * postgres: knex(pgConfig),
219
+ * },
220
+ * locations: ['./jobs/**\/*.js'],
221
+ * })
222
+ * ```
223
+ */
224
+ async init(config) {
225
+ debug_default("initializing queue manager with config: %O", config);
226
+ this.#validateConfig(config);
227
+ this.#adapterInstances.clear();
228
+ this.#defaultAdapter = config.default;
229
+ this.#adapters = config.adapters;
230
+ this.#globalRetryConfig = config.retry;
231
+ this.#globalJobOptions = config.defaultJobOptions;
232
+ this.#logger = config.logger ?? consoleLogger;
233
+ this.#jobFactory = config.jobFactory;
234
+ if (config.queues) {
235
+ for (const [queue, queueConfig] of Object.entries(config.queues)) {
236
+ this.#queueConfigs.set(queue, queueConfig);
237
+ }
238
+ }
239
+ if (config.locations && config.locations.length > 0) {
240
+ const registered = await Locator.registerFromGlob(config.locations);
241
+ if (registered === 0) {
242
+ this.#logger.warn(
243
+ `No jobs found for locations: ${config.locations.join(", ")}. Verify your glob patterns match your job files.`
244
+ );
245
+ }
246
+ }
247
+ this.#initialized = true;
248
+ return this;
249
+ }
250
+ /**
251
+ * Get an adapter instance by name.
252
+ *
253
+ * Adapter instances are cached and reused. If no name is provided,
254
+ * the default adapter is returned.
255
+ *
256
+ * @param adapter - Adapter name (optional, defaults to the default adapter)
257
+ * @returns The adapter instance
258
+ * @throws {E_QUEUE_NOT_INITIALIZED} If `init()` hasn't been called
259
+ * @throws {E_CONFIGURATION_ERROR} If the adapter is not registered
260
+ * @throws {E_ADAPTER_INIT_ERROR} If the adapter factory throws
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * // Get default adapter
265
+ * const adapter = QueueManager.use()
266
+ *
267
+ * // Get specific adapter
268
+ * const redisAdapter = QueueManager.use('redis')
269
+ * ```
270
+ */
271
+ use(adapter) {
272
+ if (!this.#initialized) {
273
+ throw new E_QUEUE_NOT_INITIALIZED();
274
+ }
275
+ if (!adapter) {
276
+ adapter = this.#defaultAdapter;
277
+ }
278
+ const cached = this.#adapterInstances.get(adapter);
279
+ if (cached) {
280
+ return cached;
281
+ }
282
+ const adapterFactory = this.#adapters[adapter];
283
+ if (!adapterFactory) {
284
+ throw new E_CONFIGURATION_ERROR([`Adapter "${adapter}" is not registered`]);
285
+ }
286
+ debug_default('using adapter "%s"', adapter);
287
+ try {
288
+ const instance = adapterFactory();
289
+ this.#adapterInstances.set(adapter, instance);
290
+ return instance;
291
+ } catch (error) {
292
+ const message = error instanceof Error ? error.message : String(error);
293
+ throw new E_ADAPTER_INIT_ERROR([adapter, message]);
294
+ }
295
+ }
296
+ /**
297
+ * Replace all adapters with a fake adapter for testing.
298
+ *
299
+ * The fake adapter records pushed jobs and exposes assertion helpers.
300
+ * Call `restore()` to return to the previous configuration.
301
+ *
302
+ * @returns The fake adapter instance for assertions
303
+ * @throws {E_QUEUE_NOT_INITIALIZED} If `init()` hasn't been called
304
+ *
305
+ * @example
306
+ * ```typescript
307
+ * const fake = QueueManager.fake()
308
+ *
309
+ * await SendEmailJob.dispatch({ to: 'user@example.com' })
310
+ *
311
+ * fake.assertPushed(SendEmailJob)
312
+ * QueueManager.restore()
313
+ * ```
314
+ */
315
+ fake() {
316
+ if (!this.#initialized) {
317
+ throw new E_QUEUE_NOT_INITIALIZED();
318
+ }
319
+ if (this.#fakeState) {
320
+ return this.#fakeState.fakeAdapter;
321
+ }
322
+ const fakeAdapter = new FakeAdapter();
323
+ this.#fakeState = {
324
+ defaultAdapter: this.#defaultAdapter,
325
+ adapters: this.#adapters,
326
+ adapterInstances: this.#adapterInstances,
327
+ globalRetryConfig: this.#globalRetryConfig,
328
+ globalJobOptions: this.#globalJobOptions,
329
+ queueConfigs: this.#queueConfigs,
330
+ logger: this.#logger,
331
+ jobFactory: this.#jobFactory,
332
+ fakeAdapter
333
+ };
334
+ const fakeFactory = () => fakeAdapter;
335
+ const nextAdapters = {};
336
+ for (const name of Object.keys(this.#fakeState.adapters)) {
337
+ nextAdapters[name] = fakeFactory;
338
+ }
339
+ this.#adapters = nextAdapters;
340
+ this.#adapterInstances = /* @__PURE__ */ new Map();
341
+ return fakeAdapter;
342
+ }
343
+ /**
344
+ * Restore adapters after calling `fake()`.
345
+ */
346
+ restore() {
347
+ if (!this.#fakeState) {
348
+ return;
349
+ }
350
+ void this.#fakeState.fakeAdapter.destroy();
351
+ for (const adapter of this.#adapterInstances.values()) {
352
+ void adapter.destroy();
353
+ }
354
+ const state = this.#fakeState;
355
+ this.#fakeState = void 0;
356
+ this.#defaultAdapter = state.defaultAdapter;
357
+ this.#adapters = state.adapters;
358
+ this.#adapterInstances = state.adapterInstances;
359
+ this.#globalRetryConfig = state.globalRetryConfig;
360
+ this.#globalJobOptions = state.globalJobOptions;
361
+ this.#queueConfigs = state.queueConfigs;
362
+ this.#logger = state.logger;
363
+ this.#jobFactory = state.jobFactory;
364
+ }
365
+ /**
366
+ * Get the merged retry configuration for a job.
367
+ *
368
+ * Configuration is merged with priority: job > queue > global.
369
+ * This allows specific jobs or queues to override global defaults.
370
+ *
371
+ * @param queue - The queue name
372
+ * @param jobRetryConfig - Optional job-level retry config
373
+ * @returns The merged retry configuration
374
+ *
375
+ * @example
376
+ * ```typescript
377
+ * // Global: maxRetries=3, Queue: maxRetries=5, Job: maxRetries=1
378
+ * // Result: maxRetries=1 (job wins)
379
+ * const config = QueueManager.getMergedRetryConfig('emails', { maxRetries: 1 })
380
+ * ```
381
+ */
382
+ getMergedRetryConfig(queue, jobRetryConfig) {
383
+ const queueConfig = this.#queueConfigs.get(queue);
384
+ const queueRetryConfig = queueConfig?.retry || {};
385
+ let maxRetries = jobRetryConfig?.maxRetries ?? queueRetryConfig.maxRetries ?? this.#globalRetryConfig?.maxRetries ?? 0;
386
+ let backoff = jobRetryConfig?.backoff || queueRetryConfig.backoff || this.#globalRetryConfig?.backoff;
387
+ return { maxRetries, backoff };
388
+ }
389
+ /**
390
+ * Get the configured job factory for custom instantiation.
391
+ *
392
+ * @returns The job factory function, or undefined if not configured
393
+ */
394
+ getJobFactory() {
395
+ return this.#jobFactory;
396
+ }
397
+ /**
398
+ * Get the merged job options for a job (priority: job > queue > global).
399
+ */
400
+ getMergedJobOptions(queue, jobOptions) {
401
+ const queueConfig = this.#queueConfigs.get(queue);
402
+ const queueJobOptions = queueConfig?.defaultJobOptions;
403
+ return {
404
+ removeOnComplete: jobOptions?.removeOnComplete ?? queueJobOptions?.removeOnComplete ?? this.#globalJobOptions?.removeOnComplete,
405
+ removeOnFail: jobOptions?.removeOnFail ?? queueJobOptions?.removeOnFail ?? this.#globalJobOptions?.removeOnFail
406
+ };
407
+ }
408
+ #validateConfig(config) {
409
+ if (!config.adapters || Object.keys(config.adapters).length === 0) {
410
+ throw new E_CONFIGURATION_ERROR(["At least one adapter must be configured"]);
411
+ }
412
+ if (!config.default) {
413
+ throw new E_CONFIGURATION_ERROR(["Default adapter must be specified"]);
414
+ }
415
+ if (!config.adapters[config.default]) {
416
+ throw new E_CONFIGURATION_ERROR([
417
+ `Default adapter "${config.default}" not found in adapters configuration`
418
+ ]);
419
+ }
420
+ for (const [name, factory] of Object.entries(config.adapters)) {
421
+ if (typeof factory !== "function") {
422
+ throw new E_CONFIGURATION_ERROR([`Adapter "${name}" must be a factory function`]);
423
+ }
424
+ }
425
+ }
426
+ /**
427
+ * Clean up all adapter instances and reset state.
428
+ *
429
+ * Call this when shutting down the application or when
430
+ * you need to reinitialize with a new configuration.
431
+ *
432
+ * @example
433
+ * ```typescript
434
+ * // On application shutdown
435
+ * await QueueManager.destroy()
436
+ * ```
437
+ */
438
+ async destroy() {
439
+ for (const [name, adapter] of this.#adapterInstances) {
440
+ debug_default('destroying adapter "%s"', name);
441
+ await adapter.destroy();
442
+ }
443
+ if (this.#fakeState) {
444
+ await this.#fakeState.fakeAdapter.destroy();
445
+ for (const [name, adapter] of this.#fakeState.adapterInstances) {
446
+ debug_default('destroying adapter "%s"', name);
447
+ await adapter.destroy();
448
+ }
449
+ }
450
+ this.#adapterInstances.clear();
451
+ this.#initialized = false;
452
+ this.#fakeState = void 0;
453
+ }
454
+ };
455
+ var QueueManager = new QueueManagerSingleton();
456
+
457
+ // src/job_dispatcher.ts
458
+ var JobDispatcher = class {
459
+ #name;
460
+ #payload;
461
+ #queue = "default";
462
+ #adapter;
463
+ #delay;
464
+ #priority;
465
+ #groupId;
466
+ /**
467
+ * Create a new job dispatcher.
468
+ *
469
+ * @param name - The job class name (used to locate the class at runtime)
470
+ * @param payload - The data to pass to the job
471
+ */
472
+ constructor(name, payload) {
473
+ this.#name = name;
474
+ this.#payload = payload;
475
+ }
476
+ /**
477
+ * Set the target queue for this job.
478
+ *
479
+ * @param queue - Queue name (default: 'default')
480
+ * @returns This dispatcher for chaining
481
+ *
482
+ * @example
483
+ * ```typescript
484
+ * await SendEmailJob.dispatch(payload).toQueue('emails')
485
+ * ```
486
+ */
487
+ toQueue(queue) {
488
+ this.#queue = queue;
489
+ return this;
490
+ }
491
+ /**
492
+ * Delay the job execution.
493
+ *
494
+ * The job will be stored in a delayed state and moved to pending
495
+ * after the delay expires.
496
+ *
497
+ * @param delay - Delay as milliseconds or duration string ('5s', '1h', '7d')
498
+ * @returns This dispatcher for chaining
499
+ *
500
+ * @example
501
+ * ```typescript
502
+ * // Send reminder in 24 hours
503
+ * await ReminderJob.dispatch(payload).in('24h')
504
+ *
505
+ * // Process in 5 minutes
506
+ * await CleanupJob.dispatch(payload).in('5m')
507
+ * ```
508
+ */
509
+ in(delay) {
510
+ this.#delay = delay;
511
+ return this;
512
+ }
513
+ /**
514
+ * Set the job priority.
515
+ *
516
+ * Lower numbers = higher priority. Jobs with lower priority values
517
+ * are processed before jobs with higher values.
518
+ *
519
+ * @param priority - Priority level (1-10, default: 5)
520
+ * @returns This dispatcher for chaining
521
+ *
522
+ * @example
523
+ * ```typescript
524
+ * // High priority job
525
+ * await UrgentJob.dispatch(payload).priority(1)
526
+ *
527
+ * // Low priority job
528
+ * await BackgroundJob.dispatch(payload).priority(10)
529
+ * ```
530
+ */
531
+ priority(priority) {
532
+ this.#priority = priority;
533
+ return this;
534
+ }
535
+ /**
536
+ * Assign this job to a group.
537
+ *
538
+ * Jobs with the same groupId can be filtered and displayed together
539
+ * in monitoring UIs. Useful for batch operations like newsletters
540
+ * or bulk exports.
541
+ *
542
+ * @param groupId - Group identifier
543
+ * @returns This dispatcher for chaining
544
+ *
545
+ * @example
546
+ * ```typescript
547
+ * // Group newsletter jobs together
548
+ * await SendEmailJob.dispatch({ to: 'user@example.com' })
549
+ * .group('newsletter-jan-2025')
550
+ * .run()
551
+ * ```
552
+ */
553
+ group(groupId) {
554
+ this.#groupId = groupId;
555
+ return this;
556
+ }
557
+ /**
558
+ * Use a specific adapter for this job.
559
+ *
560
+ * @param adapter - Adapter name or factory function
561
+ * @returns This dispatcher for chaining
562
+ *
563
+ * @example
564
+ * ```typescript
565
+ * // Use named adapter
566
+ * await Job.dispatch(payload).with('redis')
567
+ *
568
+ * // Use custom adapter instance
569
+ * await Job.dispatch(payload).with(() => new CustomAdapter())
570
+ * ```
571
+ */
572
+ with(adapter) {
573
+ this.#adapter = adapter;
574
+ return this;
575
+ }
576
+ /**
577
+ * Dispatch the job to the queue.
578
+ *
579
+ * @returns A DispatchResult containing the jobId
580
+ *
581
+ * @example
582
+ * ```typescript
583
+ * const { jobId } = await SendEmailJob.dispatch(payload).run()
584
+ * console.log(`Dispatched job: ${jobId}`)
585
+ * ```
586
+ */
587
+ async run() {
588
+ const id = randomUUID();
589
+ debug_default("dispatching job %s with id %s using payload %s", this.#name, id, this.#payload);
590
+ const adapter = this.#getAdapterInstance();
591
+ const payload = {
592
+ id,
593
+ name: this.#name,
594
+ payload: this.#payload,
595
+ attempts: 0,
596
+ priority: this.#priority,
597
+ groupId: this.#groupId
598
+ };
599
+ if (this.#delay) {
600
+ const parsedDelay = parse(this.#delay);
601
+ await adapter.pushLaterOn(this.#queue, payload, parsedDelay);
602
+ } else {
603
+ await adapter.pushOn(this.#queue, payload);
604
+ }
605
+ return {
606
+ jobId: id
607
+ };
608
+ }
609
+ /**
610
+ * Thenable implementation for auto-dispatch when awaited.
611
+ *
612
+ * Allows `await Job.dispatch(payload)` without explicit `.run()`.
613
+ *
614
+ * @param onFulfilled - Success callback
615
+ * @param onRejected - Error callback
616
+ * @returns Promise resolving to the DispatchResult
617
+ */
618
+ then(onFulfilled, onRejected) {
619
+ return this.run().then(onFulfilled, onRejected);
620
+ }
621
+ #getAdapterInstance() {
622
+ if (!this.#adapter) {
623
+ return QueueManager.use();
624
+ }
625
+ if (typeof this.#adapter === "string") {
626
+ return QueueManager.use(this.#adapter);
627
+ }
628
+ return this.#adapter();
629
+ }
630
+ };
631
+
632
+ // src/job_batch_dispatcher.ts
633
+ import { randomUUID as randomUUID2 } from "crypto";
634
+ var JobBatchDispatcher = class {
635
+ #name;
636
+ #payloads;
637
+ #queue = "default";
638
+ #adapter;
639
+ #priority;
640
+ #groupId;
641
+ /**
642
+ * Create a new batch job dispatcher.
643
+ *
644
+ * @param name - The job class name (used to locate the class at runtime)
645
+ * @param payloads - Array of data to pass to each job
646
+ */
647
+ constructor(name, payloads) {
648
+ this.#name = name;
649
+ this.#payloads = payloads;
650
+ }
651
+ /**
652
+ * Set the target queue for all jobs.
653
+ *
654
+ * @param queue - Queue name (default: 'default')
655
+ * @returns This dispatcher for chaining
656
+ *
657
+ * @example
658
+ * ```typescript
659
+ * await SendEmailJob.dispatchMany(payloads).toQueue('emails')
660
+ * ```
661
+ */
662
+ toQueue(queue) {
663
+ this.#queue = queue;
664
+ return this;
665
+ }
666
+ /**
667
+ * Set the priority for all jobs.
668
+ *
669
+ * Lower numbers = higher priority. Jobs with lower priority values
670
+ * are processed before jobs with higher values.
671
+ *
672
+ * @param priority - Priority level (1-10, default: 5)
673
+ * @returns This dispatcher for chaining
674
+ *
675
+ * @example
676
+ * ```typescript
677
+ * await UrgentJob.dispatchMany(payloads).priority(1)
678
+ * ```
679
+ */
680
+ priority(priority) {
681
+ this.#priority = priority;
682
+ return this;
683
+ }
684
+ /**
685
+ * Assign all jobs to a group.
686
+ *
687
+ * Jobs with the same groupId can be filtered and displayed together
688
+ * in monitoring UIs. Useful for batch operations like newsletters
689
+ * or bulk exports.
690
+ *
691
+ * @param groupId - Group identifier
692
+ * @returns This dispatcher for chaining
693
+ *
694
+ * @example
695
+ * ```typescript
696
+ * await SendEmailJob.dispatchMany(recipients)
697
+ * .group('newsletter-jan-2025')
698
+ * .run()
699
+ * ```
700
+ */
701
+ group(groupId) {
702
+ this.#groupId = groupId;
703
+ return this;
704
+ }
705
+ /**
706
+ * Use a specific adapter for these jobs.
707
+ *
708
+ * @param adapter - Adapter name or factory function
709
+ * @returns This dispatcher for chaining
710
+ *
711
+ * @example
712
+ * ```typescript
713
+ * await Job.dispatchMany(payloads).with('redis')
714
+ * ```
715
+ */
716
+ with(adapter) {
717
+ this.#adapter = adapter;
718
+ return this;
719
+ }
720
+ /**
721
+ * Dispatch all jobs to the queue.
722
+ *
723
+ * @returns A DispatchManyResult containing all jobIds
724
+ *
725
+ * @example
726
+ * ```typescript
727
+ * const { jobIds } = await SendEmailJob.dispatchMany(payloads).run()
728
+ * console.log(`Dispatched ${jobIds.length} jobs`)
729
+ * ```
730
+ */
731
+ async run() {
732
+ debug_default("dispatching %d jobs of type %s", this.#payloads.length, this.#name);
733
+ const adapter = this.#getAdapterInstance();
734
+ const jobs = this.#payloads.map((payload) => ({
735
+ id: randomUUID2(),
736
+ name: this.#name,
737
+ payload,
738
+ attempts: 0,
739
+ priority: this.#priority,
740
+ groupId: this.#groupId
741
+ }));
742
+ await adapter.pushManyOn(this.#queue, jobs);
743
+ return {
744
+ jobIds: jobs.map((job) => job.id)
745
+ };
746
+ }
747
+ /**
748
+ * Thenable implementation for auto-dispatch when awaited.
749
+ *
750
+ * Allows `await Job.dispatchMany(payloads)` without explicit `.run()`.
751
+ *
752
+ * @param onFulfilled - Success callback
753
+ * @param onRejected - Error callback
754
+ * @returns Promise resolving to the DispatchManyResult
755
+ */
756
+ then(onFulfilled, onRejected) {
757
+ return this.run().then(onFulfilled, onRejected);
758
+ }
759
+ #getAdapterInstance() {
760
+ if (!this.#adapter) {
761
+ return QueueManager.use();
762
+ }
763
+ if (typeof this.#adapter === "string") {
764
+ return QueueManager.use(this.#adapter);
765
+ }
766
+ return this.#adapter();
767
+ }
768
+ };
769
+
770
+ // src/schedule_builder.ts
771
+ import { CronExpressionParser } from "cron-parser";
772
+ var ScheduleBuilder = class {
773
+ #name;
774
+ #payload;
775
+ #id;
776
+ #cronExpression;
777
+ #everyMs;
778
+ #timezone = "UTC";
779
+ #from;
780
+ #to;
781
+ #limit;
782
+ constructor(name, payload) {
783
+ this.#name = name;
784
+ this.#payload = payload;
785
+ }
786
+ /**
787
+ * Set a custom schedule ID.
788
+ * If not specified, defaults to the job name.
789
+ * If a schedule with this ID exists, it will be updated (upsert).
790
+ */
791
+ id(scheduleId) {
792
+ this.#id = scheduleId;
793
+ return this;
794
+ }
795
+ /**
796
+ * Set a cron expression for the schedule.
797
+ * Mutually exclusive with `every()`.
798
+ */
799
+ cron(expression) {
800
+ this.#cronExpression = expression;
801
+ return this;
802
+ }
803
+ /**
804
+ * Set a repeating interval for the schedule.
805
+ * Mutually exclusive with `cron()`.
806
+ */
807
+ every(interval) {
808
+ this.#everyMs = parse(interval);
809
+ return this;
810
+ }
811
+ /**
812
+ * Set the timezone for cron evaluation.
813
+ * @default 'UTC'
814
+ */
815
+ timezone(tz) {
816
+ this.#timezone = tz;
817
+ return this;
818
+ }
819
+ /**
820
+ * Set the start boundary for the schedule.
821
+ * No jobs will be dispatched before this date.
822
+ */
823
+ from(date) {
824
+ this.#from = date;
825
+ return this;
826
+ }
827
+ /**
828
+ * Set the end boundary for the schedule.
829
+ * No jobs will be dispatched after this date.
830
+ */
831
+ to(date) {
832
+ this.#to = date;
833
+ return this;
834
+ }
835
+ /**
836
+ * Set both start and end boundaries for the schedule.
837
+ * Shorthand for `.from(start).to(end)`.
838
+ */
839
+ between(from, to) {
840
+ return this.from(from).to(to);
841
+ }
842
+ /**
843
+ * Set the maximum number of runs for this schedule.
844
+ */
845
+ limit(maxRuns) {
846
+ this.#limit = maxRuns;
847
+ return this;
848
+ }
849
+ /**
850
+ * Create the schedule and return the schedule ID.
851
+ */
852
+ async run() {
853
+ if (!this.#cronExpression && !this.#everyMs) {
854
+ throw new E_INVALID_SCHEDULE_CONFIG([
855
+ "Schedule must have either a cron expression or an interval"
856
+ ]);
857
+ }
858
+ if (this.#cronExpression && this.#everyMs) {
859
+ throw new E_INVALID_SCHEDULE_CONFIG([
860
+ "Schedule cannot have both a cron expression and an interval"
861
+ ]);
862
+ }
863
+ if (this.#cronExpression) {
864
+ try {
865
+ CronExpressionParser.parse(this.#cronExpression, { tz: this.#timezone });
866
+ } catch (error) {
867
+ throw new E_INVALID_CRON_EXPRESSION([this.#cronExpression, error.message]);
868
+ }
869
+ }
870
+ const config = {
871
+ id: this.#id ?? this.#name,
872
+ name: this.#name,
873
+ payload: this.#payload,
874
+ cronExpression: this.#cronExpression,
875
+ everyMs: this.#everyMs,
876
+ timezone: this.#timezone,
877
+ from: this.#from,
878
+ to: this.#to,
879
+ limit: this.#limit
880
+ };
881
+ const adapter = QueueManager.use();
882
+ const scheduleId = await adapter.createSchedule(config);
883
+ const nextRunAt = this.#calculateNextRunAt();
884
+ await adapter.updateSchedule(scheduleId, { nextRunAt });
885
+ return { scheduleId };
886
+ }
887
+ /**
888
+ * Calculate the next run time based on cron or interval.
889
+ */
890
+ #calculateNextRunAt() {
891
+ const now = /* @__PURE__ */ new Date();
892
+ let nextRun;
893
+ if (this.#cronExpression) {
894
+ const cron = CronExpressionParser.parse(this.#cronExpression, {
895
+ currentDate: now,
896
+ tz: this.#timezone
897
+ });
898
+ nextRun = cron.next().toDate();
899
+ } else {
900
+ nextRun = new Date(now.getTime() + this.#everyMs);
901
+ }
902
+ if (this.#from && nextRun < this.#from) {
903
+ if (this.#cronExpression) {
904
+ const cron = CronExpressionParser.parse(this.#cronExpression, {
905
+ currentDate: this.#from,
906
+ tz: this.#timezone
907
+ });
908
+ nextRun = cron.next().toDate();
909
+ } else {
910
+ nextRun = this.#from;
911
+ }
912
+ }
913
+ return nextRun;
914
+ }
915
+ /**
916
+ * Implement PromiseLike to allow `await builder.every('5m')` syntax.
917
+ */
918
+ then(onfulfilled, onrejected) {
919
+ return this.run().then(onfulfilled, onrejected);
920
+ }
921
+ };
922
+
923
+ // src/job.ts
924
+ var Job = class {
925
+ #payload;
926
+ #context;
927
+ #signal;
928
+ /**
929
+ * Static options for this job class.
930
+ *
931
+ * Override this property in subclasses to configure job behavior
932
+ * such as queue name, retry policy, timeout, and more.
933
+ *
934
+ * @example
935
+ * ```typescript
936
+ * class SendEmailJob extends Job<SendEmailPayload> {
937
+ * static options = {
938
+ * queue: 'emails',
939
+ * maxRetries: 3,
940
+ * timeout: '30s',
941
+ * }
942
+ * }
943
+ * ```
944
+ */
945
+ static options = {};
946
+ /**
947
+ * The payload data passed to this job instance.
948
+ *
949
+ * Contains the data provided when the job was dispatched.
950
+ * Available after the job has been hydrated by the worker.
951
+ *
952
+ * @example
953
+ * ```typescript
954
+ * async execute() {
955
+ * const { to, subject, body } = this.payload
956
+ * await sendEmail(to, subject, body)
957
+ * }
958
+ * ```
959
+ */
960
+ get payload() {
961
+ return this.#payload;
962
+ }
963
+ /**
964
+ * Context information for the current job execution.
965
+ *
966
+ * Provides metadata such as job ID, current attempt number,
967
+ * queue name, priority, and timing information.
968
+ *
969
+ * @example
970
+ * ```typescript
971
+ * async execute() {
972
+ * if (this.context.attempt > 1) {
973
+ * console.log(`Retry attempt ${this.context.attempt}`)
974
+ * }
975
+ * console.log(`Processing job ${this.context.jobId} on queue ${this.context.queue}`)
976
+ * }
977
+ * ```
978
+ */
979
+ get context() {
980
+ return this.#context;
981
+ }
982
+ /**
983
+ * The abort signal for timeout handling.
984
+ *
985
+ * Check `signal.aborted` in long-running operations to handle timeouts gracefully.
986
+ *
987
+ * @example
988
+ * ```typescript
989
+ * async execute() {
990
+ * for (const item of this.payload.items) {
991
+ * if (this.signal?.aborted) {
992
+ * throw new Error('Job timed out')
993
+ * }
994
+ * await processItem(item)
995
+ * }
996
+ * }
997
+ * ```
998
+ */
999
+ get signal() {
1000
+ return this.#signal;
1001
+ }
1002
+ /**
1003
+ * Hydrate the job with payload, context, and optional abort signal.
1004
+ *
1005
+ * This method is called by the worker after instantiation to provide
1006
+ * the job's runtime data. It should not be called directly by user code.
1007
+ *
1008
+ * @param payload - The data to be processed by this job
1009
+ * @param context - The job execution context
1010
+ * @param signal - Optional abort signal for timeout handling
1011
+ *
1012
+ * @internal
1013
+ */
1014
+ $hydrate(payload, context, signal) {
1015
+ this.#payload = payload;
1016
+ this.#context = Object.freeze(context);
1017
+ this.#signal = signal;
1018
+ }
1019
+ /**
1020
+ * Dispatch this job to the queue.
1021
+ *
1022
+ * Returns a JobDispatcher for fluent configuration before dispatching.
1023
+ * The job is not actually dispatched until `.run()` is called or the
1024
+ * dispatcher is awaited.
1025
+ *
1026
+ * @param payload - The data to pass to the job
1027
+ * @returns A JobDispatcher for fluent configuration
1028
+ *
1029
+ * @example
1030
+ * ```typescript
1031
+ * // Simple dispatch
1032
+ * await SendEmailJob.dispatch({ to: 'user@example.com', subject: 'Hello' })
1033
+ *
1034
+ * // With options
1035
+ * await SendEmailJob.dispatch({ to: 'user@example.com' })
1036
+ * .toQueue('high-priority')
1037
+ * .priority(1)
1038
+ * .in('5m')
1039
+ * .run()
1040
+ * ```
1041
+ */
1042
+ static dispatch(payload) {
1043
+ const options = this.options || {};
1044
+ const jobName = options.name || this.name;
1045
+ const dispatcher = new JobDispatcher(jobName, payload);
1046
+ if (options.queue) {
1047
+ dispatcher.toQueue(options.queue);
1048
+ }
1049
+ if (options.adapter) {
1050
+ dispatcher.with(options.adapter);
1051
+ }
1052
+ if (options.priority !== void 0) {
1053
+ dispatcher.priority(options.priority);
1054
+ }
1055
+ return dispatcher;
1056
+ }
1057
+ /**
1058
+ * Dispatch multiple jobs to the queue in a single batch.
1059
+ *
1060
+ * Returns a JobBatchDispatcher for fluent configuration before dispatching.
1061
+ * The jobs are not actually dispatched until `.run()` is called or the
1062
+ * dispatcher is awaited.
1063
+ *
1064
+ * This is more efficient than calling `dispatch()` multiple times as it
1065
+ * uses batched operations (e.g., Redis pipeline, SQL batch insert).
1066
+ *
1067
+ * @param payloads - Array of data to pass to each job
1068
+ * @returns A JobBatchDispatcher for fluent configuration
1069
+ *
1070
+ * @example
1071
+ * ```typescript
1072
+ * // Batch dispatch for newsletter
1073
+ * const { jobIds } = await SendEmailJob.dispatchMany([
1074
+ * { to: 'user1@example.com', subject: 'Newsletter' },
1075
+ * { to: 'user2@example.com', subject: 'Newsletter' },
1076
+ * ])
1077
+ * .group('newsletter-jan-2025')
1078
+ * .toQueue('emails')
1079
+ * .run()
1080
+ *
1081
+ * console.log(`Dispatched ${jobIds.length} jobs`)
1082
+ * ```
1083
+ */
1084
+ static dispatchMany(payloads) {
1085
+ const options = this.options || {};
1086
+ const jobName = options.name || this.name;
1087
+ const dispatcher = new JobBatchDispatcher(jobName, payloads);
1088
+ if (options.queue) {
1089
+ dispatcher.toQueue(options.queue);
1090
+ }
1091
+ if (options.adapter) {
1092
+ dispatcher.with(options.adapter);
1093
+ }
1094
+ if (options.priority !== void 0) {
1095
+ dispatcher.priority(options.priority);
1096
+ }
1097
+ return dispatcher;
1098
+ }
1099
+ /**
1100
+ * Create a schedule for this job.
1101
+ *
1102
+ * Returns a ScheduleBuilder for fluent configuration before creating the schedule.
1103
+ * The schedule is not actually created until `.run()` is called or the
1104
+ * builder is awaited.
1105
+ *
1106
+ * @param payload - The data to pass to the job on each run
1107
+ * @returns A ScheduleBuilder for fluent configuration
1108
+ *
1109
+ * @example
1110
+ * ```typescript
1111
+ * // Cron schedule
1112
+ * await CleanupJob.schedule({ days: 30 })
1113
+ * .id('cleanup-daily')
1114
+ * .cron('0 0 * * *')
1115
+ * .timezone('Europe/Paris')
1116
+ * .run()
1117
+ *
1118
+ * // Interval schedule
1119
+ * await SyncJob.schedule({ source: 'api' })
1120
+ * .every('5m')
1121
+ * .run()
1122
+ * ```
1123
+ */
1124
+ static schedule(payload) {
1125
+ const options = this.options || {};
1126
+ const jobName = options.name || this.name;
1127
+ return new ScheduleBuilder(jobName, payload);
1128
+ }
1129
+ };
1130
+
1131
+ // src/drivers/fake_adapter.ts
1132
+ function fake() {
1133
+ return () => new FakeAdapter();
1134
+ }
1135
+ var FakeAdapter = class {
1136
+ #queues = /* @__PURE__ */ new Map();
1137
+ #activeJobs = /* @__PURE__ */ new Map();
1138
+ #delayedJobs = /* @__PURE__ */ new Map();
1139
+ #completedJobs = /* @__PURE__ */ new Map();
1140
+ #failedJobs = /* @__PURE__ */ new Map();
1141
+ #pendingTimeouts = /* @__PURE__ */ new Set();
1142
+ #schedules = /* @__PURE__ */ new Map();
1143
+ #pushedJobs = [];
1144
+ setWorkerId(_workerId) {
1145
+ }
1146
+ getPushedJobs() {
1147
+ return [...this.#pushedJobs];
1148
+ }
1149
+ getPushedJobsOn(queue) {
1150
+ return this.#pushedJobs.filter((record) => record.queue === queue);
1151
+ }
1152
+ findPushed(matcher, query) {
1153
+ return this.#pushedJobs.find((record) => this.#matchesRecord(record, matcher, query));
1154
+ }
1155
+ clearPushedJobs() {
1156
+ this.#pushedJobs = [];
1157
+ }
1158
+ clear() {
1159
+ for (const timeout of this.#pendingTimeouts) {
1160
+ clearTimeout(timeout);
1161
+ }
1162
+ this.#pendingTimeouts.clear();
1163
+ this.#queues.clear();
1164
+ this.#activeJobs.clear();
1165
+ this.#delayedJobs.clear();
1166
+ this.#completedJobs.clear();
1167
+ this.#failedJobs.clear();
1168
+ this.#schedules.clear();
1169
+ this.#pushedJobs = [];
1170
+ }
1171
+ assertPushed(matcher, query) {
1172
+ const record = this.findPushed(matcher, query);
1173
+ assert.ok(record, this.#formatFailure("Expected job to be pushed", matcher, query));
1174
+ }
1175
+ assertNotPushed(matcher, query) {
1176
+ const record = this.findPushed(matcher, query);
1177
+ assert.ok(!record, this.#formatFailure("Expected job to not be pushed", matcher, query));
1178
+ }
1179
+ assertPushedCount(count, options) {
1180
+ const actual = options?.queue ? this.#pushedJobs.filter((record) => record.queue === options.queue).length : this.#pushedJobs.length;
1181
+ const suffix = options?.queue ? ` on "${options.queue}"` : "";
1182
+ assert.equal(actual, count, `Expected ${count} pushed job(s)${suffix}, got ${actual}`);
1183
+ }
1184
+ assertNothingPushed() {
1185
+ assert.equal(
1186
+ this.#pushedJobs.length,
1187
+ 0,
1188
+ `Expected no jobs to be pushed, got ${this.#pushedJobs.length}`
1189
+ );
1190
+ }
1191
+ async size() {
1192
+ return this.sizeOf("default");
1193
+ }
1194
+ async sizeOf(queue) {
1195
+ const jobs = this.#queues.get(queue) || [];
1196
+ return jobs.length;
1197
+ }
1198
+ async push(jobData) {
1199
+ return this.pushOn("default", jobData);
1200
+ }
1201
+ async pushOn(queue, jobData) {
1202
+ this.#recordPush(queue, jobData);
1203
+ this.#enqueue(queue, jobData);
1204
+ }
1205
+ async pushLater(jobData, delay) {
1206
+ return this.pushLaterOn("default", jobData, delay);
1207
+ }
1208
+ pushLaterOn(queue, jobData, delay) {
1209
+ this.#recordPush(queue, jobData, delay);
1210
+ this.#schedulePush(queue, jobData, delay);
1211
+ return Promise.resolve();
1212
+ }
1213
+ async pushMany(jobs) {
1214
+ return this.pushManyOn("default", jobs);
1215
+ }
1216
+ async pushManyOn(queue, jobs) {
1217
+ for (const job of jobs) {
1218
+ await this.pushOn(queue, job);
1219
+ }
1220
+ }
1221
+ async pop() {
1222
+ return this.popFrom("default");
1223
+ }
1224
+ async popFrom(queue) {
1225
+ const jobs = this.#queues.get(queue);
1226
+ if (!jobs || jobs.length === 0) {
1227
+ return null;
1228
+ }
1229
+ let bestIndex = 0;
1230
+ let bestPriority = jobs[0].priority ?? DEFAULT_PRIORITY;
1231
+ for (let i = 1; i < jobs.length; i++) {
1232
+ const priority = jobs[i].priority ?? DEFAULT_PRIORITY;
1233
+ if (priority < bestPriority) {
1234
+ bestPriority = priority;
1235
+ bestIndex = i;
1236
+ }
1237
+ }
1238
+ const [job] = jobs.splice(bestIndex, 1);
1239
+ if (!job) {
1240
+ return null;
1241
+ }
1242
+ const acquiredAt = Date.now();
1243
+ this.#activeJobs.set(job.id, { job, acquiredAt, queue });
1244
+ return { ...job, acquiredAt };
1245
+ }
1246
+ async completeJob(jobId, queue, removeOnComplete) {
1247
+ const active = this.#activeJobs.get(jobId);
1248
+ if (!active) return;
1249
+ this.#activeJobs.delete(jobId);
1250
+ if (removeOnComplete === void 0 || removeOnComplete === true) {
1251
+ return;
1252
+ }
1253
+ this.#storeHistory(queue, "completed", active.job, removeOnComplete);
1254
+ }
1255
+ async failJob(jobId, queue, error, removeOnFail) {
1256
+ const active = this.#activeJobs.get(jobId);
1257
+ if (!active) return;
1258
+ this.#activeJobs.delete(jobId);
1259
+ if (removeOnFail === void 0 || removeOnFail === true) {
1260
+ return;
1261
+ }
1262
+ this.#storeHistory(queue, "failed", active.job, removeOnFail, error);
1263
+ }
1264
+ async retryJob(jobId, queue, retryAt) {
1265
+ const active = this.#activeJobs.get(jobId);
1266
+ if (!active) return;
1267
+ this.#activeJobs.delete(jobId);
1268
+ const updatedJob = {
1269
+ ...active.job,
1270
+ attempts: (active.job.attempts || 0) + 1
1271
+ };
1272
+ if (retryAt) {
1273
+ const delay = retryAt.getTime() - Date.now();
1274
+ if (delay > 0) {
1275
+ this.#schedulePush(queue, updatedJob, delay);
1276
+ return;
1277
+ }
1278
+ }
1279
+ this.#enqueue(queue, updatedJob);
1280
+ }
1281
+ async recoverStalledJobs(queue, stalledThreshold, maxStalledCount) {
1282
+ const now = Date.now();
1283
+ let recovered = 0;
1284
+ for (const [jobId, active] of this.#activeJobs.entries()) {
1285
+ if (active.queue !== queue) {
1286
+ continue;
1287
+ }
1288
+ const isStalled = now - active.acquiredAt > stalledThreshold;
1289
+ if (!isStalled) {
1290
+ continue;
1291
+ }
1292
+ const currentStalledCount = active.job.stalledCount ?? 0;
1293
+ if (currentStalledCount >= maxStalledCount) {
1294
+ this.#activeJobs.delete(jobId);
1295
+ continue;
1296
+ }
1297
+ this.#activeJobs.delete(jobId);
1298
+ const updatedJob = {
1299
+ ...active.job,
1300
+ stalledCount: currentStalledCount + 1
1301
+ };
1302
+ this.#enqueue(active.queue, updatedJob);
1303
+ recovered++;
1304
+ }
1305
+ return recovered;
1306
+ }
1307
+ async getJob(jobId, queue) {
1308
+ const active = this.#activeJobs.get(jobId);
1309
+ if (active && active.queue === queue) {
1310
+ return { status: "active", data: active.job };
1311
+ }
1312
+ const pendingJobs = this.#queues.get(queue);
1313
+ const pending = pendingJobs?.find((job) => job.id === jobId);
1314
+ if (pending) {
1315
+ return { status: "pending", data: pending };
1316
+ }
1317
+ const delayed = this.#delayedJobs.get(queue)?.get(jobId);
1318
+ if (delayed) {
1319
+ return { status: "delayed", data: delayed.job };
1320
+ }
1321
+ const completed = this.#findHistory(this.#completedJobs, queue, jobId);
1322
+ if (completed) {
1323
+ return completed;
1324
+ }
1325
+ const failed = this.#findHistory(this.#failedJobs, queue, jobId);
1326
+ if (failed) {
1327
+ return failed;
1328
+ }
1329
+ return null;
1330
+ }
1331
+ destroy() {
1332
+ for (const timeout of this.#pendingTimeouts) {
1333
+ clearTimeout(timeout);
1334
+ }
1335
+ this.#pendingTimeouts.clear();
1336
+ return Promise.resolve();
1337
+ }
1338
+ async createSchedule(config) {
1339
+ const id = config.id ?? randomUUID3();
1340
+ const now = /* @__PURE__ */ new Date();
1341
+ const schedule = {
1342
+ id,
1343
+ name: config.name,
1344
+ payload: config.payload,
1345
+ cronExpression: config.cronExpression ?? null,
1346
+ everyMs: config.everyMs ?? null,
1347
+ timezone: config.timezone,
1348
+ from: config.from ?? null,
1349
+ to: config.to ?? null,
1350
+ limit: config.limit ?? null,
1351
+ runCount: 0,
1352
+ nextRunAt: null,
1353
+ // Will be calculated by the caller
1354
+ lastRunAt: null,
1355
+ status: "active",
1356
+ createdAt: now
1357
+ };
1358
+ this.#schedules.set(id, schedule);
1359
+ return id;
1360
+ }
1361
+ async getSchedule(id) {
1362
+ return this.#schedules.get(id) ?? null;
1363
+ }
1364
+ async listSchedules(options) {
1365
+ const schedules = Array.from(this.#schedules.values());
1366
+ if (options?.status) {
1367
+ return schedules.filter((s) => s.status === options.status);
1368
+ }
1369
+ return schedules;
1370
+ }
1371
+ async updateSchedule(id, updates) {
1372
+ const schedule = this.#schedules.get(id);
1373
+ if (!schedule) return;
1374
+ if (updates.status !== void 0) schedule.status = updates.status;
1375
+ if (updates.nextRunAt !== void 0) schedule.nextRunAt = updates.nextRunAt;
1376
+ if (updates.lastRunAt !== void 0) schedule.lastRunAt = updates.lastRunAt;
1377
+ if (updates.runCount !== void 0) schedule.runCount = updates.runCount;
1378
+ }
1379
+ async deleteSchedule(id) {
1380
+ this.#schedules.delete(id);
1381
+ }
1382
+ async claimDueSchedule() {
1383
+ const now = /* @__PURE__ */ new Date();
1384
+ const schedule = Array.from(this.#schedules.values()).find((s) => {
1385
+ if (s.status !== "active") return false;
1386
+ if (s.nextRunAt === null || s.nextRunAt > now) return false;
1387
+ if (s.limit !== null && s.runCount >= s.limit) return false;
1388
+ if (s.to !== null && now > s.to) return false;
1389
+ return true;
1390
+ });
1391
+ if (!schedule) return null;
1392
+ let nextRunAt = null;
1393
+ if (schedule.everyMs) {
1394
+ nextRunAt = new Date(now.getTime() + schedule.everyMs);
1395
+ } else if (schedule.cronExpression) {
1396
+ const cron = CronExpressionParser2.parse(schedule.cronExpression, {
1397
+ currentDate: now,
1398
+ tz: schedule.timezone || "UTC"
1399
+ });
1400
+ nextRunAt = cron.next().toDate();
1401
+ }
1402
+ const newRunCount = schedule.runCount + 1;
1403
+ if (schedule.limit !== null && newRunCount >= schedule.limit) {
1404
+ nextRunAt = null;
1405
+ }
1406
+ if (nextRunAt && schedule.to !== null && nextRunAt > schedule.to) {
1407
+ nextRunAt = null;
1408
+ }
1409
+ const claimedSchedule = { ...schedule };
1410
+ schedule.nextRunAt = nextRunAt;
1411
+ schedule.lastRunAt = now;
1412
+ schedule.runCount = newRunCount;
1413
+ return claimedSchedule;
1414
+ }
1415
+ #recordPush(queue, jobData, delay) {
1416
+ this.#pushedJobs.push({
1417
+ queue,
1418
+ job: jobData,
1419
+ delay,
1420
+ pushedAt: Date.now()
1421
+ });
1422
+ }
1423
+ #enqueue(queue, jobData) {
1424
+ if (!this.#queues.has(queue)) {
1425
+ this.#queues.set(queue, []);
1426
+ }
1427
+ this.#queues.get(queue).push(jobData);
1428
+ }
1429
+ #schedulePush(queue, jobData, delay) {
1430
+ if (!this.#delayedJobs.has(queue)) {
1431
+ this.#delayedJobs.set(queue, /* @__PURE__ */ new Map());
1432
+ }
1433
+ const executeAt = Date.now() + delay;
1434
+ this.#delayedJobs.get(queue).set(jobData.id, { job: jobData, executeAt, delay });
1435
+ const timeout = setTimeout(() => {
1436
+ this.#pendingTimeouts.delete(timeout);
1437
+ this.#delayedJobs.get(queue)?.delete(jobData.id);
1438
+ this.#enqueue(queue, jobData);
1439
+ }, delay);
1440
+ this.#pendingTimeouts.add(timeout);
1441
+ }
1442
+ #storeHistory(queue, status, job, retention, error) {
1443
+ const record = {
1444
+ status,
1445
+ data: job,
1446
+ finishedAt: Date.now(),
1447
+ error: error?.message
1448
+ };
1449
+ const store = status === "completed" ? this.#completedJobs : this.#failedJobs;
1450
+ if (!store.has(queue)) {
1451
+ store.set(queue, []);
1452
+ }
1453
+ const records = store.get(queue);
1454
+ records.push(record);
1455
+ if (retention && retention !== true) {
1456
+ this.#applyRetention(records, retention);
1457
+ }
1458
+ }
1459
+ #applyRetention(records, retention) {
1460
+ if (retention === false || retention === true) {
1461
+ return;
1462
+ }
1463
+ if (retention.age !== void 0) {
1464
+ const maxAgeMs = parse(retention.age);
1465
+ if (maxAgeMs > 0) {
1466
+ const cutoff = Date.now() - maxAgeMs;
1467
+ const filtered = records.filter((record) => (record.finishedAt ?? 0) >= cutoff);
1468
+ records.splice(0, records.length, ...filtered);
1469
+ }
1470
+ }
1471
+ if (retention.count !== void 0 && retention.count > 0 && records.length > retention.count) {
1472
+ records.splice(0, records.length - retention.count);
1473
+ }
1474
+ }
1475
+ #findHistory(store, queue, jobId) {
1476
+ const records = store.get(queue);
1477
+ if (!records) return null;
1478
+ return records.find((record) => record.data.id === jobId) ?? null;
1479
+ }
1480
+ #matchesRecord(record, matcher, query) {
1481
+ if (query?.queue && record.queue !== query.queue) {
1482
+ return false;
1483
+ }
1484
+ const matchesJob = typeof matcher === "string" ? record.job.name === matcher : this.#isJobClass(matcher) ? record.job.name === this.#getJobClassName(matcher) : matcher(record.job);
1485
+ if (!matchesJob) {
1486
+ return false;
1487
+ }
1488
+ if (query?.payload !== void 0) {
1489
+ const payloadMatcher = query.payload;
1490
+ const matchesPayload = typeof payloadMatcher === "function" ? payloadMatcher(record.job.payload) : isDeepStrictEqual(record.job.payload, payloadMatcher);
1491
+ if (!matchesPayload) {
1492
+ return false;
1493
+ }
1494
+ }
1495
+ if (query?.delay !== void 0) {
1496
+ const delayMatcher = query.delay;
1497
+ const matchesDelay = typeof delayMatcher === "function" ? delayMatcher(record.delay) : record.delay === delayMatcher;
1498
+ if (!matchesDelay) {
1499
+ return false;
1500
+ }
1501
+ }
1502
+ return true;
1503
+ }
1504
+ #formatFailure(prefix, matcher, query) {
1505
+ const parts = [prefix];
1506
+ const matcherName = this.#getMatcherName(matcher);
1507
+ if (matcherName) {
1508
+ parts.push(`for "${matcherName}"`);
1509
+ }
1510
+ if (query?.queue) {
1511
+ parts.push(`on "${query.queue}"`);
1512
+ }
1513
+ if (query?.payload !== void 0) {
1514
+ parts.push("with matching payload");
1515
+ }
1516
+ if (query?.delay !== void 0) {
1517
+ parts.push("with matching delay");
1518
+ }
1519
+ const suffix = this.#pushedJobs.length ? `Pushed jobs: ${this.#pushedJobs.map((record) => record.job.name).join(", ")}` : "Pushed jobs: none";
1520
+ return `${parts.join(" ")}. ${suffix}.`;
1521
+ }
1522
+ #getMatcherName(matcher) {
1523
+ if (typeof matcher === "string") {
1524
+ return matcher;
1525
+ }
1526
+ if (this.#isJobClass(matcher)) {
1527
+ return this.#getJobClassName(matcher);
1528
+ }
1529
+ return void 0;
1530
+ }
1531
+ #isJobClass(matcher) {
1532
+ return typeof matcher === "function" && matcher.prototype instanceof Job;
1533
+ }
1534
+ #getJobClassName(JobClass) {
1535
+ return JobClass.options?.name || JobClass.name;
1536
+ }
1537
+ };
1538
+
1539
+ export {
1540
+ debug_default,
1541
+ Locator,
1542
+ fake,
1543
+ FakeAdapter,
1544
+ QueueManager,
1545
+ JobDispatcher,
1546
+ JobBatchDispatcher,
1547
+ ScheduleBuilder,
1548
+ Job
1549
+ };
1550
+ //# sourceMappingURL=chunk-RO6VVBGK.js.map