@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 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.11...HEAD
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 { CACHE_REDIS_CLIENT } from '@bymax-one/nest-cache'
273
+ import { ConnectionManager } from '@bymax-one/nest-cache'
274
274
  import type { Redis } from 'ioredis'
275
275
 
276
276
  BymaxQueueModule.forRootAsync({
277
- inject: [CACHE_REDIS_CLIENT],
278
- useFactory: (queueRedis: Redis) => ({
279
- connection: { client: queueRedis },
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.
@@ -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 });
@@ -1,4 +1,4 @@
1
- import { Injectable, Inject, ConfigurableModuleBuilder, Module, HttpStatus, Logger, HttpException } from '@nestjs/common';
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.11",
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",