@bymax-one/nest-queue 1.2.9 → 1.2.11

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,105 @@ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
11
11
 
12
12
  ---
13
13
 
14
+ ## [1.2.11] — 2026-11-06
15
+
16
+ Action required only if you extend or replace `QueueLifecycle`, or rely on the order in which the
17
+ library closes its resources: the producer side now closes after the HTTP server stops, see the Apply
18
+ note. Healthy use is unchanged. The public API and the peer ranges are unchanged; the declaration
19
+ files differ because `QueueLifecycle` now also implements `OnApplicationShutdown` (and in JSDoc).
20
+ Measured with `diff -r` of a fresh build against the unpacked 1.2.10 tarball, the runtime bundles
21
+ (`index.mjs`, `index.cjs`) differ in code.
22
+
23
+ ### Fixed
24
+
25
+ - **A request still being served when the application closed could not enqueue.** NestJS runs every
26
+ `onModuleDestroy` hook before it stops the HTTP server, and `QueueLifecycle` closed the flow
27
+ producer, the cached queues and the library-owned Redis connection in that hook. A request that
28
+ enqueued in the window between the two (a request in flight during `app.close()`, or a
29
+ `SIGTERM` with shutdown hooks enabled) failed with `Connection is closed.` in Mode B. Those three
30
+ resources are now released in `onApplicationShutdown`, which NestJS runs after the server has
31
+ stopped. Consumption still stops first: workers drain within `shutdown.drainTimeoutMs`, then
32
+ queue-events and the connections opened for workers and queue-events close, all in
33
+ `onModuleDestroy`. `shutdown.drainOnShutdown` now runs in `onApplicationShutdown` too, right before
34
+ the queues close, so a job enqueued late is drained with the rest. The log line `Queue shutdown
35
+ complete in Nms (forced N worker(s))` is now written once, at the end, and its time covers both
36
+ hooks. In Mode A the consumer's client is untouched, as before.
37
+
38
+ ### Changed
39
+
40
+ - **The shipped bundles no longer carry comments.** `dist/server/index.mjs` and `index.cjs` are
41
+ smaller; the declaration files keep their JSDoc, and the comment removal does not change the
42
+ runtime behaviour of the bundles.
43
+
44
+ ### Documentation
45
+
46
+ - **The README describes the two shutdown hooks** and when each resource closes.
47
+
48
+ #### Apply to a derived backend
49
+
50
+ - If you subclass or wrap `QueueLifecycle`, or call `onModuleDestroy()` yourself to tear the library
51
+ down in a test, also call `onApplicationShutdown()` afterwards: the flow producer, the cached
52
+ queues and the library-owned connection are no longer closed by `onModuleDestroy()`. Nest calls
53
+ both when the application closes.
54
+ - If you enqueue from a hook that runs after the library's `onModuleDestroy` (for example your own
55
+ `beforeApplicationShutdown`), the enqueue now succeeds instead of failing with
56
+ `Connection is closed.`; if you worked around that failure, the workaround can go.
57
+
58
+ ---
59
+
60
+ ## [1.2.10] — 2026-11-01
61
+
62
+ Action required only if code relies on the status of a failed schedule: `upsertJobScheduler` no
63
+ longer reports every failure as `INVALID_REPEAT_OPTIONS` (400), see the Apply note. Healthy use is
64
+ unchanged. The public API, the types (the declaration files differ only in JSDoc comments) and the
65
+ peer ranges are unchanged. Measured with `diff -r` of a fresh build against the unpacked 1.2.9 tarball,
66
+ the runtime bundles (`index.mjs`, `index.cjs`) differ in code.
67
+
68
+ ### Fixed
69
+
70
+ - **A failed schedule write was reported as an invalid cron.** `upsertJobScheduler` turned every
71
+ error without a string `code` into `INVALID_REPEAT_OPTIONS` (400, "pattern must be a valid cron
72
+ expression"). A Redis reply error carries no `code`, so a replica refusing the write (`READONLY`),
73
+ an out-of-memory server (`OOM`) or a missing login (`NOAUTH`) was reported as the caller's mistake,
74
+ and the caller was told not to retry. The pattern is now checked first with BullMQ's own repeat
75
+ strategy, a pure function with no Redis call, and only a pattern it refuses is
76
+ `INVALID_REPEAT_OPTIONS`; every failure of the write itself reaches the caller as it is. A
77
+ consumer-supplied `settings.repeatStrategy` replaces the parser, so the pattern is not checked.
78
+ A malformed pattern on a schedule whose iteration `limit` is already reached is now rejected as
79
+ well; BullMQ's own upsert returns before parsing such a schedule and silently accepts it.
80
+ - **A failing `@OnWorkerEvent` or `@OnQueueEvent` handler could end the process.** The listener was
81
+ bound straight onto the emitter, which neither awaits it nor catches a throw: an async handler
82
+ that rejected became an unhandled rejection, which ends a Node process by default, and a
83
+ synchronous throw was re-emitted as `error`. The failure is now logged with the handler's name
84
+ and the code `queue.event_handler_failed`, and the process carries on.
85
+ - **`drainOnShutdown` failed silently.** A queue whose drain failed kept its waiting jobs with no
86
+ trace in the log; it now logs a warning naming the queue and the cause.
87
+ - **A worker whose close failed was logged as a drain timeout.** Any rejection of the graceful
88
+ close was reported as "exceeded the drain budget"; a real error, such as a lost connection, now
89
+ has its own message with the cause. The worker is force-closed either way.
90
+
91
+ ### Documentation
92
+
93
+ - **A duplicate does not add a job.** The README and the JSDoc of `enqueue`, `enqueueBulk` and the
94
+ flow methods now say that when a custom `jobId` already exists, or a `deduplication` rule keeps
95
+ the job already there, BullMQ answers with the id of the existing job together with the data of
96
+ this call, so the returned object does not describe what is stored; `getJob` returns it.
97
+ - **The scheduler and event-handler behaviour above is described in the README.**
98
+ - **Correction to 1.2.9.** On 1.2.8 only the `FlowProducer` listener could end the process; on
99
+ `Queue`, `Worker` and `QueueEvents` BullMQ's wrapper re-emitted the `TypeError` and the error was
100
+ reported with a misleading log line. 1.2.9 fixes both.
101
+
102
+ #### Apply to a derived backend
103
+
104
+ - If you branch on `INVALID_REPEAT_OPTIONS` to detect any failure of `upsertJobScheduler`, change
105
+ it: a valid pattern whose write fails now throws the original error (a Redis reply error, a
106
+ connection error), not a 400 `QueueException`. An unparseable pattern is still
107
+ `INVALID_REPEAT_OPTIONS`, now thrown before Redis is asked.
108
+ - A listener that fails is now logged instead of ending the process. If something relied on the
109
+ crash to stop a faulty deploy, watch for the log code `queue.event_handler_failed`.
110
+
111
+ ---
112
+
14
113
  ## [1.2.9] — 2026-11-01
15
114
 
16
115
  No action required: behaviour for an `Error` with a string `message` is unchanged, and the public API, the types
@@ -768,7 +867,9 @@ v6 peer range.
768
867
 
769
868
  ---
770
869
 
771
- [Unreleased]: https://github.com/bymaxone/nest-queue/compare/v1.2.9...HEAD
870
+ [Unreleased]: https://github.com/bymaxone/nest-queue/compare/v1.2.11...HEAD
871
+ [1.2.11]: https://github.com/bymaxone/nest-queue/compare/v1.2.10...v1.2.11
872
+ [1.2.10]: https://github.com/bymaxone/nest-queue/compare/v1.2.9...v1.2.10
772
873
  [1.2.9]: https://github.com/bymaxone/nest-queue/compare/v1.2.8...v1.2.9
773
874
  [1.2.8]: https://github.com/bymaxone/nest-queue/compare/v1.2.7...v1.2.8
774
875
  [1.2.7]: https://github.com/bymaxone/nest-queue/compare/v1.2.6...v1.2.7
package/README.md CHANGED
@@ -235,20 +235,20 @@ export class EmailModule {}
235
235
 
236
236
  Every option passed to `forRoot` / `forRootAsync`. Only `connection` is required.
237
237
 
238
- | Option | Type | Default | Description |
239
- | -------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
240
- | `connection` | `{ client } \| { url, options? } \| { options }` | — | **Required.** Mode A supplies an existing `ioredis` client; Mode B lets the library open its own. The arms are mutually exclusive. |
241
- | `prefix` | `string` | `'bull'` | Redis key namespace. Applied to the `Queue`, `Worker`, `QueueEvents` **and** `FlowProducer` — see [Architecture](#-architecture). |
242
- | `defaultJobOptions` | `JobsOptions` | `attempts: 3` · exponential backoff `2000` ms · `removeOnComplete: { age: 24 h, count: 1000 }` · `removeOnFail: { age: 7 d, count: 5000 }` | Merged into every job enqueued through the service; per-job options win. The bounded `removeOn*` caps are what keep Redis from growing without limit under a failure storm. |
243
- | `queueOptions` | `Partial<QueueOptions>` | `{}` | Extra BullMQ `Queue` options. `connection`, `defaultJobOptions` and `prefix` are owned by the library and cannot be set here. |
244
- | `flows.enabled` | `boolean` | `false` | `FlowService` is always provided; while disabled, every method except `close()` throws `FLOW_DISABLED`. |
245
- | `metrics.enabled` | `boolean` | `false` | `MetricsService` is always provided; while disabled, every method throws `METRICS_DISABLED`. |
246
- | `metrics.cacheTtlMs` | `number` | `5_000` | TTL of the metrics snapshot cache. `0` disables caching. Must be `>= 0`. |
247
- | `telemetry` | `Telemetry` | — | A BullMQ `Telemetry` instance (typically `new BullMQOtel(...)` from the optional `bullmq-otel` peer), attached to every `Queue` and `Worker`. |
248
- | `shutdown.drainTimeoutMs` | `number` | `30_000` | Upper bound on the graceful drain before workers are force-closed. Must be `> 0`. |
249
- | `shutdown.drainOnShutdown` | `boolean` | `false` | Development and test only: after the workers close, call `drain()` on every cached queue, which **removes** its waiting jobs (delayed and active jobs are left alone). Leave it `false` in production. |
250
- | `connectionReadyTimeoutMs` | `number` | `10_000` | Mode B only: how long to wait for Redis `ready` before failing with `CONNECTION_TIMEOUT`. |
251
- | `isGlobal` | `boolean` | `true` | Register the module globally, so `QueueService` is injectable without re-importing. |
238
+ | Option | Type | Default | Description |
239
+ | -------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
240
+ | `connection` | `{ client } \| { url, options? } \| { options }` | — | **Required.** Mode A supplies an existing `ioredis` client; Mode B lets the library open its own. The arms are mutually exclusive. |
241
+ | `prefix` | `string` | `'bull'` | Redis key namespace. Applied to the `Queue`, `Worker`, `QueueEvents` **and** `FlowProducer` — see [Architecture](#-architecture). |
242
+ | `defaultJobOptions` | `JobsOptions` | `attempts: 3` · exponential backoff `2000` ms · `removeOnComplete: { age: 24 h, count: 1000 }` · `removeOnFail: { age: 7 d, count: 5000 }` | Merged into every job enqueued through the service; per-job options win. The bounded `removeOn*` caps are what keep Redis from growing without limit under a failure storm. |
243
+ | `queueOptions` | `Partial<QueueOptions>` | `{}` | Extra BullMQ `Queue` options. `connection`, `defaultJobOptions` and `prefix` are owned by the library and cannot be set here. |
244
+ | `flows.enabled` | `boolean` | `false` | `FlowService` is always provided; while disabled, every method except `close()` throws `FLOW_DISABLED`. |
245
+ | `metrics.enabled` | `boolean` | `false` | `MetricsService` is always provided; while disabled, every method throws `METRICS_DISABLED`. |
246
+ | `metrics.cacheTtlMs` | `number` | `5_000` | TTL of the metrics snapshot cache. `0` disables caching. Must be `>= 0`. |
247
+ | `telemetry` | `Telemetry` | — | A BullMQ `Telemetry` instance (typically `new BullMQOtel(...)` from the optional `bullmq-otel` peer), attached to every `Queue` and `Worker`. |
248
+ | `shutdown.drainTimeoutMs` | `number` | `30_000` | Upper bound on the graceful drain before workers are force-closed. Must be `> 0`. |
249
+ | `shutdown.drainOnShutdown` | `boolean` | `false` | Development and test only: once the HTTP server has stopped, call `drain()` on every cached queue, which **removes** its waiting jobs (delayed and active jobs are left alone). Leave it `false` in production. |
250
+ | `connectionReadyTimeoutMs` | `number` | `10_000` | Mode B only: how long to wait for Redis `ready` before failing with `CONNECTION_TIMEOUT`. |
251
+ | `isGlobal` | `boolean` | `true` | Register the module globally, so `QueueService` is injectable without re-importing. |
252
252
 
253
253
  > [!WARNING]
254
254
  > **`shutdown.drainOnShutdown` deletes jobs.** When it is `true`, shutdown calls `drain()` on every
@@ -557,6 +557,8 @@ onFailed(job: Job | undefined, err: Error): void {
557
557
  }
558
558
  ```
559
559
 
560
+ A listener that throws, or returns a promise that rejects, is logged with the handler's name and the code `queue.event_handler_failed` and the process carries on; it is never an unhandled rejection. Handle the error inside the listener when the failure matters.
561
+
560
562
  ### `@OnQueueEvent(eventName)`
561
563
 
562
564
  Listens to BullMQ `QueueEvents` (global, cross-process events). The library opens one shared `QueueEvents` connection per queue when it discovers the first listener for it; there is no option to enable.
@@ -602,6 +604,7 @@ const schedulers = await this.queue.getJobSchedulers('notifications')
602
604
 
603
605
  Cron-with-seconds (6-field patterns) is supported. Patterns are validated by BullMQ's
604
606
  own `cron-parser`, not a hand-rolled regex.
607
+ A pattern the parser rejects is `INVALID_REPEAT_OPTIONS` (400) before Redis is asked. Every other failure of the upsert, such as a read-only replica or a lost connection, reaches you as it is.
605
608
 
606
609
  ---
607
610
 
@@ -643,6 +646,10 @@ await this.queue.enqueue(
643
646
  )
644
647
  ```
645
648
 
649
+ ### A duplicate does not add a job
650
+
651
+ When a custom `jobId` already exists, or a `deduplication` rule keeps the job that is already there (a rule with `replace` swaps it for the new one instead), `enqueue`, `enqueueBulk` and the flow methods add nothing: BullMQ answers with the id of the **existing** job together with the data and options of **this** call, so the returned object does not describe what is stored. Call `QueueService.getJob` when you need the stored job.
652
+
646
653
  ### OpenTelemetry
647
654
 
648
655
  Pass a `bullmq-otel` telemetry instance (optional peer dep) to trace spans from `enqueue` through the handler:
@@ -660,7 +667,7 @@ BymaxQueueModule.forRoot({
660
667
 
661
668
  ## 🛑 Graceful Shutdown
662
669
 
663
- `QueueLifecycle` runs automatically when NestJS closes the application context, through its `onModuleDestroy` hook (call `app.enableShutdownHooks()` so that a termination signal closes the context).
670
+ `QueueLifecycle` runs automatically when NestJS closes the application context, through its `onModuleDestroy` and `onApplicationShutdown` hooks (call `app.enableShutdownHooks()` so that a termination signal closes the context).
664
671
  It implements a **bounded drain protocol** with at-least-once semantics:
665
672
 
666
673
  1. All workers are closed with a grace period (`drainTimeoutMs`, default 30 s).
@@ -668,7 +675,11 @@ It implements a **bounded drain protocol** with at-least-once semantics:
668
675
  timeout fires — whichever comes first.
669
676
  3. On timeout, the worker is force-closed (`worker.close(true)`), preserving stalled
670
677
  jobs for reprocessing by the next instance.
671
- 4. In Mode B, Redis connections are disconnected after all workers are closed.
678
+ 4. Queue-events and the connections the library opened for workers and queue-events are closed.
679
+ 5. Once NestJS has stopped the HTTP server (`onApplicationShutdown`), the flow producer, the
680
+ cached queues and, in Mode B, the library's own Redis connection are closed. NestJS runs
681
+ `onModuleDestroy` before it stops the HTTP server, so a request that is still being served can
682
+ enqueue a job after step 4; those resources stay open until the server is down.
672
683
 
673
684
  **Handlers must be idempotent.** Use `jobId`, `deduplication`, or application-level
674
685
  idempotency keys to guard against reprocessing a job that was partially handled before