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