@boringnode/queue 0.5.1 → 0.5.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.
@@ -206,7 +206,13 @@ type Duration = number | string;
206
206
  * - `{ age?, count? }`: Keep with pruning by age and/or count
207
207
  */
208
208
  type JobRetention = boolean | {
209
+ /**
210
+ * Keep jobs newer than this duration.
211
+ */
209
212
  age?: Duration;
213
+ /**
214
+ * Keep at most this many jobs.
215
+ */
210
216
  count?: number;
211
217
  };
212
218
  /**
@@ -265,7 +271,7 @@ interface JobData {
265
271
  /**
266
272
  * Job priority (lower = higher priority).
267
273
  *
268
- * @default 0
274
+ * @default 5
269
275
  */
270
276
  priority?: number;
271
277
  /**
@@ -291,6 +297,11 @@ interface JobData {
291
297
  * ```
292
298
  */
293
299
  groupId?: string;
300
+ /**
301
+ * Timestamp (ms) when the job was dispatched.
302
+ * Used to compute queue wait time in OTel instrumentation.
303
+ */
304
+ createdAt?: number;
294
305
  /**
295
306
  * Serialized trace context for distributed tracing.
296
307
  * Injected by OTel plugin at dispatch time.
@@ -345,22 +356,28 @@ interface JobOptions {
345
356
  queue?: string;
346
357
  /**
347
358
  * Adapter name or factory to use for this job.
359
+ *
360
+ * Defaults to the queue manager's configured default adapter.
348
361
  */
349
362
  adapter?: string | (() => Adapter);
350
363
  /**
351
364
  * Maximum retry attempts before permanent failure.
352
365
  *
353
- * @default 3
366
+ * This is a convenience alias for `retry.maxRetries`.
367
+ *
368
+ * @default 0
354
369
  */
355
370
  maxRetries?: number;
356
371
  /**
357
372
  * Job priority (lower = higher priority).
358
373
  *
359
- * @default 0
374
+ * @default 5
360
375
  */
361
376
  priority?: number;
362
377
  /**
363
- * Retry configuration (backoff strategy, delays, etc.).
378
+ * Retry configuration for this job.
379
+ *
380
+ * Overrides queue-level and global retry settings.
364
381
  */
365
382
  retry?: RetryConfig;
366
383
  /**
@@ -372,10 +389,22 @@ interface JobOptions {
372
389
  /**
373
390
  * Whether to mark job as failed on timeout.
374
391
  *
375
- * @default true
392
+ * When disabled, timed out jobs follow the normal retry policy.
393
+ *
394
+ * @default false
376
395
  */
377
396
  failOnTimeout?: boolean;
397
+ /**
398
+ * Retention policy for completed jobs.
399
+ *
400
+ * By default, completed jobs are removed immediately.
401
+ */
378
402
  removeOnComplete?: JobRetention;
403
+ /**
404
+ * Retention policy for failed jobs.
405
+ *
406
+ * By default, failed jobs are removed immediately after the failure hooks run.
407
+ */
379
408
  removeOnFail?: JobRetention;
380
409
  }
381
410
  /**
@@ -443,23 +472,77 @@ type JobClass<T extends Job = Job> = (new (...args: unknown[]) => T) & {
443
472
  * ```
444
473
  */
445
474
  type JobFactory = (JobClass: JobClass) => Job | Promise<Job>;
475
+ /**
476
+ * Retry policy used by jobs, queues, or the queue manager.
477
+ */
446
478
  interface RetryConfig {
479
+ /**
480
+ * Number of retry attempts after the first failed execution.
481
+ *
482
+ * Set to `0` to disable retries.
483
+ *
484
+ * @default 0
485
+ */
447
486
  maxRetries?: number;
487
+ /**
488
+ * Factory that creates the backoff strategy used between retry attempts.
489
+ *
490
+ * If omitted, failed jobs are retried as soon as the adapter makes them
491
+ * available again.
492
+ */
448
493
  backoff?: () => BackoffStrategy$1;
449
494
  }
495
+ /**
496
+ * Built-in retry delay algorithms.
497
+ */
450
498
  type BackoffStrategy = 'exponential' | 'linear' | 'fixed';
499
+ /**
500
+ * Configuration for built-in and custom retry backoff strategies.
501
+ */
451
502
  interface BackoffConfig {
503
+ /**
504
+ * Strategy used to compute the delay before the next retry.
505
+ */
452
506
  strategy: BackoffStrategy;
507
+ /**
508
+ * Initial delay used by the strategy.
509
+ */
453
510
  baseDelay: Duration;
511
+ /**
512
+ * Upper bound for computed retry delays.
513
+ */
454
514
  maxDelay?: Duration;
515
+ /**
516
+ * Growth factor for exponential backoff.
517
+ */
455
518
  multiplier?: number;
519
+ /**
520
+ * Whether to randomize retry delays to avoid retry bursts.
521
+ */
456
522
  jitter?: boolean;
457
523
  }
524
+ /**
525
+ * Runtime configuration for a named queue.
526
+ */
458
527
  interface QueueConfig {
528
+ /**
529
+ * Adapter name used by jobs dispatched to this queue.
530
+ *
531
+ * Falls back to the queue manager's default adapter.
532
+ */
459
533
  adapter?: string;
534
+ /**
535
+ * Retry policy applied to jobs in this queue unless overridden by job options.
536
+ */
460
537
  retry?: RetryConfig;
538
+ /**
539
+ * Default job options applied to jobs in this queue unless overridden by the job.
540
+ */
461
541
  defaultJobOptions?: JobOptions;
462
542
  }
543
+ /**
544
+ * Runtime options for workers that poll queues and execute jobs.
545
+ */
463
546
  interface WorkerConfig {
464
547
  /**
465
548
  * Maximum number of jobs to process concurrently.
@@ -508,22 +591,32 @@ interface WorkerConfig {
508
591
  */
509
592
  onShutdownSignal?: () => void | Promise<void>;
510
593
  }
594
+ /**
595
+ * Event yielded by the low-level worker processing generator.
596
+ */
511
597
  type WorkerCycle = {
598
+ /** A job was acquired and execution started. */
512
599
  type: 'started';
513
600
  queue: string;
514
601
  job: JobData;
515
602
  } | {
603
+ /** A running job finished, either successfully or after failure handling. */
516
604
  type: 'completed';
517
605
  queue: string;
518
606
  job: JobData;
519
607
  } | {
608
+ /** No work was available. Consumers should wait before polling again. */
520
609
  type: 'idle';
521
610
  suggestedDelay: Duration;
522
611
  } | {
612
+ /** An unexpected worker loop error occurred. */
523
613
  type: 'error';
524
614
  error: Error;
525
615
  suggestedDelay: Duration;
526
616
  };
617
+ /**
618
+ * Factory used to lazily create adapter instances.
619
+ */
527
620
  type AdapterFactory<T extends Adapter = Adapter> = () => T;
528
621
  /**
529
622
  * Status of a schedule.
@@ -602,13 +695,59 @@ interface ScheduleListOptions {
602
695
  status?: ScheduleStatus;
603
696
  }
604
697
  interface QueueManagerConfig {
698
+ /**
699
+ * Name of the adapter used when a job does not select one explicitly.
700
+ *
701
+ * Must match one of the keys from `adapters`.
702
+ */
605
703
  default: string;
704
+ /**
705
+ * Available queue adapters keyed by name.
706
+ *
707
+ * Adapters are lazy-instantiated the first time they are used.
708
+ */
606
709
  adapters: Record<string, AdapterFactory>;
710
+ /**
711
+ * Global retry configuration applied to all jobs unless overridden by
712
+ * queue-level or job-level options.
713
+ */
607
714
  retry?: RetryConfig;
715
+ /**
716
+ * Global job options applied to all jobs unless overridden by queue-level
717
+ * or job-level options.
718
+ */
608
719
  defaultJobOptions?: JobOptions;
720
+ /**
721
+ * Per-queue configuration keyed by queue name.
722
+ *
723
+ * Use this to select adapters or defaults for specific queues.
724
+ */
609
725
  queues?: Record<string, QueueConfig>;
726
+ /**
727
+ * Worker runtime options used by `Worker` instances.
728
+ */
610
729
  worker?: WorkerConfig;
730
+ /**
731
+ * Glob patterns used to discover and register job classes.
732
+ *
733
+ * These locations are used by `init()` when `autoLoadJobs` is enabled,
734
+ * and by `QueueManager.loadJobs()` when called without arguments.
735
+ */
611
736
  locations?: string[];
737
+ /**
738
+ * Whether `init()` should immediately register jobs from configured locations.
739
+ *
740
+ * Framework integrations may disable this to defer job loading until a
741
+ * command lifecycle is ready, then call `QueueManager.loadJobs()`.
742
+ *
743
+ * @default true
744
+ */
745
+ autoLoadJobs?: boolean;
746
+ /**
747
+ * Logger used by the queue runtime.
748
+ *
749
+ * Defaults to the console logger.
750
+ */
612
751
  logger?: Logger;
613
752
  /**
614
753
  * Custom factory function for job instantiation.
@@ -1 +1 @@
1
- export { b as AcquiredJob, A as Adapter } from '../../job-DImdhRFO.js';
1
+ export { b as AcquiredJob, A as Adapter } from '../../job-Z5fBSzRX.js';
@@ -1,4 +1,4 @@
1
- import { A as Adapter, J as JobData, a as JobClass, b as AcquiredJob, c as JobRetention, d as JobRecord, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions } from '../../job-DImdhRFO.js';
1
+ import { A as Adapter, J as JobData, a as JobClass, b as AcquiredJob, c as JobRetention, d as JobRecord, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions } from '../../job-Z5fBSzRX.js';
2
2
 
3
3
  interface FakeJobRecord {
4
4
  queue: string;
@@ -23,6 +23,11 @@ declare function fake(): () => FakeAdapter;
23
23
  */
24
24
  declare class FakeAdapter implements Adapter {
25
25
  #private;
26
+ /**
27
+ * Set the function to call when the fake is disposed
28
+ */
29
+ onDispose(fn: () => void): this;
30
+ [Symbol.dispose](): void;
26
31
  setWorkerId(_workerId: string): void;
27
32
  getPushedJobs(): FakeJobRecord[];
28
33
  getPushedJobsOn(queue: string): FakeJobRecord[];
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  FakeAdapter,
3
3
  fake
4
- } from "../../chunk-VHN3XZDC.js";
4
+ } from "../../chunk-VRXHCWNK.js";
5
5
  import "../../chunk-WVLSICD4.js";
6
6
  import "../../chunk-QEFYHCL7.js";
7
7
  import "../../chunk-PZ5AY32C.js";
@@ -1,5 +1,5 @@
1
1
  import { Knex } from 'knex';
2
- import { A as Adapter, b as AcquiredJob, c as JobRetention, d as JobRecord, J as JobData, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions } from '../../job-DImdhRFO.js';
2
+ import { A as Adapter, b as AcquiredJob, c as JobRetention, d as JobRecord, J as JobData, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions } from '../../job-Z5fBSzRX.js';
3
3
 
4
4
  interface KnexAdapterOptions {
5
5
  connection: Knex;
@@ -1,5 +1,5 @@
1
1
  import { Redis, RedisOptions } from 'ioredis';
2
- import { A as Adapter, b as AcquiredJob, c as JobRetention, d as JobRecord, J as JobData, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions } from '../../job-DImdhRFO.js';
2
+ import { A as Adapter, b as AcquiredJob, c as JobRetention, d as JobRecord, J as JobData, S as ScheduleConfig, e as ScheduleData, f as ScheduleListOptions } from '../../job-Z5fBSzRX.js';
3
3
 
4
4
  type RedisConfig = Redis | RedisOptions;
5
5
  /**
@@ -298,73 +298,71 @@ var GET_JOB_SCRIPT = `
298
298
  })
299
299
  `;
300
300
  var CLAIM_SCHEDULE_SCRIPT = `
301
- local schedule_key = KEYS[1]
301
+ local schedules_index_key = KEYS[1]
302
+ local schedule_key_prefix = KEYS[2]
302
303
  local now = tonumber(ARGV[1])
303
304
 
304
- -- Get schedule data
305
- local data = redis.call('HGETALL', schedule_key)
306
- if #data == 0 then
307
- return nil
308
- end
309
-
310
- -- Convert HGETALL result to table
311
- local schedule = {}
312
- for j = 1, #data, 2 do
313
- schedule[data[j]] = data[j + 1]
314
- end
315
-
316
- -- Check if schedule is due
317
- if schedule.status ~= 'active' then
318
- return nil
319
- end
320
-
321
- local next_run_at = tonumber(schedule.next_run_at)
322
- if not next_run_at or next_run_at > now then
323
- return nil
324
- end
305
+ local ids = redis.call('SMEMBERS', schedules_index_key)
325
306
 
326
- local run_count = tonumber(schedule.run_count or '0')
327
- local run_limit = schedule.run_limit and tonumber(schedule.run_limit) or nil
328
- local to_date = schedule.to_date and tonumber(schedule.to_date) or nil
307
+ for i = 1, #ids do
308
+ local schedule_key = schedule_key_prefix .. ids[i]
329
309
 
330
- -- Check limits
331
- if run_limit and run_count >= run_limit then
332
- return nil
333
- end
334
-
335
- if to_date and now > to_date then
336
- return nil
337
- end
338
-
339
- -- This schedule is claimable - atomically update it
340
- local new_run_count = run_count + 1
341
-
342
- -- Calculate new next_run_at (simple interval-based for now)
343
- -- Complex cron calculation happens in the caller
344
- local new_next_run_at = ''
345
- local every_ms = schedule.every_ms and tonumber(schedule.every_ms) or nil
346
- if every_ms then
347
- new_next_run_at = tostring(now + every_ms)
348
- end
349
-
350
- -- Check if we've hit the limit after this run
351
- if run_limit and new_run_count >= run_limit then
352
- new_next_run_at = ''
353
- end
310
+ -- Get schedule data
311
+ local data = redis.call('HGETALL', schedule_key)
312
+ if #data > 0 then
313
+ -- Convert HGETALL result to table
314
+ local schedule = {}
315
+ for j = 1, #data, 2 do
316
+ schedule[data[j]] = data[j + 1]
317
+ end
354
318
 
355
- -- Check if past end date
356
- if to_date and new_next_run_at ~= '' and tonumber(new_next_run_at) > to_date then
357
- new_next_run_at = ''
319
+ -- Check if schedule is due
320
+ if schedule.status == 'active' then
321
+ local next_run_at = tonumber(schedule.next_run_at)
322
+
323
+ if next_run_at and next_run_at <= now then
324
+ local run_count = tonumber(schedule.run_count or '0')
325
+ local run_limit = schedule.run_limit and tonumber(schedule.run_limit) or nil
326
+ local to_date = schedule.to_date and tonumber(schedule.to_date) or nil
327
+
328
+ -- Check limits
329
+ if not (run_limit and run_count >= run_limit) and not (to_date and now > to_date) then
330
+ -- This schedule is claimable - atomically update it
331
+ local new_run_count = run_count + 1
332
+
333
+ -- Calculate new next_run_at (simple interval-based for now)
334
+ -- Complex cron calculation happens in the caller
335
+ local new_next_run_at = ''
336
+ local every_ms = schedule.every_ms and tonumber(schedule.every_ms) or nil
337
+ if every_ms then
338
+ new_next_run_at = tostring(now + every_ms)
339
+ end
340
+
341
+ -- Check if we've hit the limit after this run
342
+ if run_limit and new_run_count >= run_limit then
343
+ new_next_run_at = ''
344
+ end
345
+
346
+ -- Check if past end date
347
+ if to_date and new_next_run_at ~= '' and tonumber(new_next_run_at) > to_date then
348
+ new_next_run_at = ''
349
+ end
350
+
351
+ -- Update the schedule atomically
352
+ redis.call('HSET', schedule_key,
353
+ 'next_run_at', new_next_run_at,
354
+ 'last_run_at', tostring(now),
355
+ 'run_count', tostring(new_run_count))
356
+
357
+ -- Return the schedule data (before update) as JSON
358
+ return cjson.encode(schedule)
359
+ end
360
+ end
361
+ end
362
+ end
358
363
  end
359
364
 
360
- -- Update the schedule atomically
361
- redis.call('HSET', schedule_key,
362
- 'next_run_at', new_next_run_at,
363
- 'last_run_at', tostring(now),
364
- 'run_count', tostring(new_run_count))
365
-
366
- -- Return the schedule data (before update) as JSON
367
- return cjson.encode(schedule)
365
+ return nil
368
366
  `;
369
367
  function redis(config) {
370
368
  return () => {
@@ -665,40 +663,40 @@ var RedisAdapter = class {
665
663
  }
666
664
  async claimDueSchedule() {
667
665
  const now = Date.now();
668
- const ids = await this.#connection.smembers(schedulesIndexKey);
669
- for (const id of ids) {
670
- const scheduleKey = `${schedulesKey}::${id}`;
671
- const result = await this.#connection.eval(
672
- CLAIM_SCHEDULE_SCRIPT,
673
- 1,
674
- scheduleKey,
675
- now.toString()
676
- );
677
- if (!result) {
678
- continue;
679
- }
680
- const data = JSON.parse(result);
681
- if (data.cron_expression) {
682
- const { CronExpressionParser } = await import("cron-parser");
683
- const cron = CronExpressionParser.parse(data.cron_expression, {
684
- currentDate: new Date(now),
685
- tz: data.timezone || "UTC"
686
- });
687
- const nextRun = cron.next().toDate().getTime();
688
- const runCount = Number.parseInt(data.run_count || "0", 10) + 1;
689
- const runLimit = data.run_limit ? Number.parseInt(data.run_limit, 10) : null;
690
- const toDate = data.to_date ? Number.parseInt(data.to_date, 10) : null;
691
- let newNextRunAt = nextRun;
692
- if (runLimit !== null && runCount >= runLimit) {
693
- newNextRunAt = "";
694
- } else if (toDate && nextRun > toDate) {
695
- newNextRunAt = "";
696
- }
697
- await this.#connection.hset(scheduleKey, "next_run_at", newNextRunAt.toString());
666
+ const result = await this.#connection.eval(
667
+ CLAIM_SCHEDULE_SCRIPT,
668
+ 2,
669
+ schedulesIndexKey,
670
+ `${schedulesKey}::`,
671
+ now.toString()
672
+ );
673
+ if (!result) {
674
+ return null;
675
+ }
676
+ const data = JSON.parse(result);
677
+ if (data.cron_expression) {
678
+ const { CronExpressionParser } = await import("cron-parser");
679
+ const cron = CronExpressionParser.parse(data.cron_expression, {
680
+ currentDate: new Date(now),
681
+ tz: data.timezone || "UTC"
682
+ });
683
+ const nextRun = cron.next().toDate().getTime();
684
+ const runCount = Number.parseInt(data.run_count || "0", 10) + 1;
685
+ const runLimit = data.run_limit ? Number.parseInt(data.run_limit, 10) : null;
686
+ const toDate = data.to_date ? Number.parseInt(data.to_date, 10) : null;
687
+ let newNextRunAt = nextRun;
688
+ if (runLimit !== null && runCount >= runLimit) {
689
+ newNextRunAt = "";
690
+ } else if (toDate && nextRun > toDate) {
691
+ newNextRunAt = "";
698
692
  }
699
- return this.#hashToScheduleData(data);
693
+ await this.#connection.hset(
694
+ `${schedulesKey}::${data.id}`,
695
+ "next_run_at",
696
+ newNextRunAt.toString()
697
+ );
700
698
  }
701
- return null;
699
+ return this.#hashToScheduleData(data);
702
700
  }
703
701
  #hashToScheduleData(data) {
704
702
  return {