@bymax-one/nest-queue 1.2.4 → 1.2.6
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 +96 -1
- package/README.md +93 -62
- package/dist/server/index.cjs +1 -1
- package/dist/server/index.d.cts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,99 @@ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## [1.2.6] — 2026-10-31
|
|
15
|
+
|
|
16
|
+
No action required: behaviour, the public API, the types and the peer ranges are unchanged.
|
|
17
|
+
Measured with `diff -r` of a fresh build against the unpacked 1.2.5 tarball, `dist/` is identical;
|
|
18
|
+
the package otherwise changes in `README.md`, this file and the version in `package.json`.
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
|
|
22
|
+
- **The README Quick Start now shows where `EmailService` and the processor are registered.** The
|
|
23
|
+
`AppModule` snippet imports an `EmailModule` that lists both as providers, so a reader who copies
|
|
24
|
+
the snippets in order ends with a module graph that resolves, instead of a service no module provides
|
|
25
|
+
and a processor module the root never imports.
|
|
26
|
+
|
|
27
|
+
### Tests
|
|
28
|
+
|
|
29
|
+
- **The README Quick Start now runs.** A spec boots a Nest application context from the module
|
|
30
|
+
options, the `QueueService` enqueue and the `@Processor` / `@Process` handler copied from the
|
|
31
|
+
README (handler body aside), with BullMQ and ioredis replaced by in-memory doubles, and asserts
|
|
32
|
+
the job the service enqueues is the job the decorated handler receives. The snippet gate only
|
|
33
|
+
compiles the README; this executes it, so a wrong argument shape or an unresolved dependency fails
|
|
34
|
+
a test.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## [1.2.5] — 2026-10-31
|
|
39
|
+
|
|
40
|
+
No action required: behaviour, the public API, the types and the peer ranges are unchanged.
|
|
41
|
+
Measured with `diff -r` of a fresh build against the unpacked 1.2.4 tarball, `dist/` differs in
|
|
42
|
+
one comment line only, the description of the `drainOnShutdown` step, which sits in the bundles
|
|
43
|
+
(`index.mjs`, `index.cjs`) and in the declarations (`index.d.ts`, `index.d.cts`); the package
|
|
44
|
+
otherwise changes in `README.md`, this file and the version in `package.json`.
|
|
45
|
+
|
|
46
|
+
### Documentation
|
|
47
|
+
|
|
48
|
+
- **Several statements in `README.md` described behaviour the code does not have.** The prose and the
|
|
49
|
+
tables were checked against the source, which the snippet gate does not do:
|
|
50
|
+
- `shutdown.drainOnShutdown` was described as waiting for the queue to drain. It calls `drain()`
|
|
51
|
+
on every cached queue after the workers close, which removes the waiting jobs, so it is for
|
|
52
|
+
development and tests only.
|
|
53
|
+
- `@OnQueueEvent` was said to need `queueEvents: { enabled: true }`. No such option exists; the
|
|
54
|
+
library opens the `QueueEvents` connection when it discovers the first listener.
|
|
55
|
+
- Graceful Shutdown named `onApplicationShutdown`; `QueueLifecycle` runs from `onModuleDestroy`.
|
|
56
|
+
- The Troubleshooting entry for `CONNECTION_REQUIRES_NULL_RETRIES` told Mode A users to pass a
|
|
57
|
+
client built with `maxRetriesPerRequest: null`. Worker connections are `duplicate()` calls that
|
|
58
|
+
force it, so a stock ioredis client never triggers the error; the entry now says so.
|
|
59
|
+
- The stuck-jobs entry pointed `lockDuration` at `workerOptions` (the `options` of `@Processor`
|
|
60
|
+
and `register` are what a consumer sets) and at a `maxStalledCount` setting the library does not
|
|
61
|
+
expose.
|
|
62
|
+
- The Testing section and the badge quoted a mutation score of 99.68% against `break 95`; the
|
|
63
|
+
suite is held at 100.00% against `break 100`. The shared bundle budget is 2 500 B, not 2.5 KiB, and the gate
|
|
64
|
+
chain now includes `pnpm check:published`.
|
|
65
|
+
- `QueueException.details` was described by four keys; it carries more, always scalar. The
|
|
66
|
+
`cleanQueue` row named its second parameter `graceMs`, which is `gracePeriodMs`.
|
|
67
|
+
|
|
68
|
+
- A warning callout under the Configuration table now says plainly that `shutdown.drainOnShutdown`
|
|
69
|
+
deletes the waiting jobs of every queue the process created and must not be enabled
|
|
70
|
+
in production; the table cell alone was easy to miss.
|
|
71
|
+
|
|
72
|
+
- **Two README examples did not compile against the published types.** The OpenTelemetry example
|
|
73
|
+
built `new BullMQOtel('my-service', tracer)`, the `bullmq-otel` 1.x signature; the `^2` peer takes
|
|
74
|
+
an options object, so it is now `new BullMQOtel({ tracerName: 'my-service' })`. The
|
|
75
|
+
`WorkerRegistry.register` example passed `processor:`; the property is `handler`. Two fragments
|
|
76
|
+
also used `path` and `Module` without importing them.
|
|
77
|
+
|
|
78
|
+
- **Further README statements were wrong, and the exports nothing described are now listed.** The
|
|
79
|
+
Configuration table said that injecting `FlowService` or `MetricsService` while disabled throws;
|
|
80
|
+
both are always provided and every method throws `FLOW_DISABLED` or `METRICS_DISABLED`, except
|
|
81
|
+
`FlowService.close()`, which is a no-op while flows are disabled. The
|
|
82
|
+
`@OnQueueEvent` example declared `(jobId, returnValue)`, but the handler receives one serialized
|
|
83
|
+
payload object (`completed` carries `{ jobId, returnvalue, prev? }`), so the example and the prose
|
|
84
|
+
above it now say so. An "Other exports" table now covers the exports the README never named.
|
|
85
|
+
|
|
86
|
+
### Tests
|
|
87
|
+
|
|
88
|
+
- `pnpm check:published` now fails on a wrong method, option or field anywhere in the README.
|
|
89
|
+
It reads syntactic and semantic diagnostics together, compiles every snippet, not only those
|
|
90
|
+
that import the package, types the names the prose introduces in
|
|
91
|
+
`scripts/readme-prelude.d.ts`, resolves a relative import to the snippet that exports it and
|
|
92
|
+
fails one that no snippet satisfies. The prelude is compiled as a `.ts` file, so a type in it that
|
|
93
|
+
stops resolving is an error instead of `any`, and no syntax error is tolerated: a snippet that
|
|
94
|
+
shortens an example with a bare `...` fails and is written out in full.
|
|
95
|
+
- The Mode A null-retries probe is exercised with real ioredis clients: a stock client with finite
|
|
96
|
+
retries is accepted, a client whose `duplicate()` ignores the override is refused with
|
|
97
|
+
`CONNECTION_REQUIRES_NULL_RETRIES`, the probe is closed in both cases, and BullMQ itself refuses
|
|
98
|
+
a Worker on the connection the probe rejected.
|
|
99
|
+
- A spec fails when a name exported from `src/server/index.ts` or `src/shared/index.ts` is not
|
|
100
|
+
named in the README, so an export nobody documented cannot ship unnoticed again.
|
|
101
|
+
- The README's option-defaults table and error-code catalog are now parsed and compared with
|
|
102
|
+
`applyDefaults`, `DEFAULT_JOB_OPTIONS` and `QUEUE_ERROR_CODES`, so a default or a code that drifts
|
|
103
|
+
fails the suite instead of reaching the npm page.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
14
107
|
## [1.2.4] — 2026-10-30
|
|
15
108
|
|
|
16
109
|
No action required: behaviour, the public API, the types and the peer ranges are unchanged.
|
|
@@ -609,7 +702,9 @@ v6 peer range.
|
|
|
609
702
|
|
|
610
703
|
---
|
|
611
704
|
|
|
612
|
-
[Unreleased]: https://github.com/bymaxone/nest-queue/compare/v1.2.
|
|
705
|
+
[Unreleased]: https://github.com/bymaxone/nest-queue/compare/v1.2.6...HEAD
|
|
706
|
+
[1.2.6]: https://github.com/bymaxone/nest-queue/compare/v1.2.5...v1.2.6
|
|
707
|
+
[1.2.5]: https://github.com/bymaxone/nest-queue/compare/v1.2.4...v1.2.5
|
|
613
708
|
[1.2.4]: https://github.com/bymaxone/nest-queue/compare/v1.2.3...v1.2.4
|
|
614
709
|
[1.2.3]: https://github.com/bymaxone/nest-queue/compare/v1.2.2...v1.2.3
|
|
615
710
|
[1.2.2]: https://github.com/bymaxone/nest-queue/compare/v1.2.1...v1.2.2
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<a href="https://www.npmjs.com/package/@bymax-one/nest-queue"><img src="https://img.shields.io/npm/dm/@bymax-one/nest-queue?style=flat-square&colorA=000000&colorB=000000" alt="npm downloads" /></a>
|
|
15
15
|
<a href="https://github.com/bymaxone/nest-queue/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/bymaxone/nest-queue/ci.yml?branch=main&style=flat-square&colorA=000000&label=CI" alt="CI status" /></a>
|
|
16
16
|
<a href="https://github.com/bymaxone/nest-queue/actions/workflows/ci.yml"><img src="https://img.shields.io/badge/coverage-100%25-brightgreen?style=flat-square&colorA=000000" alt="coverage" /></a>
|
|
17
|
-
<a href="https://github.com/bymaxone/nest-queue/blob/main/docs/mutation_testing_results.md"><img src="https://img.shields.io/badge/mutation-
|
|
17
|
+
<a href="https://github.com/bymaxone/nest-queue/blob/main/docs/mutation_testing_results.md"><img src="https://img.shields.io/badge/mutation-100%25-brightgreen?style=flat-square&colorA=000000" alt="mutation score" /></a>
|
|
18
18
|
<a href="https://scorecard.dev/viewer/?uri=github.com/bymaxone/nest-queue"><img src="https://api.scorecard.dev/projects/github.com/bymaxone/nest-queue/badge?style=flat-square" alt="OpenSSF Scorecard" /></a>
|
|
19
19
|
<a href="https://github.com/bymaxone/nest-queue/blob/main/LICENSE"><img src="https://img.shields.io/github/license/bymaxone/nest-queue?style=flat-square&colorA=000000&colorB=000000" alt="license" /></a>
|
|
20
20
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-strict-3178C6?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript" /></a>
|
|
@@ -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,11 +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
|
|
222
|
+
import { Module } from '@nestjs/common'
|
|
223
|
+
import { EmailProcessor } from './email.processor'
|
|
224
|
+
import { EmailService } from './email.service'
|
|
225
|
+
|
|
219
226
|
@Module({
|
|
220
|
-
providers: [EmailProcessor],
|
|
227
|
+
providers: [EmailService, EmailProcessor],
|
|
221
228
|
})
|
|
222
229
|
export class EmailModule {}
|
|
223
230
|
```
|
|
@@ -228,20 +235,26 @@ export class EmailModule {}
|
|
|
228
235
|
|
|
229
236
|
Every option passed to `forRoot` / `forRootAsync`. Only `connection` is required.
|
|
230
237
|
|
|
231
|
-
| Option | Type | Default | Description
|
|
232
|
-
| -------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
233
|
-
| `connection` | `{ client } \| { url, options? } \| { options }` | — | **Required.** Mode A supplies an existing `ioredis` client; Mode B lets the library open its own. The
|
|
234
|
-
| `prefix` | `string` | `'bull'` | Redis key namespace. Applied to the `Queue`, `Worker`, `QueueEvents` **and** `FlowProducer` — see [Architecture](#-architecture).
|
|
235
|
-
| `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.
|
|
236
|
-
| `queueOptions` | `Partial<QueueOptions>` | `{}` | Extra BullMQ `Queue` options. `connection`, `defaultJobOptions` and `prefix` are owned by the library and cannot be set here.
|
|
237
|
-
| `flows.enabled` | `boolean` | `false` |
|
|
238
|
-
| `metrics.enabled` | `boolean` | `false` |
|
|
239
|
-
| `metrics.cacheTtlMs` | `number` | `5_000` | TTL of the metrics snapshot cache. `0` disables caching. Must be `>= 0`.
|
|
240
|
-
| `telemetry` | `Telemetry` | — | A BullMQ `Telemetry` instance (typically `new BullMQOtel(...)` from the optional `bullmq-otel` peer), attached to every `Queue` and `Worker`.
|
|
241
|
-
| `shutdown.drainTimeoutMs` | `number` | `30_000` | Upper bound on the graceful drain before workers are force-closed. Must be `> 0`.
|
|
242
|
-
| `shutdown.drainOnShutdown` | `boolean` | `false` |
|
|
243
|
-
| `connectionReadyTimeoutMs` | `number` | `10_000` | Mode B only: how long to wait for Redis `ready` before failing with `CONNECTION_TIMEOUT`.
|
|
244
|
-
| `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: 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. |
|
|
252
|
+
|
|
253
|
+
> [!WARNING]
|
|
254
|
+
> **`shutdown.drainOnShutdown` deletes jobs.** When it is `true`, shutdown calls `drain()` on every
|
|
255
|
+
> queue this process created, which removes all of that queue's waiting jobs from
|
|
256
|
+
> Redis, including the ones other instances were about to process. It exists for development and
|
|
257
|
+
> tests that need an empty queue. Never enable it in production.
|
|
245
258
|
|
|
246
259
|
Invalid options are rejected at bootstrap with `QueueException(INVALID_OPTIONS)` — never
|
|
247
260
|
silently corrected. A misconfigured queue fails when the application starts, not when the
|
|
@@ -300,8 +313,8 @@ BymaxQueueModule.forRoot({
|
|
|
300
313
|
})
|
|
301
314
|
```
|
|
302
315
|
|
|
303
|
-
The library opens one connection for queues and one per
|
|
304
|
-
|
|
316
|
+
The library opens one connection for queues, one per worker and one per queue that has an
|
|
317
|
+
`@OnQueueEvent` listener. The worker and `QueueEvents` connections carry `maxRetriesPerRequest: null`.
|
|
305
318
|
|
|
306
319
|
---
|
|
307
320
|
|
|
@@ -321,34 +334,35 @@ import type {
|
|
|
321
334
|
} from '@bymax-one/nest-queue'
|
|
322
335
|
```
|
|
323
336
|
|
|
324
|
-
| Method | Signature | Description
|
|
325
|
-
| -------------------- | -------------------------------------------------------------------------------------------- |
|
|
326
|
-
| `enqueue` | `<TData, TResult>(queue, name, data, opts?) → Promise<Job<TData, TResult>>` | Add a single job
|
|
327
|
-
| `enqueueBulk` | `<TData, TResult>(queue, jobs) → Promise<Job<TData, TResult>[]>` | Add up to 1 000 jobs in one Redis roundtrip
|
|
328
|
-
| `getJob` | `<TData, TResult>(queue, jobId) → Promise<Job<TData, TResult> \| null>` | Get job by ID; `null` when it does not exist
|
|
329
|
-
| `getJobs` | `<TData, TResult>(queue, status, start?, end?) → Promise<Job<TData, TResult>[]>` | Page jobs in one status (default `0`–`50`)
|
|
330
|
-
| `getMetrics` | `(queue) → Promise<QueueMetrics>` | Uncached snapshot of job counts per status, with a collection timestamp
|
|
331
|
-
| `pauseQueue` | `(queue) → Promise<void>` | Pause a queue
|
|
332
|
-
| `resumeQueue` | `(queue) → Promise<void>` | Resume a paused queue
|
|
333
|
-
| `cleanQueue` | `(queue,
|
|
334
|
-
| `upsertJobScheduler` | `<TData, TResult>(queue, id, repeat, template?) → Promise<Job<TData, TResult> \| undefined>` | Create or update a recurring Job Scheduler; returns the first scheduled job
|
|
335
|
-
| `removeJobScheduler` | `(queue, id) → Promise<boolean>` | Remove a Job Scheduler by ID
|
|
336
|
-
| `getJobSchedulers` | `(queue, start?, end?, asc?) → Promise<JobSchedulerJson[]>` | Page the schedulers registered on a queue
|
|
337
|
-
| `getOrCreateQueue` | `<TData, TResult>(queue, overrides?) → Queue<TData, TResult>` | Return the cached raw `Queue` instance
|
|
338
|
-
| `getCachedQueues` | `() → ReadonlyMap<string, Queue>` | Read-only view of every queue created so far
|
|
337
|
+
| Method | Signature | Description |
|
|
338
|
+
| -------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
339
|
+
| `enqueue` | `<TData, TResult>(queue, name, data, opts?) → Promise<Job<TData, TResult>>` | Add a single job |
|
|
340
|
+
| `enqueueBulk` | `<TData, TResult>(queue, jobs) → Promise<Job<TData, TResult>[]>` | Add up to 1 000 jobs in one Redis roundtrip |
|
|
341
|
+
| `getJob` | `<TData, TResult>(queue, jobId) → Promise<Job<TData, TResult> \| null>` | Get job by ID; `null` when it does not exist |
|
|
342
|
+
| `getJobs` | `<TData, TResult>(queue, status, start?, end?) → Promise<Job<TData, TResult>[]>` | Page jobs in one status (default `0`–`50`) |
|
|
343
|
+
| `getMetrics` | `(queue) → Promise<QueueMetrics>` | Uncached snapshot of job counts per status, with a collection timestamp |
|
|
344
|
+
| `pauseQueue` | `(queue) → Promise<void>` | Pause a queue |
|
|
345
|
+
| `resumeQueue` | `(queue) → Promise<void>` | Resume a paused queue |
|
|
346
|
+
| `cleanQueue` | `(queue, gracePeriodMs, limit, status?) → Promise<string[]>` | Remove up to `limit` jobs older than `gracePeriodMs` (`0` = no limit); returns removed ids |
|
|
347
|
+
| `upsertJobScheduler` | `<TData, TResult>(queue, id, repeat, template?) → Promise<Job<TData, TResult> \| undefined>` | Create or update a recurring Job Scheduler; returns the first scheduled job |
|
|
348
|
+
| `removeJobScheduler` | `(queue, id) → Promise<boolean>` | Remove a Job Scheduler by ID |
|
|
349
|
+
| `getJobSchedulers` | `(queue, start?, end?, asc?) → Promise<JobSchedulerJson[]>` | Page the schedulers registered on a queue |
|
|
350
|
+
| `getOrCreateQueue` | `<TData, TResult>(queue, overrides?) → Queue<TData, TResult>` | Return the cached raw `Queue` instance |
|
|
351
|
+
| `getCachedQueues` | `() → ReadonlyMap<string, Queue>` | Read-only view of every queue created so far |
|
|
339
352
|
|
|
340
353
|
### `WorkerRegistry`
|
|
341
354
|
|
|
342
355
|
Programmatic API to create and manage BullMQ workers at runtime.
|
|
343
356
|
|
|
344
357
|
```typescript
|
|
358
|
+
import path from 'node:path'
|
|
345
359
|
import { WorkerRegistry } from '@bymax-one/nest-queue'
|
|
346
360
|
import type { ProgrammaticWorkerConfig, SandboxedWorkerConfig } from '@bymax-one/nest-queue'
|
|
347
361
|
|
|
348
362
|
// Register an in-process worker
|
|
349
363
|
registry.register({
|
|
350
364
|
queueName: 'email',
|
|
351
|
-
|
|
365
|
+
handler: async (job) => {
|
|
352
366
|
/* … */
|
|
353
367
|
},
|
|
354
368
|
options: { concurrency: 4 },
|
|
@@ -404,6 +418,23 @@ const metrics: QueueMetrics = await this.metricsService.get('email')
|
|
|
404
418
|
const all: readonly QueueMetrics[] = await this.metricsService.getAll()
|
|
405
419
|
```
|
|
406
420
|
|
|
421
|
+
### Other exports
|
|
422
|
+
|
|
423
|
+
| Export | What it is |
|
|
424
|
+
| ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
425
|
+
| `WorkerRegistry.unregister(queue)`, `list()` | Stop and remove a worker and its connection; list the queue names that have a worker |
|
|
426
|
+
| `FlowService.addBulk(flows)`, `getProducer()`, `close()` | Add several flows at once; the underlying BullMQ `FlowProducer`; close it (a no-op while flows are disabled) |
|
|
427
|
+
| `MetricsService.invalidate(queue?)` | Drop the cached snapshot of one queue, or of all of them |
|
|
428
|
+
| `QueueEventsRegistry` | The shared `QueueEvents` per queue (`getOrCreate`, `list`), used by `@OnQueueEvent` |
|
|
429
|
+
| `BYMAX_QUEUE_OPTIONS`, `BYMAX_QUEUE_RESOLVED_OPTIONS` | Injection tokens for the options as given and with the defaults applied |
|
|
430
|
+
| `BYMAX_QUEUE_REDIS_CLIENT`, `BYMAX_QUEUE_CONNECTION_MODE` | Injection tokens for the Queue-role ioredis client and for `'mode-a-byo'` or `'mode-b-owned'` |
|
|
431
|
+
| `DEFAULT_WORKER_CONCURRENCY`, `MAX_WORKER_CONCURRENCY` | The worker concurrency used when none is given, and its ceiling |
|
|
432
|
+
| `DEFAULT_JOB_OPTIONS`, `DEFAULT_METRICS_CACHE_TTL_MS`, `DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS` | The defaults of the Configuration table |
|
|
433
|
+
| `QUEUE_ERROR_MESSAGES` | The default message of each error code |
|
|
434
|
+
| `BymaxQueueModuleOptions`, `BymaxQueueModuleAsyncOptions`, `BymaxQueueOptionsFactory`, `QueueConnectionConfig`, `QueueConnectionMode` | The option and connection types behind `forRoot` and `forRootAsync` |
|
|
435
|
+
| `ProcessorMetadata`, `ProcessHandlerMetadata`, `QueueEventListenerMetadata` | The reflection metadata the decorators write |
|
|
436
|
+
| `JobStatus`, `JOB_STATUS`, `BulkJob`, `SandboxedJob`, `WorkerEventName`, `QueueEventName`, `WorkerOptions` | Constants and types for the APIs above |
|
|
437
|
+
|
|
407
438
|
---
|
|
408
439
|
|
|
409
440
|
## 🚨 Error Code Catalog
|
|
@@ -495,12 +526,14 @@ onFailed(job: Job | undefined, err: Error): void {
|
|
|
495
526
|
|
|
496
527
|
### `@OnQueueEvent(eventName)`
|
|
497
528
|
|
|
498
|
-
Listens to BullMQ `QueueEvents` (global, cross-process events).
|
|
529
|
+
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.
|
|
530
|
+
|
|
531
|
+
The handler receives one serialized payload object, not a `Job`: `completed` carries `{ jobId, returnvalue, prev? }`, `failed` carries `{ jobId, failedReason, prev? }`, and `error` carries a real `Error`. Fetch the job with `QueueService.getJob` when you need it.
|
|
499
532
|
|
|
500
533
|
```typescript
|
|
501
534
|
@OnQueueEvent('completed')
|
|
502
|
-
onCompleted(jobId: string
|
|
503
|
-
console.log(`Job ${jobId} completed`)
|
|
535
|
+
onCompleted({ jobId, returnvalue }: { jobId: string; returnvalue: string }): void {
|
|
536
|
+
console.log(`Job ${jobId} completed with ${returnvalue}`)
|
|
504
537
|
}
|
|
505
538
|
```
|
|
506
539
|
|
|
@@ -586,7 +619,7 @@ import { BullMQOtel } from 'bullmq-otel'
|
|
|
586
619
|
|
|
587
620
|
BymaxQueueModule.forRoot({
|
|
588
621
|
connection: { url: process.env.REDIS_URL ?? 'redis://localhost:6379' },
|
|
589
|
-
telemetry: new BullMQOtel('my-service'
|
|
622
|
+
telemetry: new BullMQOtel({ tracerName: 'my-service' }),
|
|
590
623
|
})
|
|
591
624
|
```
|
|
592
625
|
|
|
@@ -594,7 +627,7 @@ BymaxQueueModule.forRoot({
|
|
|
594
627
|
|
|
595
628
|
## 🛑 Graceful Shutdown
|
|
596
629
|
|
|
597
|
-
`QueueLifecycle`
|
|
630
|
+
`QueueLifecycle` runs automatically when NestJS closes the application context, through its `onModuleDestroy` hook (call `app.enableShutdownHooks()` so that a termination signal closes the context).
|
|
598
631
|
It implements a **bounded drain protocol** with at-least-once semantics:
|
|
599
632
|
|
|
600
633
|
1. All workers are closed with a grace period (`drainTimeoutMs`, default 30 s).
|
|
@@ -708,7 +741,7 @@ Connection details arrive through injected options — the library never reads
|
|
|
708
741
|
`process.env` itself, so credentials live wherever your configuration layer keeps them.
|
|
709
742
|
No connection string, password, or TLS material is written to a log line, an exception
|
|
710
743
|
message, or an exception payload. `QueueException.details` carries only scalar
|
|
711
|
-
|
|
744
|
+
values — a reason, a limit, a queue or scheduler name, a status — never a
|
|
712
745
|
connection object and never `job.data`.
|
|
713
746
|
|
|
714
747
|
They are also not reachable by serializing the objects that hold them. The connection is
|
|
@@ -766,7 +799,7 @@ key will do it twice at some point.
|
|
|
766
799
|
| Layer | Implementation |
|
|
767
800
|
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
768
801
|
| Credentials | Injected options only; never read from `process.env`, never logged, never placed in an exception; held in a non-enumerable accessor and private fields, so serializing a service omits them |
|
|
769
|
-
| Error payloads | `QueueException.details` restricted to scalar
|
|
802
|
+
| Error payloads | `QueueException.details` restricted to scalar values — no `job.data`, no connection object |
|
|
770
803
|
| Job payloads | Treated as opaque; never deep-merged (prototype-pollution guard), never logged |
|
|
771
804
|
| Namespacing | `prefix` applied uniformly to `Queue`, `Worker`, `QueueEvents` and `FlowProducer` |
|
|
772
805
|
| Connection policy | `maxRetriesPerRequest: null` confined to duplicated consumer connections; producers keep fail-fast retries |
|
|
@@ -796,7 +829,7 @@ key will do it twice at some point.
|
|
|
796
829
|
## 🧪 Testing & Quality
|
|
797
830
|
|
|
798
831
|
- **100% line/branch coverage** on every implemented file (hard CI gate).
|
|
799
|
-
- **Mutation score
|
|
832
|
+
- **Mutation score 100.00%** (Stryker `break 100`) — see [`docs/mutation_testing_results.md`](docs/mutation_testing_results.md).
|
|
800
833
|
- **Real-Redis E2E** via Testcontainers (workers, flows, schedulers, deduplication, retries,
|
|
801
834
|
DLQ, graceful shutdown).
|
|
802
835
|
- **Type-level API tests** (`pnpm test:types`) pin the published signatures, so a refactor
|
|
@@ -804,13 +837,13 @@ key will do it twice at some point.
|
|
|
804
837
|
- **Published-shape gates**: `pnpm check:exports` resolves every entrypoint the way each
|
|
805
838
|
module-resolution mode would (all four `attw` quadrants green, `node10` included), and
|
|
806
839
|
`pnpm smoke` resolves the packed tarball from a real consumer in ESM _and_ CommonJS.
|
|
807
|
-
- **Bundle budgets** enforced (`pnpm size`): server ≤ 18 KiB brotli, shared ≤ 2
|
|
840
|
+
- **Bundle budgets** enforced (`pnpm size`): server ≤ 18 KiB brotli, shared ≤ 2 500 B.
|
|
808
841
|
- **Supply chain:** SHA-pinned third-party GitHub Actions, OSV-Scanner, TruffleHog, OpenSSF Scorecard,
|
|
809
842
|
npm publish with provenance (OIDC).
|
|
810
843
|
|
|
811
844
|
```bash
|
|
812
845
|
pnpm typecheck && pnpm test:types && pnpm lint && pnpm test:cov:all && \
|
|
813
|
-
pnpm build && pnpm size && pnpm check:exports && pnpm smoke
|
|
846
|
+
pnpm build && pnpm size && pnpm check:exports && pnpm check:published && pnpm smoke
|
|
814
847
|
```
|
|
815
848
|
|
|
816
849
|
---
|
|
@@ -852,20 +885,17 @@ implementation, so BullMQ still works, just without the native accelerator.
|
|
|
852
885
|
|
|
853
886
|
### `CONNECTION_REQUIRES_NULL_RETRIES`
|
|
854
887
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
```typescript
|
|
862
|
-
// In your cache module, create a second ioredis client for the queue
|
|
863
|
-
const queueRedis = new Redis({ ..., maxRetriesPerRequest: null })
|
|
864
|
-
```
|
|
888
|
+
BullMQ workers need `maxRetriesPerRequest: null` to block on Redis operations rather
|
|
889
|
+
than giving up after a few retries. The library does not ask you to configure it: every
|
|
890
|
+
Worker and `QueueEvents` connection is a `duplicate()` of the client you pass, made with
|
|
891
|
+
`maxRetriesPerRequest: null`, and in Mode A it checks that a duplicate really comes back
|
|
892
|
+
that way. This error means one did not — measured with ioredis, a client created with
|
|
893
|
+
`maxRetriesPerRequest: 3` duplicates to `null`, so a stock client never triggers it.
|
|
865
894
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
895
|
+
If you see it, the client you passed is not a plain ioredis client (a wrapper or a test
|
|
896
|
+
double whose `duplicate()` ignores its options). Pass the ioredis client itself, or let the
|
|
897
|
+
library open its own connection with Mode B. The producer keeps the client's own retry
|
|
898
|
+
policy, which is what lets it fail fast during an outage.
|
|
869
899
|
|
|
870
900
|
### Jobs get stuck (not processed after restart)
|
|
871
901
|
|
|
@@ -873,10 +903,11 @@ Stalled jobs are re-queued automatically by BullMQ's stalled-job check. If worke
|
|
|
873
903
|
do not pick them up:
|
|
874
904
|
|
|
875
905
|
1. Verify `drainTimeoutMs` is larger than the worst-case job runtime.
|
|
876
|
-
2. Verify `lockDuration` (in `
|
|
877
|
-
a worker that runs longer than `lockDuration` will have its
|
|
878
|
-
the job as stalled.
|
|
879
|
-
3.
|
|
906
|
+
2. Verify `lockDuration` (in the `options` you pass to `@Processor` or `register`) covers
|
|
907
|
+
the handler execution time — a worker that runs longer than `lockDuration` will have its
|
|
908
|
+
lock stolen, marking the job as stalled.
|
|
909
|
+
3. BullMQ re-queues a stalled job up to its `maxStalledCount` times (default `1`) and then
|
|
910
|
+
fails it; this library does not expose that setting.
|
|
880
911
|
|
|
881
912
|
---
|
|
882
913
|
|
package/dist/server/index.cjs
CHANGED
|
@@ -1396,7 +1396,7 @@ var QueueLifecycle = class {
|
|
|
1396
1396
|
}
|
|
1397
1397
|
/**
|
|
1398
1398
|
* Drain every cached queue when `drainOnShutdown` is enabled (dev/test only).
|
|
1399
|
-
* Removes waiting and delayed
|
|
1399
|
+
* Removes waiting jobs and leaves delayed and active ones; off by default to stay production-safe.
|
|
1400
1400
|
*/
|
|
1401
1401
|
async drainQueuesIfRequested() {
|
|
1402
1402
|
if (!this.resolved.shutdown.drainOnShutdown) return;
|
package/dist/server/index.d.cts
CHANGED
|
@@ -1067,7 +1067,7 @@ declare class QueueLifecycle implements OnModuleDestroy {
|
|
|
1067
1067
|
private closeDuplicatedConnections;
|
|
1068
1068
|
/**
|
|
1069
1069
|
* Drain every cached queue when `drainOnShutdown` is enabled (dev/test only).
|
|
1070
|
-
* Removes waiting and delayed
|
|
1070
|
+
* Removes waiting jobs and leaves delayed and active ones; off by default to stay production-safe.
|
|
1071
1071
|
*/
|
|
1072
1072
|
private drainQueuesIfRequested;
|
|
1073
1073
|
/** Close the flow producer (idempotent), swallowing any failure. */
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1067,7 +1067,7 @@ declare class QueueLifecycle implements OnModuleDestroy {
|
|
|
1067
1067
|
private closeDuplicatedConnections;
|
|
1068
1068
|
/**
|
|
1069
1069
|
* Drain every cached queue when `drainOnShutdown` is enabled (dev/test only).
|
|
1070
|
-
* Removes waiting and delayed
|
|
1070
|
+
* Removes waiting jobs and leaves delayed and active ones; off by default to stay production-safe.
|
|
1071
1071
|
*/
|
|
1072
1072
|
private drainQueuesIfRequested;
|
|
1073
1073
|
/** Close the flow producer (idempotent), swallowing any failure. */
|
package/dist/server/index.mjs
CHANGED
|
@@ -1394,7 +1394,7 @@ var QueueLifecycle = class {
|
|
|
1394
1394
|
}
|
|
1395
1395
|
/**
|
|
1396
1396
|
* Drain every cached queue when `drainOnShutdown` is enabled (dev/test only).
|
|
1397
|
-
* Removes waiting and delayed
|
|
1397
|
+
* Removes waiting jobs and leaves delayed and active ones; off by default to stay production-safe.
|
|
1398
1398
|
*/
|
|
1399
1399
|
async drainQueuesIfRequested() {
|
|
1400
1400
|
if (!this.resolved.shutdown.drainOnShutdown) return;
|
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.6",
|
|
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",
|