@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 +53 -1
- package/README.md +6 -2
- package/dist/server/index.cjs +38 -2
- package/dist/server/index.mjs +38 -2
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
```
|
package/dist/server/index.cjs
CHANGED
|
@@ -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:
|
|
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:
|
|
908
|
+
cause: describeError(cause)
|
|
873
909
|
}
|
|
874
910
|
);
|
|
875
911
|
}
|
package/dist/server/index.mjs
CHANGED
|
@@ -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:
|
|
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:
|
|
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.
|
|
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",
|