@boringnode/queue 0.0.1-alpha.3 → 0.0.1-alpha.4

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,116 +1,27 @@
1
1
  import {
2
- E_CONFIGURATION_ERROR,
2
+ parse
3
+ } from "./chunk-5PDDRF5O.js";
4
+ import {
5
+ Locator,
6
+ QueueManager,
7
+ debug_default
8
+ } from "./chunk-CD45GT6E.js";
9
+ import {
10
+ DEFAULT_ERROR_RETRY_DELAY,
11
+ DEFAULT_IDLE_DELAY,
12
+ DEFAULT_PRIORITY,
13
+ DEFAULT_STALLED_INTERVAL,
14
+ DEFAULT_STALLED_THRESHOLD,
3
15
  E_INVALID_BASE_DELAY,
4
- E_INVALID_DURATION_EXPRESSION,
5
16
  E_INVALID_MAX_DELAY,
6
17
  E_INVALID_MULTIPLIER,
7
18
  E_JOB_MAX_ATTEMPTS_REACHED,
8
19
  E_JOB_TIMEOUT,
9
- Locator,
10
- debug_default
11
- } from "./chunk-Y6KR3UIR.js";
20
+ exceptions_exports
21
+ } from "./chunk-HMGNQSSG.js";
12
22
 
13
23
  // src/job_dispatcher.ts
14
24
  import { randomUUID } from "crypto";
15
-
16
- // src/queue_manager.ts
17
- var QueueManagerSingleton = class {
18
- #defaultAdapter;
19
- #adapters = {};
20
- #adapterInstances = /* @__PURE__ */ new Map();
21
- #globalRetryConfig;
22
- #queueConfigs = /* @__PURE__ */ new Map();
23
- async init(config) {
24
- debug_default("initializing queue manager with config: %O", config);
25
- this.#validateConfig(config);
26
- this.#adapterInstances.clear();
27
- this.#defaultAdapter = config.default;
28
- this.#adapters = config.adapters;
29
- this.#globalRetryConfig = config.retry;
30
- if (config.queues) {
31
- for (const [queue, queueConfig] of Object.entries(config.queues)) {
32
- this.#queueConfigs.set(queue, queueConfig);
33
- }
34
- }
35
- await Locator.registerFromGlob(config.locations);
36
- return this;
37
- }
38
- use(adapter) {
39
- if (!adapter) {
40
- adapter = this.#defaultAdapter;
41
- }
42
- const cached = this.#adapterInstances.get(adapter);
43
- if (cached) {
44
- return cached;
45
- }
46
- const adapterFactory = this.#adapters[adapter];
47
- if (!adapterFactory) {
48
- throw new E_CONFIGURATION_ERROR([`Adapter "${adapter}" is not registered`]);
49
- }
50
- debug_default('using adapter "%s"', adapter);
51
- try {
52
- const instance = adapterFactory();
53
- this.#adapterInstances.set(adapter, instance);
54
- return instance;
55
- } catch (error) {
56
- throw new Error();
57
- }
58
- }
59
- /**
60
- * Priority: job > queue > global
61
- */
62
- getMergedRetryConfig(queue, jobRetryConfig) {
63
- const queueConfig = this.#queueConfigs.get(queue);
64
- const queueRetryConfig = queueConfig?.retry || {};
65
- let maxRetries = jobRetryConfig?.maxRetries || queueRetryConfig.maxRetries || this.#globalRetryConfig?.maxRetries || 0;
66
- let backoff = jobRetryConfig?.backoff || queueRetryConfig.backoff || this.#globalRetryConfig?.backoff;
67
- return { maxRetries, backoff };
68
- }
69
- #validateConfig(config) {
70
- if (!config.adapters || Object.keys(config.adapters).length === 0) {
71
- throw new E_CONFIGURATION_ERROR(["At least one adapter must be configured"]);
72
- }
73
- if (!config.default) {
74
- throw new E_CONFIGURATION_ERROR(["Default adapter must be specified"]);
75
- }
76
- if (!config.locations || config.locations.length === 0) {
77
- throw new E_CONFIGURATION_ERROR(["Job locations must be specified"]);
78
- }
79
- if (!config.adapters[config.default]) {
80
- throw new E_CONFIGURATION_ERROR([
81
- `Default adapter "${config.default}" not found in adapters configuration`
82
- ]);
83
- }
84
- for (const [name, factory] of Object.entries(config.adapters)) {
85
- if (typeof factory !== "function") {
86
- throw new E_CONFIGURATION_ERROR([`Adapter "${name}" must be a factory function`]);
87
- }
88
- }
89
- }
90
- async destroy() {
91
- for (const [name, adapter] of this.#adapterInstances) {
92
- debug_default('destroying adapter "%s"', name);
93
- await adapter.destroy();
94
- }
95
- this.#adapterInstances.clear();
96
- }
97
- };
98
- var QueueManager = new QueueManagerSingleton();
99
-
100
- // src/utils.ts
101
- import { parse as parseDuration } from "@lukeed/ms";
102
- function parse(duration) {
103
- if (typeof duration === "number") {
104
- return duration;
105
- }
106
- const milliseconds = parseDuration(duration);
107
- if (typeof milliseconds === "undefined") {
108
- throw new E_INVALID_DURATION_EXPRESSION([duration]);
109
- }
110
- return milliseconds;
111
- }
112
-
113
- // src/job_dispatcher.ts
114
25
  var JobDispatcher = class {
115
26
  #name;
116
27
  #payload;
@@ -118,26 +29,105 @@ var JobDispatcher = class {
118
29
  #adapter;
119
30
  #delay;
120
31
  #priority;
32
+ /**
33
+ * Create a new job dispatcher.
34
+ *
35
+ * @param name - The job class name (used to locate the class at runtime)
36
+ * @param payload - The data to pass to the job
37
+ */
121
38
  constructor(name, payload) {
122
39
  this.#name = name;
123
40
  this.#payload = payload;
124
41
  }
42
+ /**
43
+ * Set the target queue for this job.
44
+ *
45
+ * @param queue - Queue name (default: 'default')
46
+ * @returns This dispatcher for chaining
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * await SendEmailJob.dispatch(payload).toQueue('emails')
51
+ * ```
52
+ */
125
53
  toQueue(queue) {
126
54
  this.#queue = queue;
127
55
  return this;
128
56
  }
57
+ /**
58
+ * Delay the job execution.
59
+ *
60
+ * The job will be stored in a delayed state and moved to pending
61
+ * after the delay expires.
62
+ *
63
+ * @param delay - Delay as milliseconds or duration string ('5s', '1h', '7d')
64
+ * @returns This dispatcher for chaining
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * // Send reminder in 24 hours
69
+ * await ReminderJob.dispatch(payload).in('24h')
70
+ *
71
+ * // Process in 5 minutes
72
+ * await CleanupJob.dispatch(payload).in('5m')
73
+ * ```
74
+ */
129
75
  in(delay) {
130
76
  this.#delay = delay;
131
77
  return this;
132
78
  }
79
+ /**
80
+ * Set the job priority.
81
+ *
82
+ * Lower numbers = higher priority. Jobs with lower priority values
83
+ * are processed before jobs with higher values.
84
+ *
85
+ * @param priority - Priority level (1-10, default: 5)
86
+ * @returns This dispatcher for chaining
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * // High priority job
91
+ * await UrgentJob.dispatch(payload).priority(1)
92
+ *
93
+ * // Low priority job
94
+ * await BackgroundJob.dispatch(payload).priority(10)
95
+ * ```
96
+ */
133
97
  priority(priority) {
134
98
  this.#priority = priority;
135
99
  return this;
136
100
  }
101
+ /**
102
+ * Use a specific adapter for this job.
103
+ *
104
+ * @param adapter - Adapter name or factory function
105
+ * @returns This dispatcher for chaining
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * // Use named adapter
110
+ * await Job.dispatch(payload).with('redis')
111
+ *
112
+ * // Use custom adapter instance
113
+ * await Job.dispatch(payload).with(() => new CustomAdapter())
114
+ * ```
115
+ */
137
116
  with(adapter) {
138
117
  this.#adapter = adapter;
139
118
  return this;
140
119
  }
120
+ /**
121
+ * Dispatch the job to the queue.
122
+ *
123
+ * @returns The unique job ID
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * const jobId = await SendEmailJob.dispatch(payload).run()
128
+ * console.log(`Dispatched job: ${jobId}`)
129
+ * ```
130
+ */
141
131
  async run() {
142
132
  const id = randomUUID();
143
133
  debug_default("dispatching job %s with id %s using payload %s", this.#name, id, this.#payload);
@@ -157,6 +147,15 @@ var JobDispatcher = class {
157
147
  }
158
148
  return id;
159
149
  }
150
+ /**
151
+ * Thenable implementation for auto-dispatch when awaited.
152
+ *
153
+ * Allows `await Job.dispatch(payload)` without explicit `.run()`.
154
+ *
155
+ * @param onFulfilled - Success callback
156
+ * @param onRejected - Error callback
157
+ * @returns Promise resolving to the job ID
158
+ */
160
159
  then(onFulfilled, onRejected) {
161
160
  return this.run().then(onFulfilled, onRejected);
162
161
  }
@@ -174,13 +173,65 @@ var JobDispatcher = class {
174
173
  // src/job.ts
175
174
  var Job = class {
176
175
  #payload;
176
+ #context;
177
+ /** Static options for this job class (queue, retries, timeout, etc.) */
177
178
  static options = {};
179
+ /** The payload data passed to this job instance */
178
180
  get payload() {
179
181
  return this.#payload;
180
182
  }
181
- constructor(payload) {
183
+ /**
184
+ * Context information for the current job execution.
185
+ *
186
+ * Provides metadata such as job ID, current attempt number,
187
+ * queue name, priority, and timing information.
188
+ *
189
+ * @example
190
+ * ```typescript
191
+ * async execute() {
192
+ * if (this.context.attempt > 1) {
193
+ * console.log(`Retry attempt ${this.context.attempt}`)
194
+ * }
195
+ * console.log(`Processing job ${this.context.jobId} on queue ${this.context.queue}`)
196
+ * }
197
+ * ```
198
+ */
199
+ get context() {
200
+ return this.#context;
201
+ }
202
+ /**
203
+ * Create a new job instance.
204
+ *
205
+ * @param payload - The data to be processed by this job
206
+ * @param context - The job execution context (provided by the worker)
207
+ */
208
+ constructor(payload, context) {
182
209
  this.#payload = payload;
210
+ this.#context = Object.freeze(context);
183
211
  }
212
+ /**
213
+ * Dispatch this job to the queue.
214
+ *
215
+ * Returns a JobDispatcher for fluent configuration before dispatching.
216
+ * The job is not actually dispatched until `.run()` is called or the
217
+ * dispatcher is awaited.
218
+ *
219
+ * @param payload - The data to pass to the job
220
+ * @returns A JobDispatcher for fluent configuration
221
+ *
222
+ * @example
223
+ * ```typescript
224
+ * // Simple dispatch
225
+ * await SendEmailJob.dispatch({ to: 'user@example.com', subject: 'Hello' })
226
+ *
227
+ * // With options
228
+ * await SendEmailJob.dispatch({ to: 'user@example.com' })
229
+ * .toQueue('high-priority')
230
+ * .priority(1)
231
+ * .in('5m')
232
+ * .run()
233
+ * ```
234
+ */
184
235
  static dispatch(payload) {
185
236
  const dispatcher = new JobDispatcher(
186
237
  this.jobName,
@@ -206,18 +257,45 @@ import { setTimeout } from "timers/promises";
206
257
  // src/job_pool.ts
207
258
  var JobPool = class {
208
259
  #activeJobs = /* @__PURE__ */ new Map();
260
+ /** Number of currently running jobs */
209
261
  get size() {
210
262
  return this.#activeJobs.size;
211
263
  }
264
+ /**
265
+ * Check if the pool has no running jobs.
266
+ *
267
+ * @returns True if no jobs are running
268
+ */
212
269
  isEmpty() {
213
270
  return this.#activeJobs.size === 0;
214
271
  }
272
+ /**
273
+ * Check if the pool can accept more jobs.
274
+ *
275
+ * @param concurrency - Maximum number of concurrent jobs
276
+ * @returns True if there's room for more jobs
277
+ */
215
278
  hasCapacity(concurrency) {
216
279
  return this.#activeJobs.size < concurrency;
217
280
  }
281
+ /**
282
+ * Add a job to the pool.
283
+ *
284
+ * @param job - The acquired job data
285
+ * @param queue - The queue the job came from
286
+ * @param promise - Promise that resolves when the job completes
287
+ */
218
288
  add(job, queue, promise) {
219
289
  this.#activeJobs.set(job.id, { promise, job, queue });
220
290
  }
291
+ /**
292
+ * Wait for the next job to complete and return it.
293
+ *
294
+ * Uses `Promise.race()` internally, so the fastest job wins.
295
+ * The completed job is removed from the pool.
296
+ *
297
+ * @returns The first job to complete (success or failure)
298
+ */
221
299
  async waitForNextCompletion() {
222
300
  const completedJobId = await Promise.race(
223
301
  [...this.#activeJobs.entries()].map(async ([id, { promise }]) => {
@@ -232,6 +310,12 @@ var JobPool = class {
232
310
  this.#activeJobs.delete(completedJobId);
233
311
  return completed;
234
312
  }
313
+ /**
314
+ * Wait for all running jobs to complete.
315
+ *
316
+ * Used during graceful shutdown to ensure no jobs are abandoned.
317
+ * Clears the pool after all jobs finish.
318
+ */
235
319
  async drain() {
236
320
  const promises = [...this.#activeJobs.values()].map(async ({ promise }) => {
237
321
  try {
@@ -248,19 +332,46 @@ var JobPool = class {
248
332
  var Worker = class {
249
333
  #id;
250
334
  #config;
335
+ #idleDelay;
336
+ #stalledInterval;
337
+ #stalledThreshold;
338
+ #maxStalledCount;
339
+ #concurrency;
340
+ #gracefulShutdown;
341
+ #onShutdownSignal;
251
342
  #adapter;
252
343
  #running = false;
253
344
  #initialized = false;
254
345
  #generator;
255
346
  #pool;
347
+ #lastStalledCheck = 0;
348
+ #shutdownHandler;
349
+ /** Unique identifier for this worker instance */
256
350
  get id() {
257
351
  return this.#id;
258
352
  }
353
+ /**
354
+ * Create a new worker instance.
355
+ *
356
+ * @param config - Queue configuration including adapter and worker settings
357
+ */
259
358
  constructor(config) {
260
359
  this.#config = config;
261
360
  this.#id = randomUUID2();
361
+ this.#idleDelay = parse(config.worker?.idleDelay ?? DEFAULT_IDLE_DELAY);
362
+ this.#stalledInterval = parse(config.worker?.stalledInterval ?? DEFAULT_STALLED_INTERVAL);
363
+ this.#stalledThreshold = parse(config.worker?.stalledThreshold ?? DEFAULT_STALLED_THRESHOLD);
364
+ this.#maxStalledCount = config.worker?.maxStalledCount ?? 1;
365
+ this.#concurrency = config.worker?.concurrency ?? 1;
366
+ this.#gracefulShutdown = config.worker?.gracefulShutdown ?? true;
367
+ this.#onShutdownSignal = config.worker?.onShutdownSignal;
262
368
  debug_default("created worker with id %s and config %O", this.#id, config);
263
369
  }
370
+ /**
371
+ * Initialize the worker (called automatically by `start()`).
372
+ *
373
+ * Sets up the QueueManager and adapter connection.
374
+ */
264
375
  async init() {
265
376
  if (this.#initialized) {
266
377
  return;
@@ -272,6 +383,23 @@ var Worker = class {
272
383
  this.#initialized = true;
273
384
  debug_default("worker %s initialized", this.#id);
274
385
  }
386
+ /**
387
+ * Start processing jobs from the specified queues.
388
+ *
389
+ * This method blocks until the worker is stopped (via `stop()` or signal).
390
+ * Jobs are processed concurrently up to the configured concurrency limit.
391
+ *
392
+ * @param queues - Queue names to process (default: ['default'])
393
+ *
394
+ * @example
395
+ * ```typescript
396
+ * // Process single queue
397
+ * await worker.start()
398
+ *
399
+ * // Process multiple queues (priority order)
400
+ * await worker.start(['high-priority', 'default', 'low-priority'])
401
+ * ```
402
+ */
275
403
  async start(queues = ["default"]) {
276
404
  await this.init();
277
405
  if (this.#running) {
@@ -280,7 +408,7 @@ var Worker = class {
280
408
  }
281
409
  this.#running = true;
282
410
  debug_default("starting worker %s on queues: %O", this.#id, queues);
283
- await this.#setupGracefulShutdown();
411
+ this.#setupGracefulShutdown();
284
412
  for await (const cycle of this.process(queues)) {
285
413
  if (["started", "completed"].includes(cycle.type)) {
286
414
  continue;
@@ -296,6 +424,12 @@ var Worker = class {
296
424
  }
297
425
  }
298
426
  }
427
+ /**
428
+ * Stop the worker gracefully.
429
+ *
430
+ * Waits for all running jobs to complete before shutting down.
431
+ * Called automatically on SIGINT/SIGTERM if gracefulShutdown is enabled.
432
+ */
299
433
  async stop() {
300
434
  debug_default("stopping worker %s", this.#id);
301
435
  this.#running = false;
@@ -306,7 +440,29 @@ var Worker = class {
306
440
  if (this.#adapter) {
307
441
  await this.#adapter.destroy();
308
442
  }
443
+ this.#removeShutdownHandlers();
309
444
  }
445
+ /**
446
+ * Process a single cycle and return the result.
447
+ *
448
+ * Useful for testing or when you need fine-grained control.
449
+ * Each cycle may start new jobs, complete a job, or return idle.
450
+ *
451
+ * @param queues - Queue names to process
452
+ * @returns The cycle result, or null if the worker was stopped
453
+ *
454
+ * @example
455
+ * ```typescript
456
+ * const worker = new Worker(config)
457
+ *
458
+ * // Process cycles manually
459
+ * let cycle = await worker.processCycle(['default'])
460
+ * while (cycle) {
461
+ * console.log('Cycle:', cycle.type)
462
+ * cycle = await worker.processCycle(['default'])
463
+ * }
464
+ * ```
465
+ */
310
466
  async processCycle(queues) {
311
467
  await this.init();
312
468
  this.#running = true;
@@ -320,26 +476,58 @@ var Worker = class {
320
476
  }
321
477
  return result.value;
322
478
  }
479
+ /**
480
+ * Generator that yields worker cycle events.
481
+ *
482
+ * Low-level API for processing jobs. Yields events for:
483
+ * - `started`: A new job began execution
484
+ * - `completed`: A job finished (success or failure)
485
+ * - `idle`: No jobs available, suggest waiting
486
+ * - `error`: An error occurred during processing
487
+ *
488
+ * @param queues - Queue names to process
489
+ * @yields WorkerCycle events
490
+ *
491
+ * @example
492
+ * ```typescript
493
+ * for await (const cycle of worker.process(['default'])) {
494
+ * switch (cycle.type) {
495
+ * case 'started':
496
+ * console.log(`Started job ${cycle.job.id}`)
497
+ * break
498
+ * case 'completed':
499
+ * console.log(`Completed job ${cycle.job.id}`)
500
+ * break
501
+ * case 'idle':
502
+ * await sleep(cycle.suggestedDelay)
503
+ * break
504
+ * }
505
+ * }
506
+ * ```
507
+ */
323
508
  async *process(queues) {
324
- const pollingInterval = parse(this.#config.worker?.pollingInterval || "2s");
325
509
  this.#pool = new JobPool();
326
510
  while (this.#running) {
327
511
  try {
512
+ await this.#checkStalledJobs(queues);
328
513
  yield* this.#fillPool(queues);
329
514
  if (this.#pool.isEmpty()) {
330
- yield { type: "idle", suggestedDelay: pollingInterval };
515
+ yield { type: "idle", suggestedDelay: this.#idleDelay };
331
516
  continue;
332
517
  }
333
518
  const completed = await this.#pool.waitForNextCompletion();
334
519
  yield { type: "completed", queue: completed.queue, job: completed.job };
335
520
  } catch (error) {
336
- yield { type: "error", error, suggestedDelay: parse("5s") };
521
+ yield {
522
+ type: "error",
523
+ error,
524
+ suggestedDelay: parse(DEFAULT_ERROR_RETRY_DELAY)
525
+ };
337
526
  }
338
527
  }
339
528
  }
340
529
  async *#fillPool(queues) {
341
- const concurrency = this.#config.worker?.concurrency || 1;
342
- const slotsAvailable = concurrency - this.#pool.size;
530
+ const slotsAvailable = this.#concurrency - this.#pool.size;
343
531
  if (slotsAvailable <= 0) return;
344
532
  const popPromises = Array.from({ length: slotsAvailable }, () => this.#acquireNextJob(queues));
345
533
  const results = await Promise.all(popPromises);
@@ -400,7 +588,17 @@ var Worker = class {
400
588
  async #initJob(job, queue) {
401
589
  try {
402
590
  const JobClass = Locator.getOrThrow(job.name);
403
- const instance = new JobClass(job.payload);
591
+ const context = Object.freeze({
592
+ jobId: job.id,
593
+ name: job.name,
594
+ attempt: job.attempts + 1,
595
+ queue,
596
+ priority: job.priority ?? DEFAULT_PRIORITY,
597
+ acquiredAt: new Date(job.acquiredAt),
598
+ stalledCount: job.stalledCount ?? 0
599
+ });
600
+ const jobFactory = QueueManager.getJobFactory();
601
+ const instance = jobFactory ? await jobFactory(JobClass, job.payload, context) : new JobClass(job.payload, context);
404
602
  const options = JobClass.options || {};
405
603
  const timeout = this.#getJobTimeout(options);
406
604
  return { instance, options, timeout };
@@ -442,14 +640,43 @@ var Worker = class {
442
640
  }
443
641
  return null;
444
642
  }
445
- async #setupGracefulShutdown() {
446
- const shutdown = async () => {
643
+ async #checkStalledJobs(queues) {
644
+ const now = Date.now();
645
+ if (now - this.#lastStalledCheck < this.#stalledInterval) {
646
+ return;
647
+ }
648
+ this.#lastStalledCheck = now;
649
+ for (const queue of queues) {
650
+ const recovered = await this.#adapter.recoverStalledJobs(
651
+ queue,
652
+ this.#stalledThreshold,
653
+ this.#maxStalledCount
654
+ );
655
+ if (recovered > 0) {
656
+ debug_default("worker %s: recovered %d stalled jobs from queue %s", this.#id, recovered, queue);
657
+ }
658
+ }
659
+ }
660
+ #setupGracefulShutdown() {
661
+ if (!this.#gracefulShutdown) {
662
+ return;
663
+ }
664
+ this.#shutdownHandler = async () => {
447
665
  debug_default("received shutdown signal, stopping worker...");
666
+ if (this.#onShutdownSignal) {
667
+ await this.#onShutdownSignal();
668
+ }
448
669
  await this.stop();
449
- process.exit(0);
450
670
  };
451
- process.on("SIGINT", shutdown);
452
- process.on("SIGTERM", shutdown);
671
+ process.on("SIGINT", this.#shutdownHandler);
672
+ process.on("SIGTERM", this.#shutdownHandler);
673
+ }
674
+ #removeShutdownHandlers() {
675
+ if (this.#shutdownHandler) {
676
+ process.off("SIGINT", this.#shutdownHandler);
677
+ process.off("SIGTERM", this.#shutdownHandler);
678
+ this.#shutdownHandler = void 0;
679
+ }
453
680
  }
454
681
  };
455
682
 
@@ -458,10 +685,33 @@ import { RuntimeException } from "@poppinss/utils";
458
685
  import { assertUnreachable } from "@poppinss/utils/assert";
459
686
  var BackoffStrategy = class {
460
687
  #config;
688
+ /**
689
+ * Create a new backoff strategy.
690
+ *
691
+ * @param config - Backoff configuration
692
+ * @throws {E_INVALID_BASE_DELAY} If baseDelay is not positive
693
+ * @throws {E_INVALID_MAX_DELAY} If maxDelay is invalid
694
+ * @throws {E_INVALID_MULTIPLIER} If multiplier is invalid
695
+ */
461
696
  constructor(config) {
462
697
  this.#config = config;
463
698
  this.#validateConfig();
464
699
  }
700
+ /**
701
+ * Calculate the delay for a given attempt number.
702
+ *
703
+ * @param attempt - The attempt number (1-based)
704
+ * @returns Delay in milliseconds
705
+ * @throws {RuntimeException} If attempt is less than 1
706
+ *
707
+ * @example
708
+ * ```typescript
709
+ * // Exponential: 1s, 2s, 4s, 8s, 16s, ...
710
+ * strategy.calculateDelay(1) // 1000
711
+ * strategy.calculateDelay(2) // 2000
712
+ * strategy.calculateDelay(3) // 4000
713
+ * ```
714
+ */
465
715
  calculateDelay(attempt) {
466
716
  if (attempt < 1) {
467
717
  throw new RuntimeException("Attempt number must be >= 1");
@@ -489,10 +739,27 @@ var BackoffStrategy = class {
489
739
  }
490
740
  return Math.floor(delay);
491
741
  }
742
+ /**
743
+ * Get the Date when the next retry should occur.
744
+ *
745
+ * @param attempt - The attempt number (1-based)
746
+ * @returns Date for the next retry
747
+ *
748
+ * @example
749
+ * ```typescript
750
+ * const nextRetry = strategy.getNextRetryAt(3)
751
+ * console.log(`Retry at: ${nextRetry.toISOString()}`)
752
+ * ```
753
+ */
492
754
  getNextRetryAt(attempt) {
493
755
  const delay = this.calculateDelay(attempt);
494
756
  return new Date(Date.now() + delay);
495
757
  }
758
+ /**
759
+ * Get a frozen copy of the configuration.
760
+ *
761
+ * @returns Readonly configuration object
762
+ */
496
763
  getConfig() {
497
764
  return Object.freeze({ ...this.#config });
498
765
  }
@@ -560,9 +827,11 @@ function customBackoff(config) {
560
827
  }
561
828
  export {
562
829
  Job,
830
+ Locator,
563
831
  QueueManager,
564
832
  Worker,
565
833
  customBackoff,
834
+ exceptions_exports as errors,
566
835
  exponentialBackoff,
567
836
  fixedBackoff,
568
837
  linearBackoff