@bymax-one/nest-queue 1.2.5 → 1.2.7

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
@@ -11,6 +11,56 @@ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
11
11
 
12
12
  ---
13
13
 
14
+ ## [1.2.7] — 2026-10-31
15
+
16
+ Action required if your `connection.url` can be empty or malformed: an invalid or empty
17
+ `connection.url` now fails the boot with `INVALID_OPTIONS`; before 1.2.7 it silently connected to
18
+ `localhost:6379`. A valid `url` needs no change. The public API, the types (`index.d.ts` and
19
+ `index.d.cts` are identical) and the peer ranges are unchanged. Measured with `diff -r` of a fresh
20
+ build against the unpacked 1.2.6 tarball, only the two runtime bundles (`index.mjs`, `index.cjs`)
21
+ differ.
22
+
23
+ ### Fixed
24
+
25
+ - **`connection.url` is now validated.** `forRoot` refuses a `url` that is not a non-empty string
26
+ with `INVALID_OPTIONS` ("connection.url must be a non-empty string"). Before, `ioredis` read an
27
+ absent, empty, whitespace-only, `null` or object `url` as no url at all and connected to
28
+ `localhost:6379`, and a number as a port, so a missing `REDIS_URL` surfaced as a connection to the
29
+ wrong server instead of an error. A string `ioredis` cannot parse (`not a url`, `redis://h:abc`)
30
+ is refused the same way ("connection.url is not a valid Redis URL"), where it used to throw a raw
31
+ `Invalid URL` from the client constructor. Unix socket paths, `host:port` and `redis://` /
32
+ `rediss://` URLs, with or without `options`, are unaffected.
33
+ - **A worker that fails to start no longer hides the failure behind a second error.** The
34
+ `WORKER_REGISTRATION_FAILED` details were built with `String(cause)`, which throws a `TypeError`
35
+ for a null-prototype value; the cause is now described by a helper that cannot throw, so the
36
+ registration failure reaches the caller as the typed exception.
37
+
38
+ ---
39
+
40
+ ## [1.2.6] — 2026-10-31
41
+
42
+ No action required: behaviour, the public API, the types and the peer ranges are unchanged.
43
+ Measured with `diff -r` of a fresh build against the unpacked 1.2.5 tarball, `dist/` is identical;
44
+ the package otherwise changes in `README.md`, this file and the version in `package.json`.
45
+
46
+ ### Documentation
47
+
48
+ - **The README Quick Start now shows where `EmailService` and the processor are registered.** The
49
+ `AppModule` snippet imports an `EmailModule` that lists both as providers, so a reader who copies
50
+ the snippets in order ends with a module graph that resolves, instead of a service no module provides
51
+ and a processor module the root never imports.
52
+
53
+ ### Tests
54
+
55
+ - **The README Quick Start now runs.** A spec boots a Nest application context from the module
56
+ options, the `QueueService` enqueue and the `@Processor` / `@Process` handler copied from the
57
+ README (handler body aside), with BullMQ and ioredis replaced by in-memory doubles, and asserts
58
+ the job the service enqueues is the job the decorated handler receives. The snippet gate only
59
+ compiles the README; this executes it, so a wrong argument shape or an unresolved dependency fails
60
+ a test.
61
+
62
+ ---
63
+
14
64
  ## [1.2.5] — 2026-10-31
15
65
 
16
66
  No action required: behaviour, the public API, the types and the peer ranges are unchanged.
@@ -678,7 +728,9 @@ v6 peer range.
678
728
 
679
729
  ---
680
730
 
681
- [Unreleased]: https://github.com/bymaxone/nest-queue/compare/v1.2.5...HEAD
731
+ [Unreleased]: https://github.com/bymaxone/nest-queue/compare/v1.2.7...HEAD
732
+ [1.2.7]: https://github.com/bymaxone/nest-queue/compare/v1.2.6...v1.2.7
733
+ [1.2.6]: https://github.com/bymaxone/nest-queue/compare/v1.2.5...v1.2.6
682
734
  [1.2.5]: https://github.com/bymaxone/nest-queue/compare/v1.2.4...v1.2.5
683
735
  [1.2.4]: https://github.com/bymaxone/nest-queue/compare/v1.2.3...v1.2.4
684
736
  [1.2.3]: https://github.com/bymaxone/nest-queue/compare/v1.2.2...v1.2.3
package/README.md CHANGED
@@ -168,6 +168,7 @@ Wire the module in your root `AppModule`:
168
168
  ```typescript
169
169
  import { Module } from '@nestjs/common'
170
170
  import { BymaxQueueModule } from '@bymax-one/nest-queue'
171
+ import { EmailModule } from './email.module'
171
172
 
172
173
  @Module({
173
174
  imports: [
@@ -175,6 +176,7 @@ import { BymaxQueueModule } from '@bymax-one/nest-queue'
175
176
  connection: { url: process.env.REDIS_URL ?? 'redis://localhost:6379' },
176
177
  isGlobal: true,
177
178
  }),
179
+ EmailModule,
178
180
  ],
179
181
  })
180
182
  export class AppModule {}
@@ -213,14 +215,16 @@ export class EmailProcessor {
213
215
  }
214
216
  ```
215
217
 
216
- Register the processor in your module alongside `BymaxQueueModule`:
218
+ Register the service and the processor in a feature module that `AppModule` imports, like the `EmailModule`
219
+ imported above. `isGlobal: true` above makes `QueueService` injectable there without importing `BymaxQueueModule` again:
217
220
 
218
221
  ```typescript
219
222
  import { Module } from '@nestjs/common'
220
223
  import { EmailProcessor } from './email.processor'
224
+ import { EmailService } from './email.service'
221
225
 
222
226
  @Module({
223
- providers: [EmailProcessor],
227
+ providers: [EmailService, EmailProcessor],
224
228
  })
225
229
  export class EmailModule {}
226
230
  ```
@@ -84,6 +84,19 @@ function invalid(reason) {
84
84
  reason
85
85
  });
86
86
  }
87
+ function isParseableUrl(url) {
88
+ const candidate = /^rediss?:\/\//i.test(url) ? url : url.startsWith("//") ? `redis:${url}` : `redis://${url}`;
89
+ if (!URL.canParse(candidate)) return false;
90
+ const { username, password } = new URL(candidate);
91
+ let decodable = true;
92
+ try {
93
+ decodeURIComponent(username);
94
+ decodeURIComponent(password);
95
+ } catch {
96
+ decodable = false;
97
+ }
98
+ return decodable;
99
+ }
87
100
  function validateOptions(opts) {
88
101
  const cfg = opts.connection;
89
102
  if (cfg === null || typeof cfg !== "object") {
@@ -98,6 +111,15 @@ function validateOptions(opts) {
98
111
  if (hasClient && (hasUrl || hasOptions)) {
99
112
  invalid("connection.client is mutually exclusive with url/options");
100
113
  }
114
+ if (hasUrl) {
115
+ const url = cfg.url;
116
+ if (typeof url !== "string" || url.trim() === "") {
117
+ invalid("connection.url must be a non-empty string");
118
+ }
119
+ if (!isParseableUrl(url)) {
120
+ invalid("connection.url is not a valid Redis URL");
121
+ }
122
+ }
101
123
  const drainTimeoutMs = opts.shutdown?.drainTimeoutMs;
102
124
  if (drainTimeoutMs !== void 0 && drainTimeoutMs <= 0) {
103
125
  invalid("shutdown.drainTimeoutMs must be > 0");
@@ -787,6 +809,20 @@ exports.MetricsService = class MetricsService {
787
809
  exports.MetricsService = __decorateClass([
788
810
  common.Injectable()
789
811
  ], exports.MetricsService);
812
+
813
+ // src/server/utils/describe-error.ts
814
+ function describeError(cause) {
815
+ try {
816
+ if (typeof cause === "object" && cause !== null && "message" in cause) {
817
+ if (typeof cause.message === "string") return cause.message;
818
+ }
819
+ return String(cause);
820
+ } catch {
821
+ return "unknown error";
822
+ }
823
+ }
824
+
825
+ // src/server/services/worker-registry.service.ts
790
826
  var _entries;
791
827
  exports.WorkerRegistry = class WorkerRegistry {
792
828
  constructor(connection, options) {
@@ -826,7 +862,7 @@ exports.WorkerRegistry = class WorkerRegistry {
826
862
  common.HttpStatus.INTERNAL_SERVER_ERROR,
827
863
  {
828
864
  queueName,
829
- cause: cause instanceof Error ? cause.message : String(cause)
865
+ cause: describeError(cause)
830
866
  }
831
867
  );
832
868
  }
@@ -869,7 +905,7 @@ exports.WorkerRegistry = class WorkerRegistry {
869
905
  common.HttpStatus.INTERNAL_SERVER_ERROR,
870
906
  {
871
907
  queueName,
872
- cause: cause instanceof Error ? cause.message : String(cause)
908
+ cause: describeError(cause)
873
909
  }
874
910
  );
875
911
  }
@@ -82,6 +82,19 @@ function invalid(reason) {
82
82
  reason
83
83
  });
84
84
  }
85
+ function isParseableUrl(url) {
86
+ const candidate = /^rediss?:\/\//i.test(url) ? url : url.startsWith("//") ? `redis:${url}` : `redis://${url}`;
87
+ if (!URL.canParse(candidate)) return false;
88
+ const { username, password } = new URL(candidate);
89
+ let decodable = true;
90
+ try {
91
+ decodeURIComponent(username);
92
+ decodeURIComponent(password);
93
+ } catch {
94
+ decodable = false;
95
+ }
96
+ return decodable;
97
+ }
85
98
  function validateOptions(opts) {
86
99
  const cfg = opts.connection;
87
100
  if (cfg === null || typeof cfg !== "object") {
@@ -96,6 +109,15 @@ function validateOptions(opts) {
96
109
  if (hasClient && (hasUrl || hasOptions)) {
97
110
  invalid("connection.client is mutually exclusive with url/options");
98
111
  }
112
+ if (hasUrl) {
113
+ const url = cfg.url;
114
+ if (typeof url !== "string" || url.trim() === "") {
115
+ invalid("connection.url must be a non-empty string");
116
+ }
117
+ if (!isParseableUrl(url)) {
118
+ invalid("connection.url is not a valid Redis URL");
119
+ }
120
+ }
99
121
  const drainTimeoutMs = opts.shutdown?.drainTimeoutMs;
100
122
  if (drainTimeoutMs !== void 0 && drainTimeoutMs <= 0) {
101
123
  invalid("shutdown.drainTimeoutMs must be > 0");
@@ -785,6 +807,20 @@ var MetricsService = class {
785
807
  MetricsService = __decorateClass([
786
808
  Injectable()
787
809
  ], MetricsService);
810
+
811
+ // src/server/utils/describe-error.ts
812
+ function describeError(cause) {
813
+ try {
814
+ if (typeof cause === "object" && cause !== null && "message" in cause) {
815
+ if (typeof cause.message === "string") return cause.message;
816
+ }
817
+ return String(cause);
818
+ } catch {
819
+ return "unknown error";
820
+ }
821
+ }
822
+
823
+ // src/server/services/worker-registry.service.ts
788
824
  var _entries;
789
825
  var WorkerRegistry = class {
790
826
  constructor(connection, options) {
@@ -824,7 +860,7 @@ var WorkerRegistry = class {
824
860
  HttpStatus.INTERNAL_SERVER_ERROR,
825
861
  {
826
862
  queueName,
827
- cause: cause instanceof Error ? cause.message : String(cause)
863
+ cause: describeError(cause)
828
864
  }
829
865
  );
830
866
  }
@@ -867,7 +903,7 @@ var WorkerRegistry = class {
867
903
  HttpStatus.INTERNAL_SERVER_ERROR,
868
904
  {
869
905
  queueName,
870
- cause: cause instanceof Error ? cause.message : String(cause)
906
+ cause: describeError(cause)
871
907
  }
872
908
  );
873
909
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-queue",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
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",