@bymax-one/nest-queue 1.0.10 → 1.2.0
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 +46 -0
- package/README.md +13 -13
- package/dist/server/index.cjs +25 -10
- package/dist/server/index.d.cts +20 -6
- package/dist/server/index.d.ts +20 -6
- package/dist/server/index.mjs +25 -10
- 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 +11 -12
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,50 @@ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.2.0] — 2026-08-10
|
|
11
|
+
|
|
12
|
+
**Breaking, shipped as a minor by choice.** The peer ranges move to the next major of each
|
|
13
|
+
Redis/BullMQ package and the `'paused'` job state is removed from the public status surface —
|
|
14
|
+
changes that would normally warrant a major. They ship under a minor because the package has no
|
|
15
|
+
downstream consumer yet; every breaking detail is called out below so a first adopter sees it.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Peer dependencies migrated to the next major:** `bullmq` `^5` → `^6`, `bullmq-otel` `^1` →
|
|
20
|
+
`^2`, and `ioredis` `^5` → `^6`. A consumer must upgrade all three together. `ioredis` 6 keeps
|
|
21
|
+
`'legacy'` reply mapping by default, so BullMQ's reply-shape assumptions are unaffected; it does
|
|
22
|
+
require Node.js ≥ 20, already covered by this package's `engines`.
|
|
23
|
+
|
|
24
|
+
### Removed
|
|
25
|
+
|
|
26
|
+
- **`'paused'` is no longer a job status.** BullMQ 6 dropped `'paused'` from its job-state set —
|
|
27
|
+
jobs on a paused queue now live in `'wait'`. It is therefore removed from the `JobStatus` union,
|
|
28
|
+
from the `JOB_STATUS` constant (`JOB_STATUS.PAUSED` no longer exists), and from the
|
|
29
|
+
`getMetrics().counts` shape, which now reports the five remaining statuses (`waiting`, `active`,
|
|
30
|
+
`completed`, `failed`, `delayed`). The queue-lifecycle `'paused'`/`'resumed'` **events**
|
|
31
|
+
(`@OnQueueEvent`) and the `pauseQueue`/`resumeQueue` methods are unchanged — those describe the
|
|
32
|
+
queue, not a job.
|
|
33
|
+
|
|
34
|
+
### Internal
|
|
35
|
+
|
|
36
|
+
- Dev-tooling bumps: `eslint` `^10`, `typescript` `^6`, `@types/node` `^26`, plus `@eslint/js`
|
|
37
|
+
`^10`. These do not ship and do not change `dist/`. The package's `engines` stays at Node ≥ 24.
|
|
38
|
+
- Mutation gate tightened: Stryker `break`/`high` raised from 95/99 → **100** (the run is at 100%,
|
|
39
|
+
0 survivors).
|
|
40
|
+
|
|
41
|
+
## [1.0.11] — 2026-08-10
|
|
42
|
+
|
|
43
|
+
**Documentation only.** The shipped declaration comments change (`dts` preserves them); no runtime
|
|
44
|
+
code changed.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- The deduplication example — in the `QueueService` JSDoc and in
|
|
49
|
+
`docs/technical_specification.md` — now scopes the dedup `id` by tenant
|
|
50
|
+
(`` `${tenantId}:reindex:${term}` ``). The `id` is a shared key across the queue, so a
|
|
51
|
+
multi-tenant producer keying on the value alone lets one tenant's enqueue suppress another
|
|
52
|
+
tenant's job for the same value; the example previously demonstrated exactly that.
|
|
53
|
+
|
|
10
54
|
## [1.0.10] — 2026-08-06
|
|
11
55
|
|
|
12
56
|
**Documentation and tooling only.** `dist/` is byte-identical to `1.0.9`; no source file
|
|
@@ -443,6 +487,8 @@ v6 peer range.
|
|
|
443
487
|
|
|
444
488
|
---
|
|
445
489
|
|
|
490
|
+
[1.2.0]: https://github.com/bymaxone/nest-queue/compare/v1.0.11...v1.2.0
|
|
491
|
+
[1.0.11]: https://github.com/bymaxone/nest-queue/compare/v1.0.10...v1.0.11
|
|
446
492
|
[1.0.10]: https://github.com/bymaxone/nest-queue/compare/v1.0.9...v1.0.10
|
|
447
493
|
[1.0.9]: https://github.com/bymaxone/nest-queue/compare/v1.0.8...v1.0.9
|
|
448
494
|
[1.0.8]: https://github.com/bymaxone/nest-queue/compare/v1.0.7...v1.0.8
|
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
|
|
@@ -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)) {
|
|
@@ -378,6 +389,11 @@ exports.QueueService = class QueueService {
|
|
|
378
389
|
* **producers** and do not change the at-least-once guarantee on the
|
|
379
390
|
* **consumer** side.
|
|
380
391
|
*
|
|
392
|
+
* The deduplication `id` is a shared key across the queue, so a multi-tenant
|
|
393
|
+
* producer MUST scope it by tenant: keyed on the value alone
|
|
394
|
+
* (`` `reindex:${term}` ``), tenant A enqueuing a term suppresses tenant B's job
|
|
395
|
+
* for the same term. Include the tenant (`` `${tenantId}:reindex:${term}` ``).
|
|
396
|
+
*
|
|
381
397
|
* Deduplication modes (`deduplication: { id, ttl?, extend?, replace?, keepLastIfActive? }`):
|
|
382
398
|
* - Simple `{ id }` — collapse duplicates until the in-flight job completes or fails.
|
|
383
399
|
* - Throttle `{ id, ttl }` — ignore duplicates for `ttl` milliseconds.
|
|
@@ -391,9 +407,10 @@ exports.QueueService = class QueueService {
|
|
|
391
407
|
* @param data - Typed job payload.
|
|
392
408
|
* @param options - Per-job BullMQ options (priority, delay, `jobId`, `deduplication`, ...).
|
|
393
409
|
* @returns The created job.
|
|
394
|
-
* @example Throttle: at most one reindex per term every 5 seconds.
|
|
410
|
+
* @example Throttle: at most one reindex per term every 5 seconds. The dedup key
|
|
411
|
+
* carries the tenant so one tenant's term cannot suppress another's.
|
|
395
412
|
* await queueService.enqueue('search', 'reindex', { term }, {
|
|
396
|
-
* deduplication: { id:
|
|
413
|
+
* deduplication: { id: `${tenantId}:reindex:${term}`, ttl: 5_000 },
|
|
397
414
|
* })
|
|
398
415
|
*/
|
|
399
416
|
async enqueue(queueName, jobName, data, options) {
|
|
@@ -465,8 +482,7 @@ exports.QueueService = class QueueService {
|
|
|
465
482
|
"active",
|
|
466
483
|
"completed",
|
|
467
484
|
"failed",
|
|
468
|
-
"delayed"
|
|
469
|
-
"paused"
|
|
485
|
+
"delayed"
|
|
470
486
|
);
|
|
471
487
|
return {
|
|
472
488
|
queue: queueName,
|
|
@@ -1335,9 +1351,9 @@ QueueLifecycle = __decorateClass([
|
|
|
1335
1351
|
|
|
1336
1352
|
// src/server/bymax-queue.module.ts
|
|
1337
1353
|
var { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = (
|
|
1338
|
-
// The `moduleName` shapes the generated token's
|
|
1339
|
-
//
|
|
1340
|
-
//
|
|
1354
|
+
// The `moduleName` shapes the generated option token's identifier — `BYMAX_QUEUE_MODULE_OPTIONS`,
|
|
1355
|
+
// which the module spec asserts by value; emptying the name would degrade it to an opaque
|
|
1356
|
+
// `CONFIGURABLE_MODULE_OPTIONS[<hash>]`, so the argument is a caught regression, not a free label.
|
|
1341
1357
|
new common.ConfigurableModuleBuilder({ moduleName: "BymaxQueue" }).setClassMethodName("forRoot").setFactoryMethodName("createQueueOptions").setExtras({ isGlobal: true }, (definition, extras) => ({
|
|
1342
1358
|
...definition,
|
|
1343
1359
|
global: extras.isGlobal
|
|
@@ -1542,8 +1558,7 @@ var JOB_STATUS = {
|
|
|
1542
1558
|
ACTIVE: "active",
|
|
1543
1559
|
COMPLETED: "completed",
|
|
1544
1560
|
FAILED: "failed",
|
|
1545
|
-
DELAYED: "delayed"
|
|
1546
|
-
PAUSED: "paused"
|
|
1561
|
+
DELAYED: "delayed"
|
|
1547
1562
|
};
|
|
1548
1563
|
|
|
1549
1564
|
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. */
|
|
@@ -421,6 +430,11 @@ declare class QueueService {
|
|
|
421
430
|
* **producers** and do not change the at-least-once guarantee on the
|
|
422
431
|
* **consumer** side.
|
|
423
432
|
*
|
|
433
|
+
* The deduplication `id` is a shared key across the queue, so a multi-tenant
|
|
434
|
+
* producer MUST scope it by tenant: keyed on the value alone
|
|
435
|
+
* (`` `reindex:${term}` ``), tenant A enqueuing a term suppresses tenant B's job
|
|
436
|
+
* for the same term. Include the tenant (`` `${tenantId}:reindex:${term}` ``).
|
|
437
|
+
*
|
|
424
438
|
* Deduplication modes (`deduplication: { id, ttl?, extend?, replace?, keepLastIfActive? }`):
|
|
425
439
|
* - Simple `{ id }` — collapse duplicates until the in-flight job completes or fails.
|
|
426
440
|
* - Throttle `{ id, ttl }` — ignore duplicates for `ttl` milliseconds.
|
|
@@ -434,9 +448,10 @@ declare class QueueService {
|
|
|
434
448
|
* @param data - Typed job payload.
|
|
435
449
|
* @param options - Per-job BullMQ options (priority, delay, `jobId`, `deduplication`, ...).
|
|
436
450
|
* @returns The created job.
|
|
437
|
-
* @example Throttle: at most one reindex per term every 5 seconds.
|
|
451
|
+
* @example Throttle: at most one reindex per term every 5 seconds. The dedup key
|
|
452
|
+
* carries the tenant so one tenant's term cannot suppress another's.
|
|
438
453
|
* await queueService.enqueue('search', 'reindex', { term }, {
|
|
439
|
-
* deduplication: { id:
|
|
454
|
+
* deduplication: { id: `${tenantId}:reindex:${term}`, ttl: 5_000 },
|
|
440
455
|
* })
|
|
441
456
|
*/
|
|
442
457
|
enqueue<TData = unknown, TResult = unknown>(queueName: string, jobName: string, data: TData, options?: JobsOptions): Promise<TypedJob<TData, TResult>>;
|
|
@@ -1396,7 +1411,6 @@ declare const JOB_STATUS: {
|
|
|
1396
1411
|
readonly COMPLETED: "completed";
|
|
1397
1412
|
readonly FAILED: "failed";
|
|
1398
1413
|
readonly DELAYED: "delayed";
|
|
1399
|
-
readonly PAUSED: "paused";
|
|
1400
1414
|
};
|
|
1401
1415
|
|
|
1402
1416
|
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. */
|
|
@@ -421,6 +430,11 @@ declare class QueueService {
|
|
|
421
430
|
* **producers** and do not change the at-least-once guarantee on the
|
|
422
431
|
* **consumer** side.
|
|
423
432
|
*
|
|
433
|
+
* The deduplication `id` is a shared key across the queue, so a multi-tenant
|
|
434
|
+
* producer MUST scope it by tenant: keyed on the value alone
|
|
435
|
+
* (`` `reindex:${term}` ``), tenant A enqueuing a term suppresses tenant B's job
|
|
436
|
+
* for the same term. Include the tenant (`` `${tenantId}:reindex:${term}` ``).
|
|
437
|
+
*
|
|
424
438
|
* Deduplication modes (`deduplication: { id, ttl?, extend?, replace?, keepLastIfActive? }`):
|
|
425
439
|
* - Simple `{ id }` — collapse duplicates until the in-flight job completes or fails.
|
|
426
440
|
* - Throttle `{ id, ttl }` — ignore duplicates for `ttl` milliseconds.
|
|
@@ -434,9 +448,10 @@ declare class QueueService {
|
|
|
434
448
|
* @param data - Typed job payload.
|
|
435
449
|
* @param options - Per-job BullMQ options (priority, delay, `jobId`, `deduplication`, ...).
|
|
436
450
|
* @returns The created job.
|
|
437
|
-
* @example Throttle: at most one reindex per term every 5 seconds.
|
|
451
|
+
* @example Throttle: at most one reindex per term every 5 seconds. The dedup key
|
|
452
|
+
* carries the tenant so one tenant's term cannot suppress another's.
|
|
438
453
|
* await queueService.enqueue('search', 'reindex', { term }, {
|
|
439
|
-
* deduplication: { id:
|
|
454
|
+
* deduplication: { id: `${tenantId}:reindex:${term}`, ttl: 5_000 },
|
|
440
455
|
* })
|
|
441
456
|
*/
|
|
442
457
|
enqueue<TData = unknown, TResult = unknown>(queueName: string, jobName: string, data: TData, options?: JobsOptions): Promise<TypedJob<TData, TResult>>;
|
|
@@ -1396,7 +1411,6 @@ declare const JOB_STATUS: {
|
|
|
1396
1411
|
readonly COMPLETED: "completed";
|
|
1397
1412
|
readonly FAILED: "failed";
|
|
1398
1413
|
readonly DELAYED: "delayed";
|
|
1399
|
-
readonly PAUSED: "paused";
|
|
1400
1414
|
};
|
|
1401
1415
|
|
|
1402
1416
|
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)) {
|
|
@@ -376,6 +387,11 @@ var QueueService = class {
|
|
|
376
387
|
* **producers** and do not change the at-least-once guarantee on the
|
|
377
388
|
* **consumer** side.
|
|
378
389
|
*
|
|
390
|
+
* The deduplication `id` is a shared key across the queue, so a multi-tenant
|
|
391
|
+
* producer MUST scope it by tenant: keyed on the value alone
|
|
392
|
+
* (`` `reindex:${term}` ``), tenant A enqueuing a term suppresses tenant B's job
|
|
393
|
+
* for the same term. Include the tenant (`` `${tenantId}:reindex:${term}` ``).
|
|
394
|
+
*
|
|
379
395
|
* Deduplication modes (`deduplication: { id, ttl?, extend?, replace?, keepLastIfActive? }`):
|
|
380
396
|
* - Simple `{ id }` — collapse duplicates until the in-flight job completes or fails.
|
|
381
397
|
* - Throttle `{ id, ttl }` — ignore duplicates for `ttl` milliseconds.
|
|
@@ -389,9 +405,10 @@ var QueueService = class {
|
|
|
389
405
|
* @param data - Typed job payload.
|
|
390
406
|
* @param options - Per-job BullMQ options (priority, delay, `jobId`, `deduplication`, ...).
|
|
391
407
|
* @returns The created job.
|
|
392
|
-
* @example Throttle: at most one reindex per term every 5 seconds.
|
|
408
|
+
* @example Throttle: at most one reindex per term every 5 seconds. The dedup key
|
|
409
|
+
* carries the tenant so one tenant's term cannot suppress another's.
|
|
393
410
|
* await queueService.enqueue('search', 'reindex', { term }, {
|
|
394
|
-
* deduplication: { id:
|
|
411
|
+
* deduplication: { id: `${tenantId}:reindex:${term}`, ttl: 5_000 },
|
|
395
412
|
* })
|
|
396
413
|
*/
|
|
397
414
|
async enqueue(queueName, jobName, data, options) {
|
|
@@ -463,8 +480,7 @@ var QueueService = class {
|
|
|
463
480
|
"active",
|
|
464
481
|
"completed",
|
|
465
482
|
"failed",
|
|
466
|
-
"delayed"
|
|
467
|
-
"paused"
|
|
483
|
+
"delayed"
|
|
468
484
|
);
|
|
469
485
|
return {
|
|
470
486
|
queue: queueName,
|
|
@@ -1333,9 +1349,9 @@ QueueLifecycle = __decorateClass([
|
|
|
1333
1349
|
|
|
1334
1350
|
// src/server/bymax-queue.module.ts
|
|
1335
1351
|
var { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = (
|
|
1336
|
-
// The `moduleName` shapes the generated token's
|
|
1337
|
-
//
|
|
1338
|
-
//
|
|
1352
|
+
// The `moduleName` shapes the generated option token's identifier — `BYMAX_QUEUE_MODULE_OPTIONS`,
|
|
1353
|
+
// which the module spec asserts by value; emptying the name would degrade it to an opaque
|
|
1354
|
+
// `CONFIGURABLE_MODULE_OPTIONS[<hash>]`, so the argument is a caught regression, not a free label.
|
|
1339
1355
|
new ConfigurableModuleBuilder({ moduleName: "BymaxQueue" }).setClassMethodName("forRoot").setFactoryMethodName("createQueueOptions").setExtras({ isGlobal: true }, (definition, extras) => ({
|
|
1340
1356
|
...definition,
|
|
1341
1357
|
global: extras.isGlobal
|
|
@@ -1540,8 +1556,7 @@ var JOB_STATUS = {
|
|
|
1540
1556
|
ACTIVE: "active",
|
|
1541
1557
|
COMPLETED: "completed",
|
|
1542
1558
|
FAILED: "failed",
|
|
1543
|
-
DELAYED: "delayed"
|
|
1544
|
-
PAUSED: "paused"
|
|
1559
|
+
DELAYED: "delayed"
|
|
1545
1560
|
};
|
|
1546
1561
|
|
|
1547
1562
|
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.0
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
"lint": "eslint src scripts test",
|
|
63
63
|
"lint:fix": "eslint src scripts test --fix",
|
|
64
64
|
"mutation": "stryker run",
|
|
65
|
+
"mutation:full": "node -e \"require('node:fs').rmSync('reports/stryker-incremental.json',{force:true,recursive:true})\" && stryker run",
|
|
65
66
|
"mutation:dry-run": "stryker run --dryRunOnly",
|
|
66
|
-
"mutation:incremental": "stryker run --incremental",
|
|
67
67
|
"prepare": "husky",
|
|
68
68
|
"prepublishOnly": "pnpm clean && pnpm typecheck && pnpm test:types && pnpm lint && pnpm check:mutants && pnpm test:cov:all && pnpm build && pnpm check:published",
|
|
69
69
|
"release": "npm publish --provenance --access public",
|
|
@@ -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
103
|
"@stryker-mutator/core": "^9.6.1",
|
|
104
104
|
"@stryker-mutator/jest-runner": "^9.6.1",
|
|
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": [
|