@bymax-one/nest-queue 1.0.11 → 1.2.2
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 +75 -0
- package/README.md +15 -15
- package/dist/server/index.cjs +28 -12
- package/dist/server/index.d.cts +23 -8
- package/dist/server/index.d.ts +23 -8
- package/dist/server/index.mjs +28 -12
- package/dist/shared/index.cjs +1 -2
- package/dist/shared/index.d.cts +3 -4
- package/dist/shared/index.d.ts +3 -4
- package/dist/shared/index.mjs +1 -2
- package/package.json +12 -13
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,78 @@ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.2.2] — 2026-09-04
|
|
11
|
+
|
|
12
|
+
**1.2.1 was tagged but never published**, so this release carries its documentation fix as well
|
|
13
|
+
as the one below. `dist/` changes here for the first time since 1.2.0 — JSDoc reaches the emitted
|
|
14
|
+
`.d.ts`, so a doc-block correction is a change to the published types.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **The `enqueue` deduplication example asserted a safety property it did not provide.** It showed
|
|
19
|
+
`` `${tenantId}:reindex:${term}` `` and stated that "one tenant's term cannot suppress another's".
|
|
20
|
+
Two variable fields joined by a delimiter either may contain do not produce a unique key: with
|
|
21
|
+
`` `${tenantId}:${term}` ``, the pairs `('a', 'b:c')` and `('a:b', 'c')` both yield `a:b:c`. The
|
|
22
|
+
guidance now percent-encodes every variable field, and the claim states what the construction
|
|
23
|
+
delivers. This is documentation a consumer copies into their own key-building code, which is why
|
|
24
|
+
the shape mattered more than whether `:reindex:` in particular was reachable.
|
|
25
|
+
- Carried from 1.2.1: **the `MetricsService` example in `README.md` called two methods that do not
|
|
26
|
+
exist**, `getMetrics(name)` and `getAll([names])`, where the service exposes `get(name)` and a
|
|
27
|
+
no-argument `getAll()`.
|
|
28
|
+
|
|
29
|
+
### Internal
|
|
30
|
+
|
|
31
|
+
Not shipped, listed because the tag covers them: bullmq raised from 6.0.10 to 6.3.4 in the
|
|
32
|
+
workspace override; `fast-uri`, `qs` and `fastify` floors raised past published advisories; the
|
|
33
|
+
README-snippet gate now type-checks through receivers; `AGENTS.md` and its sync workflow added.
|
|
34
|
+
|
|
35
|
+
## [1.2.1] — 2026-08-30
|
|
36
|
+
|
|
37
|
+
Documentation only. `dist/` is byte-identical to `1.2.0`; the published tarball was unpacked and
|
|
38
|
+
diffed against a fresh build to confirm it rather than assert it. The release exists because
|
|
39
|
+
`README.md` ships inside the package, so a fix that stays on `main` leaves the npm page — where
|
|
40
|
+
the example is actually read — still wrong.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- **The `MetricsService` example in `README.md` called two methods that do not exist.** It showed
|
|
45
|
+
`getMetrics(name)` and `getAll([names])`; the service exposes `get(name)` and a no-argument
|
|
46
|
+
`getAll()` returning `readonly QueueMetrics[]` for every queue currently cached by
|
|
47
|
+
`QueueService`. Code copied from that example failed to compile, and the wrong `getAll` signature
|
|
48
|
+
also implied a per-call queue filter the API does not offer. Every other call in the README was
|
|
49
|
+
checked against the source in the same pass and is correct.
|
|
50
|
+
|
|
51
|
+
## [1.2.0] — 2026-08-10
|
|
52
|
+
|
|
53
|
+
**Breaking, shipped as a minor by choice.** The peer ranges move to the next major of each
|
|
54
|
+
Redis/BullMQ package and the `'paused'` job state is removed from the public status surface —
|
|
55
|
+
changes that would normally warrant a major. They ship under a minor because the package has no
|
|
56
|
+
downstream consumer yet; every breaking detail is called out below so a first adopter sees it.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- **Peer dependencies migrated to the next major:** `bullmq` `^5` → `^6`, `bullmq-otel` `^1` →
|
|
61
|
+
`^2`, and `ioredis` `^5` → `^6`. A consumer must upgrade all three together. `ioredis` 6 keeps
|
|
62
|
+
`'legacy'` reply mapping by default, so BullMQ's reply-shape assumptions are unaffected; it does
|
|
63
|
+
require Node.js ≥ 20, already covered by this package's `engines`.
|
|
64
|
+
|
|
65
|
+
### Removed
|
|
66
|
+
|
|
67
|
+
- **`'paused'` is no longer a job status.** BullMQ 6 dropped `'paused'` from its job-state set —
|
|
68
|
+
jobs on a paused queue now live in `'wait'`. It is therefore removed from the `JobStatus` union,
|
|
69
|
+
from the `JOB_STATUS` constant (`JOB_STATUS.PAUSED` no longer exists), and from the
|
|
70
|
+
`getMetrics().counts` shape, which now reports the five remaining statuses (`waiting`, `active`,
|
|
71
|
+
`completed`, `failed`, `delayed`). The queue-lifecycle `'paused'`/`'resumed'` **events**
|
|
72
|
+
(`@OnQueueEvent`) and the `pauseQueue`/`resumeQueue` methods are unchanged — those describe the
|
|
73
|
+
queue, not a job.
|
|
74
|
+
|
|
75
|
+
### Internal
|
|
76
|
+
|
|
77
|
+
- Dev-tooling bumps: `eslint` `^10`, `typescript` `^6`, `@types/node` `^26`, plus `@eslint/js`
|
|
78
|
+
`^10`. These do not ship and do not change `dist/`. The package's `engines` stays at Node ≥ 24.
|
|
79
|
+
- Mutation gate tightened: Stryker `break`/`high` raised from 95/99 → **100** (the run is at 100%,
|
|
80
|
+
0 survivors).
|
|
81
|
+
|
|
10
82
|
## [1.0.11] — 2026-08-10
|
|
11
83
|
|
|
12
84
|
**Documentation only.** The shipped declaration comments change (`dts` preserves them); no runtime
|
|
@@ -456,6 +528,9 @@ v6 peer range.
|
|
|
456
528
|
|
|
457
529
|
---
|
|
458
530
|
|
|
531
|
+
[1.2.2]: https://github.com/bymaxone/nest-queue/compare/v1.2.1...v1.2.2
|
|
532
|
+
[1.2.1]: https://github.com/bymaxone/nest-queue/compare/v1.2.0...v1.2.1
|
|
533
|
+
[1.2.0]: https://github.com/bymaxone/nest-queue/compare/v1.0.11...v1.2.0
|
|
459
534
|
[1.0.11]: https://github.com/bymaxone/nest-queue/compare/v1.0.10...v1.0.11
|
|
460
535
|
[1.0.10]: https://github.com/bymaxone/nest-queue/compare/v1.0.9...v1.0.10
|
|
461
536
|
[1.0.9]: https://github.com/bymaxone/nest-queue/compare/v1.0.8...v1.0.9
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<strong>Typed BullMQ queues for NestJS</strong><br />
|
|
9
|
-
<sub>BullMQ
|
|
9
|
+
<sub>BullMQ 6 · Workers · Flows · Job Schedulers · Deduplication · OpenTelemetry · Graceful Shutdown · Zero Runtime Dependencies</sub>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
## ✨ Overview
|
|
35
35
|
|
|
36
|
-
`@bymax-one/nest-queue` is a NestJS dynamic module that wraps **BullMQ
|
|
36
|
+
`@bymax-one/nest-queue` is a NestJS dynamic module that wraps **BullMQ 6** behind a typed,
|
|
37
37
|
opinionated surface. Instead of wiring `ioredis` connections, per-queue registration, and
|
|
38
38
|
`maxRetriesPerRequest` policy by hand, you install one library and get typed producers,
|
|
39
39
|
decorator-based workers, flows, recurring **Job Schedulers**, native deduplication,
|
|
@@ -114,11 +114,11 @@ Both subpaths ship ESM **and** CommonJS with declarations for each format, so a
|
|
|
114
114
|
|
|
115
115
|
### Peer dependency matrix
|
|
116
116
|
|
|
117
|
-
| Subpath | Required peers
|
|
118
|
-
| ------------ |
|
|
119
|
-
| `.` (server) | `@nestjs/common ^11.0.16`, `@nestjs/core ^11.1.18`, `bullmq ^
|
|
120
|
-
| `./shared` | None
|
|
121
|
-
| Optional | `bullmq-otel ^
|
|
117
|
+
| Subpath | Required peers |
|
|
118
|
+
| ------------ | ---------------------------------------------------------------------------------------------------------- |
|
|
119
|
+
| `.` (server) | `@nestjs/common ^11.0.16`, `@nestjs/core ^11.1.18`, `bullmq ^6.0`, `ioredis ^6.0`, `reflect-metadata ^0.2` |
|
|
120
|
+
| `./shared` | None |
|
|
121
|
+
| Optional | `bullmq-otel ^2` (OpenTelemetry) |
|
|
122
122
|
|
|
123
123
|
> [!IMPORTANT]
|
|
124
124
|
> `reflect-metadata` is a **global polyfill the application owns** — import it once at
|
|
@@ -400,8 +400,8 @@ BymaxQueueModule.forRoot({
|
|
|
400
400
|
import { MetricsService } from '@bymax-one/nest-queue'
|
|
401
401
|
import type { QueueMetrics } from '@bymax-one/nest-queue'
|
|
402
402
|
|
|
403
|
-
const metrics: QueueMetrics = await this.metricsService.
|
|
404
|
-
const all = await this.metricsService.getAll(
|
|
403
|
+
const metrics: QueueMetrics = await this.metricsService.get('email')
|
|
404
|
+
const all: readonly QueueMetrics[] = await this.metricsService.getAll()
|
|
405
405
|
```
|
|
406
406
|
|
|
407
407
|
---
|
|
@@ -785,11 +785,11 @@ key will do it twice at some point.
|
|
|
785
785
|
|
|
786
786
|
- **Runtime:** Node.js 24+
|
|
787
787
|
- **Framework:** NestJS 11 (`ConfigurableModuleBuilder` + `setExtras`)
|
|
788
|
-
- **Queue engine:** BullMQ `^
|
|
789
|
-
- **Telemetry:** `bullmq-otel ^
|
|
788
|
+
- **Queue engine:** BullMQ `^6.0` (peer) over `ioredis ^6.0` (peer)
|
|
789
|
+
- **Telemetry:** `bullmq-otel ^2` (optional peer)
|
|
790
790
|
- **Build:** tsup — ESM + CJS per subpath, with `.d.ts` _and_ `.d.cts` declarations
|
|
791
791
|
- **Tests:** Jest + `@testcontainers/redis` (E2E) + Stryker (mutation)
|
|
792
|
-
- **TypeScript:**
|
|
792
|
+
- **TypeScript:** 6.x strict (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`), zero `any`
|
|
793
793
|
|
|
794
794
|
---
|
|
795
795
|
|
|
@@ -884,9 +884,9 @@ do not pick them up:
|
|
|
884
884
|
|
|
885
885
|
- **NestJS only** — does not work with plain Express, standalone Fastify, or other frameworks.
|
|
886
886
|
- **Node.js only** — Deno and Bun are not supported.
|
|
887
|
-
- **BullMQ `^
|
|
888
|
-
|
|
889
|
-
[`CHANGELOG.md`](CHANGELOG.md).
|
|
887
|
+
- **BullMQ `^6.0` only** — the library targets BullMQ 6 (with `bullmq-otel ^2` for telemetry)
|
|
888
|
+
and does not support BullMQ 5; the `'paused'` job state that v6 removed is no longer exposed.
|
|
889
|
+
See the BullMQ version policy in [`CHANGELOG.md`](CHANGELOG.md).
|
|
890
890
|
- **No Redis Cluster sharding** — BullMQ's cluster support is not exercised by this library.
|
|
891
891
|
- **No built-in DLQ helper** — use the `@OnWorkerEvent('failed')` pattern above.
|
|
892
892
|
- **No Prometheus metrics export** — `getMetrics()` returns snapshots; publish them
|
package/dist/server/index.cjs
CHANGED
|
@@ -198,11 +198,22 @@ exports.ConnectionResolver = class ConnectionResolver {
|
|
|
198
198
|
return;
|
|
199
199
|
}
|
|
200
200
|
this.mode = "mode-b-owned";
|
|
201
|
-
__privateSet(this, _client, "url" in cfg ? new ioredis.Redis(cfg.url,
|
|
201
|
+
__privateSet(this, _client, "url" in cfg ? new ioredis.Redis(cfg.url, this.ownedOptions(cfg.options)) : new ioredis.Redis(this.ownedOptions(cfg.options)));
|
|
202
202
|
await this.waitReady(
|
|
203
203
|
__privateGet(this, _options).connectionReadyTimeoutMs ?? DEFAULT_CONNECTION_READY_TIMEOUT_MS
|
|
204
204
|
);
|
|
205
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Merge the consumer's options with the owned-connection policy
|
|
208
|
+
* (`lazyConnect: false`, so the client connects eagerly and `init` can wait
|
|
209
|
+
* for `ready`) and narrow the result to the shape ioredis 6's constructor
|
|
210
|
+
* accepts. The runtime object is unchanged — the cast only drops the
|
|
211
|
+
* `undefined` that `RedisOptions.replyMapping` carries and the constructor
|
|
212
|
+
* rejects.
|
|
213
|
+
*/
|
|
214
|
+
ownedOptions(options) {
|
|
215
|
+
return { ...options ?? {}, lazyConnect: false };
|
|
216
|
+
}
|
|
206
217
|
/** The resolved Queue-role client. */
|
|
207
218
|
getClient() {
|
|
208
219
|
if (!__privateGet(this, _client)) {
|
|
@@ -381,7 +392,13 @@ exports.QueueService = class QueueService {
|
|
|
381
392
|
* The deduplication `id` is a shared key across the queue, so a multi-tenant
|
|
382
393
|
* producer MUST scope it by tenant: keyed on the value alone
|
|
383
394
|
* (`` `reindex:${term}` ``), tenant A enqueuing a term suppresses tenant B's job
|
|
384
|
-
* for the same term.
|
|
395
|
+
* for the same term.
|
|
396
|
+
*
|
|
397
|
+
* Scoping alone is not enough. Two variable fields joined by a delimiter that
|
|
398
|
+
* either may contain do not produce a unique key: with `` `${tenantId}:${term}` ``,
|
|
399
|
+
* the pairs `('a', 'b:c')` and `('a:b', 'c')` both yield `a:b:c`. Percent-encode
|
|
400
|
+
* every variable field so the delimiter cannot appear inside one:
|
|
401
|
+
* `` `${encodeURIComponent(tenantId)}:${encodeURIComponent(term)}` ``.
|
|
385
402
|
*
|
|
386
403
|
* Deduplication modes (`deduplication: { id, ttl?, extend?, replace?, keepLastIfActive? }`):
|
|
387
404
|
* - Simple `{ id }` — collapse duplicates until the in-flight job completes or fails.
|
|
@@ -396,10 +413,11 @@ exports.QueueService = class QueueService {
|
|
|
396
413
|
* @param data - Typed job payload.
|
|
397
414
|
* @param options - Per-job BullMQ options (priority, delay, `jobId`, `deduplication`, ...).
|
|
398
415
|
* @returns The created job.
|
|
399
|
-
* @example Throttle: at most one reindex per term every 5 seconds.
|
|
400
|
-
*
|
|
416
|
+
* @example Throttle: at most one reindex per term every 5 seconds. Both variable
|
|
417
|
+
* fields are percent-encoded, so no other tenant/term pair can build this key.
|
|
418
|
+
* const dedupId = `${encodeURIComponent(tenantId)}:reindex:${encodeURIComponent(term)}`
|
|
401
419
|
* await queueService.enqueue('search', 'reindex', { term }, {
|
|
402
|
-
* deduplication: { id:
|
|
420
|
+
* deduplication: { id: dedupId, ttl: 5_000 },
|
|
403
421
|
* })
|
|
404
422
|
*/
|
|
405
423
|
async enqueue(queueName, jobName, data, options) {
|
|
@@ -471,8 +489,7 @@ exports.QueueService = class QueueService {
|
|
|
471
489
|
"active",
|
|
472
490
|
"completed",
|
|
473
491
|
"failed",
|
|
474
|
-
"delayed"
|
|
475
|
-
"paused"
|
|
492
|
+
"delayed"
|
|
476
493
|
);
|
|
477
494
|
return {
|
|
478
495
|
queue: queueName,
|
|
@@ -1341,9 +1358,9 @@ QueueLifecycle = __decorateClass([
|
|
|
1341
1358
|
|
|
1342
1359
|
// src/server/bymax-queue.module.ts
|
|
1343
1360
|
var { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = (
|
|
1344
|
-
// The `moduleName` shapes the generated token's
|
|
1345
|
-
//
|
|
1346
|
-
//
|
|
1361
|
+
// The `moduleName` shapes the generated option token's identifier — `BYMAX_QUEUE_MODULE_OPTIONS`,
|
|
1362
|
+
// which the module spec asserts by value; emptying the name would degrade it to an opaque
|
|
1363
|
+
// `CONFIGURABLE_MODULE_OPTIONS[<hash>]`, so the argument is a caught regression, not a free label.
|
|
1347
1364
|
new common.ConfigurableModuleBuilder({ moduleName: "BymaxQueue" }).setClassMethodName("forRoot").setFactoryMethodName("createQueueOptions").setExtras({ isGlobal: true }, (definition, extras) => ({
|
|
1348
1365
|
...definition,
|
|
1349
1366
|
global: extras.isGlobal
|
|
@@ -1548,8 +1565,7 @@ var JOB_STATUS = {
|
|
|
1548
1565
|
ACTIVE: "active",
|
|
1549
1566
|
COMPLETED: "completed",
|
|
1550
1567
|
FAILED: "failed",
|
|
1551
|
-
DELAYED: "delayed"
|
|
1552
|
-
PAUSED: "paused"
|
|
1568
|
+
DELAYED: "delayed"
|
|
1553
1569
|
};
|
|
1554
1570
|
|
|
1555
1571
|
exports.BYMAX_QUEUE_CONNECTION_MODE = BYMAX_QUEUE_CONNECTION_MODE;
|
package/dist/server/index.d.cts
CHANGED
|
@@ -269,7 +269,7 @@ interface BulkJob<TData = unknown> {
|
|
|
269
269
|
* @example
|
|
270
270
|
* const status: JobStatus = 'waiting'
|
|
271
271
|
*/
|
|
272
|
-
type JobStatus = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed'
|
|
272
|
+
type JobStatus = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed';
|
|
273
273
|
|
|
274
274
|
/**
|
|
275
275
|
* @fileoverview Instantaneous queue metrics snapshot type.
|
|
@@ -282,14 +282,14 @@ type JobStatus = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed' | 'pa
|
|
|
282
282
|
* @example
|
|
283
283
|
* const metrics: QueueMetrics = {
|
|
284
284
|
* queue: 'email',
|
|
285
|
-
* counts: { waiting: 3, active: 1, completed: 120, failed: 2, delayed: 0
|
|
285
|
+
* counts: { waiting: 3, active: 1, completed: 120, failed: 2, delayed: 0 },
|
|
286
286
|
* collectedAt: '2026-06-26T12:00:00.000Z',
|
|
287
287
|
* }
|
|
288
288
|
*/
|
|
289
289
|
interface QueueMetrics {
|
|
290
290
|
/** Queue name the snapshot was taken from. */
|
|
291
291
|
queue: string;
|
|
292
|
-
/** Number of jobs in each of the
|
|
292
|
+
/** Number of jobs in each of the five BullMQ statuses. */
|
|
293
293
|
counts: Record<JobStatus, number>;
|
|
294
294
|
/** ISO 8601 UTC timestamp when the snapshot was collected. */
|
|
295
295
|
collectedAt: string;
|
|
@@ -359,6 +359,15 @@ declare class ConnectionResolver {
|
|
|
359
359
|
constructor(options: BymaxQueueModuleOptions);
|
|
360
360
|
/** Resolve and validate the connection. Call once during module bootstrap. */
|
|
361
361
|
init(): Promise<void>;
|
|
362
|
+
/**
|
|
363
|
+
* Merge the consumer's options with the owned-connection policy
|
|
364
|
+
* (`lazyConnect: false`, so the client connects eagerly and `init` can wait
|
|
365
|
+
* for `ready`) and narrow the result to the shape ioredis 6's constructor
|
|
366
|
+
* accepts. The runtime object is unchanged — the cast only drops the
|
|
367
|
+
* `undefined` that `RedisOptions.replyMapping` carries and the constructor
|
|
368
|
+
* rejects.
|
|
369
|
+
*/
|
|
370
|
+
private ownedOptions;
|
|
362
371
|
/** The resolved Queue-role client. */
|
|
363
372
|
getClient(): Redis;
|
|
364
373
|
/** The resolved connection mode. */
|
|
@@ -424,7 +433,13 @@ declare class QueueService {
|
|
|
424
433
|
* The deduplication `id` is a shared key across the queue, so a multi-tenant
|
|
425
434
|
* producer MUST scope it by tenant: keyed on the value alone
|
|
426
435
|
* (`` `reindex:${term}` ``), tenant A enqueuing a term suppresses tenant B's job
|
|
427
|
-
* for the same term.
|
|
436
|
+
* for the same term.
|
|
437
|
+
*
|
|
438
|
+
* Scoping alone is not enough. Two variable fields joined by a delimiter that
|
|
439
|
+
* either may contain do not produce a unique key: with `` `${tenantId}:${term}` ``,
|
|
440
|
+
* the pairs `('a', 'b:c')` and `('a:b', 'c')` both yield `a:b:c`. Percent-encode
|
|
441
|
+
* every variable field so the delimiter cannot appear inside one:
|
|
442
|
+
* `` `${encodeURIComponent(tenantId)}:${encodeURIComponent(term)}` ``.
|
|
428
443
|
*
|
|
429
444
|
* Deduplication modes (`deduplication: { id, ttl?, extend?, replace?, keepLastIfActive? }`):
|
|
430
445
|
* - Simple `{ id }` — collapse duplicates until the in-flight job completes or fails.
|
|
@@ -439,10 +454,11 @@ declare class QueueService {
|
|
|
439
454
|
* @param data - Typed job payload.
|
|
440
455
|
* @param options - Per-job BullMQ options (priority, delay, `jobId`, `deduplication`, ...).
|
|
441
456
|
* @returns The created job.
|
|
442
|
-
* @example Throttle: at most one reindex per term every 5 seconds.
|
|
443
|
-
*
|
|
457
|
+
* @example Throttle: at most one reindex per term every 5 seconds. Both variable
|
|
458
|
+
* fields are percent-encoded, so no other tenant/term pair can build this key.
|
|
459
|
+
* const dedupId = `${encodeURIComponent(tenantId)}:reindex:${encodeURIComponent(term)}`
|
|
444
460
|
* await queueService.enqueue('search', 'reindex', { term }, {
|
|
445
|
-
* deduplication: { id:
|
|
461
|
+
* deduplication: { id: dedupId, ttl: 5_000 },
|
|
446
462
|
* })
|
|
447
463
|
*/
|
|
448
464
|
enqueue<TData = unknown, TResult = unknown>(queueName: string, jobName: string, data: TData, options?: JobsOptions): Promise<TypedJob<TData, TResult>>;
|
|
@@ -1402,7 +1418,6 @@ declare const JOB_STATUS: {
|
|
|
1402
1418
|
readonly COMPLETED: "completed";
|
|
1403
1419
|
readonly FAILED: "failed";
|
|
1404
1420
|
readonly DELAYED: "delayed";
|
|
1405
|
-
readonly PAUSED: "paused";
|
|
1406
1421
|
};
|
|
1407
1422
|
|
|
1408
1423
|
export { BYMAX_QUEUE_CONNECTION_MODE, BYMAX_QUEUE_OPTIONS, BYMAX_QUEUE_REDIS_CLIENT, BYMAX_QUEUE_RESOLVED_OPTIONS, type BulkJob, BymaxQueueModule, type BymaxQueueModuleAsyncOptions, type BymaxQueueModuleOptions, type BymaxQueueOptionsFactory, ConnectionResolver, DEFAULT_JOB_OPTIONS, DEFAULT_METRICS_CACHE_TTL_MS, DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS, DEFAULT_WORKER_CONCURRENCY, FlowService, JOB_STATUS, type JobSchedulerRepeatOptions, type JobStatus, MAX_WORKER_CONCURRENCY, MetricsService, OnQueueEvent, OnWorkerEvent, Process, type ProcessHandlerMetadata, Processor, type ProcessorMetadata, type ProgrammaticWorkerConfig, QUEUE_ERROR_CODES, QUEUE_ERROR_MESSAGES, type QueueConnectionConfig, type QueueConnectionMode, type QueueErrorCode, type QueueEventListenerMetadata, type QueueEventName, QueueEventsRegistry, QueueException, QueueLifecycle, type QueueMetrics, QueueService, type SandboxedWorkerConfig, type WorkerEventName, type WorkerOptions, WorkerRegistry };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -269,7 +269,7 @@ interface BulkJob<TData = unknown> {
|
|
|
269
269
|
* @example
|
|
270
270
|
* const status: JobStatus = 'waiting'
|
|
271
271
|
*/
|
|
272
|
-
type JobStatus = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed'
|
|
272
|
+
type JobStatus = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed';
|
|
273
273
|
|
|
274
274
|
/**
|
|
275
275
|
* @fileoverview Instantaneous queue metrics snapshot type.
|
|
@@ -282,14 +282,14 @@ type JobStatus = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed' | 'pa
|
|
|
282
282
|
* @example
|
|
283
283
|
* const metrics: QueueMetrics = {
|
|
284
284
|
* queue: 'email',
|
|
285
|
-
* counts: { waiting: 3, active: 1, completed: 120, failed: 2, delayed: 0
|
|
285
|
+
* counts: { waiting: 3, active: 1, completed: 120, failed: 2, delayed: 0 },
|
|
286
286
|
* collectedAt: '2026-06-26T12:00:00.000Z',
|
|
287
287
|
* }
|
|
288
288
|
*/
|
|
289
289
|
interface QueueMetrics {
|
|
290
290
|
/** Queue name the snapshot was taken from. */
|
|
291
291
|
queue: string;
|
|
292
|
-
/** Number of jobs in each of the
|
|
292
|
+
/** Number of jobs in each of the five BullMQ statuses. */
|
|
293
293
|
counts: Record<JobStatus, number>;
|
|
294
294
|
/** ISO 8601 UTC timestamp when the snapshot was collected. */
|
|
295
295
|
collectedAt: string;
|
|
@@ -359,6 +359,15 @@ declare class ConnectionResolver {
|
|
|
359
359
|
constructor(options: BymaxQueueModuleOptions);
|
|
360
360
|
/** Resolve and validate the connection. Call once during module bootstrap. */
|
|
361
361
|
init(): Promise<void>;
|
|
362
|
+
/**
|
|
363
|
+
* Merge the consumer's options with the owned-connection policy
|
|
364
|
+
* (`lazyConnect: false`, so the client connects eagerly and `init` can wait
|
|
365
|
+
* for `ready`) and narrow the result to the shape ioredis 6's constructor
|
|
366
|
+
* accepts. The runtime object is unchanged — the cast only drops the
|
|
367
|
+
* `undefined` that `RedisOptions.replyMapping` carries and the constructor
|
|
368
|
+
* rejects.
|
|
369
|
+
*/
|
|
370
|
+
private ownedOptions;
|
|
362
371
|
/** The resolved Queue-role client. */
|
|
363
372
|
getClient(): Redis;
|
|
364
373
|
/** The resolved connection mode. */
|
|
@@ -424,7 +433,13 @@ declare class QueueService {
|
|
|
424
433
|
* The deduplication `id` is a shared key across the queue, so a multi-tenant
|
|
425
434
|
* producer MUST scope it by tenant: keyed on the value alone
|
|
426
435
|
* (`` `reindex:${term}` ``), tenant A enqueuing a term suppresses tenant B's job
|
|
427
|
-
* for the same term.
|
|
436
|
+
* for the same term.
|
|
437
|
+
*
|
|
438
|
+
* Scoping alone is not enough. Two variable fields joined by a delimiter that
|
|
439
|
+
* either may contain do not produce a unique key: with `` `${tenantId}:${term}` ``,
|
|
440
|
+
* the pairs `('a', 'b:c')` and `('a:b', 'c')` both yield `a:b:c`. Percent-encode
|
|
441
|
+
* every variable field so the delimiter cannot appear inside one:
|
|
442
|
+
* `` `${encodeURIComponent(tenantId)}:${encodeURIComponent(term)}` ``.
|
|
428
443
|
*
|
|
429
444
|
* Deduplication modes (`deduplication: { id, ttl?, extend?, replace?, keepLastIfActive? }`):
|
|
430
445
|
* - Simple `{ id }` — collapse duplicates until the in-flight job completes or fails.
|
|
@@ -439,10 +454,11 @@ declare class QueueService {
|
|
|
439
454
|
* @param data - Typed job payload.
|
|
440
455
|
* @param options - Per-job BullMQ options (priority, delay, `jobId`, `deduplication`, ...).
|
|
441
456
|
* @returns The created job.
|
|
442
|
-
* @example Throttle: at most one reindex per term every 5 seconds.
|
|
443
|
-
*
|
|
457
|
+
* @example Throttle: at most one reindex per term every 5 seconds. Both variable
|
|
458
|
+
* fields are percent-encoded, so no other tenant/term pair can build this key.
|
|
459
|
+
* const dedupId = `${encodeURIComponent(tenantId)}:reindex:${encodeURIComponent(term)}`
|
|
444
460
|
* await queueService.enqueue('search', 'reindex', { term }, {
|
|
445
|
-
* deduplication: { id:
|
|
461
|
+
* deduplication: { id: dedupId, ttl: 5_000 },
|
|
446
462
|
* })
|
|
447
463
|
*/
|
|
448
464
|
enqueue<TData = unknown, TResult = unknown>(queueName: string, jobName: string, data: TData, options?: JobsOptions): Promise<TypedJob<TData, TResult>>;
|
|
@@ -1402,7 +1418,6 @@ declare const JOB_STATUS: {
|
|
|
1402
1418
|
readonly COMPLETED: "completed";
|
|
1403
1419
|
readonly FAILED: "failed";
|
|
1404
1420
|
readonly DELAYED: "delayed";
|
|
1405
|
-
readonly PAUSED: "paused";
|
|
1406
1421
|
};
|
|
1407
1422
|
|
|
1408
1423
|
export { BYMAX_QUEUE_CONNECTION_MODE, BYMAX_QUEUE_OPTIONS, BYMAX_QUEUE_REDIS_CLIENT, BYMAX_QUEUE_RESOLVED_OPTIONS, type BulkJob, BymaxQueueModule, type BymaxQueueModuleAsyncOptions, type BymaxQueueModuleOptions, type BymaxQueueOptionsFactory, ConnectionResolver, DEFAULT_JOB_OPTIONS, DEFAULT_METRICS_CACHE_TTL_MS, DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS, DEFAULT_WORKER_CONCURRENCY, FlowService, JOB_STATUS, type JobSchedulerRepeatOptions, type JobStatus, MAX_WORKER_CONCURRENCY, MetricsService, OnQueueEvent, OnWorkerEvent, Process, type ProcessHandlerMetadata, Processor, type ProcessorMetadata, type ProgrammaticWorkerConfig, QUEUE_ERROR_CODES, QUEUE_ERROR_MESSAGES, type QueueConnectionConfig, type QueueConnectionMode, type QueueErrorCode, type QueueEventListenerMetadata, type QueueEventName, QueueEventsRegistry, QueueException, QueueLifecycle, type QueueMetrics, QueueService, type SandboxedWorkerConfig, type WorkerEventName, type WorkerOptions, WorkerRegistry };
|
package/dist/server/index.mjs
CHANGED
|
@@ -196,11 +196,22 @@ var ConnectionResolver = class {
|
|
|
196
196
|
return;
|
|
197
197
|
}
|
|
198
198
|
this.mode = "mode-b-owned";
|
|
199
|
-
__privateSet(this, _client, "url" in cfg ? new Redis(cfg.url,
|
|
199
|
+
__privateSet(this, _client, "url" in cfg ? new Redis(cfg.url, this.ownedOptions(cfg.options)) : new Redis(this.ownedOptions(cfg.options)));
|
|
200
200
|
await this.waitReady(
|
|
201
201
|
__privateGet(this, _options).connectionReadyTimeoutMs ?? DEFAULT_CONNECTION_READY_TIMEOUT_MS
|
|
202
202
|
);
|
|
203
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Merge the consumer's options with the owned-connection policy
|
|
206
|
+
* (`lazyConnect: false`, so the client connects eagerly and `init` can wait
|
|
207
|
+
* for `ready`) and narrow the result to the shape ioredis 6's constructor
|
|
208
|
+
* accepts. The runtime object is unchanged — the cast only drops the
|
|
209
|
+
* `undefined` that `RedisOptions.replyMapping` carries and the constructor
|
|
210
|
+
* rejects.
|
|
211
|
+
*/
|
|
212
|
+
ownedOptions(options) {
|
|
213
|
+
return { ...options ?? {}, lazyConnect: false };
|
|
214
|
+
}
|
|
204
215
|
/** The resolved Queue-role client. */
|
|
205
216
|
getClient() {
|
|
206
217
|
if (!__privateGet(this, _client)) {
|
|
@@ -379,7 +390,13 @@ var QueueService = class {
|
|
|
379
390
|
* The deduplication `id` is a shared key across the queue, so a multi-tenant
|
|
380
391
|
* producer MUST scope it by tenant: keyed on the value alone
|
|
381
392
|
* (`` `reindex:${term}` ``), tenant A enqueuing a term suppresses tenant B's job
|
|
382
|
-
* for the same term.
|
|
393
|
+
* for the same term.
|
|
394
|
+
*
|
|
395
|
+
* Scoping alone is not enough. Two variable fields joined by a delimiter that
|
|
396
|
+
* either may contain do not produce a unique key: with `` `${tenantId}:${term}` ``,
|
|
397
|
+
* the pairs `('a', 'b:c')` and `('a:b', 'c')` both yield `a:b:c`. Percent-encode
|
|
398
|
+
* every variable field so the delimiter cannot appear inside one:
|
|
399
|
+
* `` `${encodeURIComponent(tenantId)}:${encodeURIComponent(term)}` ``.
|
|
383
400
|
*
|
|
384
401
|
* Deduplication modes (`deduplication: { id, ttl?, extend?, replace?, keepLastIfActive? }`):
|
|
385
402
|
* - Simple `{ id }` — collapse duplicates until the in-flight job completes or fails.
|
|
@@ -394,10 +411,11 @@ var QueueService = class {
|
|
|
394
411
|
* @param data - Typed job payload.
|
|
395
412
|
* @param options - Per-job BullMQ options (priority, delay, `jobId`, `deduplication`, ...).
|
|
396
413
|
* @returns The created job.
|
|
397
|
-
* @example Throttle: at most one reindex per term every 5 seconds.
|
|
398
|
-
*
|
|
414
|
+
* @example Throttle: at most one reindex per term every 5 seconds. Both variable
|
|
415
|
+
* fields are percent-encoded, so no other tenant/term pair can build this key.
|
|
416
|
+
* const dedupId = `${encodeURIComponent(tenantId)}:reindex:${encodeURIComponent(term)}`
|
|
399
417
|
* await queueService.enqueue('search', 'reindex', { term }, {
|
|
400
|
-
* deduplication: { id:
|
|
418
|
+
* deduplication: { id: dedupId, ttl: 5_000 },
|
|
401
419
|
* })
|
|
402
420
|
*/
|
|
403
421
|
async enqueue(queueName, jobName, data, options) {
|
|
@@ -469,8 +487,7 @@ var QueueService = class {
|
|
|
469
487
|
"active",
|
|
470
488
|
"completed",
|
|
471
489
|
"failed",
|
|
472
|
-
"delayed"
|
|
473
|
-
"paused"
|
|
490
|
+
"delayed"
|
|
474
491
|
);
|
|
475
492
|
return {
|
|
476
493
|
queue: queueName,
|
|
@@ -1339,9 +1356,9 @@ QueueLifecycle = __decorateClass([
|
|
|
1339
1356
|
|
|
1340
1357
|
// src/server/bymax-queue.module.ts
|
|
1341
1358
|
var { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = (
|
|
1342
|
-
// The `moduleName` shapes the generated token's
|
|
1343
|
-
//
|
|
1344
|
-
//
|
|
1359
|
+
// The `moduleName` shapes the generated option token's identifier — `BYMAX_QUEUE_MODULE_OPTIONS`,
|
|
1360
|
+
// which the module spec asserts by value; emptying the name would degrade it to an opaque
|
|
1361
|
+
// `CONFIGURABLE_MODULE_OPTIONS[<hash>]`, so the argument is a caught regression, not a free label.
|
|
1345
1362
|
new ConfigurableModuleBuilder({ moduleName: "BymaxQueue" }).setClassMethodName("forRoot").setFactoryMethodName("createQueueOptions").setExtras({ isGlobal: true }, (definition, extras) => ({
|
|
1346
1363
|
...definition,
|
|
1347
1364
|
global: extras.isGlobal
|
|
@@ -1546,8 +1563,7 @@ var JOB_STATUS = {
|
|
|
1546
1563
|
ACTIVE: "active",
|
|
1547
1564
|
COMPLETED: "completed",
|
|
1548
1565
|
FAILED: "failed",
|
|
1549
|
-
DELAYED: "delayed"
|
|
1550
|
-
PAUSED: "paused"
|
|
1566
|
+
DELAYED: "delayed"
|
|
1551
1567
|
};
|
|
1552
1568
|
|
|
1553
1569
|
export { BYMAX_QUEUE_CONNECTION_MODE, BYMAX_QUEUE_OPTIONS, BYMAX_QUEUE_REDIS_CLIENT, BYMAX_QUEUE_RESOLVED_OPTIONS, BymaxQueueModule, ConnectionResolver, DEFAULT_JOB_OPTIONS, DEFAULT_METRICS_CACHE_TTL_MS, DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS, DEFAULT_WORKER_CONCURRENCY, FlowService, JOB_STATUS, MAX_WORKER_CONCURRENCY, MetricsService, OnQueueEvent, OnWorkerEvent, Process, Processor, QUEUE_ERROR_CODES, QUEUE_ERROR_MESSAGES, QueueEventsRegistry, QueueException, QueueService, WorkerRegistry };
|
package/dist/shared/index.cjs
CHANGED
package/dist/shared/index.d.cts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @example
|
|
10
10
|
* const status: JobStatus = 'waiting'
|
|
11
11
|
*/
|
|
12
|
-
type JobStatus = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed'
|
|
12
|
+
type JobStatus = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @fileoverview Instantaneous queue metrics snapshot type.
|
|
@@ -22,14 +22,14 @@ type JobStatus = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed' | 'pa
|
|
|
22
22
|
* @example
|
|
23
23
|
* const metrics: QueueMetrics = {
|
|
24
24
|
* queue: 'email',
|
|
25
|
-
* counts: { waiting: 3, active: 1, completed: 120, failed: 2, delayed: 0
|
|
25
|
+
* counts: { waiting: 3, active: 1, completed: 120, failed: 2, delayed: 0 },
|
|
26
26
|
* collectedAt: '2026-06-26T12:00:00.000Z',
|
|
27
27
|
* }
|
|
28
28
|
*/
|
|
29
29
|
interface QueueMetrics {
|
|
30
30
|
/** Queue name the snapshot was taken from. */
|
|
31
31
|
queue: string;
|
|
32
|
-
/** Number of jobs in each of the
|
|
32
|
+
/** Number of jobs in each of the five BullMQ statuses. */
|
|
33
33
|
counts: Record<JobStatus, number>;
|
|
34
34
|
/** ISO 8601 UTC timestamp when the snapshot was collected. */
|
|
35
35
|
collectedAt: string;
|
|
@@ -94,7 +94,6 @@ declare const JOB_STATUS: {
|
|
|
94
94
|
readonly COMPLETED: "completed";
|
|
95
95
|
readonly FAILED: "failed";
|
|
96
96
|
readonly DELAYED: "delayed";
|
|
97
|
-
readonly PAUSED: "paused";
|
|
98
97
|
};
|
|
99
98
|
|
|
100
99
|
/**
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @example
|
|
10
10
|
* const status: JobStatus = 'waiting'
|
|
11
11
|
*/
|
|
12
|
-
type JobStatus = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed'
|
|
12
|
+
type JobStatus = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @fileoverview Instantaneous queue metrics snapshot type.
|
|
@@ -22,14 +22,14 @@ type JobStatus = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed' | 'pa
|
|
|
22
22
|
* @example
|
|
23
23
|
* const metrics: QueueMetrics = {
|
|
24
24
|
* queue: 'email',
|
|
25
|
-
* counts: { waiting: 3, active: 1, completed: 120, failed: 2, delayed: 0
|
|
25
|
+
* counts: { waiting: 3, active: 1, completed: 120, failed: 2, delayed: 0 },
|
|
26
26
|
* collectedAt: '2026-06-26T12:00:00.000Z',
|
|
27
27
|
* }
|
|
28
28
|
*/
|
|
29
29
|
interface QueueMetrics {
|
|
30
30
|
/** Queue name the snapshot was taken from. */
|
|
31
31
|
queue: string;
|
|
32
|
-
/** Number of jobs in each of the
|
|
32
|
+
/** Number of jobs in each of the five BullMQ statuses. */
|
|
33
33
|
counts: Record<JobStatus, number>;
|
|
34
34
|
/** ISO 8601 UTC timestamp when the snapshot was collected. */
|
|
35
35
|
collectedAt: string;
|
|
@@ -94,7 +94,6 @@ declare const JOB_STATUS: {
|
|
|
94
94
|
readonly COMPLETED: "completed";
|
|
95
95
|
readonly FAILED: "failed";
|
|
96
96
|
readonly DELAYED: "delayed";
|
|
97
|
-
readonly PAUSED: "paused";
|
|
98
97
|
};
|
|
99
98
|
|
|
100
99
|
/**
|
package/dist/shared/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bymax-one/nest-queue",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.2",
|
|
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",
|
|
@@ -82,9 +82,9 @@
|
|
|
82
82
|
"peerDependencies": {
|
|
83
83
|
"@nestjs/common": "^11.0.16",
|
|
84
84
|
"@nestjs/core": "^11.1.18",
|
|
85
|
-
"bullmq": "^
|
|
86
|
-
"bullmq-otel": "^
|
|
87
|
-
"ioredis": "^
|
|
85
|
+
"bullmq": "^6.0.0",
|
|
86
|
+
"bullmq-otel": "^2.0.0",
|
|
87
|
+
"ioredis": "^6.0.0",
|
|
88
88
|
"reflect-metadata": "^0.2.0"
|
|
89
89
|
},
|
|
90
90
|
"peerDependenciesMeta": {
|
|
@@ -96,20 +96,19 @@
|
|
|
96
96
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
97
97
|
"@commitlint/cli": "^21.2.1",
|
|
98
98
|
"@commitlint/config-conventional": "^21.2.0",
|
|
99
|
-
"@eslint/js": "^
|
|
99
|
+
"@eslint/js": "^10.0.1",
|
|
100
100
|
"@nestjs/common": "^11.1.28",
|
|
101
101
|
"@nestjs/core": "^11.1.28",
|
|
102
102
|
"@nestjs/testing": "^11.1.28",
|
|
103
|
-
"@stryker-mutator/core": "^
|
|
104
|
-
"@stryker-mutator/jest-runner": "^
|
|
103
|
+
"@stryker-mutator/core": "^10.0.0",
|
|
104
|
+
"@stryker-mutator/jest-runner": "^10.0.0",
|
|
105
105
|
"@types/jest": "^30.0.0",
|
|
106
|
-
"@types/node": "^
|
|
107
|
-
"bullmq": "^
|
|
108
|
-
"eslint": "^
|
|
106
|
+
"@types/node": "^26.2.0",
|
|
107
|
+
"bullmq": "^6.0.0",
|
|
108
|
+
"eslint": "^10.8.1",
|
|
109
109
|
"eslint-config-prettier": "^10.1.8",
|
|
110
110
|
"husky": "^9.1.7",
|
|
111
|
-
"ioredis": "
|
|
112
|
-
"ioredis-mock": "^8.9.0",
|
|
111
|
+
"ioredis": "^6.0.0",
|
|
113
112
|
"jest": "^30.0.0",
|
|
114
113
|
"jest-environment-node": "^30.4.1",
|
|
115
114
|
"lint-staged": "^17.2.0",
|
|
@@ -119,7 +118,7 @@
|
|
|
119
118
|
"testcontainers": "^12.0.4",
|
|
120
119
|
"ts-jest": "^29.4.12",
|
|
121
120
|
"tsup": "^8.3.0",
|
|
122
|
-
"typescript": "^
|
|
121
|
+
"typescript": "^6.0.3",
|
|
123
122
|
"typescript-eslint": "^8.65.0"
|
|
124
123
|
},
|
|
125
124
|
"keywords": [
|