@bymax-one/nest-queue 1.2.10 → 1.2.11

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.
@@ -163,7 +163,6 @@ function applyDefaults(opts) {
163
163
  Object.defineProperty(base, "connection", {
164
164
  get: () => connection,
165
165
  enumerable: false,
166
- // Stryker disable next-line BooleanLiteral: equivalent HERE — this object is `Object.freeze`d on the way out, and freezing makes every property non-configurable regardless. The flag stays because it states the guarantee at the point the accessor is defined, and because the sibling packages that withhold secrets the same way are NOT frozen, where it is load-bearing
167
166
  configurable: false
168
167
  });
169
168
  return Object.freeze(base);
@@ -200,21 +199,11 @@ function duplicateConnection(client) {
200
199
  var _client, _options;
201
200
  var ConnectionResolver = class {
202
201
  constructor(options) {
203
- /**
204
- * The resolved Redis client.
205
- *
206
- * An ECMAScript private field rather than a TypeScript `private` one, which
207
- * is erased at runtime: an ioredis instance carries `options.password` as a
208
- * plain field, so leaving this enumerable would let anything that serializes
209
- * a service holding this resolver walk into the credentials.
210
- */
211
202
  __privateAdd(this, _client);
212
- /** The consumer's module options, which carry the Redis credentials. */
213
203
  __privateAdd(this, _options);
214
204
  __publicField(this, "mode");
215
205
  __privateSet(this, _options, options);
216
206
  }
217
- /** Resolve and validate the connection. Call once during module bootstrap. */
218
207
  async init() {
219
208
  const cfg = __privateGet(this, _options).connection;
220
209
  if ("client" in cfg) {
@@ -227,18 +216,9 @@ var ConnectionResolver = class {
227
216
  __privateGet(this, _options).connectionReadyTimeoutMs ?? DEFAULT_CONNECTION_READY_TIMEOUT_MS
228
217
  );
229
218
  }
230
- /**
231
- * Merge the consumer's options with the owned-connection policy
232
- * (`lazyConnect: false`, so the client connects eagerly and `init` can wait
233
- * for `ready`) and narrow the result to the shape ioredis 6's constructor
234
- * accepts. The runtime object is unchanged — the cast only drops the
235
- * `undefined` that `RedisOptions.replyMapping` carries and the constructor
236
- * rejects.
237
- */
238
219
  ownedOptions(options) {
239
220
  return { ...options ?? {}, lazyConnect: false };
240
221
  }
241
- /** The resolved Queue-role client. */
242
222
  getClient() {
243
223
  if (!__privateGet(this, _client)) {
244
224
  throw new QueueException(
@@ -251,7 +231,6 @@ var ConnectionResolver = class {
251
231
  }
252
232
  return __privateGet(this, _client);
253
233
  }
254
- /** The resolved connection mode. */
255
234
  getMode() {
256
235
  if (!this.mode) {
257
236
  throw new QueueException(
@@ -264,11 +243,9 @@ var ConnectionResolver = class {
264
243
  }
265
244
  return this.mode;
266
245
  }
267
- /** Whether the library owns (and must close) the connection. */
268
246
  isOwned() {
269
247
  return this.mode === "mode-b-owned";
270
248
  }
271
- /** Close the library-owned connection on shutdown; never touch a BYO client. */
272
249
  async teardown() {
273
250
  if (this.isOwned() && __privateGet(this, _client)) {
274
251
  const client = __privateGet(this, _client);
@@ -277,7 +254,6 @@ var ConnectionResolver = class {
277
254
  });
278
255
  }
279
256
  }
280
- /** Validate and adopt a bring-your-own client for the Queue role. */
281
257
  initModeA(client) {
282
258
  this.mode = "mode-a-byo";
283
259
  __privateSet(this, _client, client);
@@ -295,7 +271,6 @@ var ConnectionResolver = class {
295
271
  probe.disconnect();
296
272
  }
297
273
  }
298
- /** Resolve when the owned client reaches `ready`, or reject on timeout/error. */
299
274
  async waitReady(timeoutMs) {
300
275
  const client = this.getClient();
301
276
  if (client.status === "ready") return;
@@ -407,14 +382,6 @@ var QueueService = class {
407
382
  }
408
383
  connection;
409
384
  options;
410
- /**
411
- * Return the cached `Queue` for `queueName`, or create one with the module
412
- * defaults applied. Subsequent calls with the same name return the same
413
- * instance.
414
- *
415
- * @param queueName - Unique queue name (used as the Redis key prefix).
416
- * @param overrides - Optional per-queue option overrides.
417
- */
418
385
  getOrCreateQueue(queueName, overrides) {
419
386
  const existing = __privateGet(this, _queues).get(queueName);
420
387
  if (existing) return existing;
@@ -430,77 +397,13 @@ var QueueService = class {
430
397
  __privateGet(this, _queues).set(queueName, queue);
431
398
  return queue;
432
399
  }
433
- /** Resolve the raw cached queue (default generics) for internal delegation. */
434
400
  queueFor(queueName) {
435
401
  return this.getOrCreateQueue(queueName);
436
402
  }
437
- /**
438
- * Add a single job to a queue. The queue is created lazily if needed.
439
- *
440
- * Delivery is **at-least-once** — a handler may run more than once for the same
441
- * job (worker crash, lock expiry, or a shutdown force-close). Make handlers
442
- * idempotent: use an idempotency key on writes, prefer upserts over inserts, or
443
- * keep an "already-processed" marker keyed by `job.id`.
444
- *
445
- * `options` surfaces BullMQ natives directly — including `jobId` (idempotent
446
- * insert: a second add with the same id is a no-op while the first exists) and
447
- * `deduplication` (windowed deduplication). The library writes no deduplication
448
- * code; BullMQ owns the behavior. The deduplication key is independent of
449
- * `jobId`, so both can be set together or apart. Both collapse duplicate
450
- * **producers** and do not change the at-least-once guarantee on the
451
- * **consumer** side.
452
- *
453
- * The deduplication `id` is a shared key across the queue, so a multi-tenant
454
- * producer MUST scope it by tenant: keyed on the value alone
455
- * (`` `reindex:${term}` ``), tenant A enqueuing a term suppresses tenant B's job
456
- * for the same term.
457
- *
458
- * Scoping alone is not enough. Two variable fields joined by a delimiter that
459
- * either may contain do not produce a unique key: with `` `${tenantId}:${term}` ``,
460
- * the pairs `('a', 'b:c')` and `('a:b', 'c')` both yield `a:b:c`. Percent-encode
461
- * every variable field so the delimiter cannot appear inside one:
462
- * `` `${encodeURIComponent(tenantId)}:${encodeURIComponent(term)}` ``.
463
- *
464
- * Deduplication modes (`deduplication: { id, ttl?, extend?, replace?, keepLastIfActive? }`):
465
- * - Simple `{ id }` — collapse duplicates until the in-flight job completes or fails.
466
- * - Throttle `{ id, ttl }` — ignore duplicates for `ttl` milliseconds.
467
- * - Debounce `{ id, ttl, extend: true, replace: true }` — keep only the latest
468
- * data; each duplicate resets the TTL.
469
- * - keep-last-if-active `{ id, keepLastIfActive: true }` — while a job is active,
470
- * store the latest data and run a single follow-up when it finishes.
471
- *
472
- * @param queueName - Target queue (created lazily if absent).
473
- * @param jobName - Job identifier used by `@Process` to dispatch.
474
- * @param data - Typed job payload.
475
- * @param options - Per-job BullMQ options (priority, delay, `jobId`, `deduplication`, ...).
476
- * @returns The job BullMQ returned. When a custom `jobId` already exists, or a `deduplication`
477
- * rule keeps the job already there (one with `replace` swaps it for the new job), nothing new
478
- * is added: BullMQ answers with the id of the EXISTING job and the data and options of THIS
479
- * call, so the returned object does not describe what is stored. Call
480
- * {@link QueueService.getJob} when you need the stored job.
481
- * @example Throttle: at most one reindex per term every 5 seconds. Both variable
482
- * fields are percent-encoded, so no other tenant/term pair can build this key.
483
- * const dedupId = `${encodeURIComponent(tenantId)}:reindex:${encodeURIComponent(term)}`
484
- * await queueService.enqueue('search', 'reindex', { term }, {
485
- * deduplication: { id: dedupId, ttl: 5_000 },
486
- * })
487
- */
488
403
  async enqueue(queueName, jobName, data, options) {
489
404
  const job = await this.queueFor(queueName).add(jobName, data, options);
490
405
  return job;
491
406
  }
492
- /**
493
- * Enqueue multiple jobs in a single Redis roundtrip. The batch is bounded by
494
- * {@link MAX_BULK_SIZE} to guard against a self-inflicted memory spike; any
495
- * failure is wrapped in a `QueueException`. Delivery is at-least-once, so every
496
- * job's handler must be idempotent (see {@link QueueService.enqueue}).
497
- *
498
- * @param queueName - Target queue.
499
- * @param jobs - Job descriptors (length must not exceed the bulk size cap).
500
- * @returns The jobs BullMQ returned, in input order. An entry whose custom `jobId` already
501
- * exists is not added again: it carries the EXISTING id with the data of this call, as for
502
- * {@link QueueService.enqueue}.
503
- */
504
407
  async enqueueBulk(queueName, jobs) {
505
408
  if (jobs.length > MAX_BULK_SIZE) {
506
409
  throw new QueueException(
@@ -532,33 +435,14 @@ var QueueService = class {
532
435
  );
533
436
  }
534
437
  }
535
- /**
536
- * Fetch a job by id. Returns `null` when the job does not exist.
537
- *
538
- * @param queueName - Target queue.
539
- * @param jobId - The job identifier.
540
- */
541
438
  async getJob(queueName, jobId) {
542
439
  const job = await this.queueFor(queueName).getJob(jobId);
543
440
  return job ?? null;
544
441
  }
545
- /**
546
- * Fetch jobs in a given status with pagination.
547
- *
548
- * @param queueName - Target queue.
549
- * @param status - Status filter.
550
- * @param start - Starting index (inclusive). Default: 0.
551
- * @param end - Ending index (inclusive). Default: 50.
552
- */
553
442
  async getJobs(queueName, status, start = 0, end = 50) {
554
443
  const jobs = await this.queueFor(queueName).getJobs([status], start, end);
555
444
  return jobs;
556
445
  }
557
- /**
558
- * Return an uncached snapshot of job counts grouped by status.
559
- *
560
- * @param queueName - Target queue.
561
- */
562
446
  async getMetrics(queueName) {
563
447
  const counts = await this.queueFor(queueName).getJobCounts(
564
448
  "waiting",
@@ -573,24 +457,6 @@ var QueueService = class {
573
457
  collectedAt: (/* @__PURE__ */ new Date()).toISOString()
574
458
  };
575
459
  }
576
- /**
577
- * Create or update a Job Scheduler — the current BullMQ recurring-jobs API.
578
- * Idempotent by `schedulerId`: BullMQ performs an atomic `override: true`
579
- * upsert, so re-registering on every boot updates the schedule in place and
580
- * never duplicates it. This supersedes the repeatable-jobs surface removed in
581
- * BullMQ v6.
582
- *
583
- * The schedule is validated first, and a cron pattern BullMQ's parser rejects
584
- * is `INVALID_REPEAT_OPTIONS` (400) before Redis is asked; any other failure
585
- * propagates as it is.
586
- *
587
- * @param queueName - Target queue.
588
- * @param schedulerId - Stable, unique scheduler identifier (the upsert key).
589
- * @param repeat - Cron `pattern` OR `every` (ms), plus optional tz/limit/start/end.
590
- * @param template - Optional job template; `name` defaults to `schedulerId`, `data` to `{}`.
591
- * @returns The first scheduled (delayed) job, or `undefined`.
592
- * @throws {QueueException} `INVALID_REPEAT_OPTIONS` (400) for an invalid schedule.
593
- */
594
460
  async upsertJobScheduler(queueName, schedulerId, repeat, template) {
595
461
  validateJobSchedulerOptions(repeat);
596
462
  const queue = this.queueFor(queueName);
@@ -603,62 +469,24 @@ var QueueService = class {
603
469
  const job = await queue.upsertJobScheduler(schedulerId, repeat, jobTemplate);
604
470
  return job;
605
471
  }
606
- /**
607
- * Remove a Job Scheduler by id.
608
- *
609
- * @param queueName - Target queue.
610
- * @param schedulerId - The scheduler identifier to remove.
611
- * @returns `true` when a scheduler was removed, `false` otherwise.
612
- */
613
472
  async removeJobScheduler(queueName, schedulerId) {
614
473
  return this.queueFor(queueName).removeJobScheduler(schedulerId);
615
474
  }
616
- /**
617
- * List the Job Schedulers registered on a queue, for inspection or health checks.
618
- *
619
- * @param queueName - Target queue.
620
- * @param start - Page start index (inclusive). Default: 0.
621
- * @param end - Page end index (inclusive). Default: 50.
622
- * @param asc - Sort ascending by next run time. Default: true.
623
- * @returns The registered schedulers in the requested page.
624
- */
625
475
  async getJobSchedulers(queueName, start = 0, end = 50, asc = true) {
626
476
  return this.queueFor(queueName).getJobSchedulers(start, end, asc);
627
477
  }
628
- /** Pause a queue, halting the processing of new jobs. */
629
478
  async pauseQueue(queueName) {
630
479
  await this.queueFor(queueName).pause();
631
480
  }
632
- /** Resume a previously paused queue. */
633
481
  async resumeQueue(queueName) {
634
482
  await this.queueFor(queueName).resume();
635
483
  }
636
- /**
637
- * Remove up to `limit` jobs older than `gracePeriodMs` in a given status. The
638
- * argument order mirrors BullMQ's `Queue.clean(grace, limit, type?)` exactly;
639
- * `limit` is required and `0` means no limit. Returns the removed job ids.
640
- *
641
- * @param queueName - Target queue.
642
- * @param gracePeriodMs - Keep jobs younger than this many milliseconds.
643
- * @param limit - Maximum number of jobs to remove (`0` = no limit).
644
- * @param status - Status of jobs to clean.
645
- */
646
484
  async cleanQueue(queueName, gracePeriodMs, limit, status) {
647
485
  return this.queueFor(queueName).clean(gracePeriodMs, limit, status);
648
486
  }
649
- /** Return a read-only view of the cached queues. */
650
487
  getCachedQueues() {
651
488
  return __privateGet(this, _queues);
652
489
  }
653
- /**
654
- * Close every cached queue (swallowing per-queue errors) and clear the cache.
655
- *
656
- * Deliberately NOT named `onModuleDestroy`: NestJS invokes lifecycle hooks by
657
- * method name, so a hook here would run on Nest's own schedule, concurrently
658
- * with — and possibly before — the bounded drain in `QueueLifecycle`. Closing a
659
- * queue while its worker is still draining is exactly what the ordered sequence
660
- * exists to prevent. `QueueLifecycle` is the only caller.
661
- */
662
490
  async closeAll() {
663
491
  for (const queue of __privateGet(this, _queues).values()) {
664
492
  await queue.close().catch(() => void 0);
@@ -679,77 +507,26 @@ var FlowService = class {
679
507
  if (enabled) {
680
508
  this.producer = new FlowProducer({
681
509
  connection: connection.getClient(),
682
- // Match the producer Queue's key prefix so flow jobs land in the same
683
- // keyspace the workers poll; a non-default prefix breaks flows otherwise.
684
510
  prefix,
685
511
  ...telemetry ? { telemetry } : {}
686
512
  });
687
513
  attachDefaultErrorListener(this.producer, this.logger, "FlowProducer", "*");
688
514
  }
689
515
  }
690
- /**
691
- * Add a flow tree (a parent with its descendant children). The root job
692
- * becomes processable only after every descendant has completed.
693
- *
694
- * By BullMQ default a failed (retry-exhausted) child does NOT fail its parent:
695
- * the parent stays in the `waiting-children` state until the child eventually
696
- * completes. To make a child's failure propagate upward and fail its parent,
697
- * set `failParentOnFailure: true` on that child (it cascades when ancestors
698
- * also set it). To let a parent proceed despite a failed child, set
699
- * `ignoreDependencyOnFailure: true` on that child. Bound each child's
700
- * `attempts` so an unrecoverable child cannot leave the parent waiting forever.
701
- *
702
- * @param flow - Flow tree definition.
703
- * @returns The root `JobNode` containing the created job and its children.
704
- * A job whose custom `jobId` already exists is not added again: its node carries the EXISTING
705
- * id with the data of this call, so the tree does not describe what is stored; call
706
- * `QueueService.getJob` when you need it.
707
- * @throws {QueueException} `FLOW_DISABLED` (503) when flows are not enabled.
708
- */
709
516
  async add(flow) {
710
517
  return this.requireProducer().add(flow);
711
518
  }
712
- /**
713
- * Add multiple flow trees in a single Redis roundtrip.
714
- *
715
- * @param flows - Flow tree definitions.
716
- * @returns The root `JobNode` for each flow, in input order.
717
- * A job whose custom `jobId` already exists is not added again; see {@link FlowService.add}.
718
- * @throws {QueueException} `FLOW_DISABLED` (503) when flows are not enabled.
719
- */
720
519
  async addBulk(flows) {
721
520
  return this.requireProducer().addBulk([...flows]);
722
521
  }
723
- /**
724
- * Escape hatch returning the underlying `FlowProducer` for advanced use cases.
725
- * Prefer {@link FlowService.add} / {@link FlowService.addBulk} when possible.
726
- *
727
- * @returns The underlying BullMQ `FlowProducer`.
728
- * @throws {QueueException} `FLOW_DISABLED` (503) when flows are not enabled.
729
- */
730
522
  getProducer() {
731
523
  return this.requireProducer();
732
524
  }
733
- /**
734
- * Close the producer when active; a no-op when inactive. A failed `close()` is
735
- * swallowed so it never aborts the shutdown sequence.
736
- *
737
- * Deliberately NOT named `onModuleDestroy`: NestJS invokes lifecycle hooks by
738
- * method name, so a hook here would run in addition to the call the ordered
739
- * sequence already makes, closing the producer twice. `QueueLifecycle` is the
740
- * only caller.
741
- */
742
525
  async close() {
743
526
  if (this.producer) {
744
527
  await this.producer.close().catch(() => void 0);
745
528
  }
746
529
  }
747
- /**
748
- * Return the active producer or throw when flows are disabled. The producer is
749
- * present if and only if flows were enabled at construction time.
750
- *
751
- * @throws {QueueException} `FLOW_DISABLED` (503) when no producer exists.
752
- */
753
530
  requireProducer() {
754
531
  if (!this.producer) {
755
532
  throw new QueueException(QUEUE_ERROR_CODES.FLOW_DISABLED, HttpStatus.SERVICE_UNAVAILABLE);
@@ -770,14 +547,6 @@ var MetricsService = class {
770
547
  enabled;
771
548
  ttlMs;
772
549
  cache = /* @__PURE__ */ new Map();
773
- /**
774
- * Return cached metrics for `queueName`, or fetch fresh (and cache them) on a
775
- * miss or after expiry.
776
- *
777
- * @param queueName - Target queue.
778
- * @returns The current metrics snapshot.
779
- * @throws {QueueException} `METRICS_DISABLED` (503) when metrics are disabled.
780
- */
781
550
  async get(queueName) {
782
551
  this.ensureEnabled();
783
552
  const entry = this.cache.get(queueName);
@@ -788,23 +557,11 @@ var MetricsService = class {
788
557
  this.cache.set(queueName, { metrics, expiresAt: Date.now() + this.ttlMs });
789
558
  return metrics;
790
559
  }
791
- /**
792
- * Return a metrics snapshot for every queue currently cached in `QueueService`.
793
- *
794
- * @returns Metrics for all known queues.
795
- * @throws {QueueException} `METRICS_DISABLED` (503) when metrics are disabled.
796
- */
797
560
  async getAll() {
798
561
  this.ensureEnabled();
799
562
  const names = Array.from(this.queueService.getCachedQueues().keys());
800
563
  return Promise.all(names.map((name) => this.get(name)));
801
564
  }
802
- /**
803
- * Invalidate the cache for `queueName`, or the whole cache when omitted.
804
- *
805
- * @param queueName - The queue entry to drop; omit to clear everything.
806
- * @throws {QueueException} `METRICS_DISABLED` (503) when metrics are disabled.
807
- */
808
565
  invalidate(queueName) {
809
566
  this.ensureEnabled();
810
567
  if (queueName === void 0) {
@@ -813,11 +570,6 @@ var MetricsService = class {
813
570
  }
814
571
  this.cache.delete(queueName);
815
572
  }
816
- /**
817
- * Guard every operation behind the opt-in flag.
818
- *
819
- * @throws {QueueException} `METRICS_DISABLED` (503) when metrics are disabled.
820
- */
821
573
  ensureEnabled() {
822
574
  if (!this.enabled) {
823
575
  throw new QueueException(QUEUE_ERROR_CODES.METRICS_DISABLED, HttpStatus.SERVICE_UNAVAILABLE);
@@ -837,19 +589,6 @@ var WorkerRegistry = class {
837
589
  }
838
590
  connection;
839
591
  options;
840
- /**
841
- * Register an in-process worker with a handler function. The BullMQ `Worker`
842
- * constructor is synchronous — no Redis handshake is performed at registration
843
- * time. The duplicated connection is disconnected immediately if construction
844
- * fails, preventing a Redis connection leak.
845
- *
846
- * @param config - Queue name, handler, and optional worker options.
847
- * @returns The created BullMQ `Worker` instance.
848
- * @throws {QueueException} `DUPLICATE_PROCESSOR` when a worker for
849
- * `queueName` already exists.
850
- * @throws {QueueException} `WORKER_REGISTRATION_FAILED` when options are
851
- * invalid or the BullMQ `Worker` constructor throws.
852
- */
853
592
  register(config) {
854
593
  const { queueName, handler, options } = config;
855
594
  this.guardDuplicate(queueName);
@@ -874,21 +613,6 @@ var WorkerRegistry = class {
874
613
  __privateGet(this, _entries).set(queueName, { worker, connection: conn });
875
614
  return worker;
876
615
  }
877
- /**
878
- * Register a sandboxed (file-based, out-of-process) worker. The processor
879
- * file must be an absolute path or a `file:` URL pointing to a compiled
880
- * `.js`/`.cjs`/`.mjs` artifact. NestJS DI is **not** available inside the
881
- * processor. The BullMQ `Worker` constructor is synchronous.
882
- *
883
- * @param config - Queue name, processor file path/URL, and optional options.
884
- * @returns The created BullMQ `Worker` instance.
885
- * @throws {QueueException} `WORKER_REGISTRATION_FAILED` when `processorFile`
886
- * is a relative path, has a disallowed extension, or uses a non-`file:` URL protocol.
887
- * @throws {QueueException} `DUPLICATE_PROCESSOR` when a worker for
888
- * `queueName` already exists.
889
- * @throws {QueueException} `WORKER_REGISTRATION_FAILED` when options are
890
- * invalid or the BullMQ `Worker` constructor throws.
891
- */
892
616
  registerSandboxed(config) {
893
617
  const { queueName, processorFile, options } = config;
894
618
  this.validateProcessorFile(processorFile);
@@ -917,13 +641,6 @@ var WorkerRegistry = class {
917
641
  __privateGet(this, _entries).set(queueName, { worker, connection: conn });
918
642
  return worker;
919
643
  }
920
- /**
921
- * Stop and remove the worker for the given queue, releasing the duplicated
922
- * connection the library opened for it. If no worker exists for `queueName`
923
- * this is a no-op.
924
- *
925
- * @param queueName - The queue whose worker should be removed.
926
- */
927
644
  async unregister(queueName) {
928
645
  const entry = __privateGet(this, _entries).get(queueName);
929
646
  if (!entry) return;
@@ -931,44 +648,19 @@ var WorkerRegistry = class {
931
648
  await this.closeConnection(entry.connection);
932
649
  __privateGet(this, _entries).delete(queueName);
933
650
  }
934
- /**
935
- * Returns the queue names of all currently registered workers.
936
- *
937
- * @returns An immutable array of queue names.
938
- */
939
651
  list() {
940
652
  return Array.from(__privateGet(this, _entries).keys());
941
653
  }
942
- /**
943
- * Returns a read-only view of the registered workers keyed by queue name.
944
- * Intended for the shutdown orchestrator only.
945
- *
946
- * @returns A read-only map of queue name to worker.
947
- */
948
654
  getAll() {
949
655
  return new Map(
950
656
  Array.from(__privateGet(this, _entries), ([name, entry]) => [name, entry.worker])
951
657
  );
952
658
  }
953
- /**
954
- * Returns a read-only view of the duplicated connections the library opened
955
- * for its workers, keyed by queue name. Intended for the shutdown orchestrator
956
- * so it can close exactly the connections the library created (Mode A never
957
- * exposes the consumer's shared client here).
958
- *
959
- * @returns A read-only map of queue name to duplicated connection.
960
- */
961
659
  getConnections() {
962
660
  return new Map(
963
661
  Array.from(__privateGet(this, _entries), ([name, entry]) => [name, entry.connection])
964
662
  );
965
663
  }
966
- /**
967
- * Gracefully quit a duplicated connection, falling back to a hard disconnect
968
- * if the quit handshake fails so a socket is never leaked.
969
- *
970
- * @param connection - The duplicated connection to release.
971
- */
972
664
  async closeConnection(connection) {
973
665
  try {
974
666
  await connection.quit();
@@ -976,12 +668,6 @@ var WorkerRegistry = class {
976
668
  connection.disconnect();
977
669
  }
978
670
  }
979
- /**
980
- * Guard against registering two workers for the same queue.
981
- *
982
- * @param queueName - Queue name to check.
983
- * @throws {QueueException} `DUPLICATE_PROCESSOR` if the queue is already registered.
984
- */
985
671
  guardDuplicate(queueName) {
986
672
  if (__privateGet(this, _entries).has(queueName)) {
987
673
  throw new QueueException(
@@ -991,14 +677,6 @@ var WorkerRegistry = class {
991
677
  );
992
678
  }
993
679
  }
994
- /**
995
- * Validate tunable options before constructing a Worker.
996
- * Enforces `concurrency` in `[1, MAX_WORKER_CONCURRENCY]` and
997
- * `limiter.max`/`limiter.duration` `>= 1`.
998
- *
999
- * @param opts - The caller-supplied options (may be undefined).
1000
- * @throws {QueueException} `WORKER_REGISTRATION_FAILED` when values are out of range.
1001
- */
1002
680
  validateOptions(opts) {
1003
681
  if (opts?.concurrency !== void 0) {
1004
682
  if (opts.concurrency < 1) {
@@ -1045,15 +723,6 @@ var WorkerRegistry = class {
1045
723
  }
1046
724
  }
1047
725
  }
1048
- /**
1049
- * Validate that `processorFile` is a safe, reachable artifact path. Accepts
1050
- * absolute paths with `.js`/`.cjs`/`.mjs` extensions and `file:` URLs. Rejects
1051
- * relative paths, non-JS extensions, and non-`file:` URL protocols to prevent
1052
- * path-traversal and arbitrary-file-execution attacks.
1053
- *
1054
- * @param processorFile - The path or URL supplied by the caller.
1055
- * @throws {QueueException} `WORKER_REGISTRATION_FAILED` when validation fails.
1056
- */
1057
726
  validateProcessorFile(processorFile) {
1058
727
  if (processorFile instanceof URL) {
1059
728
  if (processorFile.protocol !== "file:") {
@@ -1079,13 +748,6 @@ var WorkerRegistry = class {
1079
748
  }
1080
749
  this.assertProcessorExtension(processorFile);
1081
750
  }
1082
- /**
1083
- * Reject a processor path whose extension is not an executable JS module,
1084
- * the last line of defense against loading an arbitrary file as a worker.
1085
- *
1086
- * @param filePath - The filesystem path (or file: URL pathname) to check.
1087
- * @throws {QueueException} `WORKER_REGISTRATION_FAILED` for a disallowed extension.
1088
- */
1089
751
  assertProcessorExtension(filePath) {
1090
752
  const allowedExtensions = /* @__PURE__ */ new Set([".js", ".cjs", ".mjs"]);
1091
753
  if (!allowedExtensions.has(extname(filePath))) {
@@ -1098,23 +760,11 @@ var WorkerRegistry = class {
1098
760
  );
1099
761
  }
1100
762
  }
1101
- /**
1102
- * Build a BullMQ `WorkerOptions` object from the library's `WorkerOptions`
1103
- * shape. Only forwards optional fields that are explicitly set to preserve
1104
- * `exactOptionalPropertyTypes` correctness. Accepts the pre-created duplicated
1105
- * connection so callers can close it on construction failure.
1106
- *
1107
- * @param opts - Library-level worker options.
1108
- * @param conn - Pre-created duplicated Redis connection.
1109
- * @returns A BullMQ-compatible options object.
1110
- */
1111
763
  buildBullOptions(opts, conn) {
1112
764
  const result = {
1113
765
  connection: conn,
1114
766
  concurrency: opts?.concurrency ?? DEFAULT_WORKER_CONCURRENCY,
1115
767
  autorun: opts?.autorun ?? true,
1116
- // Match the producer Queue's key prefix so the worker polls the same
1117
- // keyspace; without this a non-default prefix leaves jobs unconsumed.
1118
768
  prefix: this.options.prefix
1119
769
  };
1120
770
  if (opts?.limiter !== void 0) result.limiter = opts.limiter;
@@ -1141,14 +791,6 @@ var QueueEventsRegistry = class {
1141
791
  }
1142
792
  connection;
1143
793
  options;
1144
- /**
1145
- * Returns the cached `QueueEvents` for `queueName`, creating one on first
1146
- * call. Subsequent calls for the same name return the same instance without
1147
- * opening a second Redis connection.
1148
- *
1149
- * @param queueName - The BullMQ queue to observe.
1150
- * @returns The `QueueEvents` instance for the queue.
1151
- */
1152
794
  getOrCreate(queueName) {
1153
795
  const existing = __privateGet(this, _events).get(queueName);
1154
796
  if (existing) return existing;
@@ -1165,35 +807,12 @@ var QueueEventsRegistry = class {
1165
807
  __privateGet(this, _connections).set(queueName, conn);
1166
808
  return qe;
1167
809
  }
1168
- /**
1169
- * Returns the queue names for which a `QueueEvents` connection is open.
1170
- *
1171
- * @returns An immutable array of queue names.
1172
- */
1173
810
  list() {
1174
811
  return Array.from(__privateGet(this, _events).keys());
1175
812
  }
1176
- /**
1177
- * Returns the live `QueueEvents` map. Intended for the shutdown orchestrator.
1178
- *
1179
- * @returns A read-only view of the internal map.
1180
- */
1181
813
  getAll() {
1182
814
  return __privateGet(this, _events);
1183
815
  }
1184
- /**
1185
- * Returns a read-only view of the duplicated connections the library opened
1186
- * for its `QueueEvents`, keyed by queue name. Intended for the shutdown
1187
- * orchestrator so it can close exactly the connections the library created
1188
- * (Mode A never exposes the consumer's shared client here).
1189
- *
1190
- * Returns a copy. `ReadonlyMap` is a compile-time claim only — handing back the
1191
- * live registry lets any consumer cast it away and delete a connection the
1192
- * shutdown sequence is about to close. The allocation costs nothing on a path
1193
- * that runs once per shutdown.
1194
- *
1195
- * @returns A read-only snapshot of queue name to duplicated connection.
1196
- */
1197
816
  getConnections() {
1198
817
  return new Map(__privateGet(this, _connections));
1199
818
  }
@@ -1225,10 +844,6 @@ var ProcessorDiscoveryService = class {
1225
844
  events;
1226
845
  logger = new Logger(ProcessorDiscoveryService.name);
1227
846
  registeredQueues = /* @__PURE__ */ new Set();
1228
- /**
1229
- * Entry point called by NestJS after the module is fully initialized.
1230
- * Discovers all `@Processor`-annotated providers and wires them.
1231
- */
1232
847
  onModuleInit() {
1233
848
  const providers = this.discovery.getProviders();
1234
849
  for (const wrapper of providers) {
@@ -1240,15 +855,6 @@ var ProcessorDiscoveryService = class {
1240
855
  this.wireProcessor(instance, processorMeta);
1241
856
  }
1242
857
  }
1243
- /**
1244
- * Build a dispatcher function that routes `job.name` to the correct handler.
1245
- * Named handlers take priority; a catch-all (`@Process()`) handles everything
1246
- * else. Throws if no matching handler is found at job-processing time.
1247
- *
1248
- * @param instance - The provider instance whose methods serve as handlers.
1249
- * @param handlers - Accumulated `@Process` metadata entries for the class.
1250
- * @returns A dispatcher `(job: Job) => Promise<unknown>`.
1251
- */
1252
858
  buildDispatcher(instance, handlers) {
1253
859
  const namedHandlers = /* @__PURE__ */ new Map();
1254
860
  let catchAll;
@@ -1273,13 +879,6 @@ var ProcessorDiscoveryService = class {
1273
879
  );
1274
880
  };
1275
881
  }
1276
- /**
1277
- * Wire a single discovered processor class: validate uniqueness, register a
1278
- * Worker, bind worker-local and queue-global event listeners.
1279
- *
1280
- * @param instance - The resolved provider instance.
1281
- * @param processorMeta - Metadata written by `@Processor`.
1282
- */
1283
882
  wireProcessor(instance, processorMeta) {
1284
883
  const { queueName, workerOptions, _warnedNoConcurrency } = processorMeta;
1285
884
  const ctor = instance.constructor;
@@ -1337,20 +936,6 @@ var ProcessorDiscoveryService = class {
1337
936
  }
1338
937
  }
1339
938
  }
1340
- /**
1341
- * Bind an event listener so a failing handler is reported instead of escaping.
1342
- *
1343
- * An `EventEmitter` neither awaits a listener nor catches its throw: a rejected promise becomes
1344
- * an `unhandledRejection`, which ends the process by default, and BullMQ re-emits a synchronous
1345
- * throw as `error`. A failing handler is the consumer's defect, never a reason to
1346
- * stop the application, but it must still be visible, so the failure is logged with the handler
1347
- * and a stable code and the process carries on.
1348
- *
1349
- * @param instance - The provider the handler belongs to (its `this`).
1350
- * @param method - The handler method.
1351
- * @param origin - Which decorator, event and method name the listener comes from.
1352
- * @returns The listener to register on the emitter.
1353
- */
1354
939
  guardListener(instance, method, origin) {
1355
940
  const report = (cause) => {
1356
941
  this.logger.error(
@@ -1390,29 +975,23 @@ var QueueLifecycle = class {
1390
975
  connection;
1391
976
  resolved;
1392
977
  logger = new Logger(QueueLifecycle.name);
1393
- /**
1394
- * Run the ordered shutdown sequence. Called by NestJS when the application
1395
- * context closes (e.g. on `SIGTERM` with shutdown hooks enabled).
1396
- */
978
+ startedAt;
979
+ forcedWorkers = 0;
1397
980
  async onModuleDestroy() {
1398
- const start = Date.now();
1399
- const forced = await this.closeWorkers();
981
+ this.startedAt = Date.now();
982
+ this.forcedWorkers = await this.closeWorkers();
1400
983
  await this.closeQueueEvents();
1401
984
  await this.closeDuplicatedConnections();
985
+ }
986
+ async onApplicationShutdown() {
1402
987
  await this.drainQueuesIfRequested();
1403
988
  await this.closeFlowProducer();
1404
989
  await this.closeQueues();
1405
990
  await this.teardownConnection();
1406
991
  this.logger.log(
1407
- `Queue shutdown complete in ${String(Date.now() - start)}ms (forced ${String(forced)} worker(s))`
992
+ `Queue shutdown complete in ${String(Date.now() - (this.startedAt ?? Date.now()))}ms (forced ${String(this.forcedWorkers)} worker(s))`
1408
993
  );
1409
994
  }
1410
- /**
1411
- * Close every worker with a bounded drain, force-closing any that exceed the
1412
- * budget. Returns the number of workers that had to be force-closed.
1413
- *
1414
- * @returns The count of force-closed workers.
1415
- */
1416
995
  async closeWorkers() {
1417
996
  let forced = 0;
1418
997
  const drainTimeoutMs = this.resolved.shutdown.drainTimeoutMs;
@@ -1429,14 +1008,6 @@ var QueueLifecycle = class {
1429
1008
  }
1430
1009
  return forced;
1431
1010
  }
1432
- /**
1433
- * Race a graceful `worker.close()` against a timer. `worker.close()` already
1434
- * drains active jobs and takes no timeout argument, so the timer is the only
1435
- * way to bound the wait. Rejects with a timeout error when the budget elapses.
1436
- *
1437
- * @param worker - The worker to close.
1438
- * @param timeoutMs - The drain budget in milliseconds.
1439
- */
1440
1011
  async closeWorkerWithTimeout(worker, timeoutMs) {
1441
1012
  let timer;
1442
1013
  const closed = worker.close();
@@ -1454,18 +1025,11 @@ var QueueLifecycle = class {
1454
1025
  clearTimeout(timer);
1455
1026
  }
1456
1027
  }
1457
- /** Close every cached `QueueEvents`, swallowing individual failures. */
1458
1028
  async closeQueueEvents() {
1459
1029
  for (const [, queueEvents] of this.events.getAll()) {
1460
1030
  await queueEvents.close().catch(() => void 0);
1461
1031
  }
1462
1032
  }
1463
- /**
1464
- * Close the duplicated worker/queue-events connections the library opened.
1465
- * BullMQ treats a passed-in client as shared and never closes it, so the
1466
- * library must release these explicitly. In Mode A the consumer's shared
1467
- * client is never among them.
1468
- */
1469
1033
  async closeDuplicatedConnections() {
1470
1034
  const connections = [
1471
1035
  ...this.workers.getConnections().values(),
@@ -1479,10 +1043,6 @@ var QueueLifecycle = class {
1479
1043
  }
1480
1044
  }
1481
1045
  }
1482
- /**
1483
- * Drain every cached queue when `drainOnShutdown` is enabled (dev/test only).
1484
- * Removes waiting jobs and leaves delayed and active ones; off by default to stay production-safe.
1485
- */
1486
1046
  async drainQueuesIfRequested() {
1487
1047
  if (!this.resolved.shutdown.drainOnShutdown) return;
1488
1048
  for (const [name, queue] of this.queues.getCachedQueues()) {
@@ -1493,18 +1053,12 @@ var QueueLifecycle = class {
1493
1053
  });
1494
1054
  }
1495
1055
  }
1496
- /** Close the flow producer (idempotent), swallowing any failure. */
1497
1056
  async closeFlowProducer() {
1498
1057
  await this.flow.close().catch(() => void 0);
1499
1058
  }
1500
- /** Close every cached queue and clear the cache, swallowing individual failures. */
1501
1059
  async closeQueues() {
1502
1060
  await this.queues.closeAll().catch(() => void 0);
1503
1061
  }
1504
- /**
1505
- * Tear down the Queue-role connection. Mode B quits the library-owned client;
1506
- * Mode A is a no-op for the consumer's shared client.
1507
- */
1508
1062
  async teardownConnection() {
1509
1063
  await this.connection.teardown().catch(() => void 0);
1510
1064
  }
@@ -1521,24 +1075,12 @@ QueueLifecycle = __decorateClass([
1521
1075
 
1522
1076
  // src/server/bymax-queue.module.ts
1523
1077
  var { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = (
1524
- // The `moduleName` shapes the generated option token's identifier — `BYMAX_QUEUE_MODULE_OPTIONS`,
1525
- // which the module spec asserts by value; emptying the name would degrade it to an opaque
1526
- // `CONFIGURABLE_MODULE_OPTIONS[<hash>]`, so the argument is a caught regression, not a free label.
1527
1078
  new ConfigurableModuleBuilder({ moduleName: "BymaxQueue" }).setClassMethodName("forRoot").setFactoryMethodName("createQueueOptions").setExtras({ isGlobal: true }, (definition, extras) => ({
1528
1079
  ...definition,
1529
1080
  global: extras.isGlobal
1530
1081
  })).build()
1531
1082
  );
1532
1083
  var BymaxQueueModule = class extends ConfigurableModuleClass {
1533
- /**
1534
- * Synchronous registration. Validates options, resolves defaults, and wires the
1535
- * connection resolver (initialized via async factory), the queue service, the
1536
- * worker registry, the queue-events registry, processor discovery, and the
1537
- * graceful-shutdown lifecycle.
1538
- *
1539
- * @param options - Static module options.
1540
- * @returns The configured dynamic module.
1541
- */
1542
1084
  static forRoot(options) {
1543
1085
  validateOptions(options);
1544
1086
  const resolved = applyDefaults(options);
@@ -1564,17 +1106,6 @@ var BymaxQueueModule = class extends ConfigurableModuleClass {
1564
1106
  exports: BymaxQueueModule.buildExports()
1565
1107
  };
1566
1108
  }
1567
- /**
1568
- * Asynchronous registration. Use when options depend on other modules
1569
- * (ConfigService, BymaxCacheModule, etc.). Mirrors the NestJS standard async
1570
- * dynamic-module pattern (`useFactory` | `useClass` | `useExisting` + `inject`)
1571
- * and registers the same provider graph and exports as {@link forRoot}, with
1572
- * `global` applied from the `isGlobal` extra. The resolved options and the
1573
- * initialized connection are derived from the async-resolved options at runtime.
1574
- *
1575
- * @param options - Asynchronous module options.
1576
- * @returns The configured dynamic module.
1577
- */
1578
1109
  static forRootAsync(options) {
1579
1110
  const base = super.forRootAsync(options);
1580
1111
  return {
@@ -1605,14 +1136,6 @@ var BymaxQueueModule = class extends ConfigurableModuleClass {
1605
1136
  exports: BymaxQueueModule.buildExports()
1606
1137
  };
1607
1138
  }
1608
- /**
1609
- * Providers shared verbatim by both registration paths: the options alias, the
1610
- * initialized connection resolver, the resolved client/mode tokens, and the
1611
- * singleton services. `BYMAX_QUEUE_RESOLVED_OPTIONS`, `FlowService`, and
1612
- * `MetricsService` are intentionally omitted — they differ per path.
1613
- *
1614
- * @returns The shared provider list.
1615
- */
1616
1139
  static buildCoreProviders() {
1617
1140
  return [
1618
1141
  { provide: BYMAX_QUEUE_OPTIONS, useExisting: MODULE_OPTIONS_TOKEN },
@@ -1642,11 +1165,6 @@ var BymaxQueueModule = class extends ConfigurableModuleClass {
1642
1165
  QueueLifecycle
1643
1166
  ];
1644
1167
  }
1645
- /**
1646
- * The public exports shared by both registration paths.
1647
- *
1648
- * @returns A fresh exports list (services and injection tokens).
1649
- */
1650
1168
  static buildExports() {
1651
1169
  return [
1652
1170
  QueueService,