@bymax-one/nest-queue 1.0.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 +123 -0
- package/LICENSE +21 -0
- package/README.md +858 -0
- package/dist/server/index.cjs +1518 -0
- package/dist/server/index.d.cts +1386 -0
- package/dist/server/index.d.ts +1386 -0
- package/dist/server/index.mjs +1500 -0
- package/dist/shared/index.cjs +32 -0
- package/dist/shared/index.d.cts +132 -0
- package/dist/shared/index.d.ts +132 -0
- package/dist/shared/index.mjs +29 -0
- package/package.json +153 -0
|
@@ -0,0 +1,1386 @@
|
|
|
1
|
+
import * as _nestjs_common from '@nestjs/common';
|
|
2
|
+
import { ModuleMetadata, Type, DynamicModule, OnModuleDestroy, HttpException, HttpStatus } from '@nestjs/common';
|
|
3
|
+
import { JobsOptions, QueueOptions, Telemetry, BulkJobOptions, Queue, Job, FlowJob, JobNode, FlowProducer, Worker, QueueEvents } from 'bullmq';
|
|
4
|
+
export { FlowJob, FlowProducer, Job, JobNode, JobSchedulerJson, JobsOptions, Queue, QueueEvents, SandboxedJob, Telemetry, Worker } from 'bullmq';
|
|
5
|
+
import { Redis, RedisOptions } from 'ioredis';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview Dual-mode Redis connection configuration contract.
|
|
9
|
+
* @layer server/interfaces
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Connection configuration — Mode A (bring your own client) or Mode B
|
|
14
|
+
* (library-owned), discriminated by the presence of `client`, `url`, or
|
|
15
|
+
* `options`. The three arms are mutually exclusive.
|
|
16
|
+
*
|
|
17
|
+
* Mode A: the caller passes a `Redis` already configured for BullMQ (typically
|
|
18
|
+
* obtained from `@bymax-one/nest-cache`). The library uses it AS-IS for the
|
|
19
|
+
* Queue and FlowProducer role and never closes it; Worker/QueueEvents roles get
|
|
20
|
+
* a duplicated connection forced to `maxRetriesPerRequest: null`.
|
|
21
|
+
*
|
|
22
|
+
* Mode B: the library opens its own `ioredis` from a URL or options object. The
|
|
23
|
+
* Queue/FlowProducer connection keeps ioredis' default retries (so enqueue
|
|
24
|
+
* fails fast during a Redis outage); the library closes it on shutdown.
|
|
25
|
+
*
|
|
26
|
+
* @example Mode A
|
|
27
|
+
* const config: QueueConnectionConfig = { client: queueRedis }
|
|
28
|
+
* @example Mode B (url)
|
|
29
|
+
* const config: QueueConnectionConfig = { url: 'redis://localhost:6379', options: { db: 1 } }
|
|
30
|
+
* @example Mode B (options only)
|
|
31
|
+
* const config: QueueConnectionConfig = { options: { host: 'localhost', port: 6379, db: 1 } }
|
|
32
|
+
*/
|
|
33
|
+
type QueueConnectionConfig = {
|
|
34
|
+
client: Redis;
|
|
35
|
+
ownsConnection?: false;
|
|
36
|
+
} | {
|
|
37
|
+
url: string;
|
|
38
|
+
options?: Partial<RedisOptions>;
|
|
39
|
+
} | {
|
|
40
|
+
options: RedisOptions;
|
|
41
|
+
};
|
|
42
|
+
/** Internal discriminator the ConnectionResolver uses to tag the active mode. */
|
|
43
|
+
type QueueConnectionMode = 'mode-a-byo' | 'mode-b-owned';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @fileoverview Synchronous and asynchronous module configuration contracts.
|
|
47
|
+
* @layer server/interfaces
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Synchronous configuration for `BymaxQueueModule.forRoot()`.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* BymaxQueueModule.forRoot({ connection: { url: 'redis://localhost:6379' } })
|
|
55
|
+
*/
|
|
56
|
+
interface BymaxQueueModuleOptions {
|
|
57
|
+
/** Redis connection configuration. Required. */
|
|
58
|
+
connection: QueueConnectionConfig;
|
|
59
|
+
/** Default options applied to every job enqueued through the service. */
|
|
60
|
+
defaultJobOptions?: JobsOptions;
|
|
61
|
+
/** Prefix for all Redis keys (multi-tenant isolation). Default: 'bull'. */
|
|
62
|
+
prefix?: string;
|
|
63
|
+
/** Default options applied to every Queue, merged with the job/prefix defaults. */
|
|
64
|
+
queueOptions?: Partial<Omit<QueueOptions, 'connection' | 'defaultJobOptions' | 'prefix'>>;
|
|
65
|
+
/** Flow Producer configuration. Opt-in. */
|
|
66
|
+
flows?: {
|
|
67
|
+
enabled?: boolean;
|
|
68
|
+
};
|
|
69
|
+
/** Metrics collection configuration. Opt-in. */
|
|
70
|
+
metrics?: {
|
|
71
|
+
enabled?: boolean;
|
|
72
|
+
cacheTtlMs?: number;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Opt-in OpenTelemetry — a BullMQ `Telemetry` (typically `new BullMQOtel(...)`
|
|
76
|
+
* from the optional peer `bullmq-otel`); attached to every Queue and Worker.
|
|
77
|
+
*/
|
|
78
|
+
telemetry?: Telemetry;
|
|
79
|
+
/** Behavior on application shutdown. */
|
|
80
|
+
shutdown?: {
|
|
81
|
+
drainTimeoutMs?: number;
|
|
82
|
+
drainOnShutdown?: boolean;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Register the module globally. Mapped to `DynamicModule.global` by the
|
|
86
|
+
* configurable module builder's `setExtras` (no hand-written `@Global`).
|
|
87
|
+
* Default: true.
|
|
88
|
+
*/
|
|
89
|
+
isGlobal?: boolean;
|
|
90
|
+
/** Mode B only: milliseconds to wait for Redis `ready` before throwing. Default: 10_000. */
|
|
91
|
+
connectionReadyTimeoutMs?: number;
|
|
92
|
+
}
|
|
93
|
+
/** A factory that produces module options, for async configuration. */
|
|
94
|
+
interface BymaxQueueOptionsFactory {
|
|
95
|
+
/** Build the module options synchronously or asynchronously. */
|
|
96
|
+
createQueueOptions(): Promise<BymaxQueueModuleOptions> | BymaxQueueModuleOptions;
|
|
97
|
+
}
|
|
98
|
+
/** Asynchronous configuration mirroring the NestJS async dynamic-module pattern. */
|
|
99
|
+
interface BymaxQueueModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
|
100
|
+
/** Factory returning the module options. */
|
|
101
|
+
useFactory?: (...args: unknown[]) => Promise<BymaxQueueModuleOptions> | BymaxQueueModuleOptions;
|
|
102
|
+
/** Class implementing the options factory. */
|
|
103
|
+
useClass?: Type<BymaxQueueOptionsFactory>;
|
|
104
|
+
/** Existing provider implementing the options factory. */
|
|
105
|
+
useExisting?: Type<BymaxQueueOptionsFactory>;
|
|
106
|
+
/** Providers injected into the factory. */
|
|
107
|
+
inject?: readonly (Type<unknown> | string | symbol)[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Generated configurable-module artifacts; `forRoot`/`forRootAsync` register the module. */
|
|
111
|
+
declare const ConfigurableModuleClass: _nestjs_common.ConfigurableModuleCls<BymaxQueueModuleOptions, "forRoot", "createQueueOptions", {
|
|
112
|
+
isGlobal: boolean;
|
|
113
|
+
}>;
|
|
114
|
+
declare const OPTIONS_TYPE: BymaxQueueModuleOptions & Partial<{
|
|
115
|
+
isGlobal: boolean;
|
|
116
|
+
}>;
|
|
117
|
+
declare const ASYNC_OPTIONS_TYPE: _nestjs_common.ConfigurableModuleAsyncOptions<BymaxQueueModuleOptions, "createQueueOptions"> & Partial<{
|
|
118
|
+
isGlobal: boolean;
|
|
119
|
+
}>;
|
|
120
|
+
/**
|
|
121
|
+
* Root module for the queue library. Use `forRoot` for synchronous registration
|
|
122
|
+
* or `forRootAsync` when the options depend on other modules.
|
|
123
|
+
*
|
|
124
|
+
* @example Synchronous
|
|
125
|
+
* BymaxQueueModule.forRoot({ connection: { url: 'redis://localhost:6379' } })
|
|
126
|
+
* @example Asynchronous (Mode A — bring your own client)
|
|
127
|
+
* BymaxQueueModule.forRootAsync({
|
|
128
|
+
* inject: [REDIS_CLIENT],
|
|
129
|
+
* useFactory: (client: Redis) => ({ connection: { client } }),
|
|
130
|
+
* })
|
|
131
|
+
*/
|
|
132
|
+
declare class BymaxQueueModule extends ConfigurableModuleClass {
|
|
133
|
+
/**
|
|
134
|
+
* Synchronous registration. Validates options, resolves defaults, and wires the
|
|
135
|
+
* connection resolver (initialized via async factory), the queue service, the
|
|
136
|
+
* worker registry, the queue-events registry, processor discovery, and the
|
|
137
|
+
* graceful-shutdown lifecycle.
|
|
138
|
+
*
|
|
139
|
+
* @param options - Static module options.
|
|
140
|
+
* @returns The configured dynamic module.
|
|
141
|
+
*/
|
|
142
|
+
static forRoot(options: typeof OPTIONS_TYPE): DynamicModule;
|
|
143
|
+
/**
|
|
144
|
+
* Asynchronous registration. Use when options depend on other modules
|
|
145
|
+
* (ConfigService, BymaxCacheModule, etc.). Mirrors the NestJS standard async
|
|
146
|
+
* dynamic-module pattern (`useFactory` | `useClass` | `useExisting` + `inject`)
|
|
147
|
+
* and registers the same provider graph and exports as {@link forRoot}, with
|
|
148
|
+
* `global` applied from the `isGlobal` extra. The resolved options and the
|
|
149
|
+
* initialized connection are derived from the async-resolved options at runtime.
|
|
150
|
+
*
|
|
151
|
+
* @param options - Asynchronous module options.
|
|
152
|
+
* @returns The configured dynamic module.
|
|
153
|
+
*/
|
|
154
|
+
static forRootAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule;
|
|
155
|
+
/**
|
|
156
|
+
* Providers shared verbatim by both registration paths: the options alias, the
|
|
157
|
+
* initialized connection resolver, the resolved client/mode tokens, and the
|
|
158
|
+
* singleton services. `BYMAX_QUEUE_RESOLVED_OPTIONS`, `FlowService`, and
|
|
159
|
+
* `MetricsService` are intentionally omitted — they differ per path.
|
|
160
|
+
*
|
|
161
|
+
* @returns The shared provider list.
|
|
162
|
+
*/
|
|
163
|
+
private static buildCoreProviders;
|
|
164
|
+
/**
|
|
165
|
+
* The public exports shared by both registration paths.
|
|
166
|
+
*
|
|
167
|
+
* @returns A fresh exports list (services and injection tokens).
|
|
168
|
+
*/
|
|
169
|
+
private static buildExports;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @fileoverview Symbol injection tokens for the module's providers.
|
|
174
|
+
* @layer server/constants
|
|
175
|
+
*/
|
|
176
|
+
/** Token for the raw, user-supplied module options. */
|
|
177
|
+
declare const BYMAX_QUEUE_OPTIONS: unique symbol;
|
|
178
|
+
/** Token for the resolved Redis client used by the Queue role. */
|
|
179
|
+
declare const BYMAX_QUEUE_REDIS_CLIENT: unique symbol;
|
|
180
|
+
/** Token for the resolved connection mode (`mode-a-byo` or `mode-b-owned`). */
|
|
181
|
+
declare const BYMAX_QUEUE_CONNECTION_MODE: unique symbol;
|
|
182
|
+
/** Token for the frozen, defaults-applied module options. */
|
|
183
|
+
declare const BYMAX_QUEUE_RESOLVED_OPTIONS: unique symbol;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @fileoverview Fully-resolved module options with defaults applied.
|
|
187
|
+
* @layer server/config
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
/** Fully-resolved options — every optional field of the input is filled. */
|
|
191
|
+
interface ResolvedQueueOptions {
|
|
192
|
+
/** Connection configuration, passed through untouched. */
|
|
193
|
+
connection: QueueConnectionConfig;
|
|
194
|
+
/** Default job options, merged over the library defaults. */
|
|
195
|
+
defaultJobOptions: JobsOptions;
|
|
196
|
+
/** Redis key prefix. */
|
|
197
|
+
prefix: string;
|
|
198
|
+
/** Extra Queue options applied to every queue. */
|
|
199
|
+
queueOptions: Partial<Omit<QueueOptions, 'connection' | 'defaultJobOptions' | 'prefix'>>;
|
|
200
|
+
/** Flow support configuration. */
|
|
201
|
+
flows: {
|
|
202
|
+
enabled: boolean;
|
|
203
|
+
};
|
|
204
|
+
/** Metrics support configuration. */
|
|
205
|
+
metrics: {
|
|
206
|
+
enabled: boolean;
|
|
207
|
+
cacheTtlMs: number;
|
|
208
|
+
};
|
|
209
|
+
/** Shutdown behavior configuration. */
|
|
210
|
+
shutdown: {
|
|
211
|
+
drainTimeoutMs: number;
|
|
212
|
+
drainOnShutdown: boolean;
|
|
213
|
+
};
|
|
214
|
+
/** Optional OpenTelemetry instance, attached to every Queue/Worker when present. */
|
|
215
|
+
telemetry?: Telemetry;
|
|
216
|
+
/** Mode B ready timeout in milliseconds. */
|
|
217
|
+
connectionReadyTimeoutMs: number;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @fileoverview Job descriptor used by the bulk-enqueue API.
|
|
222
|
+
* @layer server/interfaces
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* A single job descriptor used by `enqueueBulk`.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* const job: BulkJob<{ to: string }> = { name: 'send', data: { to: 'a@b.com' } }
|
|
230
|
+
*/
|
|
231
|
+
interface BulkJob<TData = unknown> {
|
|
232
|
+
/** Job name used by handlers to dispatch. */
|
|
233
|
+
name: string;
|
|
234
|
+
/** Typed job payload. */
|
|
235
|
+
data: TData;
|
|
236
|
+
/** Per-job BullMQ options (priority, delay, jobId, deduplication, ...). */
|
|
237
|
+
options?: BulkJobOptions;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @fileoverview Job status union shared across runtimes.
|
|
242
|
+
* @layer shared/types
|
|
243
|
+
*/
|
|
244
|
+
/**
|
|
245
|
+
* Snapshot statuses BullMQ exposes via `Queue.getJobCounts()`.
|
|
246
|
+
* Kept in sync with BullMQ's internal status set.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* const status: JobStatus = 'waiting'
|
|
250
|
+
*/
|
|
251
|
+
type JobStatus = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed' | 'paused';
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @fileoverview Instantaneous queue metrics snapshot type.
|
|
255
|
+
* @layer shared/types
|
|
256
|
+
*/
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Instantaneous snapshot of a queue's job counts grouped by status.
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* const metrics: QueueMetrics = {
|
|
263
|
+
* queue: 'email',
|
|
264
|
+
* counts: { waiting: 3, active: 1, completed: 120, failed: 2, delayed: 0, paused: 0 },
|
|
265
|
+
* collectedAt: '2026-06-26T12:00:00.000Z',
|
|
266
|
+
* }
|
|
267
|
+
*/
|
|
268
|
+
interface QueueMetrics {
|
|
269
|
+
/** Queue name the snapshot was taken from. */
|
|
270
|
+
queue: string;
|
|
271
|
+
/** Number of jobs in each of the six BullMQ statuses. */
|
|
272
|
+
counts: Record<JobStatus, number>;
|
|
273
|
+
/** ISO 8601 UTC timestamp when the snapshot was collected. */
|
|
274
|
+
collectedAt: string;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @fileoverview Discriminated union for the BullMQ Job Schedulers (recurring jobs) API.
|
|
279
|
+
* @layer shared/types
|
|
280
|
+
*/
|
|
281
|
+
/**
|
|
282
|
+
* Discriminated union for the BullMQ Job Schedulers API — the current
|
|
283
|
+
* recurring-jobs surface (the legacy repeatable API is removed in BullMQ v6).
|
|
284
|
+
* Either `pattern` (cron) OR `every` (ms interval) is provided, never both.
|
|
285
|
+
*
|
|
286
|
+
* A thin, validated projection of BullMQ's `RepeatOptions` minus the internal
|
|
287
|
+
* scheduler `key`.
|
|
288
|
+
*
|
|
289
|
+
* @example Cron schedule
|
|
290
|
+
* const daily: JobSchedulerRepeatOptions = { pattern: '0 3 * * *', tz: 'America/Sao_Paulo' }
|
|
291
|
+
* @example Fixed interval
|
|
292
|
+
* const everyMinute: JobSchedulerRepeatOptions = { every: 60_000 }
|
|
293
|
+
*/
|
|
294
|
+
type JobSchedulerRepeatOptions = {
|
|
295
|
+
/** Crontab expression (5-field, or 6-field with seconds). */
|
|
296
|
+
pattern: string;
|
|
297
|
+
/** IANA timezone (e.g. 'America/Sao_Paulo'). Default: UTC. */
|
|
298
|
+
tz?: string;
|
|
299
|
+
/** Optional cap on the number of runs. */
|
|
300
|
+
limit?: number;
|
|
301
|
+
/** Start time (epoch ms or ISO string). */
|
|
302
|
+
startDate?: number | string;
|
|
303
|
+
/** Stop time (epoch ms or ISO string). Must be in the future or BullMQ throws. */
|
|
304
|
+
endDate?: number | string;
|
|
305
|
+
} | {
|
|
306
|
+
/** Interval in milliseconds between runs. */
|
|
307
|
+
every: number;
|
|
308
|
+
/** Optional cap on the number of runs. */
|
|
309
|
+
limit?: number;
|
|
310
|
+
/** Phase offset (ms) applied to interval schedulers. */
|
|
311
|
+
offset?: number;
|
|
312
|
+
/** Start time (epoch ms or ISO string). */
|
|
313
|
+
startDate?: number | string;
|
|
314
|
+
/** Stop time (epoch ms or ISO string). */
|
|
315
|
+
endDate?: number | string;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @fileoverview Dual-mode Redis connection resolver. Resolves a BYO client
|
|
320
|
+
* (Mode A) or opens a library-owned ioredis with a ready timeout (Mode B), and
|
|
321
|
+
* enforces the per-role `maxRetriesPerRequest` policy.
|
|
322
|
+
* @layer server/services
|
|
323
|
+
*/
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Resolves the Queue-role Redis connection and tracks ownership so the lifecycle
|
|
327
|
+
* knows whether to close it on shutdown.
|
|
328
|
+
*
|
|
329
|
+
* - Mode A (BYO): the received client is used as-is for the Queue/FlowProducer
|
|
330
|
+
* role and never closed; a duplicated probe verifies that the blocking-role
|
|
331
|
+
* override resolves to `maxRetriesPerRequest: null`.
|
|
332
|
+
* - Mode B (owned): the library opens its own client (URL or options) and waits
|
|
333
|
+
* for `ready`, closing it on shutdown.
|
|
334
|
+
*/
|
|
335
|
+
declare class ConnectionResolver {
|
|
336
|
+
private readonly options;
|
|
337
|
+
private client;
|
|
338
|
+
private mode;
|
|
339
|
+
constructor(options: BymaxQueueModuleOptions);
|
|
340
|
+
/** Resolve and validate the connection. Call once during module bootstrap. */
|
|
341
|
+
init(): Promise<void>;
|
|
342
|
+
/** The resolved Queue-role client. */
|
|
343
|
+
getClient(): Redis;
|
|
344
|
+
/** The resolved connection mode. */
|
|
345
|
+
getMode(): QueueConnectionMode;
|
|
346
|
+
/** Whether the library owns (and must close) the connection. */
|
|
347
|
+
isOwned(): boolean;
|
|
348
|
+
/** Close the library-owned connection on shutdown; never touch a BYO client. */
|
|
349
|
+
teardown(): Promise<void>;
|
|
350
|
+
/** Validate and adopt a bring-your-own client for the Queue role. */
|
|
351
|
+
private initModeA;
|
|
352
|
+
/** Resolve when the owned client reaches `ready`, or reject on timeout/error. */
|
|
353
|
+
private waitReady;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* @fileoverview Base queue service — per-name Queue cache, typed enqueue/inspect,
|
|
358
|
+
* uncached metrics, and control helpers over BullMQ.
|
|
359
|
+
* @layer server/services
|
|
360
|
+
*/
|
|
361
|
+
|
|
362
|
+
/** A typed job in the library's public shape. */
|
|
363
|
+
type TypedJob<TData, TResult> = Job<TData, TResult>;
|
|
364
|
+
/** Status filter accepted by {@link QueueService.cleanQueue}, matching BullMQ. */
|
|
365
|
+
type CleanableStatus = 'completed' | 'failed' | 'delayed' | 'wait' | 'active' | 'paused';
|
|
366
|
+
/**
|
|
367
|
+
* The central interaction point with BullMQ queues. Maintains a per-name `Queue`
|
|
368
|
+
* cache, applies module defaults to every queue, and exposes typed helpers for
|
|
369
|
+
* enqueueing, inspection, metrics, and lifecycle control.
|
|
370
|
+
*/
|
|
371
|
+
declare class QueueService {
|
|
372
|
+
private readonly connection;
|
|
373
|
+
private readonly options;
|
|
374
|
+
private readonly logger;
|
|
375
|
+
private readonly queues;
|
|
376
|
+
constructor(connection: ConnectionResolver, options: ResolvedQueueOptions);
|
|
377
|
+
/**
|
|
378
|
+
* Return the cached `Queue` for `queueName`, or create one with the module
|
|
379
|
+
* defaults applied. Subsequent calls with the same name return the same
|
|
380
|
+
* instance.
|
|
381
|
+
*
|
|
382
|
+
* @param queueName - Unique queue name (used as the Redis key prefix).
|
|
383
|
+
* @param overrides - Optional per-queue option overrides.
|
|
384
|
+
*/
|
|
385
|
+
getOrCreateQueue<TData = unknown, TResult = unknown>(queueName: string, overrides?: Partial<Omit<QueueOptions, 'connection' | 'prefix'>>): Queue<TData, TResult>;
|
|
386
|
+
/** Resolve the raw cached queue (default generics) for internal delegation. */
|
|
387
|
+
private queueFor;
|
|
388
|
+
/**
|
|
389
|
+
* Add a single job to a queue. The queue is created lazily if needed.
|
|
390
|
+
*
|
|
391
|
+
* Delivery is **at-least-once** — a handler may run more than once for the same
|
|
392
|
+
* job (worker crash, lock expiry, or a shutdown force-close). Make handlers
|
|
393
|
+
* idempotent: use an idempotency key on writes, prefer upserts over inserts, or
|
|
394
|
+
* keep an "already-processed" marker keyed by `job.id`.
|
|
395
|
+
*
|
|
396
|
+
* `options` surfaces BullMQ natives directly — including `jobId` (idempotent
|
|
397
|
+
* insert: a second add with the same id is a no-op while the first exists) and
|
|
398
|
+
* `deduplication` (windowed deduplication). The library writes no deduplication
|
|
399
|
+
* code; BullMQ owns the behavior. The deduplication key is independent of
|
|
400
|
+
* `jobId`, so both can be set together or apart. Both collapse duplicate
|
|
401
|
+
* **producers** and do not change the at-least-once guarantee on the
|
|
402
|
+
* **consumer** side.
|
|
403
|
+
*
|
|
404
|
+
* Deduplication modes (`deduplication: { id, ttl?, extend?, replace?, keepLastIfActive? }`):
|
|
405
|
+
* - Simple `{ id }` — collapse duplicates until the in-flight job completes or fails.
|
|
406
|
+
* - Throttle `{ id, ttl }` — ignore duplicates for `ttl` milliseconds.
|
|
407
|
+
* - Debounce `{ id, ttl, extend: true, replace: true }` — keep only the latest
|
|
408
|
+
* data; each duplicate resets the TTL.
|
|
409
|
+
* - keep-last-if-active `{ id, keepLastIfActive: true }` — while a job is active,
|
|
410
|
+
* store the latest data and run a single follow-up when it finishes.
|
|
411
|
+
*
|
|
412
|
+
* @param queueName - Target queue (created lazily if absent).
|
|
413
|
+
* @param jobName - Job identifier used by `@Process` to dispatch.
|
|
414
|
+
* @param data - Typed job payload.
|
|
415
|
+
* @param options - Per-job BullMQ options (priority, delay, `jobId`, `deduplication`, ...).
|
|
416
|
+
* @returns The created job.
|
|
417
|
+
* @example Throttle: at most one reindex per term every 5 seconds.
|
|
418
|
+
* await queueService.enqueue('search', 'reindex', { term }, {
|
|
419
|
+
* deduplication: { id: `reindex:${term}`, ttl: 5_000 },
|
|
420
|
+
* })
|
|
421
|
+
*/
|
|
422
|
+
enqueue<TData = unknown, TResult = unknown>(queueName: string, jobName: string, data: TData, options?: JobsOptions): Promise<TypedJob<TData, TResult>>;
|
|
423
|
+
/**
|
|
424
|
+
* Enqueue multiple jobs in a single Redis roundtrip. The batch is bounded by
|
|
425
|
+
* {@link MAX_BULK_SIZE} to guard against a self-inflicted memory spike; any
|
|
426
|
+
* failure is wrapped in a `QueueException`. Delivery is at-least-once, so every
|
|
427
|
+
* job's handler must be idempotent (see {@link QueueService.enqueue}).
|
|
428
|
+
*
|
|
429
|
+
* @param queueName - Target queue.
|
|
430
|
+
* @param jobs - Job descriptors (length must not exceed the bulk size cap).
|
|
431
|
+
*/
|
|
432
|
+
enqueueBulk<TData = unknown, TResult = unknown>(queueName: string, jobs: readonly BulkJob<TData>[]): Promise<TypedJob<TData, TResult>[]>;
|
|
433
|
+
/**
|
|
434
|
+
* Fetch a job by id. Returns `null` when the job does not exist.
|
|
435
|
+
*
|
|
436
|
+
* @param queueName - Target queue.
|
|
437
|
+
* @param jobId - The job identifier.
|
|
438
|
+
*/
|
|
439
|
+
getJob<TData = unknown, TResult = unknown>(queueName: string, jobId: string): Promise<TypedJob<TData, TResult> | null>;
|
|
440
|
+
/**
|
|
441
|
+
* Fetch jobs in a given status with pagination.
|
|
442
|
+
*
|
|
443
|
+
* @param queueName - Target queue.
|
|
444
|
+
* @param status - Status filter.
|
|
445
|
+
* @param start - Starting index (inclusive). Default: 0.
|
|
446
|
+
* @param end - Ending index (inclusive). Default: 50.
|
|
447
|
+
*/
|
|
448
|
+
getJobs<TData = unknown, TResult = unknown>(queueName: string, status: JobStatus, start?: number, end?: number): Promise<TypedJob<TData, TResult>[]>;
|
|
449
|
+
/**
|
|
450
|
+
* Return an uncached snapshot of job counts grouped by status.
|
|
451
|
+
*
|
|
452
|
+
* @param queueName - Target queue.
|
|
453
|
+
*/
|
|
454
|
+
getMetrics(queueName: string): Promise<QueueMetrics>;
|
|
455
|
+
/**
|
|
456
|
+
* Create or update a Job Scheduler — the current BullMQ recurring-jobs API.
|
|
457
|
+
* Idempotent by `schedulerId`: BullMQ performs an atomic `override: true`
|
|
458
|
+
* upsert, so re-registering on every boot updates the schedule in place and
|
|
459
|
+
* never duplicates it. This supersedes the repeatable-jobs surface removed in
|
|
460
|
+
* BullMQ v6.
|
|
461
|
+
*
|
|
462
|
+
* The schedule is structurally validated first; the cron string itself is
|
|
463
|
+
* parsed by BullMQ at registration time, and a parse failure is rethrown as
|
|
464
|
+
* `INVALID_REPEAT_OPTIONS` (400).
|
|
465
|
+
*
|
|
466
|
+
* @param queueName - Target queue.
|
|
467
|
+
* @param schedulerId - Stable, unique scheduler identifier (the upsert key).
|
|
468
|
+
* @param repeat - Cron `pattern` OR `every` (ms), plus optional tz/limit/start/end.
|
|
469
|
+
* @param template - Optional job template; `name` defaults to `schedulerId`, `data` to `{}`.
|
|
470
|
+
* @returns The first scheduled (delayed) job, or `undefined`.
|
|
471
|
+
* @throws {QueueException} `INVALID_REPEAT_OPTIONS` (400) for an invalid schedule.
|
|
472
|
+
*/
|
|
473
|
+
upsertJobScheduler<TData = unknown, TResult = unknown>(queueName: string, schedulerId: string, repeat: JobSchedulerRepeatOptions, template?: {
|
|
474
|
+
name?: string;
|
|
475
|
+
data?: TData;
|
|
476
|
+
opts?: JobsOptions;
|
|
477
|
+
}): Promise<Job<TData, TResult> | undefined>;
|
|
478
|
+
/**
|
|
479
|
+
* Remove a Job Scheduler by id.
|
|
480
|
+
*
|
|
481
|
+
* @param queueName - Target queue.
|
|
482
|
+
* @param schedulerId - The scheduler identifier to remove.
|
|
483
|
+
* @returns `true` when a scheduler was removed, `false` otherwise.
|
|
484
|
+
*/
|
|
485
|
+
removeJobScheduler(queueName: string, schedulerId: string): Promise<boolean>;
|
|
486
|
+
/**
|
|
487
|
+
* List the Job Schedulers registered on a queue, for inspection or health checks.
|
|
488
|
+
*
|
|
489
|
+
* @param queueName - Target queue.
|
|
490
|
+
* @param start - Page start index (inclusive). Default: 0.
|
|
491
|
+
* @param end - Page end index (inclusive). Default: 50.
|
|
492
|
+
* @param asc - Sort ascending by next run time. Default: true.
|
|
493
|
+
* @returns The registered schedulers in the requested page.
|
|
494
|
+
*/
|
|
495
|
+
getJobSchedulers(queueName: string, start?: number, end?: number, asc?: boolean): Promise<Awaited<ReturnType<Queue['getJobSchedulers']>>>;
|
|
496
|
+
/** Pause a queue, halting the processing of new jobs. */
|
|
497
|
+
pauseQueue(queueName: string): Promise<void>;
|
|
498
|
+
/** Resume a previously paused queue. */
|
|
499
|
+
resumeQueue(queueName: string): Promise<void>;
|
|
500
|
+
/**
|
|
501
|
+
* Remove up to `limit` jobs older than `gracePeriodMs` in a given status. The
|
|
502
|
+
* argument order mirrors BullMQ's `Queue.clean(grace, limit, type?)` exactly;
|
|
503
|
+
* `limit` is required and `0` means no limit. Returns the removed job ids.
|
|
504
|
+
*
|
|
505
|
+
* @param queueName - Target queue.
|
|
506
|
+
* @param gracePeriodMs - Keep jobs younger than this many milliseconds.
|
|
507
|
+
* @param limit - Maximum number of jobs to remove (`0` = no limit).
|
|
508
|
+
* @param status - Status of jobs to clean.
|
|
509
|
+
*/
|
|
510
|
+
cleanQueue(queueName: string, gracePeriodMs: number, limit: number, status?: CleanableStatus): Promise<string[]>;
|
|
511
|
+
/** Return a read-only view of the cached queues. */
|
|
512
|
+
getCachedQueues(): ReadonlyMap<string, Queue>;
|
|
513
|
+
/**
|
|
514
|
+
* Close every cached queue (swallowing per-queue errors) and clear the cache.
|
|
515
|
+
*
|
|
516
|
+
* Deliberately NOT named `onModuleDestroy`: NestJS invokes lifecycle hooks by
|
|
517
|
+
* method name, so a hook here would run on Nest's own schedule, concurrently
|
|
518
|
+
* with — and possibly before — the bounded drain in `QueueLifecycle`. Closing a
|
|
519
|
+
* queue while its worker is still draining is exactly what the ordered sequence
|
|
520
|
+
* exists to prevent. `QueueLifecycle` is the only caller.
|
|
521
|
+
*/
|
|
522
|
+
closeAll(): Promise<void>;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* @fileoverview FlowService — a guarded wrapper over the BullMQ `FlowProducer`
|
|
527
|
+
* for hierarchical (parent/child) job graphs. Opt-in: the provider is always
|
|
528
|
+
* registered so injecting it never fails, but every method throws
|
|
529
|
+
* `FLOW_DISABLED` (503) unless `flows.enabled` was set.
|
|
530
|
+
* @layer server/services
|
|
531
|
+
*/
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Wrapper over the BullMQ `FlowProducer` for hierarchical (parent/child) job
|
|
535
|
+
* graphs. The producer runs on the main connection (Mode A received client or
|
|
536
|
+
* Mode B library-owned client) and is created only when flows are enabled.
|
|
537
|
+
*
|
|
538
|
+
* Opt-in: this provider is registered unconditionally so that injecting it never
|
|
539
|
+
* raises `UnknownDependenciesException`; instead, every operation throws
|
|
540
|
+
* `QueueException(FLOW_DISABLED, 503)` when `flows.enabled !== true`.
|
|
541
|
+
*
|
|
542
|
+
* @example
|
|
543
|
+
* await flowService.add({
|
|
544
|
+
* name: 'generate-pdf',
|
|
545
|
+
* queueName: 'pdf',
|
|
546
|
+
* data: { reportId: '123' },
|
|
547
|
+
* children: [{ name: 'fetch-data', queueName: 'data', data: { reportId: '123' } }],
|
|
548
|
+
* })
|
|
549
|
+
*/
|
|
550
|
+
declare class FlowService {
|
|
551
|
+
private readonly logger;
|
|
552
|
+
private readonly producer?;
|
|
553
|
+
constructor(connection: ConnectionResolver, enabled: boolean, prefix: string, telemetry?: Telemetry);
|
|
554
|
+
/**
|
|
555
|
+
* Add a flow tree (a parent with its descendant children). The root job
|
|
556
|
+
* becomes processable only after every descendant has completed.
|
|
557
|
+
*
|
|
558
|
+
* By BullMQ default a failed (retry-exhausted) child does NOT fail its parent:
|
|
559
|
+
* the parent stays in the `waiting-children` state until the child eventually
|
|
560
|
+
* completes. To make a child's failure propagate upward and fail its parent,
|
|
561
|
+
* set `failParentOnFailure: true` on that child (it cascades when ancestors
|
|
562
|
+
* also set it). To let a parent proceed despite a failed child, set
|
|
563
|
+
* `ignoreDependencyOnFailure: true` on that child. Bound each child's
|
|
564
|
+
* `attempts` so an unrecoverable child cannot leave the parent waiting forever.
|
|
565
|
+
*
|
|
566
|
+
* @param flow - Flow tree definition.
|
|
567
|
+
* @returns The root `JobNode` containing the created job and its children.
|
|
568
|
+
* @throws {QueueException} `FLOW_DISABLED` (503) when flows are not enabled.
|
|
569
|
+
*/
|
|
570
|
+
add(flow: FlowJob): Promise<JobNode>;
|
|
571
|
+
/**
|
|
572
|
+
* Add multiple flow trees in a single Redis roundtrip.
|
|
573
|
+
*
|
|
574
|
+
* @param flows - Flow tree definitions.
|
|
575
|
+
* @returns The root `JobNode` for each flow, in input order.
|
|
576
|
+
* @throws {QueueException} `FLOW_DISABLED` (503) when flows are not enabled.
|
|
577
|
+
*/
|
|
578
|
+
addBulk(flows: readonly FlowJob[]): Promise<JobNode[]>;
|
|
579
|
+
/**
|
|
580
|
+
* Escape hatch returning the underlying `FlowProducer` for advanced use cases.
|
|
581
|
+
* Prefer {@link FlowService.add} / {@link FlowService.addBulk} when possible.
|
|
582
|
+
*
|
|
583
|
+
* @returns The underlying BullMQ `FlowProducer`.
|
|
584
|
+
* @throws {QueueException} `FLOW_DISABLED` (503) when flows are not enabled.
|
|
585
|
+
*/
|
|
586
|
+
getProducer(): FlowProducer;
|
|
587
|
+
/**
|
|
588
|
+
* Close the producer when active; a no-op when inactive. A failed `close()` is
|
|
589
|
+
* swallowed so it never aborts the shutdown sequence.
|
|
590
|
+
*
|
|
591
|
+
* Deliberately NOT named `onModuleDestroy`: NestJS invokes lifecycle hooks by
|
|
592
|
+
* method name, so a hook here would run in addition to the call the ordered
|
|
593
|
+
* sequence already makes, closing the producer twice. `QueueLifecycle` is the
|
|
594
|
+
* only caller.
|
|
595
|
+
*/
|
|
596
|
+
close(): Promise<void>;
|
|
597
|
+
/**
|
|
598
|
+
* Return the active producer or throw when flows are disabled. The producer is
|
|
599
|
+
* present if and only if flows were enabled at construction time.
|
|
600
|
+
*
|
|
601
|
+
* @throws {QueueException} `FLOW_DISABLED` (503) when no producer exists.
|
|
602
|
+
*/
|
|
603
|
+
private requireProducer;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* @fileoverview MetricsService — an opt-in, in-memory TTL cache over
|
|
608
|
+
* `QueueService.getMetrics`. Delegates one direction (Metrics → Queue) so there
|
|
609
|
+
* is no circular dependency. Guarded: throws `METRICS_DISABLED` (503) unless
|
|
610
|
+
* `metrics.enabled` was set.
|
|
611
|
+
* @layer server/services
|
|
612
|
+
*/
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Opt-in in-memory TTL cache over `QueueService.getMetrics`. A per-second
|
|
616
|
+
* `/health` poll reads from the cache instead of hammering Redis. The cache
|
|
617
|
+
* delegates to `QueueService` in a single direction, so there is no circular
|
|
618
|
+
* dependency. Every method throws `METRICS_DISABLED` (503) when metrics are not
|
|
619
|
+
* enabled.
|
|
620
|
+
*
|
|
621
|
+
* @example Health check pattern (consumer side — `@nestjs/terminus`, not a dependency here)
|
|
622
|
+
* @Injectable()
|
|
623
|
+
* class QueueHealthIndicator extends HealthIndicator {
|
|
624
|
+
* constructor(private readonly metrics: MetricsService) { super() }
|
|
625
|
+
* async isHealthy(key: string): Promise<HealthIndicatorResult> {
|
|
626
|
+
* const all = await this.metrics.getAll()
|
|
627
|
+
* const stuck = all.filter((m) => m.counts.active > 100 || m.counts.failed > 1000)
|
|
628
|
+
* return this.getStatus(key, stuck.length === 0, { stuck: stuck.map((m) => m.queue) })
|
|
629
|
+
* }
|
|
630
|
+
* }
|
|
631
|
+
*/
|
|
632
|
+
declare class MetricsService {
|
|
633
|
+
private readonly queueService;
|
|
634
|
+
private readonly enabled;
|
|
635
|
+
private readonly ttlMs;
|
|
636
|
+
private readonly cache;
|
|
637
|
+
constructor(queueService: QueueService, enabled: boolean, ttlMs: number);
|
|
638
|
+
/**
|
|
639
|
+
* Return cached metrics for `queueName`, or fetch fresh (and cache them) on a
|
|
640
|
+
* miss or after expiry.
|
|
641
|
+
*
|
|
642
|
+
* @param queueName - Target queue.
|
|
643
|
+
* @returns The current metrics snapshot.
|
|
644
|
+
* @throws {QueueException} `METRICS_DISABLED` (503) when metrics are disabled.
|
|
645
|
+
*/
|
|
646
|
+
get(queueName: string): Promise<QueueMetrics>;
|
|
647
|
+
/**
|
|
648
|
+
* Return a metrics snapshot for every queue currently cached in `QueueService`.
|
|
649
|
+
*
|
|
650
|
+
* @returns Metrics for all known queues.
|
|
651
|
+
* @throws {QueueException} `METRICS_DISABLED` (503) when metrics are disabled.
|
|
652
|
+
*/
|
|
653
|
+
getAll(): Promise<readonly QueueMetrics[]>;
|
|
654
|
+
/**
|
|
655
|
+
* Invalidate the cache for `queueName`, or the whole cache when omitted.
|
|
656
|
+
*
|
|
657
|
+
* @param queueName - The queue entry to drop; omit to clear everything.
|
|
658
|
+
* @throws {QueueException} `METRICS_DISABLED` (503) when metrics are disabled.
|
|
659
|
+
*/
|
|
660
|
+
invalidate(queueName?: string): void;
|
|
661
|
+
/**
|
|
662
|
+
* Guard every operation behind the opt-in flag.
|
|
663
|
+
*
|
|
664
|
+
* @throws {QueueException} `METRICS_DISABLED` (503) when metrics are disabled.
|
|
665
|
+
*/
|
|
666
|
+
private ensureEnabled;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* @fileoverview Worker tuning options surfaced when registering a processor.
|
|
671
|
+
* @layer server/interfaces
|
|
672
|
+
*/
|
|
673
|
+
/**
|
|
674
|
+
* Tunables applied when registering a Worker (via decorator or programmatically).
|
|
675
|
+
* Maps to the subset of BullMQ's worker options the library exposes through its
|
|
676
|
+
* defaults.
|
|
677
|
+
*
|
|
678
|
+
* There is intentionally NO `sandboxed` boolean. BullMQ sandboxed processors are
|
|
679
|
+
* created by passing a FILE PATH (not a class) to the Worker constructor, so they
|
|
680
|
+
* run out-of-process and cannot use NestJS dependency injection. A toggle on a
|
|
681
|
+
* DI-managed processor is therefore impossible — sandboxed work uses a separate
|
|
682
|
+
* registration path (`WorkerRegistry.registerSandboxed`).
|
|
683
|
+
*
|
|
684
|
+
* @example
|
|
685
|
+
* const options: WorkerOptions = { concurrency: 5, limiter: { max: 10, duration: 1000 } }
|
|
686
|
+
*/
|
|
687
|
+
interface WorkerOptions {
|
|
688
|
+
/** Maximum number of jobs processed concurrently. Default: 2. */
|
|
689
|
+
concurrency?: number;
|
|
690
|
+
/** Rate limiter — at most `max` jobs per `duration` milliseconds. */
|
|
691
|
+
limiter?: {
|
|
692
|
+
max: number;
|
|
693
|
+
duration: number;
|
|
694
|
+
};
|
|
695
|
+
/** Start the worker automatically on registration. Default: true. */
|
|
696
|
+
autorun?: boolean;
|
|
697
|
+
/** Lock duration for active jobs, in milliseconds. Default: 30_000. */
|
|
698
|
+
lockDuration?: number;
|
|
699
|
+
/** Interval between stalled-job checks, in milliseconds. Default: 30_000. */
|
|
700
|
+
stalledInterval?: number;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* @fileoverview `WorkerRegistry` — creates, tracks, and destroys BullMQ
|
|
705
|
+
* `Worker` instances. Provides both an in-process path (handler function, NestJS
|
|
706
|
+
* DI available) and a file-based sandboxed path (no NestJS DI). Every worker
|
|
707
|
+
* connection is a duplicate of the Queue-role client with
|
|
708
|
+
* `maxRetriesPerRequest: null`, as required by BullMQ blocking commands. The
|
|
709
|
+
* duplicated connection is tracked alongside the worker so the shutdown
|
|
710
|
+
* orchestrator can close exactly the connections the library opened.
|
|
711
|
+
* @layer server/services
|
|
712
|
+
*/
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Configuration for registering an in-process (NestJS-DI-backed) worker.
|
|
716
|
+
*
|
|
717
|
+
* @template TData - Shape of the job data payload.
|
|
718
|
+
* @template TResult - Shape of the job result.
|
|
719
|
+
*/
|
|
720
|
+
interface ProgrammaticWorkerConfig<TData = unknown, TResult = unknown> {
|
|
721
|
+
/** BullMQ queue the worker should consume. */
|
|
722
|
+
queueName: string;
|
|
723
|
+
/** Async function that processes a single job. Must be idempotent. */
|
|
724
|
+
handler: (job: Job<TData, TResult>) => Promise<TResult>;
|
|
725
|
+
/** Optional worker tuning overrides. */
|
|
726
|
+
options?: WorkerOptions;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Configuration for registering a sandboxed (file-based, out-of-process) worker.
|
|
730
|
+
* The processor runs in a separate Node.js process and cannot access NestJS DI.
|
|
731
|
+
* `processorFile` must be an absolute path or a `file:` URL with a `.js`, `.cjs`,
|
|
732
|
+
* or `.mjs` extension.
|
|
733
|
+
*
|
|
734
|
+
* @see https://docs.bullmq.io/guide/workers/sandboxed-processors
|
|
735
|
+
*/
|
|
736
|
+
interface SandboxedWorkerConfig {
|
|
737
|
+
/** BullMQ queue the worker should consume. */
|
|
738
|
+
queueName: string;
|
|
739
|
+
/**
|
|
740
|
+
* Absolute path or `file:` URL to the compiled `.js`/`.cjs`/`.mjs` processor
|
|
741
|
+
* artifact. Relative paths and non-JS extensions are rejected at registration
|
|
742
|
+
* time. The file must export a default async function
|
|
743
|
+
* `(job: Job) => Promise<unknown>`.
|
|
744
|
+
*/
|
|
745
|
+
processorFile: string | URL;
|
|
746
|
+
/** Optional worker tuning overrides, including `useWorkerThreads`. */
|
|
747
|
+
options?: WorkerOptions & {
|
|
748
|
+
useWorkerThreads?: boolean;
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Creates and lifecycle-manages BullMQ `Worker` instances on behalf of the
|
|
753
|
+
* module. Supports two registration paths:
|
|
754
|
+
*
|
|
755
|
+
* - **Programmatic** (`register`) — in-process handler; NestJS DI is available
|
|
756
|
+
* inside the handler function.
|
|
757
|
+
* - **Sandboxed** (`registerSandboxed`) — file-based, out-of-process; the
|
|
758
|
+
* processor file is loaded in a separate Node.js child process and has **no**
|
|
759
|
+
* access to NestJS DI.
|
|
760
|
+
*
|
|
761
|
+
* All workers use a duplicated Redis connection (`maxRetriesPerRequest: null`)
|
|
762
|
+
* for the blocking `BRPOPLPUSH`/`BZPOPMIN`/`BLMOVE` commands. The duplicated
|
|
763
|
+
* connection is closed immediately when the Worker constructor fails, preventing
|
|
764
|
+
* a connection leak.
|
|
765
|
+
*
|
|
766
|
+
* @example
|
|
767
|
+
* const worker = registry.register({
|
|
768
|
+
* queueName: 'email',
|
|
769
|
+
* handler: async (job) => sendEmail(job.data),
|
|
770
|
+
* options: { concurrency: 5 },
|
|
771
|
+
* })
|
|
772
|
+
*/
|
|
773
|
+
declare class WorkerRegistry {
|
|
774
|
+
private readonly connection;
|
|
775
|
+
private readonly options;
|
|
776
|
+
private readonly logger;
|
|
777
|
+
private readonly entries;
|
|
778
|
+
constructor(connection: ConnectionResolver, options: ResolvedQueueOptions);
|
|
779
|
+
/**
|
|
780
|
+
* Register an in-process worker with a handler function. The BullMQ `Worker`
|
|
781
|
+
* constructor is synchronous — no Redis handshake is performed at registration
|
|
782
|
+
* time. The duplicated connection is disconnected immediately if construction
|
|
783
|
+
* fails, preventing a Redis connection leak.
|
|
784
|
+
*
|
|
785
|
+
* @param config - Queue name, handler, and optional worker options.
|
|
786
|
+
* @returns The created BullMQ `Worker` instance.
|
|
787
|
+
* @throws {QueueException} `DUPLICATE_PROCESSOR` when a worker for
|
|
788
|
+
* `queueName` already exists.
|
|
789
|
+
* @throws {QueueException} `WORKER_REGISTRATION_FAILED` when options are
|
|
790
|
+
* invalid or the BullMQ `Worker` constructor throws.
|
|
791
|
+
*/
|
|
792
|
+
register<TData = unknown, TResult = unknown>(config: ProgrammaticWorkerConfig<TData, TResult>): Worker<TData, TResult>;
|
|
793
|
+
/**
|
|
794
|
+
* Register a sandboxed (file-based, out-of-process) worker. The processor
|
|
795
|
+
* file must be an absolute path or a `file:` URL pointing to a compiled
|
|
796
|
+
* `.js`/`.cjs`/`.mjs` artifact. NestJS DI is **not** available inside the
|
|
797
|
+
* processor. The BullMQ `Worker` constructor is synchronous.
|
|
798
|
+
*
|
|
799
|
+
* @param config - Queue name, processor file path/URL, and optional options.
|
|
800
|
+
* @returns The created BullMQ `Worker` instance.
|
|
801
|
+
* @throws {QueueException} `WORKER_REGISTRATION_FAILED` when `processorFile`
|
|
802
|
+
* is a relative path, has a disallowed extension, or uses a non-`file:` URL protocol.
|
|
803
|
+
* @throws {QueueException} `DUPLICATE_PROCESSOR` when a worker for
|
|
804
|
+
* `queueName` already exists.
|
|
805
|
+
* @throws {QueueException} `WORKER_REGISTRATION_FAILED` when options are
|
|
806
|
+
* invalid or the BullMQ `Worker` constructor throws.
|
|
807
|
+
*/
|
|
808
|
+
registerSandboxed(config: SandboxedWorkerConfig): Worker;
|
|
809
|
+
/**
|
|
810
|
+
* Stop and remove the worker for the given queue, releasing the duplicated
|
|
811
|
+
* connection the library opened for it. If no worker exists for `queueName`
|
|
812
|
+
* this is a no-op.
|
|
813
|
+
*
|
|
814
|
+
* @param queueName - The queue whose worker should be removed.
|
|
815
|
+
*/
|
|
816
|
+
unregister(queueName: string): Promise<void>;
|
|
817
|
+
/**
|
|
818
|
+
* Returns the queue names of all currently registered workers.
|
|
819
|
+
*
|
|
820
|
+
* @returns An immutable array of queue names.
|
|
821
|
+
*/
|
|
822
|
+
list(): readonly string[];
|
|
823
|
+
/**
|
|
824
|
+
* Returns a read-only view of the registered workers keyed by queue name.
|
|
825
|
+
* Intended for the shutdown orchestrator only.
|
|
826
|
+
*
|
|
827
|
+
* @returns A read-only map of queue name to worker.
|
|
828
|
+
*/
|
|
829
|
+
getAll(): ReadonlyMap<string, Worker>;
|
|
830
|
+
/**
|
|
831
|
+
* Returns a read-only view of the duplicated connections the library opened
|
|
832
|
+
* for its workers, keyed by queue name. Intended for the shutdown orchestrator
|
|
833
|
+
* so it can close exactly the connections the library created (Mode A never
|
|
834
|
+
* exposes the consumer's shared client here).
|
|
835
|
+
*
|
|
836
|
+
* @returns A read-only map of queue name to duplicated connection.
|
|
837
|
+
*/
|
|
838
|
+
getConnections(): ReadonlyMap<string, Redis>;
|
|
839
|
+
/**
|
|
840
|
+
* Gracefully quit a duplicated connection, falling back to a hard disconnect
|
|
841
|
+
* if the quit handshake fails so a socket is never leaked.
|
|
842
|
+
*
|
|
843
|
+
* @param connection - The duplicated connection to release.
|
|
844
|
+
*/
|
|
845
|
+
private closeConnection;
|
|
846
|
+
/**
|
|
847
|
+
* Guard against registering two workers for the same queue.
|
|
848
|
+
*
|
|
849
|
+
* @param queueName - Queue name to check.
|
|
850
|
+
* @throws {QueueException} `DUPLICATE_PROCESSOR` if the queue is already registered.
|
|
851
|
+
*/
|
|
852
|
+
private guardDuplicate;
|
|
853
|
+
/**
|
|
854
|
+
* Validate tunable options before constructing a Worker.
|
|
855
|
+
* Enforces `concurrency` in `[1, MAX_WORKER_CONCURRENCY]` and
|
|
856
|
+
* `limiter.max`/`limiter.duration` `>= 1`.
|
|
857
|
+
*
|
|
858
|
+
* @param opts - The caller-supplied options (may be undefined).
|
|
859
|
+
* @throws {QueueException} `WORKER_REGISTRATION_FAILED` when values are out of range.
|
|
860
|
+
*/
|
|
861
|
+
private validateOptions;
|
|
862
|
+
/**
|
|
863
|
+
* Validate that `processorFile` is a safe, reachable artifact path. Accepts
|
|
864
|
+
* absolute paths with `.js`/`.cjs`/`.mjs` extensions and `file:` URLs. Rejects
|
|
865
|
+
* relative paths, non-JS extensions, and non-`file:` URL protocols to prevent
|
|
866
|
+
* path-traversal and arbitrary-file-execution attacks.
|
|
867
|
+
*
|
|
868
|
+
* @param processorFile - The path or URL supplied by the caller.
|
|
869
|
+
* @throws {QueueException} `WORKER_REGISTRATION_FAILED` when validation fails.
|
|
870
|
+
*/
|
|
871
|
+
private validateProcessorFile;
|
|
872
|
+
/**
|
|
873
|
+
* Reject a processor path whose extension is not an executable JS module,
|
|
874
|
+
* the last line of defense against loading an arbitrary file as a worker.
|
|
875
|
+
*
|
|
876
|
+
* @param filePath - The filesystem path (or file: URL pathname) to check.
|
|
877
|
+
* @throws {QueueException} `WORKER_REGISTRATION_FAILED` for a disallowed extension.
|
|
878
|
+
*/
|
|
879
|
+
private assertProcessorExtension;
|
|
880
|
+
/**
|
|
881
|
+
* Build a BullMQ `WorkerOptions` object from the library's `WorkerOptions`
|
|
882
|
+
* shape. Only forwards optional fields that are explicitly set to preserve
|
|
883
|
+
* `exactOptionalPropertyTypes` correctness. Accepts the pre-created duplicated
|
|
884
|
+
* connection so callers can close it on construction failure.
|
|
885
|
+
*
|
|
886
|
+
* @param opts - Library-level worker options.
|
|
887
|
+
* @param conn - Pre-created duplicated Redis connection.
|
|
888
|
+
* @returns A BullMQ-compatible options object.
|
|
889
|
+
*/
|
|
890
|
+
private buildBullOptions;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* @fileoverview `QueueEventsRegistry` — lazily creates and caches one BullMQ
|
|
895
|
+
* `QueueEvents` instance per queue. A `QueueEvents` connection is opened only
|
|
896
|
+
* when at least one `@OnQueueEvent` listener is registered for that queue,
|
|
897
|
+
* avoiding unnecessary Redis connections. Each duplicated connection is tracked
|
|
898
|
+
* so the shutdown orchestrator can close exactly what the library opened.
|
|
899
|
+
* @layer server/services
|
|
900
|
+
*/
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Lazily creates and caches one BullMQ `QueueEvents` instance per queue.
|
|
904
|
+
* The backing Redis connection uses `maxRetriesPerRequest: null` (via
|
|
905
|
+
* `duplicateConnection`) because `QueueEvents` subscribes via blocking
|
|
906
|
+
* `SUBSCRIBE` commands.
|
|
907
|
+
*
|
|
908
|
+
* A single `QueueEvents` is shared across all `@OnQueueEvent` listeners for
|
|
909
|
+
* the same queue. Connections are opened on demand — only when
|
|
910
|
+
* `getOrCreate(queueName)` is first called for a given queue. Shutdown is
|
|
911
|
+
* orchestrated centrally by the queue lifecycle service, which closes each
|
|
912
|
+
* `QueueEvents` and then releases the duplicated connection exposed here.
|
|
913
|
+
*
|
|
914
|
+
* @example
|
|
915
|
+
* const qe = registry.getOrCreate('email')
|
|
916
|
+
* qe.on('completed', ({ jobId }) => console.log(jobId))
|
|
917
|
+
*/
|
|
918
|
+
declare class QueueEventsRegistry {
|
|
919
|
+
private readonly connection;
|
|
920
|
+
private readonly options;
|
|
921
|
+
private readonly logger;
|
|
922
|
+
private readonly events;
|
|
923
|
+
private readonly connections;
|
|
924
|
+
constructor(connection: ConnectionResolver, options: ResolvedQueueOptions);
|
|
925
|
+
/**
|
|
926
|
+
* Returns the cached `QueueEvents` for `queueName`, creating one on first
|
|
927
|
+
* call. Subsequent calls for the same name return the same instance without
|
|
928
|
+
* opening a second Redis connection.
|
|
929
|
+
*
|
|
930
|
+
* @param queueName - The BullMQ queue to observe.
|
|
931
|
+
* @returns The `QueueEvents` instance for the queue.
|
|
932
|
+
*/
|
|
933
|
+
getOrCreate(queueName: string): QueueEvents;
|
|
934
|
+
/**
|
|
935
|
+
* Returns the queue names for which a `QueueEvents` connection is open.
|
|
936
|
+
*
|
|
937
|
+
* @returns An immutable array of queue names.
|
|
938
|
+
*/
|
|
939
|
+
list(): readonly string[];
|
|
940
|
+
/**
|
|
941
|
+
* Returns the live `QueueEvents` map. Intended for the shutdown orchestrator.
|
|
942
|
+
*
|
|
943
|
+
* @returns A read-only view of the internal map.
|
|
944
|
+
*/
|
|
945
|
+
getAll(): ReadonlyMap<string, QueueEvents>;
|
|
946
|
+
/**
|
|
947
|
+
* Returns a read-only view of the duplicated connections the library opened
|
|
948
|
+
* for its `QueueEvents`, keyed by queue name. Intended for the shutdown
|
|
949
|
+
* orchestrator so it can close exactly the connections the library created
|
|
950
|
+
* (Mode A never exposes the consumer's shared client here).
|
|
951
|
+
*
|
|
952
|
+
* Returns a copy. `ReadonlyMap` is a compile-time claim only — handing back the
|
|
953
|
+
* live registry lets any consumer cast it away and delete a connection the
|
|
954
|
+
* shutdown sequence is about to close. The allocation costs nothing on a path
|
|
955
|
+
* that runs once per shutdown.
|
|
956
|
+
*
|
|
957
|
+
* @returns A read-only snapshot of queue name to duplicated connection.
|
|
958
|
+
*/
|
|
959
|
+
getConnections(): ReadonlyMap<string, Redis>;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* @fileoverview `QueueLifecycle` — the single, ordered graceful-shutdown
|
|
964
|
+
* orchestrator. On `onModuleDestroy` it drains every worker within a bounded
|
|
965
|
+
* budget, then tears down queue-events, the connections the library opened,
|
|
966
|
+
* the flow producer, the cached queues, and finally the Queue-role connection.
|
|
967
|
+
*
|
|
968
|
+
* Delivery is at-least-once: when a worker exceeds the drain budget it is
|
|
969
|
+
* force-closed and its in-flight job becomes `stalled` and is retried by another
|
|
970
|
+
* worker. Handlers must therefore be idempotent, and `lockDuration` should
|
|
971
|
+
* comfortably exceed the worst-case handler runtime so healthy long jobs are
|
|
972
|
+
* never treated as stalled.
|
|
973
|
+
* @layer server/lifecycle
|
|
974
|
+
*/
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Ordered graceful-shutdown service. Implementing the protocol in one place
|
|
978
|
+
* guarantees the teardown order — workers drain first, then queue-events, then
|
|
979
|
+
* the duplicated connections, then the flow producer, the queues, and finally
|
|
980
|
+
* the Queue-role connection. Every step swallows its own errors so a single
|
|
981
|
+
* failure never aborts the rest of the sequence.
|
|
982
|
+
*
|
|
983
|
+
* Connection ownership: in **Mode B** the library-owned Queue-role client is
|
|
984
|
+
* quit by the connection resolver; in **Mode A** that step is a no-op and only
|
|
985
|
+
* the duplicated worker/queue-events connections the library created are closed
|
|
986
|
+
* — the consumer's shared client is never touched.
|
|
987
|
+
*/
|
|
988
|
+
declare class QueueLifecycle implements OnModuleDestroy {
|
|
989
|
+
private readonly workers;
|
|
990
|
+
private readonly events;
|
|
991
|
+
private readonly queues;
|
|
992
|
+
private readonly flow;
|
|
993
|
+
private readonly connection;
|
|
994
|
+
private readonly resolved;
|
|
995
|
+
private readonly logger;
|
|
996
|
+
constructor(workers: WorkerRegistry, events: QueueEventsRegistry, queues: QueueService, flow: FlowService, connection: ConnectionResolver, resolved: ResolvedQueueOptions);
|
|
997
|
+
/**
|
|
998
|
+
* Run the ordered shutdown sequence. Called by NestJS when the application
|
|
999
|
+
* context closes (e.g. on `SIGTERM` with shutdown hooks enabled).
|
|
1000
|
+
*/
|
|
1001
|
+
onModuleDestroy(): Promise<void>;
|
|
1002
|
+
/**
|
|
1003
|
+
* Close every worker with a bounded drain, force-closing any that exceed the
|
|
1004
|
+
* budget. Returns the number of workers that had to be force-closed.
|
|
1005
|
+
*
|
|
1006
|
+
* @returns The count of force-closed workers.
|
|
1007
|
+
*/
|
|
1008
|
+
private closeWorkers;
|
|
1009
|
+
/**
|
|
1010
|
+
* Race a graceful `worker.close()` against a timer. `worker.close()` already
|
|
1011
|
+
* drains active jobs and takes no timeout argument, so the timer is the only
|
|
1012
|
+
* way to bound the wait. Rejects with a timeout error when the budget elapses.
|
|
1013
|
+
*
|
|
1014
|
+
* @param worker - The worker to close.
|
|
1015
|
+
* @param timeoutMs - The drain budget in milliseconds.
|
|
1016
|
+
*/
|
|
1017
|
+
private closeWorkerWithTimeout;
|
|
1018
|
+
/** Close every cached `QueueEvents`, swallowing individual failures. */
|
|
1019
|
+
private closeQueueEvents;
|
|
1020
|
+
/**
|
|
1021
|
+
* Close the duplicated worker/queue-events connections the library opened.
|
|
1022
|
+
* BullMQ treats a passed-in client as shared and never closes it, so the
|
|
1023
|
+
* library must release these explicitly. In Mode A the consumer's shared
|
|
1024
|
+
* client is never among them.
|
|
1025
|
+
*/
|
|
1026
|
+
private closeDuplicatedConnections;
|
|
1027
|
+
/**
|
|
1028
|
+
* Drain every cached queue when `drainOnShutdown` is enabled (dev/test only).
|
|
1029
|
+
* Removes waiting and delayed jobs; off by default to stay production-safe.
|
|
1030
|
+
*/
|
|
1031
|
+
private drainQueuesIfRequested;
|
|
1032
|
+
/** Close the flow producer (idempotent), swallowing any failure. */
|
|
1033
|
+
private closeFlowProducer;
|
|
1034
|
+
/** Close every cached queue and clear the cache, swallowing individual failures. */
|
|
1035
|
+
private closeQueues;
|
|
1036
|
+
/**
|
|
1037
|
+
* Tear down the Queue-role connection. Mode B quits the library-owned client;
|
|
1038
|
+
* Mode A is a no-op for the consumer's shared client.
|
|
1039
|
+
*/
|
|
1040
|
+
private teardownConnection;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* @fileoverview `@Processor` class decorator — marks a provider as the handler
|
|
1045
|
+
* for a named BullMQ queue. Writes {@link ProcessorMetadata} to class-level
|
|
1046
|
+
* reflection metadata under {@link PROCESSOR_METADATA_KEY}.
|
|
1047
|
+
* @layer server/decorators
|
|
1048
|
+
*/
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Marks a class as the processor for the given queue. The class must be
|
|
1052
|
+
* registered as a NestJS provider so that `ProcessorDiscoveryService` can
|
|
1053
|
+
* discover and wire it during module initialization.
|
|
1054
|
+
*
|
|
1055
|
+
* When `options.concurrency` is omitted the library falls back to
|
|
1056
|
+
* {@link DEFAULT_WORKER_CONCURRENCY} and emits a `Logger.warn` at discovery
|
|
1057
|
+
* time (not here — the decorator itself is side-effect-free).
|
|
1058
|
+
*
|
|
1059
|
+
* Delivery is at-least-once: every `@Process` handler on the class MUST be
|
|
1060
|
+
* idempotent (idempotency key on writes, upserts over inserts, or an
|
|
1061
|
+
* "already-processed" marker keyed by `job.id`). Set `lockDuration` comfortably
|
|
1062
|
+
* above the worst-case handler runtime so healthy long jobs are never treated as
|
|
1063
|
+
* stalled.
|
|
1064
|
+
*
|
|
1065
|
+
* @param queueName - The BullMQ queue this class processes.
|
|
1066
|
+
* @param options - Optional worker tuning overrides.
|
|
1067
|
+
*
|
|
1068
|
+
* @example
|
|
1069
|
+
* `@Processor`('email', { concurrency: 5 })
|
|
1070
|
+
* class EmailProcessor {
|
|
1071
|
+
* `@Process`()
|
|
1072
|
+
* async handle(job: Job<EmailPayload>) { ... }
|
|
1073
|
+
* }
|
|
1074
|
+
*/
|
|
1075
|
+
declare function Processor(queueName: string, options?: WorkerOptions): ClassDecorator;
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* @fileoverview `@Process` method decorator — registers a method as the handler
|
|
1079
|
+
* for a specific job name (or as the catch-all when no name is given). Metadata
|
|
1080
|
+
* is accumulated in an array so multiple `@Process` annotations on the same
|
|
1081
|
+
* class coexist without overwriting each other.
|
|
1082
|
+
* @layer server/decorators
|
|
1083
|
+
*/
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Marks a method as a job processor. If `jobName` is supplied the method is
|
|
1087
|
+
* only invoked for jobs whose `name` matches; if omitted the method acts as a
|
|
1088
|
+
* catch-all for all other job names on the same queue.
|
|
1089
|
+
*
|
|
1090
|
+
* Multiple `@Process` decorators on the same class are supported — each adds
|
|
1091
|
+
* an entry to the accumulated metadata array.
|
|
1092
|
+
*
|
|
1093
|
+
* Delivery is at-least-once: a handler may run more than once for the same job
|
|
1094
|
+
* (worker crash, lock expiry, or a shutdown force-close). Handlers MUST be
|
|
1095
|
+
* idempotent — use an idempotency key on writes, prefer upserts over inserts, or
|
|
1096
|
+
* keep an "already-processed" marker keyed by `job.id`.
|
|
1097
|
+
*
|
|
1098
|
+
* @param jobName - Optional job-name filter. Omit for a catch-all handler.
|
|
1099
|
+
*
|
|
1100
|
+
* @example
|
|
1101
|
+
* `@Process`('send-email')
|
|
1102
|
+
* async handleSend(job: Job<EmailPayload>) { ... }
|
|
1103
|
+
*
|
|
1104
|
+
* `@Process`()
|
|
1105
|
+
* async handleOthers(job: Job<EmailPayload>) { ... }
|
|
1106
|
+
*/
|
|
1107
|
+
declare function Process(jobName?: string): MethodDecorator;
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* @fileoverview `@OnWorkerEvent` method decorator — binds a method as a
|
|
1111
|
+
* worker-local event listener. The handler is attached directly to the BullMQ
|
|
1112
|
+
* `Worker` instance (no extra Redis connection) and receives the **full**
|
|
1113
|
+
* `Job` object, including `job.data`, `job.returnvalue`, `job.attemptsMade`,
|
|
1114
|
+
* and timing fields. Use `'progress'` to observe `job.updateProgress()`.
|
|
1115
|
+
* @layer server/decorators
|
|
1116
|
+
*/
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Union of BullMQ `Worker` event names surfaced by this library. Handlers
|
|
1120
|
+
* registered with `@OnWorkerEvent` receive the **full** `Job` instance for
|
|
1121
|
+
* job-scoped events (completed, failed, active, progress) and scalars for
|
|
1122
|
+
* infrastructure events (stalled, closing, closed, error).
|
|
1123
|
+
*
|
|
1124
|
+
* @see https://docs.bullmq.io/guide/workers/worker-events
|
|
1125
|
+
*/
|
|
1126
|
+
type WorkerEventName = 'completed' | 'failed' | 'progress' | 'active' | 'stalled' | 'closing' | 'closed' | 'error';
|
|
1127
|
+
/**
|
|
1128
|
+
* Marks a method as a worker-local event listener for the given event. The
|
|
1129
|
+
* method is bound to the BullMQ `Worker` via `worker.on(event, fn)` at
|
|
1130
|
+
* discovery time. Because worker-local events are emitted directly on the
|
|
1131
|
+
* `Worker` instance, **no extra Redis connection is opened**.
|
|
1132
|
+
*
|
|
1133
|
+
* Handler receives:
|
|
1134
|
+
* - `completed` — `(job: Job, returnValue: unknown, prev: string) => void`
|
|
1135
|
+
* - `failed` — `(job: Job | undefined, error: Error, prev: string) => void`
|
|
1136
|
+
* - `progress` — `(job: Job, progress: number | object) => void`
|
|
1137
|
+
* - `active` — `(job: Job, prev: string) => void`
|
|
1138
|
+
* - `stalled` — `(jobId: string, prev: string) => void`
|
|
1139
|
+
* - `error` — `(error: Error) => void`
|
|
1140
|
+
* - `closing` — `(msg: string) => void`
|
|
1141
|
+
* - `closed` — `() => void`
|
|
1142
|
+
*
|
|
1143
|
+
* Multiple `@OnWorkerEvent` decorators on the same class accumulate without
|
|
1144
|
+
* overwriting each other.
|
|
1145
|
+
*
|
|
1146
|
+
* Delivery is at-least-once. A `'stalled'` job — for example after a shutdown
|
|
1147
|
+
* force-close or a lock expiry — is re-run by another worker: this is the
|
|
1148
|
+
* visible at-least-once path, so handlers must be idempotent. Set `lockDuration`
|
|
1149
|
+
* comfortably above the worst-case handler runtime to avoid false stalls.
|
|
1150
|
+
*
|
|
1151
|
+
* Dead-letter-queue pattern (`'failed'`): the library ships no DLQ service, but
|
|
1152
|
+
* the endorsed pattern is a `'failed'` listener that, once retries are exhausted
|
|
1153
|
+
* (`job.attemptsMade >= (job.opts.attempts ?? 1)`), re-enqueues the payload plus
|
|
1154
|
+
* failure metadata onto a `<queue>-dlq` queue via `QueueService.enqueue`. Use a
|
|
1155
|
+
* stable DLQ `jobId` (e.g. `dlq-${job.id}`) so a redelivered `'failed'` event is
|
|
1156
|
+
* idempotent and never double-inserts (a custom BullMQ job id must not contain `:`).
|
|
1157
|
+
*
|
|
1158
|
+
* @param eventName - The worker event to listen for.
|
|
1159
|
+
*
|
|
1160
|
+
* @example
|
|
1161
|
+
* `@OnWorkerEvent`('completed')
|
|
1162
|
+
* onCompleted(job: Job, returnValue: unknown) {
|
|
1163
|
+
* this.logger.log(`Job ${job.id} done`, returnValue)
|
|
1164
|
+
* }
|
|
1165
|
+
*/
|
|
1166
|
+
declare function OnWorkerEvent(eventName: WorkerEventName): MethodDecorator;
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* @fileoverview `@OnQueueEvent` method decorator — binds a method as a global
|
|
1170
|
+
* queue-event listener. The handler is attached to a per-queue `QueueEvents`
|
|
1171
|
+
* instance (one extra Redis connection per queue, opened lazily) and receives
|
|
1172
|
+
* a **serialized** payload — only `jobId` and stringified fields such as
|
|
1173
|
+
* `returnvalue` (a string, not the actual return value) or `failedReason`.
|
|
1174
|
+
* To inspect the full `Job`, call `QueueService.getJob(jobId)`.
|
|
1175
|
+
* @layer server/decorators
|
|
1176
|
+
*/
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Union of BullMQ `QueueEvents` event names surfaced by this library. Listeners
|
|
1180
|
+
* registered with `@OnQueueEvent` receive a serialized event payload, NOT the
|
|
1181
|
+
* full `Job` instance.
|
|
1182
|
+
*
|
|
1183
|
+
* @see https://docs.bullmq.io/guide/workers/queueevents
|
|
1184
|
+
*/
|
|
1185
|
+
type QueueEventName = 'completed' | 'failed' | 'active' | 'progress' | 'stalled' | 'waiting' | 'delayed' | 'paused' | 'resumed' | 'cleaned' | 'error';
|
|
1186
|
+
/**
|
|
1187
|
+
* Marks a method as a global queue-event listener. The method is bound to a
|
|
1188
|
+
* lazily-created `QueueEvents` instance via `queueEvents.on(event, fn)` at
|
|
1189
|
+
* discovery time. A single `QueueEvents` connection is shared across all
|
|
1190
|
+
* `@OnQueueEvent` listeners for the same queue.
|
|
1191
|
+
*
|
|
1192
|
+
* The handler receives a **serialized** payload, e.g.:
|
|
1193
|
+
* - `completed` — `{ jobId: string, returnvalue: string, prev?: string }`
|
|
1194
|
+
* - `failed` — `{ jobId: string, failedReason: string, prev?: string }`
|
|
1195
|
+
* - `active` — `{ jobId: string, prev?: string }`
|
|
1196
|
+
* - `progress` — `{ jobId: string, data: number | object }`
|
|
1197
|
+
* - `waiting` — `{ jobId: string }`
|
|
1198
|
+
*
|
|
1199
|
+
* `error` is the exception to that rule: it carries a real `Error` instance, not
|
|
1200
|
+
* a serialized payload, because it reports a fault on the connection rather than
|
|
1201
|
+
* a transition of any single job — so there is no `jobId` to look up. The library
|
|
1202
|
+
* registers its own listener for it, which logs and keeps an unlistened emission
|
|
1203
|
+
* from throwing; a handler declared here runs in addition to that one.
|
|
1204
|
+
*
|
|
1205
|
+
* To retrieve the full job, call `QueueService.getJob(payload.jobId)`, which
|
|
1206
|
+
* may return `null` if the job was already evicted by `removeOnComplete`.
|
|
1207
|
+
*
|
|
1208
|
+
* Multiple `@OnQueueEvent` decorators on the same class accumulate without
|
|
1209
|
+
* overwriting each other.
|
|
1210
|
+
*
|
|
1211
|
+
* @param eventName - The queue event to listen for.
|
|
1212
|
+
*
|
|
1213
|
+
* @example
|
|
1214
|
+
* `@OnQueueEvent`('completed')
|
|
1215
|
+
* async onCompleted({ jobId, returnvalue }: { jobId: string; returnvalue: string }) {
|
|
1216
|
+
* this.logger.log(`Job ${jobId} finished with result ${returnvalue}`)
|
|
1217
|
+
* }
|
|
1218
|
+
*/
|
|
1219
|
+
declare function OnQueueEvent(eventName: QueueEventName): MethodDecorator;
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* @fileoverview Reflection metadata shapes attached by the processor decorators.
|
|
1223
|
+
* @layer server/interfaces
|
|
1224
|
+
*/
|
|
1225
|
+
|
|
1226
|
+
/** Metadata attached by the processor decorator and read on worker discovery. */
|
|
1227
|
+
interface ProcessorMetadata {
|
|
1228
|
+
/** Queue the annotated class processes. */
|
|
1229
|
+
queueName: string;
|
|
1230
|
+
/** Worker tuning options for the queue. */
|
|
1231
|
+
workerOptions: WorkerOptions;
|
|
1232
|
+
/**
|
|
1233
|
+
* Internal flag set when the caller omitted `concurrency` on the decorator.
|
|
1234
|
+
* The discovery service logs a warning (naming the queue and the fallback value)
|
|
1235
|
+
* at bootstrap time; the decorator itself must never log.
|
|
1236
|
+
*
|
|
1237
|
+
* @internal
|
|
1238
|
+
*/
|
|
1239
|
+
_warnedNoConcurrency?: boolean;
|
|
1240
|
+
}
|
|
1241
|
+
/** Metadata attached by the process-handler decorator on a handler method. */
|
|
1242
|
+
interface ProcessHandlerMetadata {
|
|
1243
|
+
/** Specific job-name filter; `undefined` means catch-all. */
|
|
1244
|
+
jobName?: string;
|
|
1245
|
+
/** Method key on the host class used by the registry to resolve dispatch. */
|
|
1246
|
+
methodKey: string | symbol;
|
|
1247
|
+
}
|
|
1248
|
+
/** Metadata attached by the queue-event listener decorator on a listener method. */
|
|
1249
|
+
interface QueueEventListenerMetadata {
|
|
1250
|
+
/** Queue event the method listens for. */
|
|
1251
|
+
eventName: string;
|
|
1252
|
+
/** Method key on the host class. */
|
|
1253
|
+
methodKey: string | symbol;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* @fileoverview Standardized, HTTP-shaped exception for queue operations.
|
|
1258
|
+
* @layer server/errors
|
|
1259
|
+
*/
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Standardized exception for queue operations. The response body always follows
|
|
1263
|
+
* the shape `{ error: { code, message, details } }`. The `code` is a stable,
|
|
1264
|
+
* transport-independent identifier; consumers catching this exception outside an
|
|
1265
|
+
* HTTP request should branch on `code`, not the HTTP status or the message text.
|
|
1266
|
+
* The code lives in the response envelope — read it via `getResponse()`.
|
|
1267
|
+
*
|
|
1268
|
+
* `details` must never carry secrets (connection strings, passwords, `job.data`)
|
|
1269
|
+
* — only scalar configuration values such as `{ actualValue, expectedValue }`.
|
|
1270
|
+
*
|
|
1271
|
+
* @example Throwing
|
|
1272
|
+
* throw new QueueException('queue.connection_timeout', 500, { timeoutMs: 10_000 })
|
|
1273
|
+
* @example Branching on the code in a consumer
|
|
1274
|
+
* catch (err) {
|
|
1275
|
+
* if (err instanceof QueueException) {
|
|
1276
|
+
* const { code } = (err.getResponse() as { error: { code: string } }).error
|
|
1277
|
+
* if (code === QUEUE_ERROR_CODES.FLOW_DISABLED) enableFlows()
|
|
1278
|
+
* }
|
|
1279
|
+
* }
|
|
1280
|
+
*/
|
|
1281
|
+
declare class QueueException extends HttpException {
|
|
1282
|
+
constructor(code: string, statusCode?: HttpStatus, details?: Record<string, unknown>);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* @fileoverview Stable error-code catalog emitted by the library, plus the
|
|
1287
|
+
* derived code union. Single source of truth, re-exported by the server barrel.
|
|
1288
|
+
* @layer shared/constants
|
|
1289
|
+
*/
|
|
1290
|
+
/**
|
|
1291
|
+
* Stable, transport-independent error codes emitted through `QueueException`.
|
|
1292
|
+
* Declared `as const` so each value keeps its literal type in the emitted
|
|
1293
|
+
* declaration files.
|
|
1294
|
+
*
|
|
1295
|
+
* @example
|
|
1296
|
+
* if (error.code === QUEUE_ERROR_CODES.JOB_NOT_FOUND) { return null }
|
|
1297
|
+
*/
|
|
1298
|
+
declare const QUEUE_ERROR_CODES: {
|
|
1299
|
+
readonly CONNECTION_INVALID: "queue.connection_invalid";
|
|
1300
|
+
readonly CONNECTION_REQUIRES_NULL_RETRIES: "queue.connection_requires_null_retries";
|
|
1301
|
+
readonly CONNECTION_TIMEOUT: "queue.connection_timeout";
|
|
1302
|
+
readonly QUEUE_NOT_FOUND: "queue.queue_not_found";
|
|
1303
|
+
readonly JOB_NOT_FOUND: "queue.job_not_found";
|
|
1304
|
+
readonly INVALID_JOB_DATA: "queue.invalid_job_data";
|
|
1305
|
+
readonly INVALID_REPEAT_OPTIONS: "queue.invalid_repeat_options";
|
|
1306
|
+
readonly DUPLICATE_PROCESSOR: "queue.duplicate_processor";
|
|
1307
|
+
readonly FLOW_DISABLED: "queue.flow_disabled";
|
|
1308
|
+
readonly METRICS_DISABLED: "queue.metrics_disabled";
|
|
1309
|
+
readonly SHUTDOWN_TIMEOUT_EXCEEDED: "queue.shutdown_timeout_exceeded";
|
|
1310
|
+
readonly BULK_ENQUEUE_FAILED: "queue.bulk_enqueue_failed";
|
|
1311
|
+
readonly WORKER_REGISTRATION_FAILED: "queue.worker_registration_failed";
|
|
1312
|
+
readonly INVALID_OPTIONS: "queue.invalid_options";
|
|
1313
|
+
};
|
|
1314
|
+
/** Union of every value in {@link QUEUE_ERROR_CODES}. */
|
|
1315
|
+
type QueueErrorCode = (typeof QUEUE_ERROR_CODES)[keyof typeof QUEUE_ERROR_CODES];
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* @fileoverview Server-side error-code re-exports plus the human-readable
|
|
1319
|
+
* message catalog keyed by code.
|
|
1320
|
+
* @layer server/constants
|
|
1321
|
+
*/
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Human-readable messages keyed by error code — mirrors the specification's
|
|
1325
|
+
* error catalog. Every value of {@link QUEUE_ERROR_CODES} has an entry.
|
|
1326
|
+
*/
|
|
1327
|
+
declare const QUEUE_ERROR_MESSAGES: Record<string, string>;
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* @fileoverview Consolidated default values applied to module options.
|
|
1331
|
+
* @layer server/constants
|
|
1332
|
+
*/
|
|
1333
|
+
/** Default worker concurrency — a non-serial starting point, tuned per workload. */
|
|
1334
|
+
declare const DEFAULT_WORKER_CONCURRENCY: 2;
|
|
1335
|
+
/**
|
|
1336
|
+
* Maximum allowed concurrency per Worker. Values above this ceiling exhaust the
|
|
1337
|
+
* event loop, heap, and lock-renewal timers before providing meaningful throughput
|
|
1338
|
+
* gains. Single-digit hundreds are typical for I/O-bound workloads (BullMQ docs).
|
|
1339
|
+
*/
|
|
1340
|
+
declare const MAX_WORKER_CONCURRENCY: 1000;
|
|
1341
|
+
/**
|
|
1342
|
+
* Default options applied to every job enqueued through the service. The
|
|
1343
|
+
* `removeOnFail.count` cap bounds Redis memory growth under failure storms.
|
|
1344
|
+
*/
|
|
1345
|
+
declare const DEFAULT_JOB_OPTIONS: {
|
|
1346
|
+
readonly attempts: 3;
|
|
1347
|
+
readonly backoff: {
|
|
1348
|
+
readonly type: "exponential";
|
|
1349
|
+
readonly delay: 2000;
|
|
1350
|
+
};
|
|
1351
|
+
readonly removeOnComplete: {
|
|
1352
|
+
readonly age: number;
|
|
1353
|
+
readonly count: 1000;
|
|
1354
|
+
};
|
|
1355
|
+
readonly removeOnFail: {
|
|
1356
|
+
readonly age: number;
|
|
1357
|
+
readonly count: 5000;
|
|
1358
|
+
};
|
|
1359
|
+
};
|
|
1360
|
+
/** Default TTL (ms) for the metrics cache. */
|
|
1361
|
+
declare const DEFAULT_METRICS_CACHE_TTL_MS: 5000;
|
|
1362
|
+
/** Default window (ms) for in-flight jobs to drain before force-close on shutdown. */
|
|
1363
|
+
declare const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS: 30000;
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* @fileoverview Canonical job-status constant object.
|
|
1367
|
+
* @layer shared/constants
|
|
1368
|
+
*/
|
|
1369
|
+
/**
|
|
1370
|
+
* Canonical job status constants — use these in business logic instead of
|
|
1371
|
+
* hardcoded string literals. Declared `as const` so the literal types survive
|
|
1372
|
+
* into the emitted declaration files.
|
|
1373
|
+
*
|
|
1374
|
+
* @example
|
|
1375
|
+
* if (status === JOB_STATUS.FAILED) { retry() }
|
|
1376
|
+
*/
|
|
1377
|
+
declare const JOB_STATUS: {
|
|
1378
|
+
readonly WAITING: "waiting";
|
|
1379
|
+
readonly ACTIVE: "active";
|
|
1380
|
+
readonly COMPLETED: "completed";
|
|
1381
|
+
readonly FAILED: "failed";
|
|
1382
|
+
readonly DELAYED: "delayed";
|
|
1383
|
+
readonly PAUSED: "paused";
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
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 };
|