@bymax-one/nest-queue 1.2.11 → 1.2.12
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 +30 -1
- package/README.md +8 -4
- package/dist/server/index.cjs +36 -13
- package/dist/server/index.mjs +37 -14
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,34 @@ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## [1.2.12] — 2026-11-09
|
|
15
|
+
|
|
16
|
+
No action required. The public API, the declaration files and the peer ranges are unchanged
|
|
17
|
+
(`check-api-surface` reports no difference against 1.2.11); the runtime bundles (`index.mjs`,
|
|
18
|
+
`index.cjs`) differ in code, measured with `diff -r` of a fresh build against the unpacked 1.2.11
|
|
19
|
+
tarball. The package also ships a corrected README.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **An outage on a connection the library opens no longer prints `[ioredis] Unhandled error event`
|
|
24
|
+
to stderr.** In Mode B the owned connection listened for `error` only while it waited for
|
|
25
|
+
`ready`, so until BullMQ built something on it (BullMQ adds its own listener to a client it is
|
|
26
|
+
handed) a later outage reached ioredis with no listener, and ioredis wrote one line per
|
|
27
|
+
reconnect attempt to raw stderr, outside the structured logger. The owned connection now keeps a
|
|
28
|
+
listener for its whole life that logs through the module's logger. The same holds for the
|
|
29
|
+
duplicate opened for each `QueueEvents` (BullMQ listens on its own copy, not on this one) and for
|
|
30
|
+
the probe the Mode A check duplicates. Every error is logged as it arrives, with no throttling.
|
|
31
|
+
A client you pass in Mode A is yours and the library adds no listener to it.
|
|
32
|
+
|
|
33
|
+
### Documentation
|
|
34
|
+
|
|
35
|
+
- **The README's Mode A example now compiles and runs.** It imported `CACHE_REDIS_CLIENT` from
|
|
36
|
+
`@bymax-one/nest-cache`, a name that package does not export. The example now injects
|
|
37
|
+
`ConnectionManager` and passes `cache.getClient()`, and says that `getClient()` is typed
|
|
38
|
+
`Redis | Cluster` while Mode A takes a standalone `Redis`.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
14
42
|
## [1.2.11] — 2026-11-06
|
|
15
43
|
|
|
16
44
|
Action required only if you extend or replace `QueueLifecycle`, or rely on the order in which the
|
|
@@ -867,7 +895,8 @@ v6 peer range.
|
|
|
867
895
|
|
|
868
896
|
---
|
|
869
897
|
|
|
870
|
-
[Unreleased]: https://github.com/bymaxone/nest-queue/compare/v1.2.
|
|
898
|
+
[Unreleased]: https://github.com/bymaxone/nest-queue/compare/v1.2.12...HEAD
|
|
899
|
+
[1.2.12]: https://github.com/bymaxone/nest-queue/compare/v1.2.11...v1.2.12
|
|
871
900
|
[1.2.11]: https://github.com/bymaxone/nest-queue/compare/v1.2.10...v1.2.11
|
|
872
901
|
[1.2.10]: https://github.com/bymaxone/nest-queue/compare/v1.2.9...v1.2.10
|
|
873
902
|
[1.2.9]: https://github.com/bymaxone/nest-queue/compare/v1.2.8...v1.2.9
|
package/README.md
CHANGED
|
@@ -270,18 +270,22 @@ Worker and QueueEvents connections are duplicated from this client with
|
|
|
270
270
|
|
|
271
271
|
```typescript
|
|
272
272
|
import { BymaxQueueModule } from '@bymax-one/nest-queue'
|
|
273
|
-
import {
|
|
273
|
+
import { ConnectionManager } from '@bymax-one/nest-cache'
|
|
274
274
|
import type { Redis } from 'ioredis'
|
|
275
275
|
|
|
276
276
|
BymaxQueueModule.forRootAsync({
|
|
277
|
-
inject: [
|
|
278
|
-
useFactory: (
|
|
279
|
-
connection: { client:
|
|
277
|
+
inject: [ConnectionManager],
|
|
278
|
+
useFactory: (cache: ConnectionManager) => ({
|
|
279
|
+
connection: { client: cache.getClient() as Redis },
|
|
280
280
|
isGlobal: true,
|
|
281
281
|
}),
|
|
282
282
|
})
|
|
283
283
|
```
|
|
284
284
|
|
|
285
|
+
`BymaxCacheModule` is global by default and provides `ConnectionManager`; `getClient()` returns the shared
|
|
286
|
+
client. It is typed `Redis | Cluster`, and Mode A takes a standalone `Redis`, so run
|
|
287
|
+
`@bymax-one/nest-cache` in standalone mode.
|
|
288
|
+
|
|
285
289
|
Mode A is the recommended setup when you already have `@bymax-one/nest-cache` in the app.
|
|
286
290
|
One client is shared; the lib duplicates it per role and applies the correct retry policy
|
|
287
291
|
without touching your cache connection.
|
package/dist/server/index.cjs
CHANGED
|
@@ -192,15 +192,46 @@ function isClientUsable(client) {
|
|
|
192
192
|
return client.status === "ready" || client.status === "connecting";
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
// src/server/utils/describe-error.ts
|
|
196
|
+
function describeError(cause) {
|
|
197
|
+
try {
|
|
198
|
+
if (typeof cause === "object" && cause !== null && "message" in cause) {
|
|
199
|
+
if (typeof cause.message === "string") return cause.message;
|
|
200
|
+
}
|
|
201
|
+
return String(cause);
|
|
202
|
+
} catch {
|
|
203
|
+
return "unknown error";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/server/utils/attach-client-error-listener.ts
|
|
208
|
+
function describeClientError(err) {
|
|
209
|
+
const message = describeError(err);
|
|
210
|
+
if (message !== "") return message;
|
|
211
|
+
try {
|
|
212
|
+
const { name, code } = err;
|
|
213
|
+
const label = typeof name === "string" && name !== "" ? name : "unknown error";
|
|
214
|
+
return typeof code === "string" ? `${label} [${code}]` : label;
|
|
215
|
+
} catch {
|
|
216
|
+
return "unknown error";
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function attachClientErrorListener(client, logger, role) {
|
|
220
|
+
client.on("error", (err) => {
|
|
221
|
+
logger.error(`Redis connection (${role}) emitted an error: ${describeClientError(err)}`);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
195
225
|
// src/server/utils/duplicate-connection.ts
|
|
196
226
|
function duplicateConnection(client) {
|
|
197
227
|
return client.duplicate({ maxRetriesPerRequest: null });
|
|
198
228
|
}
|
|
199
229
|
|
|
200
230
|
// src/server/services/connection-resolver.service.ts
|
|
201
|
-
var _client, _options;
|
|
231
|
+
var _logger, _client, _options;
|
|
202
232
|
exports.ConnectionResolver = class ConnectionResolver {
|
|
203
233
|
constructor(options) {
|
|
234
|
+
__privateAdd(this, _logger, new common.Logger(exports.ConnectionResolver.name));
|
|
204
235
|
__privateAdd(this, _client);
|
|
205
236
|
__privateAdd(this, _options);
|
|
206
237
|
__publicField(this, "mode");
|
|
@@ -214,6 +245,7 @@ exports.ConnectionResolver = class ConnectionResolver {
|
|
|
214
245
|
}
|
|
215
246
|
this.mode = "mode-b-owned";
|
|
216
247
|
__privateSet(this, _client, "url" in cfg ? new ioredis.Redis(cfg.url, this.ownedOptions(cfg.options)) : new ioredis.Redis(this.ownedOptions(cfg.options)));
|
|
248
|
+
attachClientErrorListener(__privateGet(this, _client), __privateGet(this, _logger), "owned");
|
|
217
249
|
await this.waitReady(
|
|
218
250
|
__privateGet(this, _options).connectionReadyTimeoutMs ?? DEFAULT_CONNECTION_READY_TIMEOUT_MS
|
|
219
251
|
);
|
|
@@ -267,6 +299,7 @@ exports.ConnectionResolver = class ConnectionResolver {
|
|
|
267
299
|
);
|
|
268
300
|
}
|
|
269
301
|
const probe = duplicateConnection(client);
|
|
302
|
+
attachClientErrorListener(probe, __privateGet(this, _logger), "probe");
|
|
270
303
|
try {
|
|
271
304
|
assertBlockingConnection(probe);
|
|
272
305
|
} finally {
|
|
@@ -305,6 +338,7 @@ exports.ConnectionResolver = class ConnectionResolver {
|
|
|
305
338
|
});
|
|
306
339
|
}
|
|
307
340
|
};
|
|
341
|
+
_logger = new WeakMap();
|
|
308
342
|
_client = new WeakMap();
|
|
309
343
|
_options = new WeakMap();
|
|
310
344
|
exports.ConnectionResolver = __decorateClass([
|
|
@@ -343,18 +377,6 @@ function validateJobSchedulerOptions(repeat) {
|
|
|
343
377
|
}
|
|
344
378
|
}
|
|
345
379
|
|
|
346
|
-
// src/server/utils/describe-error.ts
|
|
347
|
-
function describeError(cause) {
|
|
348
|
-
try {
|
|
349
|
-
if (typeof cause === "object" && cause !== null && "message" in cause) {
|
|
350
|
-
if (typeof cause.message === "string") return cause.message;
|
|
351
|
-
}
|
|
352
|
-
return String(cause);
|
|
353
|
-
} catch {
|
|
354
|
-
return "unknown error";
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
380
|
// src/server/utils/attach-error-listener.ts
|
|
359
381
|
function attachDefaultErrorListener(emitter, logger, kind, queueName) {
|
|
360
382
|
emitter.on("error", (err) => {
|
|
@@ -797,6 +819,7 @@ exports.QueueEventsRegistry = class QueueEventsRegistry {
|
|
|
797
819
|
const existing = __privateGet(this, _events).get(queueName);
|
|
798
820
|
if (existing) return existing;
|
|
799
821
|
const conn = duplicateConnection(this.connection.getClient());
|
|
822
|
+
attachClientErrorListener(conn, this.logger, `queue events: ${queueName}`);
|
|
800
823
|
let qe;
|
|
801
824
|
try {
|
|
802
825
|
qe = new bullmq.QueueEvents(queueName, { connection: conn, prefix: this.options.prefix });
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Injectable, Inject, ConfigurableModuleBuilder, Module,
|
|
1
|
+
import { Injectable, Inject, ConfigurableModuleBuilder, Module, Logger, HttpStatus, 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, defaultRepeatStrategy } from 'bullmq';
|
|
@@ -190,15 +190,46 @@ function isClientUsable(client) {
|
|
|
190
190
|
return client.status === "ready" || client.status === "connecting";
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
// src/server/utils/describe-error.ts
|
|
194
|
+
function describeError(cause) {
|
|
195
|
+
try {
|
|
196
|
+
if (typeof cause === "object" && cause !== null && "message" in cause) {
|
|
197
|
+
if (typeof cause.message === "string") return cause.message;
|
|
198
|
+
}
|
|
199
|
+
return String(cause);
|
|
200
|
+
} catch {
|
|
201
|
+
return "unknown error";
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/server/utils/attach-client-error-listener.ts
|
|
206
|
+
function describeClientError(err) {
|
|
207
|
+
const message = describeError(err);
|
|
208
|
+
if (message !== "") return message;
|
|
209
|
+
try {
|
|
210
|
+
const { name, code } = err;
|
|
211
|
+
const label = typeof name === "string" && name !== "" ? name : "unknown error";
|
|
212
|
+
return typeof code === "string" ? `${label} [${code}]` : label;
|
|
213
|
+
} catch {
|
|
214
|
+
return "unknown error";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function attachClientErrorListener(client, logger, role) {
|
|
218
|
+
client.on("error", (err) => {
|
|
219
|
+
logger.error(`Redis connection (${role}) emitted an error: ${describeClientError(err)}`);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
193
223
|
// src/server/utils/duplicate-connection.ts
|
|
194
224
|
function duplicateConnection(client) {
|
|
195
225
|
return client.duplicate({ maxRetriesPerRequest: null });
|
|
196
226
|
}
|
|
197
227
|
|
|
198
228
|
// src/server/services/connection-resolver.service.ts
|
|
199
|
-
var _client, _options;
|
|
229
|
+
var _logger, _client, _options;
|
|
200
230
|
var ConnectionResolver = class {
|
|
201
231
|
constructor(options) {
|
|
232
|
+
__privateAdd(this, _logger, new Logger(ConnectionResolver.name));
|
|
202
233
|
__privateAdd(this, _client);
|
|
203
234
|
__privateAdd(this, _options);
|
|
204
235
|
__publicField(this, "mode");
|
|
@@ -212,6 +243,7 @@ var ConnectionResolver = class {
|
|
|
212
243
|
}
|
|
213
244
|
this.mode = "mode-b-owned";
|
|
214
245
|
__privateSet(this, _client, "url" in cfg ? new Redis(cfg.url, this.ownedOptions(cfg.options)) : new Redis(this.ownedOptions(cfg.options)));
|
|
246
|
+
attachClientErrorListener(__privateGet(this, _client), __privateGet(this, _logger), "owned");
|
|
215
247
|
await this.waitReady(
|
|
216
248
|
__privateGet(this, _options).connectionReadyTimeoutMs ?? DEFAULT_CONNECTION_READY_TIMEOUT_MS
|
|
217
249
|
);
|
|
@@ -265,6 +297,7 @@ var ConnectionResolver = class {
|
|
|
265
297
|
);
|
|
266
298
|
}
|
|
267
299
|
const probe = duplicateConnection(client);
|
|
300
|
+
attachClientErrorListener(probe, __privateGet(this, _logger), "probe");
|
|
268
301
|
try {
|
|
269
302
|
assertBlockingConnection(probe);
|
|
270
303
|
} finally {
|
|
@@ -303,6 +336,7 @@ var ConnectionResolver = class {
|
|
|
303
336
|
});
|
|
304
337
|
}
|
|
305
338
|
};
|
|
339
|
+
_logger = new WeakMap();
|
|
306
340
|
_client = new WeakMap();
|
|
307
341
|
_options = new WeakMap();
|
|
308
342
|
ConnectionResolver = __decorateClass([
|
|
@@ -341,18 +375,6 @@ function validateJobSchedulerOptions(repeat) {
|
|
|
341
375
|
}
|
|
342
376
|
}
|
|
343
377
|
|
|
344
|
-
// src/server/utils/describe-error.ts
|
|
345
|
-
function describeError(cause) {
|
|
346
|
-
try {
|
|
347
|
-
if (typeof cause === "object" && cause !== null && "message" in cause) {
|
|
348
|
-
if (typeof cause.message === "string") return cause.message;
|
|
349
|
-
}
|
|
350
|
-
return String(cause);
|
|
351
|
-
} catch {
|
|
352
|
-
return "unknown error";
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
378
|
// src/server/utils/attach-error-listener.ts
|
|
357
379
|
function attachDefaultErrorListener(emitter, logger, kind, queueName) {
|
|
358
380
|
emitter.on("error", (err) => {
|
|
@@ -795,6 +817,7 @@ var QueueEventsRegistry = class {
|
|
|
795
817
|
const existing = __privateGet(this, _events).get(queueName);
|
|
796
818
|
if (existing) return existing;
|
|
797
819
|
const conn = duplicateConnection(this.connection.getClient());
|
|
820
|
+
attachClientErrorListener(conn, this.logger, `queue events: ${queueName}`);
|
|
798
821
|
let qe;
|
|
799
822
|
try {
|
|
800
823
|
qe = new QueueEvents(queueName, { connection: conn, prefix: this.options.prefix });
|
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.12",
|
|
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",
|
|
@@ -58,10 +58,12 @@
|
|
|
58
58
|
"check:api": "node scripts/check-api-surface.mjs",
|
|
59
59
|
"check:exports": "attw --pack .",
|
|
60
60
|
"check:mutants": "node scripts/check-mutation-directives.mjs",
|
|
61
|
+
"check:file-length": "node scripts/check-file-length.mjs",
|
|
61
62
|
"check:published": "node scripts/check-published-surface.mjs",
|
|
62
63
|
"clean": "rm -rf dist coverage",
|
|
63
64
|
"lint": "eslint src scripts test",
|
|
64
65
|
"lint:fix": "eslint src scripts test --fix",
|
|
66
|
+
"mutation:const-tables": "node scripts/mutate-const-tables.mjs -- pnpm exec jest --config jest.mutants.config.ts --findRelatedTests {file} --coverage=false --silent",
|
|
65
67
|
"mutation": "stryker run",
|
|
66
68
|
"mutation:full": "node -e \"require('node:fs').rmSync('reports/stryker-incremental.json',{force:true,recursive:true})\" && stryker run",
|
|
67
69
|
"mutation:dry-run": "stryker run --dryRunOnly",
|
|
@@ -95,6 +97,7 @@
|
|
|
95
97
|
},
|
|
96
98
|
"devDependencies": {
|
|
97
99
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
100
|
+
"@bymax-one/nest-cache": "1.2.8",
|
|
98
101
|
"@commitlint/cli": "^21.2.1",
|
|
99
102
|
"@commitlint/config-conventional": "^21.2.0",
|
|
100
103
|
"@eslint/js": "^10.0.1",
|