@bymax-one/nest-queue 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,87 @@ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ---
9
9
 
10
+ ## [Unreleased]
11
+
12
+ ---
13
+
14
+ ## [1.2.4] — 2026-10-30
15
+
16
+ No action required: behaviour, the public API, the types and the peer ranges are unchanged.
17
+
18
+ The status codes a `QueueException` carries are the same as in 1.2.3, but the bundle under `dist/`
19
+ is not byte-identical, and the differences are listed below so a reader does not have to take
20
+ "no change" on trust.
21
+
22
+ ### Changed
23
+
24
+ - `QueueException` is now thrown with the named `HttpStatus` members (`BAD_REQUEST`,
25
+ `INTERNAL_SERVER_ERROR`, `SERVICE_UNAVAILABLE`) instead of the numbers 400, 500 and 503, at every
26
+ call site. The values are the same; the change satisfies `no-unsafe-enum-assignment`. Against a
27
+ fresh build of 1.2.3, the CommonJS bundle is identical once those members are replaced by their
28
+ numeric values, the ES module bundle differs only in the order of its `@nestjs/common` imports and
29
+ in line wrapping, and the type declarations differ in one JSDoc line (`FlowService`, which now
30
+ names `HttpStatus.SERVICE_UNAVAILABLE`).
31
+
32
+ ### Security
33
+
34
+ Not shipped:
35
+
36
+ - Dependency resolution of the tooling the repository builds and tests with was raised to patched
37
+ versions: `fastify`, `undici`, `fast-uri`, `js-yaml`, `brace-expansion`, `qs`, `zod`,
38
+ `handlebars`, `@grpc/grpc-js` and `@nestjs/platform-fastify`. None of these ships in the package,
39
+ which still declares `"dependencies": {}`; they reach it only through development and example-app
40
+ tooling.
41
+ - `osv-scanner.toml` ignores `GHSA-gcqx-w48m-w2h4` (`deepmerge` 4.3.1, no patched release) until
42
+ 2027-01-27. The package is reached only through `jest-config`, a development dependency.
43
+
44
+ ### Documentation
45
+
46
+ - The README supply-chain wording now says that third-party GitHub Actions are pinned to a commit SHA,
47
+ which is what the workflows do; the organisation's own reusable workflows follow a version tag.
48
+
49
+ ### Internal
50
+
51
+ Not shipped:
52
+
53
+ - Tests kill the four mutants that survived the full mutation run, and the one equivalent mutant is
54
+ disabled in place with the reason beside it. The mutation score is 100%.
55
+ - The README snippet check (`scripts/check-published-surface.mjs`) decides whether a snippet
56
+ declares a receiver from the parsed snippet rather than from its text, so decorated constructor
57
+ parameters, object-literal keys and JSX are classified correctly.
58
+ - Development and GitHub Actions dependencies were updated (Stryker, Jest, ts-jest, Prettier,
59
+ typescript-eslint, testcontainers, NestJS 11.2.x, and the CodeQL, pnpm, harden-runner, TruffleHog
60
+ and setup-node actions).
61
+ - The shared review-rules block in `AGENTS.md` was synchronised with its canonical copy.
62
+ - `SECURITY.md`, `CONTRIBUTING.md`, `CLAUDE.md`, `AGENTS.md` and the mutation-testing documents were
63
+ brought in line with the current configuration.
64
+
65
+ ---
66
+
67
+ ## [1.2.3] — 2026-09-05
68
+
69
+ **Nothing that runs in this package changed.** A fresh build of the release commit was packed and
70
+ compared against the published 1.2.2 tarball: `dist/` is identical file for file, and `package.json`
71
+ differs only in the version string. The remaining difference is this CHANGELOG entry, which ships
72
+ because `CHANGELOG.md` does.
73
+
74
+ It exists because the estate releases on a cadence, so a library whose artifact is unchanged
75
+ publishes anyway and says so rather than implying a benefit it does not deliver. **There is no
76
+ reason to upgrade from 1.2.2**, and a consumer on `^1.2.0` gains nothing by resolving to it.
77
+
78
+ ### Internal
79
+
80
+ Not shipped — `files` is `dist`, `LICENSE`, `README.md`, `CHANGELOG.md`, and the only file touched
81
+ since 1.2.2 is `AGENTS.md`:
82
+
83
+ - The shared review-rules block was synchronised with its canonical copy, bringing in an invariant
84
+ for keys built from more than one field — the generalised form of the deduplication defect this
85
+ package corrected in 1.2.2.
86
+ - Two repository-specific review rules were corrected: one rested its case on a defect that had
87
+ since been fixed, so the claim had become false; the other required an authorship finding to
88
+ quote a resolvable commit id, a criterion the shared block superseded by placing author and
89
+ committer identity out of scope entirely.
90
+
10
91
  ## [1.2.2] — 2026-09-04
11
92
 
12
93
  **1.2.1 was tagged but never published**, so this release carries its documentation fix as well
@@ -528,6 +609,9 @@ v6 peer range.
528
609
 
529
610
  ---
530
611
 
612
+ [Unreleased]: https://github.com/bymaxone/nest-queue/compare/v1.2.4...HEAD
613
+ [1.2.4]: https://github.com/bymaxone/nest-queue/compare/v1.2.3...v1.2.4
614
+ [1.2.3]: https://github.com/bymaxone/nest-queue/compare/v1.2.2...v1.2.3
531
615
  [1.2.2]: https://github.com/bymaxone/nest-queue/compare/v1.2.1...v1.2.2
532
616
  [1.2.1]: https://github.com/bymaxone/nest-queue/compare/v1.2.0...v1.2.1
533
617
  [1.2.0]: https://github.com/bymaxone/nest-queue/compare/v1.0.11...v1.2.0
package/README.md CHANGED
@@ -771,7 +771,7 @@ key will do it twice at some point.
771
771
  | Namespacing | `prefix` applied uniformly to `Queue`, `Worker`, `QueueEvents` and `FlowProducer` |
772
772
  | Connection policy | `maxRetriesPerRequest: null` confined to duplicated consumer connections; producers keep fail-fast retries |
773
773
  | Delivery | At-least-once with a bounded drain; unacknowledged work is re-queued by BullMQ's stalled check |
774
- | Supply chain | `dependencies: {}`; SHA-pinned Actions, OSV-Scanner, TruffleHog, OpenSSF Scorecard; npm publish over OIDC |
774
+ | Supply chain | `dependencies: {}`; SHA-pinned third-party Actions, OSV-Scanner, TruffleHog, OpenSSF Scorecard; npm publish over OIDC |
775
775
  | Input validation | Options validated at bootstrap; cron patterns validated by BullMQ's own parser, never a hand-rolled regex |
776
776
 
777
777
  > [!IMPORTANT]
@@ -805,7 +805,7 @@ key will do it twice at some point.
805
805
  module-resolution mode would (all four `attw` quadrants green, `node10` included), and
806
806
  `pnpm smoke` resolves the packed tarball from a real consumer in ESM _and_ CommonJS.
807
807
  - **Bundle budgets** enforced (`pnpm size`): server ≤ 18 KiB brotli, shared ≤ 2.5 KiB.
808
- - **Supply chain:** SHA-pinned GitHub Actions, OSV-Scanner, TruffleHog, OpenSSF Scorecard,
808
+ - **Supply chain:** SHA-pinned third-party GitHub Actions, OSV-Scanner, TruffleHog, OpenSSF Scorecard,
809
809
  npm publish with provenance (OIDC).
810
810
 
811
811
  ```bash
@@ -80,7 +80,9 @@ var QueueException = class extends common.HttpException {
80
80
 
81
81
  // src/server/config/validate-options.ts
82
82
  function invalid(reason) {
83
- throw new QueueException(QUEUE_ERROR_CODES.INVALID_OPTIONS, 500, { reason });
83
+ throw new QueueException(QUEUE_ERROR_CODES.INVALID_OPTIONS, common.HttpStatus.INTERNAL_SERVER_ERROR, {
84
+ reason
85
+ });
84
86
  }
85
87
  function validateOptions(opts) {
86
88
  const cfg = opts.connection;
@@ -152,15 +154,17 @@ var BYMAX_QUEUE_OPTIONS = /* @__PURE__ */ Symbol("BYMAX_QUEUE_OPTIONS");
152
154
  var BYMAX_QUEUE_REDIS_CLIENT = /* @__PURE__ */ Symbol("BYMAX_QUEUE_REDIS_CLIENT");
153
155
  var BYMAX_QUEUE_CONNECTION_MODE = /* @__PURE__ */ Symbol("BYMAX_QUEUE_CONNECTION_MODE");
154
156
  var BYMAX_QUEUE_RESOLVED_OPTIONS = /* @__PURE__ */ Symbol("BYMAX_QUEUE_RESOLVED_OPTIONS");
155
-
156
- // src/server/utils/validate-connection.ts
157
157
  function assertBlockingConnection(client) {
158
158
  const actualValue = client.options?.maxRetriesPerRequest;
159
159
  if (actualValue !== null) {
160
- throw new QueueException(QUEUE_ERROR_CODES.CONNECTION_REQUIRES_NULL_RETRIES, 500, {
161
- actualValue: actualValue ?? null,
162
- expectedValue: null
163
- });
160
+ throw new QueueException(
161
+ QUEUE_ERROR_CODES.CONNECTION_REQUIRES_NULL_RETRIES,
162
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
163
+ {
164
+ actualValue: actualValue ?? null,
165
+ expectedValue: null
166
+ }
167
+ );
164
168
  }
165
169
  }
166
170
  function isClientUsable(client) {
@@ -217,18 +221,26 @@ exports.ConnectionResolver = class ConnectionResolver {
217
221
  /** The resolved Queue-role client. */
218
222
  getClient() {
219
223
  if (!__privateGet(this, _client)) {
220
- throw new QueueException(QUEUE_ERROR_CODES.CONNECTION_INVALID, 500, {
221
- reason: "not initialized"
222
- });
224
+ throw new QueueException(
225
+ QUEUE_ERROR_CODES.CONNECTION_INVALID,
226
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
227
+ {
228
+ reason: "not initialized"
229
+ }
230
+ );
223
231
  }
224
232
  return __privateGet(this, _client);
225
233
  }
226
234
  /** The resolved connection mode. */
227
235
  getMode() {
228
236
  if (!this.mode) {
229
- throw new QueueException(QUEUE_ERROR_CODES.CONNECTION_INVALID, 500, {
230
- reason: "not initialized"
231
- });
237
+ throw new QueueException(
238
+ QUEUE_ERROR_CODES.CONNECTION_INVALID,
239
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
240
+ {
241
+ reason: "not initialized"
242
+ }
243
+ );
232
244
  }
233
245
  return this.mode;
234
246
  }
@@ -250,7 +262,11 @@ exports.ConnectionResolver = class ConnectionResolver {
250
262
  this.mode = "mode-a-byo";
251
263
  __privateSet(this, _client, client);
252
264
  if (!isClientUsable(client)) {
253
- throw new QueueException(QUEUE_ERROR_CODES.CONNECTION_INVALID, 500, { status: client.status });
265
+ throw new QueueException(
266
+ QUEUE_ERROR_CODES.CONNECTION_INVALID,
267
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
268
+ { status: client.status }
269
+ );
254
270
  }
255
271
  const probe = duplicateConnection(client);
256
272
  try {
@@ -279,7 +295,13 @@ exports.ConnectionResolver = class ConnectionResolver {
279
295
  };
280
296
  const timer = setTimeout(() => {
281
297
  cleanup();
282
- reject(new QueueException(QUEUE_ERROR_CODES.CONNECTION_TIMEOUT, 500, { timeoutMs }));
298
+ reject(
299
+ new QueueException(
300
+ QUEUE_ERROR_CODES.CONNECTION_TIMEOUT,
301
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
302
+ { timeoutMs }
303
+ )
304
+ );
283
305
  }, timeoutMs);
284
306
  client.once("ready", onReady);
285
307
  client.once("error", onError);
@@ -292,10 +314,10 @@ exports.ConnectionResolver = __decorateClass([
292
314
  common.Injectable(),
293
315
  __decorateParam(0, common.Inject(BYMAX_QUEUE_OPTIONS))
294
316
  ], exports.ConnectionResolver);
295
-
296
- // src/server/utils/validate-job-scheduler-options.ts
297
317
  function invalid2(reason) {
298
- throw new QueueException(QUEUE_ERROR_CODES.INVALID_REPEAT_OPTIONS, 400, { reason });
318
+ throw new QueueException(QUEUE_ERROR_CODES.INVALID_REPEAT_OPTIONS, common.HttpStatus.BAD_REQUEST, {
319
+ reason
320
+ });
299
321
  }
300
322
  function toEpochMs(date) {
301
323
  return typeof date === "number" ? date : Date.parse(date);
@@ -435,11 +457,15 @@ exports.QueueService = class QueueService {
435
457
  */
436
458
  async enqueueBulk(queueName, jobs) {
437
459
  if (jobs.length > MAX_BULK_SIZE) {
438
- throw new QueueException(QUEUE_ERROR_CODES.BULK_ENQUEUE_FAILED, 500, {
439
- reason: "batch size exceeds limit",
440
- limit: MAX_BULK_SIZE,
441
- received: jobs.length
442
- });
460
+ throw new QueueException(
461
+ QUEUE_ERROR_CODES.BULK_ENQUEUE_FAILED,
462
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
463
+ {
464
+ reason: "batch size exceeds limit",
465
+ limit: MAX_BULK_SIZE,
466
+ received: jobs.length
467
+ }
468
+ );
443
469
  }
444
470
  try {
445
471
  const created = await this.queueFor(queueName).addBulk(
@@ -451,9 +477,13 @@ exports.QueueService = class QueueService {
451
477
  );
452
478
  return created;
453
479
  } catch (err) {
454
- throw new QueueException(QUEUE_ERROR_CODES.BULK_ENQUEUE_FAILED, 500, {
455
- cause: err.message
456
- });
480
+ throw new QueueException(
481
+ QUEUE_ERROR_CODES.BULK_ENQUEUE_FAILED,
482
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
483
+ {
484
+ cause: err.message
485
+ }
486
+ );
457
487
  }
458
488
  }
459
489
  /**
@@ -529,7 +559,7 @@ exports.QueueService = class QueueService {
529
559
  return job2;
530
560
  } catch (err) {
531
561
  if (isOperationalError(err)) throw err;
532
- throw new QueueException(QUEUE_ERROR_CODES.INVALID_REPEAT_OPTIONS, 400, {
562
+ throw new QueueException(QUEUE_ERROR_CODES.INVALID_REPEAT_OPTIONS, common.HttpStatus.BAD_REQUEST, {
533
563
  reason: "pattern must be a valid cron expression (5- or 6-field)"
534
564
  });
535
565
  }
@@ -682,7 +712,7 @@ exports.FlowService = class FlowService {
682
712
  */
683
713
  requireProducer() {
684
714
  if (!this.producer) {
685
- throw new QueueException(QUEUE_ERROR_CODES.FLOW_DISABLED, 503);
715
+ throw new QueueException(QUEUE_ERROR_CODES.FLOW_DISABLED, common.HttpStatus.SERVICE_UNAVAILABLE);
686
716
  }
687
717
  return this.producer;
688
718
  }
@@ -750,7 +780,7 @@ exports.MetricsService = class MetricsService {
750
780
  */
751
781
  ensureEnabled() {
752
782
  if (!this.enabled) {
753
- throw new QueueException(QUEUE_ERROR_CODES.METRICS_DISABLED, 503);
783
+ throw new QueueException(QUEUE_ERROR_CODES.METRICS_DISABLED, common.HttpStatus.SERVICE_UNAVAILABLE);
754
784
  }
755
785
  }
756
786
  };
@@ -791,10 +821,14 @@ exports.WorkerRegistry = class WorkerRegistry {
791
821
  worker = new bullmq.Worker(queueName, handler, bullOpts);
792
822
  } catch (cause) {
793
823
  conn.disconnect();
794
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
795
- queueName,
796
- cause: cause instanceof Error ? cause.message : String(cause)
797
- });
824
+ throw new QueueException(
825
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
826
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
827
+ {
828
+ queueName,
829
+ cause: cause instanceof Error ? cause.message : String(cause)
830
+ }
831
+ );
798
832
  }
799
833
  attachDefaultErrorListener(worker, this.logger, "Worker", queueName);
800
834
  __privateGet(this, _entries).set(queueName, { worker, connection: conn });
@@ -830,10 +864,14 @@ exports.WorkerRegistry = class WorkerRegistry {
830
864
  worker = new bullmq.Worker(queueName, processorFile, bullOpts);
831
865
  } catch (cause) {
832
866
  conn.disconnect();
833
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
834
- queueName,
835
- cause: cause instanceof Error ? cause.message : String(cause)
836
- });
867
+ throw new QueueException(
868
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
869
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
870
+ {
871
+ queueName,
872
+ cause: cause instanceof Error ? cause.message : String(cause)
873
+ }
874
+ );
837
875
  }
838
876
  attachDefaultErrorListener(worker, this.logger, "Worker", queueName);
839
877
  __privateGet(this, _entries).set(queueName, { worker, connection: conn });
@@ -906,7 +944,11 @@ exports.WorkerRegistry = class WorkerRegistry {
906
944
  */
907
945
  guardDuplicate(queueName) {
908
946
  if (__privateGet(this, _entries).has(queueName)) {
909
- throw new QueueException(QUEUE_ERROR_CODES.DUPLICATE_PROCESSOR, 500, { queueName });
947
+ throw new QueueException(
948
+ QUEUE_ERROR_CODES.DUPLICATE_PROCESSOR,
949
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
950
+ { queueName }
951
+ );
910
952
  }
911
953
  }
912
954
  /**
@@ -920,30 +962,46 @@ exports.WorkerRegistry = class WorkerRegistry {
920
962
  validateOptions(opts) {
921
963
  if (opts?.concurrency !== void 0) {
922
964
  if (opts.concurrency < 1) {
923
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
924
- reason: "concurrency must be >= 1",
925
- actual: opts.concurrency
926
- });
965
+ throw new QueueException(
966
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
967
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
968
+ {
969
+ reason: "concurrency must be >= 1",
970
+ actual: opts.concurrency
971
+ }
972
+ );
927
973
  }
928
974
  if (opts.concurrency > MAX_WORKER_CONCURRENCY) {
929
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
930
- reason: `concurrency must be <= ${MAX_WORKER_CONCURRENCY.toString()}`,
931
- actual: opts.concurrency
932
- });
975
+ throw new QueueException(
976
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
977
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
978
+ {
979
+ reason: `concurrency must be <= ${MAX_WORKER_CONCURRENCY.toString()}`,
980
+ actual: opts.concurrency
981
+ }
982
+ );
933
983
  }
934
984
  }
935
985
  if (opts?.limiter !== void 0) {
936
986
  if (opts.limiter.max < 1) {
937
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
938
- reason: "limiter.max must be >= 1",
939
- actual: opts.limiter.max
940
- });
987
+ throw new QueueException(
988
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
989
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
990
+ {
991
+ reason: "limiter.max must be >= 1",
992
+ actual: opts.limiter.max
993
+ }
994
+ );
941
995
  }
942
996
  if (opts.limiter.duration < 1) {
943
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
944
- reason: "limiter.duration must be >= 1",
945
- actual: opts.limiter.duration
946
- });
997
+ throw new QueueException(
998
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
999
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
1000
+ {
1001
+ reason: "limiter.duration must be >= 1",
1002
+ actual: opts.limiter.duration
1003
+ }
1004
+ );
947
1005
  }
948
1006
  }
949
1007
  }
@@ -959,17 +1017,25 @@ exports.WorkerRegistry = class WorkerRegistry {
959
1017
  validateProcessorFile(processorFile) {
960
1018
  if (processorFile instanceof URL) {
961
1019
  if (processorFile.protocol !== "file:") {
962
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
963
- reason: "processorFile URL must use the file: protocol"
964
- });
1020
+ throw new QueueException(
1021
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
1022
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
1023
+ {
1024
+ reason: "processorFile URL must use the file: protocol"
1025
+ }
1026
+ );
965
1027
  }
966
1028
  this.assertProcessorExtension(processorFile.pathname);
967
1029
  return;
968
1030
  }
969
1031
  if (!path.isAbsolute(processorFile)) {
970
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
971
- reason: "processorFile must be an absolute path"
972
- });
1032
+ throw new QueueException(
1033
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
1034
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
1035
+ {
1036
+ reason: "processorFile must be an absolute path"
1037
+ }
1038
+ );
973
1039
  }
974
1040
  this.assertProcessorExtension(processorFile);
975
1041
  }
@@ -983,9 +1049,13 @@ exports.WorkerRegistry = class WorkerRegistry {
983
1049
  assertProcessorExtension(filePath) {
984
1050
  const allowedExtensions = /* @__PURE__ */ new Set([".js", ".cjs", ".mjs"]);
985
1051
  if (!allowedExtensions.has(path.extname(filePath))) {
986
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
987
- reason: "processorFile must have a .js, .cjs, or .mjs extension"
988
- });
1052
+ throw new QueueException(
1053
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
1054
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
1055
+ {
1056
+ reason: "processorFile must have a .js, .cjs, or .mjs extension"
1057
+ }
1058
+ );
989
1059
  }
990
1060
  }
991
1061
  /**
@@ -1173,7 +1243,11 @@ var ProcessorDiscoveryService = class {
1173
1243
  const { queueName, workerOptions, _warnedNoConcurrency } = processorMeta;
1174
1244
  const ctor = instance.constructor;
1175
1245
  if (this.registeredQueues.has(queueName)) {
1176
- throw new QueueException(QUEUE_ERROR_CODES.DUPLICATE_PROCESSOR, 500, { queueName });
1246
+ throw new QueueException(
1247
+ QUEUE_ERROR_CODES.DUPLICATE_PROCESSOR,
1248
+ common.HttpStatus.INTERNAL_SERVER_ERROR,
1249
+ { queueName }
1250
+ );
1177
1251
  }
1178
1252
  this.registeredQueues.add(queueName);
1179
1253
  if (_warnedNoConcurrency) {
@@ -579,7 +579,7 @@ declare class QueueService {
579
579
  *
580
580
  * Opt-in: this provider is registered unconditionally so that injecting it never
581
581
  * raises `UnknownDependenciesException`; instead, every operation throws
582
- * `QueueException(FLOW_DISABLED, 503)` when `flows.enabled !== true`.
582
+ * `QueueException(FLOW_DISABLED, HttpStatus.SERVICE_UNAVAILABLE)` when `flows.enabled !== true`.
583
583
  *
584
584
  * @example
585
585
  * await flowService.add({
@@ -579,7 +579,7 @@ declare class QueueService {
579
579
  *
580
580
  * Opt-in: this provider is registered unconditionally so that injecting it never
581
581
  * raises `UnknownDependenciesException`; instead, every operation throws
582
- * `QueueException(FLOW_DISABLED, 503)` when `flows.enabled !== true`.
582
+ * `QueueException(FLOW_DISABLED, HttpStatus.SERVICE_UNAVAILABLE)` when `flows.enabled !== true`.
583
583
  *
584
584
  * @example
585
585
  * await flowService.add({
@@ -1,4 +1,4 @@
1
- import { Injectable, Inject, ConfigurableModuleBuilder, Module, Logger, HttpException, HttpStatus } from '@nestjs/common';
1
+ import { Injectable, Inject, ConfigurableModuleBuilder, Module, HttpStatus, Logger, HttpException } from '@nestjs/common';
2
2
  import { DiscoveryService, DiscoveryModule } from '@nestjs/core';
3
3
  import { Redis } from 'ioredis';
4
4
  import { Worker, QueueEvents, Queue, FlowProducer } from 'bullmq';
@@ -78,7 +78,9 @@ var QueueException = class extends HttpException {
78
78
 
79
79
  // src/server/config/validate-options.ts
80
80
  function invalid(reason) {
81
- throw new QueueException(QUEUE_ERROR_CODES.INVALID_OPTIONS, 500, { reason });
81
+ throw new QueueException(QUEUE_ERROR_CODES.INVALID_OPTIONS, HttpStatus.INTERNAL_SERVER_ERROR, {
82
+ reason
83
+ });
82
84
  }
83
85
  function validateOptions(opts) {
84
86
  const cfg = opts.connection;
@@ -150,15 +152,17 @@ var BYMAX_QUEUE_OPTIONS = /* @__PURE__ */ Symbol("BYMAX_QUEUE_OPTIONS");
150
152
  var BYMAX_QUEUE_REDIS_CLIENT = /* @__PURE__ */ Symbol("BYMAX_QUEUE_REDIS_CLIENT");
151
153
  var BYMAX_QUEUE_CONNECTION_MODE = /* @__PURE__ */ Symbol("BYMAX_QUEUE_CONNECTION_MODE");
152
154
  var BYMAX_QUEUE_RESOLVED_OPTIONS = /* @__PURE__ */ Symbol("BYMAX_QUEUE_RESOLVED_OPTIONS");
153
-
154
- // src/server/utils/validate-connection.ts
155
155
  function assertBlockingConnection(client) {
156
156
  const actualValue = client.options?.maxRetriesPerRequest;
157
157
  if (actualValue !== null) {
158
- throw new QueueException(QUEUE_ERROR_CODES.CONNECTION_REQUIRES_NULL_RETRIES, 500, {
159
- actualValue: actualValue ?? null,
160
- expectedValue: null
161
- });
158
+ throw new QueueException(
159
+ QUEUE_ERROR_CODES.CONNECTION_REQUIRES_NULL_RETRIES,
160
+ HttpStatus.INTERNAL_SERVER_ERROR,
161
+ {
162
+ actualValue: actualValue ?? null,
163
+ expectedValue: null
164
+ }
165
+ );
162
166
  }
163
167
  }
164
168
  function isClientUsable(client) {
@@ -215,18 +219,26 @@ var ConnectionResolver = class {
215
219
  /** The resolved Queue-role client. */
216
220
  getClient() {
217
221
  if (!__privateGet(this, _client)) {
218
- throw new QueueException(QUEUE_ERROR_CODES.CONNECTION_INVALID, 500, {
219
- reason: "not initialized"
220
- });
222
+ throw new QueueException(
223
+ QUEUE_ERROR_CODES.CONNECTION_INVALID,
224
+ HttpStatus.INTERNAL_SERVER_ERROR,
225
+ {
226
+ reason: "not initialized"
227
+ }
228
+ );
221
229
  }
222
230
  return __privateGet(this, _client);
223
231
  }
224
232
  /** The resolved connection mode. */
225
233
  getMode() {
226
234
  if (!this.mode) {
227
- throw new QueueException(QUEUE_ERROR_CODES.CONNECTION_INVALID, 500, {
228
- reason: "not initialized"
229
- });
235
+ throw new QueueException(
236
+ QUEUE_ERROR_CODES.CONNECTION_INVALID,
237
+ HttpStatus.INTERNAL_SERVER_ERROR,
238
+ {
239
+ reason: "not initialized"
240
+ }
241
+ );
230
242
  }
231
243
  return this.mode;
232
244
  }
@@ -248,7 +260,11 @@ var ConnectionResolver = class {
248
260
  this.mode = "mode-a-byo";
249
261
  __privateSet(this, _client, client);
250
262
  if (!isClientUsable(client)) {
251
- throw new QueueException(QUEUE_ERROR_CODES.CONNECTION_INVALID, 500, { status: client.status });
263
+ throw new QueueException(
264
+ QUEUE_ERROR_CODES.CONNECTION_INVALID,
265
+ HttpStatus.INTERNAL_SERVER_ERROR,
266
+ { status: client.status }
267
+ );
252
268
  }
253
269
  const probe = duplicateConnection(client);
254
270
  try {
@@ -277,7 +293,13 @@ var ConnectionResolver = class {
277
293
  };
278
294
  const timer = setTimeout(() => {
279
295
  cleanup();
280
- reject(new QueueException(QUEUE_ERROR_CODES.CONNECTION_TIMEOUT, 500, { timeoutMs }));
296
+ reject(
297
+ new QueueException(
298
+ QUEUE_ERROR_CODES.CONNECTION_TIMEOUT,
299
+ HttpStatus.INTERNAL_SERVER_ERROR,
300
+ { timeoutMs }
301
+ )
302
+ );
281
303
  }, timeoutMs);
282
304
  client.once("ready", onReady);
283
305
  client.once("error", onError);
@@ -290,10 +312,10 @@ ConnectionResolver = __decorateClass([
290
312
  Injectable(),
291
313
  __decorateParam(0, Inject(BYMAX_QUEUE_OPTIONS))
292
314
  ], ConnectionResolver);
293
-
294
- // src/server/utils/validate-job-scheduler-options.ts
295
315
  function invalid2(reason) {
296
- throw new QueueException(QUEUE_ERROR_CODES.INVALID_REPEAT_OPTIONS, 400, { reason });
316
+ throw new QueueException(QUEUE_ERROR_CODES.INVALID_REPEAT_OPTIONS, HttpStatus.BAD_REQUEST, {
317
+ reason
318
+ });
297
319
  }
298
320
  function toEpochMs(date) {
299
321
  return typeof date === "number" ? date : Date.parse(date);
@@ -433,11 +455,15 @@ var QueueService = class {
433
455
  */
434
456
  async enqueueBulk(queueName, jobs) {
435
457
  if (jobs.length > MAX_BULK_SIZE) {
436
- throw new QueueException(QUEUE_ERROR_CODES.BULK_ENQUEUE_FAILED, 500, {
437
- reason: "batch size exceeds limit",
438
- limit: MAX_BULK_SIZE,
439
- received: jobs.length
440
- });
458
+ throw new QueueException(
459
+ QUEUE_ERROR_CODES.BULK_ENQUEUE_FAILED,
460
+ HttpStatus.INTERNAL_SERVER_ERROR,
461
+ {
462
+ reason: "batch size exceeds limit",
463
+ limit: MAX_BULK_SIZE,
464
+ received: jobs.length
465
+ }
466
+ );
441
467
  }
442
468
  try {
443
469
  const created = await this.queueFor(queueName).addBulk(
@@ -449,9 +475,13 @@ var QueueService = class {
449
475
  );
450
476
  return created;
451
477
  } catch (err) {
452
- throw new QueueException(QUEUE_ERROR_CODES.BULK_ENQUEUE_FAILED, 500, {
453
- cause: err.message
454
- });
478
+ throw new QueueException(
479
+ QUEUE_ERROR_CODES.BULK_ENQUEUE_FAILED,
480
+ HttpStatus.INTERNAL_SERVER_ERROR,
481
+ {
482
+ cause: err.message
483
+ }
484
+ );
455
485
  }
456
486
  }
457
487
  /**
@@ -527,7 +557,7 @@ var QueueService = class {
527
557
  return job2;
528
558
  } catch (err) {
529
559
  if (isOperationalError(err)) throw err;
530
- throw new QueueException(QUEUE_ERROR_CODES.INVALID_REPEAT_OPTIONS, 400, {
560
+ throw new QueueException(QUEUE_ERROR_CODES.INVALID_REPEAT_OPTIONS, HttpStatus.BAD_REQUEST, {
531
561
  reason: "pattern must be a valid cron expression (5- or 6-field)"
532
562
  });
533
563
  }
@@ -680,7 +710,7 @@ var FlowService = class {
680
710
  */
681
711
  requireProducer() {
682
712
  if (!this.producer) {
683
- throw new QueueException(QUEUE_ERROR_CODES.FLOW_DISABLED, 503);
713
+ throw new QueueException(QUEUE_ERROR_CODES.FLOW_DISABLED, HttpStatus.SERVICE_UNAVAILABLE);
684
714
  }
685
715
  return this.producer;
686
716
  }
@@ -748,7 +778,7 @@ var MetricsService = class {
748
778
  */
749
779
  ensureEnabled() {
750
780
  if (!this.enabled) {
751
- throw new QueueException(QUEUE_ERROR_CODES.METRICS_DISABLED, 503);
781
+ throw new QueueException(QUEUE_ERROR_CODES.METRICS_DISABLED, HttpStatus.SERVICE_UNAVAILABLE);
752
782
  }
753
783
  }
754
784
  };
@@ -789,10 +819,14 @@ var WorkerRegistry = class {
789
819
  worker = new Worker(queueName, handler, bullOpts);
790
820
  } catch (cause) {
791
821
  conn.disconnect();
792
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
793
- queueName,
794
- cause: cause instanceof Error ? cause.message : String(cause)
795
- });
822
+ throw new QueueException(
823
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
824
+ HttpStatus.INTERNAL_SERVER_ERROR,
825
+ {
826
+ queueName,
827
+ cause: cause instanceof Error ? cause.message : String(cause)
828
+ }
829
+ );
796
830
  }
797
831
  attachDefaultErrorListener(worker, this.logger, "Worker", queueName);
798
832
  __privateGet(this, _entries).set(queueName, { worker, connection: conn });
@@ -828,10 +862,14 @@ var WorkerRegistry = class {
828
862
  worker = new Worker(queueName, processorFile, bullOpts);
829
863
  } catch (cause) {
830
864
  conn.disconnect();
831
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
832
- queueName,
833
- cause: cause instanceof Error ? cause.message : String(cause)
834
- });
865
+ throw new QueueException(
866
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
867
+ HttpStatus.INTERNAL_SERVER_ERROR,
868
+ {
869
+ queueName,
870
+ cause: cause instanceof Error ? cause.message : String(cause)
871
+ }
872
+ );
835
873
  }
836
874
  attachDefaultErrorListener(worker, this.logger, "Worker", queueName);
837
875
  __privateGet(this, _entries).set(queueName, { worker, connection: conn });
@@ -904,7 +942,11 @@ var WorkerRegistry = class {
904
942
  */
905
943
  guardDuplicate(queueName) {
906
944
  if (__privateGet(this, _entries).has(queueName)) {
907
- throw new QueueException(QUEUE_ERROR_CODES.DUPLICATE_PROCESSOR, 500, { queueName });
945
+ throw new QueueException(
946
+ QUEUE_ERROR_CODES.DUPLICATE_PROCESSOR,
947
+ HttpStatus.INTERNAL_SERVER_ERROR,
948
+ { queueName }
949
+ );
908
950
  }
909
951
  }
910
952
  /**
@@ -918,30 +960,46 @@ var WorkerRegistry = class {
918
960
  validateOptions(opts) {
919
961
  if (opts?.concurrency !== void 0) {
920
962
  if (opts.concurrency < 1) {
921
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
922
- reason: "concurrency must be >= 1",
923
- actual: opts.concurrency
924
- });
963
+ throw new QueueException(
964
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
965
+ HttpStatus.INTERNAL_SERVER_ERROR,
966
+ {
967
+ reason: "concurrency must be >= 1",
968
+ actual: opts.concurrency
969
+ }
970
+ );
925
971
  }
926
972
  if (opts.concurrency > MAX_WORKER_CONCURRENCY) {
927
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
928
- reason: `concurrency must be <= ${MAX_WORKER_CONCURRENCY.toString()}`,
929
- actual: opts.concurrency
930
- });
973
+ throw new QueueException(
974
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
975
+ HttpStatus.INTERNAL_SERVER_ERROR,
976
+ {
977
+ reason: `concurrency must be <= ${MAX_WORKER_CONCURRENCY.toString()}`,
978
+ actual: opts.concurrency
979
+ }
980
+ );
931
981
  }
932
982
  }
933
983
  if (opts?.limiter !== void 0) {
934
984
  if (opts.limiter.max < 1) {
935
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
936
- reason: "limiter.max must be >= 1",
937
- actual: opts.limiter.max
938
- });
985
+ throw new QueueException(
986
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
987
+ HttpStatus.INTERNAL_SERVER_ERROR,
988
+ {
989
+ reason: "limiter.max must be >= 1",
990
+ actual: opts.limiter.max
991
+ }
992
+ );
939
993
  }
940
994
  if (opts.limiter.duration < 1) {
941
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
942
- reason: "limiter.duration must be >= 1",
943
- actual: opts.limiter.duration
944
- });
995
+ throw new QueueException(
996
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
997
+ HttpStatus.INTERNAL_SERVER_ERROR,
998
+ {
999
+ reason: "limiter.duration must be >= 1",
1000
+ actual: opts.limiter.duration
1001
+ }
1002
+ );
945
1003
  }
946
1004
  }
947
1005
  }
@@ -957,17 +1015,25 @@ var WorkerRegistry = class {
957
1015
  validateProcessorFile(processorFile) {
958
1016
  if (processorFile instanceof URL) {
959
1017
  if (processorFile.protocol !== "file:") {
960
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
961
- reason: "processorFile URL must use the file: protocol"
962
- });
1018
+ throw new QueueException(
1019
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
1020
+ HttpStatus.INTERNAL_SERVER_ERROR,
1021
+ {
1022
+ reason: "processorFile URL must use the file: protocol"
1023
+ }
1024
+ );
963
1025
  }
964
1026
  this.assertProcessorExtension(processorFile.pathname);
965
1027
  return;
966
1028
  }
967
1029
  if (!isAbsolute(processorFile)) {
968
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
969
- reason: "processorFile must be an absolute path"
970
- });
1030
+ throw new QueueException(
1031
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
1032
+ HttpStatus.INTERNAL_SERVER_ERROR,
1033
+ {
1034
+ reason: "processorFile must be an absolute path"
1035
+ }
1036
+ );
971
1037
  }
972
1038
  this.assertProcessorExtension(processorFile);
973
1039
  }
@@ -981,9 +1047,13 @@ var WorkerRegistry = class {
981
1047
  assertProcessorExtension(filePath) {
982
1048
  const allowedExtensions = /* @__PURE__ */ new Set([".js", ".cjs", ".mjs"]);
983
1049
  if (!allowedExtensions.has(extname(filePath))) {
984
- throw new QueueException(QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED, 500, {
985
- reason: "processorFile must have a .js, .cjs, or .mjs extension"
986
- });
1050
+ throw new QueueException(
1051
+ QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
1052
+ HttpStatus.INTERNAL_SERVER_ERROR,
1053
+ {
1054
+ reason: "processorFile must have a .js, .cjs, or .mjs extension"
1055
+ }
1056
+ );
987
1057
  }
988
1058
  }
989
1059
  /**
@@ -1171,7 +1241,11 @@ var ProcessorDiscoveryService = class {
1171
1241
  const { queueName, workerOptions, _warnedNoConcurrency } = processorMeta;
1172
1242
  const ctor = instance.constructor;
1173
1243
  if (this.registeredQueues.has(queueName)) {
1174
- throw new QueueException(QUEUE_ERROR_CODES.DUPLICATE_PROCESSOR, 500, { queueName });
1244
+ throw new QueueException(
1245
+ QUEUE_ERROR_CODES.DUPLICATE_PROCESSOR,
1246
+ HttpStatus.INTERNAL_SERVER_ERROR,
1247
+ { queueName }
1248
+ );
1175
1249
  }
1176
1250
  this.registeredQueues.add(queueName);
1177
1251
  if (_warnedNoConcurrency) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-queue",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
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",
@@ -103,7 +103,7 @@
103
103
  "@stryker-mutator/core": "^10.0.0",
104
104
  "@stryker-mutator/jest-runner": "^10.0.0",
105
105
  "@types/jest": "^30.0.0",
106
- "@types/node": "^26.2.0",
106
+ "@types/node": "^24.19.2",
107
107
  "bullmq": "^6.0.0",
108
108
  "eslint": "^10.8.1",
109
109
  "eslint-config-prettier": "^10.1.8",