@bymax-one/nest-queue 1.2.14 → 1.2.16

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.
@@ -88,6 +88,11 @@ function invalid(reason) {
88
88
  reason
89
89
  });
90
90
  }
91
+ function checkTimerDelay(name, value) {
92
+ if (value !== void 0 && !isTimerDelayMs(value)) {
93
+ invalid(`${name} must be a whole number from 1 to ${String(MAX_TIMER_DELAY_MS)}`);
94
+ }
95
+ }
91
96
  function isParseableUrl(url) {
92
97
  const candidate = /^rediss?:\/\//i.test(url) ? url : url.startsWith("//") ? `redis:${url}` : `redis://${url}`;
93
98
  if (!URL.canParse(candidate)) return false;
@@ -128,15 +133,13 @@ function validateOptions(opts) {
128
133
  if (drainTimeoutMs !== void 0 && drainTimeoutMs <= 0) {
129
134
  invalid("shutdown.drainTimeoutMs must be > 0");
130
135
  }
136
+ checkTimerDelay("shutdown.drainTimeoutMs", drainTimeoutMs);
131
137
  const cacheTtlMs = opts.metrics?.cacheTtlMs;
132
138
  if (cacheTtlMs !== void 0 && cacheTtlMs < 0) {
133
139
  invalid("metrics.cacheTtlMs must be >= 0");
134
140
  }
135
- for (const key of ["commandTimeoutMs", "enqueueTimeoutMs"]) {
136
- const value = opts[key];
137
- if (value !== void 0 && !isTimerDelayMs(value)) {
138
- invalid(`${key} must be a whole number from 1 to ${String(MAX_TIMER_DELAY_MS)}`);
139
- }
141
+ for (const key of ["commandTimeoutMs", "enqueueTimeoutMs", "connectionReadyTimeoutMs"]) {
142
+ checkTimerDelay(key, opts[key]);
140
143
  }
141
144
  }
142
145
 
@@ -155,6 +158,7 @@ var DEFAULT_ENQUEUE_TIMEOUT_MS = 1e4;
155
158
  var MAX_JOBS_PAGE_SIZE = 1e3;
156
159
  var DEFAULT_METRICS_CACHE_TTL_MS = 5e3;
157
160
  var DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS = 3e4;
161
+ var MAX_BULK_SIZE = 1e3;
158
162
 
159
163
  // src/server/config/resolved-options.ts
160
164
  function applyDefaults(opts) {
@@ -248,13 +252,14 @@ function duplicateConnection(client) {
248
252
  }
249
253
 
250
254
  // src/server/services/connection-resolver.service.ts
251
- var _logger, _client, _options;
255
+ var _logger, _client, _options, _pendingReady;
252
256
  var ConnectionResolver = class {
253
257
  constructor(options) {
254
258
  __privateAdd(this, _logger, new Logger(ConnectionResolver.name));
255
259
  __privateAdd(this, _client);
256
260
  __privateAdd(this, _options);
257
261
  __publicField(this, "mode");
262
+ __privateAdd(this, _pendingReady);
258
263
  __privateSet(this, _options, options);
259
264
  }
260
265
  async init() {
@@ -310,6 +315,21 @@ var ConnectionResolver = class {
310
315
  isOwned() {
311
316
  return this.mode === "mode-b-owned";
312
317
  }
318
+ awaitReady() {
319
+ const client = this.getClient();
320
+ if (!WAITING_STATUSES.has(client.status)) return Promise.resolve();
321
+ __privateGet(this, _pendingReady) ?? __privateSet(this, _pendingReady, new Promise((resolve) => {
322
+ const release = () => {
323
+ client.off("ready", release);
324
+ client.off("end", release);
325
+ __privateSet(this, _pendingReady, void 0);
326
+ resolve();
327
+ };
328
+ client.on("ready", release);
329
+ client.on("end", release);
330
+ }));
331
+ return __privateGet(this, _pendingReady);
332
+ }
313
333
  async teardown() {
314
334
  if (this.isOwned() && __privateGet(this, _client)) {
315
335
  const client = __privateGet(this, _client);
@@ -330,7 +350,7 @@ var ConnectionResolver = class {
330
350
  }
331
351
  if (client.options.maxRetriesPerRequest === null) {
332
352
  __privateGet(this, _logger).warn(
333
- "The shared client has maxRetriesPerRequest: null, so a producer call made while Redis is unreachable waits for as long as the outage lasts; enqueue is bounded by enqueueTimeoutMs, other QueueService calls are not. Give the client a finite limit to fail fast."
353
+ "The shared client has maxRetriesPerRequest: null, so a command issued while Redis is unreachable stays queued in the client for as long as the outage lasts. QueueService calls are bounded by enqueueTimeoutMs and the caller is released, but the command is not cancelled. Give the client a finite limit to fail fast."
334
354
  );
335
355
  }
336
356
  const probe = duplicateConnection(client);
@@ -380,10 +400,17 @@ var ConnectionResolver = class {
380
400
  _logger = new WeakMap();
381
401
  _client = new WeakMap();
382
402
  _options = new WeakMap();
403
+ _pendingReady = new WeakMap();
383
404
  ConnectionResolver = __decorateClass([
384
405
  Injectable(),
385
406
  __decorateParam(0, Inject(BYMAX_QUEUE_OPTIONS))
386
407
  ], ConnectionResolver);
408
+ var WAITING_STATUSES = /* @__PURE__ */ new Set([
409
+ "connecting",
410
+ "connect",
411
+ "reconnecting",
412
+ "close"
413
+ ]);
387
414
  function isReplyError(err) {
388
415
  return typeof err === "object" && err !== null && err.name === "ReplyError";
389
416
  }
@@ -432,6 +459,13 @@ function attachDefaultErrorListener(emitter, logger, kind, queueName) {
432
459
  logger.error(`${kind} for queue "${queueName}" emitted an error: ${describeError(err)}`);
433
460
  });
434
461
  }
462
+ function assertPageWindow(operation, start, end) {
463
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end - start >= MAX_JOBS_PAGE_SIZE) {
464
+ throw new QueueException(QUEUE_ERROR_CODES.INVALID_OPTIONS, HttpStatus.BAD_REQUEST, {
465
+ reason: `${operation} needs integers with 0 <= start <= end and at most ${String(MAX_JOBS_PAGE_SIZE)} jobs per call`
466
+ });
467
+ }
468
+ }
435
469
 
436
470
  // src/server/utils/with-deadline.ts
437
471
  function withDeadline(work, timeoutMs, onTimeout) {
@@ -454,6 +488,18 @@ function timeoutException(operation, timeoutMs, cause) {
454
488
  if (cause !== void 0) exception.cause = cause;
455
489
  return exception;
456
490
  }
491
+ function unavailableException(operation, cause) {
492
+ const exception = new QueueException(
493
+ QUEUE_ERROR_CODES.CONNECTION_TIMEOUT,
494
+ HttpStatus.INTERNAL_SERVER_ERROR,
495
+ { operation, reason: "offline" }
496
+ );
497
+ exception.cause = cause;
498
+ return exception;
499
+ }
500
+ function isOfflineRejection(err) {
501
+ return err instanceof Error && err.message === "Stream isn't writeable and enableOfflineQueue options is false";
502
+ }
457
503
  function isCommandTimeout(err) {
458
504
  return err instanceof Error && err.message === "Command timed out";
459
505
  }
@@ -461,12 +507,12 @@ async function withCommandTimeoutCode(operation, commandTimeoutMs, work) {
461
507
  try {
462
508
  return await work;
463
509
  } catch (err) {
464
- throw isCommandTimeout(err) ? timeoutException(operation, commandTimeoutMs(), err) : err;
510
+ if (isCommandTimeout(err)) throw timeoutException(operation, commandTimeoutMs(), err);
511
+ throw isOfflineRejection(err) ? unavailableException(operation, err) : err;
465
512
  }
466
513
  }
467
514
 
468
515
  // src/server/services/queue.service.ts
469
- var MAX_BULK_SIZE = 1e3;
470
516
  function assertValidCron(repeat, queueOptions) {
471
517
  if (queueOptions.settings?.repeatStrategy !== void 0) return;
472
518
  try {
@@ -477,13 +523,14 @@ function assertValidCron(repeat, queueOptions) {
477
523
  });
478
524
  }
479
525
  }
480
- var _queues;
526
+ var _queues, _warm;
481
527
  var QueueService = class {
482
528
  constructor(connection, options) {
483
529
  this.connection = connection;
484
530
  this.options = options;
485
531
  __publicField(this, "logger", new Logger(QueueService.name));
486
532
  __privateAdd(this, _queues, /* @__PURE__ */ new Map());
533
+ __privateAdd(this, _warm, /* @__PURE__ */ new Set());
487
534
  }
488
535
  connection;
489
536
  options;
@@ -502,27 +549,38 @@ var QueueService = class {
502
549
  __privateGet(this, _queues).set(queueName, queue);
503
550
  return queue;
504
551
  }
505
- enqueueTimeout(operation) {
506
- return timeoutException(operation, this.options.enqueueTimeoutMs);
507
- }
508
- call(operation, work) {
552
+ call(operation, queueName, run) {
553
+ let expired = false;
554
+ const guarded = withDeadline(
555
+ this.afterReady(queueName).then(() => {
556
+ if (expired) throw new Error("the call had already timed out");
557
+ return run();
558
+ }),
559
+ this.options.enqueueTimeoutMs,
560
+ () => {
561
+ expired = true;
562
+ return timeoutException(operation, this.options.enqueueTimeoutMs);
563
+ }
564
+ );
509
565
  return withCommandTimeoutCode(
510
566
  operation,
511
567
  () => this.connection.getClient().options.commandTimeout ?? this.options.commandTimeoutMs,
512
- work
568
+ guarded
513
569
  );
514
570
  }
571
+ async afterReady(queueName) {
572
+ if (__privateGet(this, _warm).has(queueName)) return;
573
+ await this.connection.awaitReady();
574
+ __privateGet(this, _warm).add(queueName);
575
+ }
515
576
  queueFor(queueName) {
516
577
  return this.getOrCreateQueue(queueName);
517
578
  }
518
579
  async enqueue(queueName, jobName, data, options) {
519
580
  const job = await this.call(
520
581
  "enqueue",
521
- withDeadline(
522
- this.queueFor(queueName).add(jobName, data, options),
523
- this.options.enqueueTimeoutMs,
524
- () => this.enqueueTimeout("enqueue")
525
- )
582
+ queueName,
583
+ () => this.queueFor(queueName).add(jobName, data, options)
526
584
  );
527
585
  return job;
528
586
  }
@@ -541,16 +599,13 @@ var QueueService = class {
541
599
  try {
542
600
  const created = await this.call(
543
601
  "enqueueBulk",
544
- withDeadline(
545
- this.queueFor(queueName).addBulk(
546
- jobs.map((job) => ({
547
- name: job.name,
548
- data: job.data,
549
- ...job.options === void 0 ? {} : { opts: job.options }
550
- }))
551
- ),
552
- this.options.enqueueTimeoutMs,
553
- () => this.enqueueTimeout("enqueueBulk")
602
+ queueName,
603
+ () => this.queueFor(queueName).addBulk(
604
+ jobs.map((job) => ({
605
+ name: job.name,
606
+ data: job.data,
607
+ ...job.options === void 0 ? {} : { opts: job.options }
608
+ }))
554
609
  )
555
610
  );
556
611
  return created;
@@ -568,22 +623,23 @@ var QueueService = class {
568
623
  }
569
624
  }
570
625
  async getJob(queueName, jobId) {
571
- const job = await this.call("getJob", this.queueFor(queueName).getJob(jobId));
626
+ const job = await this.call("getJob", queueName, () => this.queueFor(queueName).getJob(jobId));
572
627
  return job ?? null;
573
628
  }
574
629
  async getJobs(queueName, status, start = 0, end = 50) {
575
- if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end - start >= MAX_JOBS_PAGE_SIZE) {
576
- throw new QueueException(QUEUE_ERROR_CODES.INVALID_OPTIONS, HttpStatus.BAD_REQUEST, {
577
- reason: `getJobs needs integers with 0 <= start <= end and at most ${String(MAX_JOBS_PAGE_SIZE)} jobs per call`
578
- });
579
- }
580
- const jobs = await this.call("getJobs", this.queueFor(queueName).getJobs([status], start, end));
630
+ assertPageWindow("getJobs", start, end);
631
+ const jobs = await this.call(
632
+ "getJobs",
633
+ queueName,
634
+ () => this.queueFor(queueName).getJobs([status], start, end)
635
+ );
581
636
  return jobs;
582
637
  }
583
638
  async getMetrics(queueName) {
584
639
  const counts = await this.call(
585
640
  "getMetrics",
586
- this.queueFor(queueName).getJobCounts("waiting", "active", "completed", "failed", "delayed")
641
+ queueName,
642
+ () => this.queueFor(queueName).getJobCounts("waiting", "active", "completed", "failed", "delayed")
587
643
  );
588
644
  return {
589
645
  queue: queueName,
@@ -602,24 +658,38 @@ var QueueService = class {
602
658
  if ("pattern" in repeat) assertValidCron(repeat, this.options.queueOptions);
603
659
  const job = await this.call(
604
660
  "upsertJobScheduler",
605
- queue.upsertJobScheduler(schedulerId, repeat, jobTemplate)
661
+ queueName,
662
+ () => queue.upsertJobScheduler(schedulerId, repeat, jobTemplate)
606
663
  );
607
664
  return job;
608
665
  }
609
666
  async removeJobScheduler(queueName, schedulerId) {
610
- return this.call("removeJobScheduler", this.queueFor(queueName).removeJobScheduler(schedulerId));
667
+ return this.call(
668
+ "removeJobScheduler",
669
+ queueName,
670
+ () => this.queueFor(queueName).removeJobScheduler(schedulerId)
671
+ );
611
672
  }
612
673
  async getJobSchedulers(queueName, start = 0, end = 50, asc = true) {
613
- return this.call("getJobSchedulers", this.queueFor(queueName).getJobSchedulers(start, end, asc));
674
+ assertPageWindow("getJobSchedulers", start, end);
675
+ return this.call(
676
+ "getJobSchedulers",
677
+ queueName,
678
+ () => this.queueFor(queueName).getJobSchedulers(start, end, asc)
679
+ );
614
680
  }
615
681
  async pauseQueue(queueName) {
616
- await this.call("pauseQueue", this.queueFor(queueName).pause());
682
+ await this.call("pauseQueue", queueName, () => this.queueFor(queueName).pause());
617
683
  }
618
684
  async resumeQueue(queueName) {
619
- await this.call("resumeQueue", this.queueFor(queueName).resume());
685
+ await this.call("resumeQueue", queueName, () => this.queueFor(queueName).resume());
620
686
  }
621
687
  async cleanQueue(queueName, gracePeriodMs, limit, status) {
622
- return this.call("cleanQueue", this.queueFor(queueName).clean(gracePeriodMs, limit, status));
688
+ return this.call(
689
+ "cleanQueue",
690
+ queueName,
691
+ () => this.queueFor(queueName).clean(gracePeriodMs, limit, status)
692
+ );
623
693
  }
624
694
  getCachedQueues() {
625
695
  return __privateGet(this, _queues);
@@ -629,9 +699,11 @@ var QueueService = class {
629
699
  await queue.close().catch(() => void 0);
630
700
  }
631
701
  __privateGet(this, _queues).clear();
702
+ __privateGet(this, _warm).clear();
632
703
  }
633
704
  };
634
705
  _queues = new WeakMap();
706
+ _warm = new WeakMap();
635
707
  QueueService = __decorateClass([
636
708
  Injectable(),
637
709
  __decorateParam(0, Inject(ConnectionResolver)),
@@ -640,13 +712,15 @@ QueueService = __decorateClass([
640
712
  function errorCodeOf(err) {
641
713
  return err.getResponse().error.code;
642
714
  }
643
- var _connection;
715
+ var _connection, _deadlineMs;
644
716
  var FlowService = class {
645
- constructor(connection, enabled, prefix, telemetry) {
717
+ constructor(connection, enabled, prefix, telemetry, enqueueTimeoutMs = DEFAULT_ENQUEUE_TIMEOUT_MS) {
646
718
  __publicField(this, "logger", new Logger(FlowService.name));
647
719
  __publicField(this, "producer");
648
720
  __privateAdd(this, _connection);
721
+ __privateAdd(this, _deadlineMs);
649
722
  __privateSet(this, _connection, connection);
723
+ __privateSet(this, _deadlineMs, enqueueTimeoutMs);
650
724
  if (enabled) {
651
725
  this.producer = new FlowProducer({
652
726
  connection: connection.getClient(),
@@ -657,17 +731,24 @@ var FlowService = class {
657
731
  }
658
732
  }
659
733
  async add(flow) {
660
- return withCommandTimeoutCode(
661
- "addFlow",
662
- () => this.commandTimeout(),
663
- this.requireProducer().add(flow)
664
- );
734
+ return this.bounded("addFlow", this.requireProducer().add(flow));
665
735
  }
666
736
  async addBulk(flows) {
737
+ const producer = this.requireProducer();
738
+ if (flows.length > MAX_BULK_SIZE) {
739
+ throw new QueueException(
740
+ QUEUE_ERROR_CODES.BULK_ENQUEUE_FAILED,
741
+ HttpStatus.INTERNAL_SERVER_ERROR,
742
+ { reason: "batch size exceeds limit", limit: MAX_BULK_SIZE, received: flows.length }
743
+ );
744
+ }
745
+ return this.bounded("addFlowBulk", producer.addBulk([...flows]));
746
+ }
747
+ bounded(operation, work) {
667
748
  return withCommandTimeoutCode(
668
- "addFlowBulk",
749
+ operation,
669
750
  () => this.commandTimeout(),
670
- this.requireProducer().addBulk([...flows])
751
+ withDeadline(work, __privateGet(this, _deadlineMs), () => timeoutException(operation, __privateGet(this, _deadlineMs)))
671
752
  );
672
753
  }
673
754
  commandTimeout() {
@@ -689,6 +770,7 @@ var FlowService = class {
689
770
  }
690
771
  };
691
772
  _connection = new WeakMap();
773
+ _deadlineMs = new WeakMap();
692
774
  FlowService = __decorateClass([
693
775
  Injectable()
694
776
  ], FlowService);
@@ -734,6 +816,41 @@ var MetricsService = class {
734
816
  MetricsService = __decorateClass([
735
817
  Injectable()
736
818
  ], MetricsService);
819
+
820
+ // src/server/utils/worker-option-problem.ts
821
+ var WORKER_TIMER_MIN_MS = 1e3;
822
+ function countProblem(name, value, min, max) {
823
+ if (!Number.isInteger(value)) return { reason: `${name} must be a whole number`, actual: value };
824
+ if (value < min) return { reason: `${name} must be >= ${String(min)}`, actual: value };
825
+ if (value > max) return { reason: `${name} must be <= ${String(max)}`, actual: value };
826
+ return void 0;
827
+ }
828
+ function timerProblem(name, value) {
829
+ if (value === void 0) return void 0;
830
+ if (Number.isInteger(value) && value >= WORKER_TIMER_MIN_MS && value <= MAX_TIMER_DELAY_MS) {
831
+ return void 0;
832
+ }
833
+ return {
834
+ reason: `${name} must be a whole number from ${String(WORKER_TIMER_MIN_MS)} to ${String(MAX_TIMER_DELAY_MS)}`,
835
+ actual: value
836
+ };
837
+ }
838
+ function workerOptionProblem(opts) {
839
+ if (opts === void 0) return void 0;
840
+ const { concurrency, limiter, lockDuration, stalledInterval } = opts;
841
+ return (concurrency === void 0 ? void 0 : countProblem("concurrency", concurrency, 1, MAX_WORKER_CONCURRENCY)) ?? (limiter === void 0 ? void 0 : countProblem("limiter.max", limiter.max, 1, Number.POSITIVE_INFINITY) ?? countProblem("limiter.duration", limiter.duration, 1, Number.POSITIVE_INFINITY)) ?? timerProblem("lockDuration", lockDuration) ?? timerProblem("stalledInterval", stalledInterval);
842
+ }
843
+
844
+ // src/server/utils/abandoned-workers.ts
845
+ var abandoned = /* @__PURE__ */ new WeakSet();
846
+ function abandonWorker(worker) {
847
+ abandoned.add(worker);
848
+ }
849
+ function isAbandoned(worker) {
850
+ return abandoned.has(worker);
851
+ }
852
+
853
+ // src/server/services/worker-registry.service.ts
737
854
  var _entries;
738
855
  var WorkerRegistry = class {
739
856
  constructor(connection, options) {
@@ -754,7 +871,7 @@ var WorkerRegistry = class {
754
871
  try {
755
872
  worker = new Worker(
756
873
  queueName,
757
- toProcessor(handler, options?.jobTimeoutMs),
874
+ toProcessor(handler, options?.jobTimeoutMs, () => isAbandoned(worker)),
758
875
  bullOpts
759
876
  );
760
877
  } catch (cause) {
@@ -847,51 +964,15 @@ var WorkerRegistry = class {
847
964
  }
848
965
  }
849
966
  validateOptions(opts) {
850
- if (opts?.concurrency !== void 0) {
851
- if (opts.concurrency < 1) {
852
- throw new QueueException(
853
- QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
854
- HttpStatus.INTERNAL_SERVER_ERROR,
855
- {
856
- reason: "concurrency must be >= 1",
857
- actual: opts.concurrency
858
- }
859
- );
860
- }
861
- if (opts.concurrency > MAX_WORKER_CONCURRENCY) {
862
- throw new QueueException(
863
- QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
864
- HttpStatus.INTERNAL_SERVER_ERROR,
865
- {
866
- reason: `concurrency must be <= ${MAX_WORKER_CONCURRENCY.toString()}`,
867
- actual: opts.concurrency
868
- }
869
- );
870
- }
967
+ const problem = workerOptionProblem(opts);
968
+ if (problem !== void 0) {
969
+ throw new QueueException(
970
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
971
+ HttpStatus.INTERNAL_SERVER_ERROR,
972
+ problem
973
+ );
871
974
  }
872
975
  this.validateJobTimeout(opts);
873
- if (opts?.limiter !== void 0) {
874
- if (opts.limiter.max < 1) {
875
- throw new QueueException(
876
- QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
877
- HttpStatus.INTERNAL_SERVER_ERROR,
878
- {
879
- reason: "limiter.max must be >= 1",
880
- actual: opts.limiter.max
881
- }
882
- );
883
- }
884
- if (opts.limiter.duration < 1) {
885
- throw new QueueException(
886
- QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
887
- HttpStatus.INTERNAL_SERVER_ERROR,
888
- {
889
- reason: "limiter.duration must be >= 1",
890
- actual: opts.limiter.duration
891
- }
892
- );
893
- }
894
- }
895
976
  }
896
977
  validateJobTimeout(opts) {
897
978
  if (opts?.jobTimeoutMs !== void 0 && !isTimerDelayMs(opts.jobTimeoutMs)) {
@@ -968,7 +1049,7 @@ var JobTimeoutError = class extends Error {
968
1049
  this.name = "JobTimeoutError";
969
1050
  }
970
1051
  };
971
- function toProcessor(handler, timeoutMs) {
1052
+ function adaptHandler(handler, timeoutMs) {
972
1053
  if (timeoutMs === void 0) return (job, _token, signal) => handler(job, signal);
973
1054
  return (job, _token, signal) => {
974
1055
  const own = new AbortController();
@@ -990,6 +1071,16 @@ function toProcessor(handler, timeoutMs) {
990
1071
  });
991
1072
  };
992
1073
  }
1074
+ function toProcessor(handler, timeoutMs, abandoned2) {
1075
+ const run = adaptHandler(handler, timeoutMs);
1076
+ return (job, token, signal) => run(job, token, signal).then(
1077
+ (value) => abandoned2() ? new Promise(() => void 0) : value,
1078
+ (error) => {
1079
+ if (abandoned2()) return new Promise(() => void 0);
1080
+ throw error;
1081
+ }
1082
+ );
1083
+ }
993
1084
  var _events, _connections;
994
1085
  var QueueEventsRegistry = class {
995
1086
  constructor(connection, options) {
@@ -1168,6 +1259,7 @@ ProcessorDiscoveryService = __decorateClass([
1168
1259
  __decorateParam(1, Inject(WorkerRegistry)),
1169
1260
  __decorateParam(2, Inject(QueueEventsRegistry))
1170
1261
  ], ProcessorDiscoveryService);
1262
+ var FORCE_CLOSE_GRACE_MS = 1e3;
1171
1263
  var DrainTimeoutError = class extends Error {
1172
1264
  };
1173
1265
  var QueueLifecycle = class {
@@ -1201,25 +1293,33 @@ var QueueLifecycle = class {
1201
1293
  await this.closeQueues();
1202
1294
  await this.teardownConnection();
1203
1295
  this.logger.log(
1204
- `Queue shutdown complete in ${String(Date.now() - (this.startedAt ?? Date.now()))}ms (forced ${String(this.forcedWorkers)} worker(s))`
1296
+ `Queue shutdown complete in ${String(Date.now() - (this.startedAt ?? Date.now()))}ms (${String(this.forcedWorkers)} worker(s) stopped without draining)`
1205
1297
  );
1206
1298
  }
1207
1299
  async closeWorkers(deadline) {
1208
- let forced = 0;
1300
+ let undrained = 0;
1209
1301
  await Promise.all(
1210
1302
  Array.from(this.workers.getAll(), async ([name, worker]) => {
1211
1303
  try {
1212
- await bounded(worker.close(), deadline);
1304
+ await bounded(
1305
+ worker.pause().then(() => worker.close()),
1306
+ deadline
1307
+ );
1213
1308
  } catch (cause) {
1214
- forced++;
1309
+ undrained++;
1310
+ abandonWorker(worker);
1311
+ const stopped = "it is stopped without waiting for its in-flight job, which becomes stalled and is retried";
1215
1312
  this.logger.warn(
1216
- cause instanceof DrainTimeoutError ? `Worker "${name}" exceeded the ${String(this.resolved.shutdown.drainTimeoutMs)}ms drain budget \u2014 forcing close (${QUEUE_ERROR_CODES.SHUTDOWN_TIMEOUT_EXCEEDED}); the in-flight job becomes stalled and is retried` : `Worker "${name}" failed to close gracefully (${describeError(cause)}) \u2014 forcing close; the in-flight job becomes stalled and is retried`
1313
+ cause instanceof DrainTimeoutError ? `Worker "${name}" exceeded the ${String(this.resolved.shutdown.drainTimeoutMs)}ms drain budget (${QUEUE_ERROR_CODES.SHUTDOWN_TIMEOUT_EXCEEDED}); ${stopped}` : `Worker "${name}" failed to close gracefully (${describeError(cause)}); ${stopped}`
1314
+ );
1315
+ const deadlineError = () => new DrainTimeoutError();
1316
+ await withDeadline(worker.close(true), FORCE_CLOSE_GRACE_MS, deadlineError).catch(
1317
+ () => void 0
1217
1318
  );
1218
- void worker.close(true).catch(() => void 0);
1219
1319
  }
1220
1320
  })
1221
1321
  );
1222
- return forced;
1322
+ return undrained;
1223
1323
  }
1224
1324
  async closeQueueEvents(deadline) {
1225
1325
  await Promise.all(
@@ -1294,7 +1394,13 @@ var BymaxQueueModule = class extends ConfigurableModuleClass {
1294
1394
  { provide: BYMAX_QUEUE_RESOLVED_OPTIONS, useValue: resolved },
1295
1395
  {
1296
1396
  provide: FlowService,
1297
- useFactory: (conn) => new FlowService(conn, resolved.flows.enabled, resolved.prefix, resolved.telemetry),
1397
+ useFactory: (conn) => new FlowService(
1398
+ conn,
1399
+ resolved.flows.enabled,
1400
+ resolved.prefix,
1401
+ resolved.telemetry,
1402
+ resolved.enqueueTimeoutMs
1403
+ ),
1298
1404
  inject: [ConnectionResolver]
1299
1405
  },
1300
1406
  {
@@ -1324,7 +1430,13 @@ var BymaxQueueModule = class extends ConfigurableModuleClass {
1324
1430
  },
1325
1431
  {
1326
1432
  provide: FlowService,
1327
- useFactory: (conn, resolved) => new FlowService(conn, resolved.flows.enabled, resolved.prefix, resolved.telemetry),
1433
+ useFactory: (conn, resolved) => new FlowService(
1434
+ conn,
1435
+ resolved.flows.enabled,
1436
+ resolved.prefix,
1437
+ resolved.telemetry,
1438
+ resolved.enqueueTimeoutMs
1439
+ ),
1328
1440
  inject: [ConnectionResolver, BYMAX_QUEUE_RESOLVED_OPTIONS]
1329
1441
  },
1330
1442
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-queue",
3
- "version": "1.2.14",
3
+ "version": "1.2.16",
4
4
  "description": "NestJS dynamic module wrapping BullMQ — typed jobs, flows, job schedulers, deduplication, OpenTelemetry, graceful shutdown",
5
5
  "author": "Bymax One <support@bymax.one>",
6
6
  "license": "MIT",