@bymax-one/nest-queue 1.2.10 → 1.2.12
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 +77 -1
- package/README.md +28 -20
- package/dist/server/index.cjs +43 -502
- package/dist/server/index.d.cts +29 -10
- package/dist/server/index.d.ts +29 -10
- package/dist/server/index.mjs +44 -503
- package/package.json +5 -2
package/dist/server/index.cjs
CHANGED
|
@@ -165,7 +165,6 @@ function applyDefaults(opts) {
|
|
|
165
165
|
Object.defineProperty(base, "connection", {
|
|
166
166
|
get: () => connection,
|
|
167
167
|
enumerable: false,
|
|
168
|
-
// Stryker disable next-line BooleanLiteral: equivalent HERE — this object is `Object.freeze`d on the way out, and freezing makes every property non-configurable regardless. The flag stays because it states the guarantee at the point the accessor is defined, and because the sibling packages that withhold secrets the same way are NOT frozen, where it is load-bearing
|
|
169
168
|
configurable: false
|
|
170
169
|
});
|
|
171
170
|
return Object.freeze(base);
|
|
@@ -193,30 +192,51 @@ function isClientUsable(client) {
|
|
|
193
192
|
return client.status === "ready" || client.status === "connecting";
|
|
194
193
|
}
|
|
195
194
|
|
|
195
|
+
// src/server/utils/describe-error.ts
|
|
196
|
+
function describeError(cause) {
|
|
197
|
+
try {
|
|
198
|
+
if (typeof cause === "object" && cause !== null && "message" in cause) {
|
|
199
|
+
if (typeof cause.message === "string") return cause.message;
|
|
200
|
+
}
|
|
201
|
+
return String(cause);
|
|
202
|
+
} catch {
|
|
203
|
+
return "unknown error";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/server/utils/attach-client-error-listener.ts
|
|
208
|
+
function describeClientError(err) {
|
|
209
|
+
const message = describeError(err);
|
|
210
|
+
if (message !== "") return message;
|
|
211
|
+
try {
|
|
212
|
+
const { name, code } = err;
|
|
213
|
+
const label = typeof name === "string" && name !== "" ? name : "unknown error";
|
|
214
|
+
return typeof code === "string" ? `${label} [${code}]` : label;
|
|
215
|
+
} catch {
|
|
216
|
+
return "unknown error";
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function attachClientErrorListener(client, logger, role) {
|
|
220
|
+
client.on("error", (err) => {
|
|
221
|
+
logger.error(`Redis connection (${role}) emitted an error: ${describeClientError(err)}`);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
196
225
|
// src/server/utils/duplicate-connection.ts
|
|
197
226
|
function duplicateConnection(client) {
|
|
198
227
|
return client.duplicate({ maxRetriesPerRequest: null });
|
|
199
228
|
}
|
|
200
229
|
|
|
201
230
|
// src/server/services/connection-resolver.service.ts
|
|
202
|
-
var _client, _options;
|
|
231
|
+
var _logger, _client, _options;
|
|
203
232
|
exports.ConnectionResolver = class ConnectionResolver {
|
|
204
233
|
constructor(options) {
|
|
205
|
-
|
|
206
|
-
* The resolved Redis client.
|
|
207
|
-
*
|
|
208
|
-
* An ECMAScript private field rather than a TypeScript `private` one, which
|
|
209
|
-
* is erased at runtime: an ioredis instance carries `options.password` as a
|
|
210
|
-
* plain field, so leaving this enumerable would let anything that serializes
|
|
211
|
-
* a service holding this resolver walk into the credentials.
|
|
212
|
-
*/
|
|
234
|
+
__privateAdd(this, _logger, new common.Logger(exports.ConnectionResolver.name));
|
|
213
235
|
__privateAdd(this, _client);
|
|
214
|
-
/** The consumer's module options, which carry the Redis credentials. */
|
|
215
236
|
__privateAdd(this, _options);
|
|
216
237
|
__publicField(this, "mode");
|
|
217
238
|
__privateSet(this, _options, options);
|
|
218
239
|
}
|
|
219
|
-
/** Resolve and validate the connection. Call once during module bootstrap. */
|
|
220
240
|
async init() {
|
|
221
241
|
const cfg = __privateGet(this, _options).connection;
|
|
222
242
|
if ("client" in cfg) {
|
|
@@ -225,22 +245,14 @@ exports.ConnectionResolver = class ConnectionResolver {
|
|
|
225
245
|
}
|
|
226
246
|
this.mode = "mode-b-owned";
|
|
227
247
|
__privateSet(this, _client, "url" in cfg ? new ioredis.Redis(cfg.url, this.ownedOptions(cfg.options)) : new ioredis.Redis(this.ownedOptions(cfg.options)));
|
|
248
|
+
attachClientErrorListener(__privateGet(this, _client), __privateGet(this, _logger), "owned");
|
|
228
249
|
await this.waitReady(
|
|
229
250
|
__privateGet(this, _options).connectionReadyTimeoutMs ?? DEFAULT_CONNECTION_READY_TIMEOUT_MS
|
|
230
251
|
);
|
|
231
252
|
}
|
|
232
|
-
/**
|
|
233
|
-
* Merge the consumer's options with the owned-connection policy
|
|
234
|
-
* (`lazyConnect: false`, so the client connects eagerly and `init` can wait
|
|
235
|
-
* for `ready`) and narrow the result to the shape ioredis 6's constructor
|
|
236
|
-
* accepts. The runtime object is unchanged — the cast only drops the
|
|
237
|
-
* `undefined` that `RedisOptions.replyMapping` carries and the constructor
|
|
238
|
-
* rejects.
|
|
239
|
-
*/
|
|
240
253
|
ownedOptions(options) {
|
|
241
254
|
return { ...options ?? {}, lazyConnect: false };
|
|
242
255
|
}
|
|
243
|
-
/** The resolved Queue-role client. */
|
|
244
256
|
getClient() {
|
|
245
257
|
if (!__privateGet(this, _client)) {
|
|
246
258
|
throw new QueueException(
|
|
@@ -253,7 +265,6 @@ exports.ConnectionResolver = class ConnectionResolver {
|
|
|
253
265
|
}
|
|
254
266
|
return __privateGet(this, _client);
|
|
255
267
|
}
|
|
256
|
-
/** The resolved connection mode. */
|
|
257
268
|
getMode() {
|
|
258
269
|
if (!this.mode) {
|
|
259
270
|
throw new QueueException(
|
|
@@ -266,11 +277,9 @@ exports.ConnectionResolver = class ConnectionResolver {
|
|
|
266
277
|
}
|
|
267
278
|
return this.mode;
|
|
268
279
|
}
|
|
269
|
-
/** Whether the library owns (and must close) the connection. */
|
|
270
280
|
isOwned() {
|
|
271
281
|
return this.mode === "mode-b-owned";
|
|
272
282
|
}
|
|
273
|
-
/** Close the library-owned connection on shutdown; never touch a BYO client. */
|
|
274
283
|
async teardown() {
|
|
275
284
|
if (this.isOwned() && __privateGet(this, _client)) {
|
|
276
285
|
const client = __privateGet(this, _client);
|
|
@@ -279,7 +288,6 @@ exports.ConnectionResolver = class ConnectionResolver {
|
|
|
279
288
|
});
|
|
280
289
|
}
|
|
281
290
|
}
|
|
282
|
-
/** Validate and adopt a bring-your-own client for the Queue role. */
|
|
283
291
|
initModeA(client) {
|
|
284
292
|
this.mode = "mode-a-byo";
|
|
285
293
|
__privateSet(this, _client, client);
|
|
@@ -291,13 +299,13 @@ exports.ConnectionResolver = class ConnectionResolver {
|
|
|
291
299
|
);
|
|
292
300
|
}
|
|
293
301
|
const probe = duplicateConnection(client);
|
|
302
|
+
attachClientErrorListener(probe, __privateGet(this, _logger), "probe");
|
|
294
303
|
try {
|
|
295
304
|
assertBlockingConnection(probe);
|
|
296
305
|
} finally {
|
|
297
306
|
probe.disconnect();
|
|
298
307
|
}
|
|
299
308
|
}
|
|
300
|
-
/** Resolve when the owned client reaches `ready`, or reject on timeout/error. */
|
|
301
309
|
async waitReady(timeoutMs) {
|
|
302
310
|
const client = this.getClient();
|
|
303
311
|
if (client.status === "ready") return;
|
|
@@ -330,6 +338,7 @@ exports.ConnectionResolver = class ConnectionResolver {
|
|
|
330
338
|
});
|
|
331
339
|
}
|
|
332
340
|
};
|
|
341
|
+
_logger = new WeakMap();
|
|
333
342
|
_client = new WeakMap();
|
|
334
343
|
_options = new WeakMap();
|
|
335
344
|
exports.ConnectionResolver = __decorateClass([
|
|
@@ -368,18 +377,6 @@ function validateJobSchedulerOptions(repeat) {
|
|
|
368
377
|
}
|
|
369
378
|
}
|
|
370
379
|
|
|
371
|
-
// src/server/utils/describe-error.ts
|
|
372
|
-
function describeError(cause) {
|
|
373
|
-
try {
|
|
374
|
-
if (typeof cause === "object" && cause !== null && "message" in cause) {
|
|
375
|
-
if (typeof cause.message === "string") return cause.message;
|
|
376
|
-
}
|
|
377
|
-
return String(cause);
|
|
378
|
-
} catch {
|
|
379
|
-
return "unknown error";
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
380
|
// src/server/utils/attach-error-listener.ts
|
|
384
381
|
function attachDefaultErrorListener(emitter, logger, kind, queueName) {
|
|
385
382
|
emitter.on("error", (err) => {
|
|
@@ -409,14 +406,6 @@ exports.QueueService = class QueueService {
|
|
|
409
406
|
}
|
|
410
407
|
connection;
|
|
411
408
|
options;
|
|
412
|
-
/**
|
|
413
|
-
* Return the cached `Queue` for `queueName`, or create one with the module
|
|
414
|
-
* defaults applied. Subsequent calls with the same name return the same
|
|
415
|
-
* instance.
|
|
416
|
-
*
|
|
417
|
-
* @param queueName - Unique queue name (used as the Redis key prefix).
|
|
418
|
-
* @param overrides - Optional per-queue option overrides.
|
|
419
|
-
*/
|
|
420
409
|
getOrCreateQueue(queueName, overrides) {
|
|
421
410
|
const existing = __privateGet(this, _queues).get(queueName);
|
|
422
411
|
if (existing) return existing;
|
|
@@ -432,77 +421,13 @@ exports.QueueService = class QueueService {
|
|
|
432
421
|
__privateGet(this, _queues).set(queueName, queue);
|
|
433
422
|
return queue;
|
|
434
423
|
}
|
|
435
|
-
/** Resolve the raw cached queue (default generics) for internal delegation. */
|
|
436
424
|
queueFor(queueName) {
|
|
437
425
|
return this.getOrCreateQueue(queueName);
|
|
438
426
|
}
|
|
439
|
-
/**
|
|
440
|
-
* Add a single job to a queue. The queue is created lazily if needed.
|
|
441
|
-
*
|
|
442
|
-
* Delivery is **at-least-once** — a handler may run more than once for the same
|
|
443
|
-
* job (worker crash, lock expiry, or a shutdown force-close). Make handlers
|
|
444
|
-
* idempotent: use an idempotency key on writes, prefer upserts over inserts, or
|
|
445
|
-
* keep an "already-processed" marker keyed by `job.id`.
|
|
446
|
-
*
|
|
447
|
-
* `options` surfaces BullMQ natives directly — including `jobId` (idempotent
|
|
448
|
-
* insert: a second add with the same id is a no-op while the first exists) and
|
|
449
|
-
* `deduplication` (windowed deduplication). The library writes no deduplication
|
|
450
|
-
* code; BullMQ owns the behavior. The deduplication key is independent of
|
|
451
|
-
* `jobId`, so both can be set together or apart. Both collapse duplicate
|
|
452
|
-
* **producers** and do not change the at-least-once guarantee on the
|
|
453
|
-
* **consumer** side.
|
|
454
|
-
*
|
|
455
|
-
* The deduplication `id` is a shared key across the queue, so a multi-tenant
|
|
456
|
-
* producer MUST scope it by tenant: keyed on the value alone
|
|
457
|
-
* (`` `reindex:${term}` ``), tenant A enqueuing a term suppresses tenant B's job
|
|
458
|
-
* for the same term.
|
|
459
|
-
*
|
|
460
|
-
* Scoping alone is not enough. Two variable fields joined by a delimiter that
|
|
461
|
-
* either may contain do not produce a unique key: with `` `${tenantId}:${term}` ``,
|
|
462
|
-
* the pairs `('a', 'b:c')` and `('a:b', 'c')` both yield `a:b:c`. Percent-encode
|
|
463
|
-
* every variable field so the delimiter cannot appear inside one:
|
|
464
|
-
* `` `${encodeURIComponent(tenantId)}:${encodeURIComponent(term)}` ``.
|
|
465
|
-
*
|
|
466
|
-
* Deduplication modes (`deduplication: { id, ttl?, extend?, replace?, keepLastIfActive? }`):
|
|
467
|
-
* - Simple `{ id }` — collapse duplicates until the in-flight job completes or fails.
|
|
468
|
-
* - Throttle `{ id, ttl }` — ignore duplicates for `ttl` milliseconds.
|
|
469
|
-
* - Debounce `{ id, ttl, extend: true, replace: true }` — keep only the latest
|
|
470
|
-
* data; each duplicate resets the TTL.
|
|
471
|
-
* - keep-last-if-active `{ id, keepLastIfActive: true }` — while a job is active,
|
|
472
|
-
* store the latest data and run a single follow-up when it finishes.
|
|
473
|
-
*
|
|
474
|
-
* @param queueName - Target queue (created lazily if absent).
|
|
475
|
-
* @param jobName - Job identifier used by `@Process` to dispatch.
|
|
476
|
-
* @param data - Typed job payload.
|
|
477
|
-
* @param options - Per-job BullMQ options (priority, delay, `jobId`, `deduplication`, ...).
|
|
478
|
-
* @returns The job BullMQ returned. When a custom `jobId` already exists, or a `deduplication`
|
|
479
|
-
* rule keeps the job already there (one with `replace` swaps it for the new job), nothing new
|
|
480
|
-
* is added: BullMQ answers with the id of the EXISTING job and the data and options of THIS
|
|
481
|
-
* call, so the returned object does not describe what is stored. Call
|
|
482
|
-
* {@link QueueService.getJob} when you need the stored job.
|
|
483
|
-
* @example Throttle: at most one reindex per term every 5 seconds. Both variable
|
|
484
|
-
* fields are percent-encoded, so no other tenant/term pair can build this key.
|
|
485
|
-
* const dedupId = `${encodeURIComponent(tenantId)}:reindex:${encodeURIComponent(term)}`
|
|
486
|
-
* await queueService.enqueue('search', 'reindex', { term }, {
|
|
487
|
-
* deduplication: { id: dedupId, ttl: 5_000 },
|
|
488
|
-
* })
|
|
489
|
-
*/
|
|
490
427
|
async enqueue(queueName, jobName, data, options) {
|
|
491
428
|
const job = await this.queueFor(queueName).add(jobName, data, options);
|
|
492
429
|
return job;
|
|
493
430
|
}
|
|
494
|
-
/**
|
|
495
|
-
* Enqueue multiple jobs in a single Redis roundtrip. The batch is bounded by
|
|
496
|
-
* {@link MAX_BULK_SIZE} to guard against a self-inflicted memory spike; any
|
|
497
|
-
* failure is wrapped in a `QueueException`. Delivery is at-least-once, so every
|
|
498
|
-
* job's handler must be idempotent (see {@link QueueService.enqueue}).
|
|
499
|
-
*
|
|
500
|
-
* @param queueName - Target queue.
|
|
501
|
-
* @param jobs - Job descriptors (length must not exceed the bulk size cap).
|
|
502
|
-
* @returns The jobs BullMQ returned, in input order. An entry whose custom `jobId` already
|
|
503
|
-
* exists is not added again: it carries the EXISTING id with the data of this call, as for
|
|
504
|
-
* {@link QueueService.enqueue}.
|
|
505
|
-
*/
|
|
506
431
|
async enqueueBulk(queueName, jobs) {
|
|
507
432
|
if (jobs.length > MAX_BULK_SIZE) {
|
|
508
433
|
throw new QueueException(
|
|
@@ -534,33 +459,14 @@ exports.QueueService = class QueueService {
|
|
|
534
459
|
);
|
|
535
460
|
}
|
|
536
461
|
}
|
|
537
|
-
/**
|
|
538
|
-
* Fetch a job by id. Returns `null` when the job does not exist.
|
|
539
|
-
*
|
|
540
|
-
* @param queueName - Target queue.
|
|
541
|
-
* @param jobId - The job identifier.
|
|
542
|
-
*/
|
|
543
462
|
async getJob(queueName, jobId) {
|
|
544
463
|
const job = await this.queueFor(queueName).getJob(jobId);
|
|
545
464
|
return job ?? null;
|
|
546
465
|
}
|
|
547
|
-
/**
|
|
548
|
-
* Fetch jobs in a given status with pagination.
|
|
549
|
-
*
|
|
550
|
-
* @param queueName - Target queue.
|
|
551
|
-
* @param status - Status filter.
|
|
552
|
-
* @param start - Starting index (inclusive). Default: 0.
|
|
553
|
-
* @param end - Ending index (inclusive). Default: 50.
|
|
554
|
-
*/
|
|
555
466
|
async getJobs(queueName, status, start = 0, end = 50) {
|
|
556
467
|
const jobs = await this.queueFor(queueName).getJobs([status], start, end);
|
|
557
468
|
return jobs;
|
|
558
469
|
}
|
|
559
|
-
/**
|
|
560
|
-
* Return an uncached snapshot of job counts grouped by status.
|
|
561
|
-
*
|
|
562
|
-
* @param queueName - Target queue.
|
|
563
|
-
*/
|
|
564
470
|
async getMetrics(queueName) {
|
|
565
471
|
const counts = await this.queueFor(queueName).getJobCounts(
|
|
566
472
|
"waiting",
|
|
@@ -575,24 +481,6 @@ exports.QueueService = class QueueService {
|
|
|
575
481
|
collectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
576
482
|
};
|
|
577
483
|
}
|
|
578
|
-
/**
|
|
579
|
-
* Create or update a Job Scheduler — the current BullMQ recurring-jobs API.
|
|
580
|
-
* Idempotent by `schedulerId`: BullMQ performs an atomic `override: true`
|
|
581
|
-
* upsert, so re-registering on every boot updates the schedule in place and
|
|
582
|
-
* never duplicates it. This supersedes the repeatable-jobs surface removed in
|
|
583
|
-
* BullMQ v6.
|
|
584
|
-
*
|
|
585
|
-
* The schedule is validated first, and a cron pattern BullMQ's parser rejects
|
|
586
|
-
* is `INVALID_REPEAT_OPTIONS` (400) before Redis is asked; any other failure
|
|
587
|
-
* propagates as it is.
|
|
588
|
-
*
|
|
589
|
-
* @param queueName - Target queue.
|
|
590
|
-
* @param schedulerId - Stable, unique scheduler identifier (the upsert key).
|
|
591
|
-
* @param repeat - Cron `pattern` OR `every` (ms), plus optional tz/limit/start/end.
|
|
592
|
-
* @param template - Optional job template; `name` defaults to `schedulerId`, `data` to `{}`.
|
|
593
|
-
* @returns The first scheduled (delayed) job, or `undefined`.
|
|
594
|
-
* @throws {QueueException} `INVALID_REPEAT_OPTIONS` (400) for an invalid schedule.
|
|
595
|
-
*/
|
|
596
484
|
async upsertJobScheduler(queueName, schedulerId, repeat, template) {
|
|
597
485
|
validateJobSchedulerOptions(repeat);
|
|
598
486
|
const queue = this.queueFor(queueName);
|
|
@@ -605,62 +493,24 @@ exports.QueueService = class QueueService {
|
|
|
605
493
|
const job = await queue.upsertJobScheduler(schedulerId, repeat, jobTemplate);
|
|
606
494
|
return job;
|
|
607
495
|
}
|
|
608
|
-
/**
|
|
609
|
-
* Remove a Job Scheduler by id.
|
|
610
|
-
*
|
|
611
|
-
* @param queueName - Target queue.
|
|
612
|
-
* @param schedulerId - The scheduler identifier to remove.
|
|
613
|
-
* @returns `true` when a scheduler was removed, `false` otherwise.
|
|
614
|
-
*/
|
|
615
496
|
async removeJobScheduler(queueName, schedulerId) {
|
|
616
497
|
return this.queueFor(queueName).removeJobScheduler(schedulerId);
|
|
617
498
|
}
|
|
618
|
-
/**
|
|
619
|
-
* List the Job Schedulers registered on a queue, for inspection or health checks.
|
|
620
|
-
*
|
|
621
|
-
* @param queueName - Target queue.
|
|
622
|
-
* @param start - Page start index (inclusive). Default: 0.
|
|
623
|
-
* @param end - Page end index (inclusive). Default: 50.
|
|
624
|
-
* @param asc - Sort ascending by next run time. Default: true.
|
|
625
|
-
* @returns The registered schedulers in the requested page.
|
|
626
|
-
*/
|
|
627
499
|
async getJobSchedulers(queueName, start = 0, end = 50, asc = true) {
|
|
628
500
|
return this.queueFor(queueName).getJobSchedulers(start, end, asc);
|
|
629
501
|
}
|
|
630
|
-
/** Pause a queue, halting the processing of new jobs. */
|
|
631
502
|
async pauseQueue(queueName) {
|
|
632
503
|
await this.queueFor(queueName).pause();
|
|
633
504
|
}
|
|
634
|
-
/** Resume a previously paused queue. */
|
|
635
505
|
async resumeQueue(queueName) {
|
|
636
506
|
await this.queueFor(queueName).resume();
|
|
637
507
|
}
|
|
638
|
-
/**
|
|
639
|
-
* Remove up to `limit` jobs older than `gracePeriodMs` in a given status. The
|
|
640
|
-
* argument order mirrors BullMQ's `Queue.clean(grace, limit, type?)` exactly;
|
|
641
|
-
* `limit` is required and `0` means no limit. Returns the removed job ids.
|
|
642
|
-
*
|
|
643
|
-
* @param queueName - Target queue.
|
|
644
|
-
* @param gracePeriodMs - Keep jobs younger than this many milliseconds.
|
|
645
|
-
* @param limit - Maximum number of jobs to remove (`0` = no limit).
|
|
646
|
-
* @param status - Status of jobs to clean.
|
|
647
|
-
*/
|
|
648
508
|
async cleanQueue(queueName, gracePeriodMs, limit, status) {
|
|
649
509
|
return this.queueFor(queueName).clean(gracePeriodMs, limit, status);
|
|
650
510
|
}
|
|
651
|
-
/** Return a read-only view of the cached queues. */
|
|
652
511
|
getCachedQueues() {
|
|
653
512
|
return __privateGet(this, _queues);
|
|
654
513
|
}
|
|
655
|
-
/**
|
|
656
|
-
* Close every cached queue (swallowing per-queue errors) and clear the cache.
|
|
657
|
-
*
|
|
658
|
-
* Deliberately NOT named `onModuleDestroy`: NestJS invokes lifecycle hooks by
|
|
659
|
-
* method name, so a hook here would run on Nest's own schedule, concurrently
|
|
660
|
-
* with — and possibly before — the bounded drain in `QueueLifecycle`. Closing a
|
|
661
|
-
* queue while its worker is still draining is exactly what the ordered sequence
|
|
662
|
-
* exists to prevent. `QueueLifecycle` is the only caller.
|
|
663
|
-
*/
|
|
664
514
|
async closeAll() {
|
|
665
515
|
for (const queue of __privateGet(this, _queues).values()) {
|
|
666
516
|
await queue.close().catch(() => void 0);
|
|
@@ -681,77 +531,26 @@ exports.FlowService = class FlowService {
|
|
|
681
531
|
if (enabled) {
|
|
682
532
|
this.producer = new bullmq.FlowProducer({
|
|
683
533
|
connection: connection.getClient(),
|
|
684
|
-
// Match the producer Queue's key prefix so flow jobs land in the same
|
|
685
|
-
// keyspace the workers poll; a non-default prefix breaks flows otherwise.
|
|
686
534
|
prefix,
|
|
687
535
|
...telemetry ? { telemetry } : {}
|
|
688
536
|
});
|
|
689
537
|
attachDefaultErrorListener(this.producer, this.logger, "FlowProducer", "*");
|
|
690
538
|
}
|
|
691
539
|
}
|
|
692
|
-
/**
|
|
693
|
-
* Add a flow tree (a parent with its descendant children). The root job
|
|
694
|
-
* becomes processable only after every descendant has completed.
|
|
695
|
-
*
|
|
696
|
-
* By BullMQ default a failed (retry-exhausted) child does NOT fail its parent:
|
|
697
|
-
* the parent stays in the `waiting-children` state until the child eventually
|
|
698
|
-
* completes. To make a child's failure propagate upward and fail its parent,
|
|
699
|
-
* set `failParentOnFailure: true` on that child (it cascades when ancestors
|
|
700
|
-
* also set it). To let a parent proceed despite a failed child, set
|
|
701
|
-
* `ignoreDependencyOnFailure: true` on that child. Bound each child's
|
|
702
|
-
* `attempts` so an unrecoverable child cannot leave the parent waiting forever.
|
|
703
|
-
*
|
|
704
|
-
* @param flow - Flow tree definition.
|
|
705
|
-
* @returns The root `JobNode` containing the created job and its children.
|
|
706
|
-
* A job whose custom `jobId` already exists is not added again: its node carries the EXISTING
|
|
707
|
-
* id with the data of this call, so the tree does not describe what is stored; call
|
|
708
|
-
* `QueueService.getJob` when you need it.
|
|
709
|
-
* @throws {QueueException} `FLOW_DISABLED` (503) when flows are not enabled.
|
|
710
|
-
*/
|
|
711
540
|
async add(flow) {
|
|
712
541
|
return this.requireProducer().add(flow);
|
|
713
542
|
}
|
|
714
|
-
/**
|
|
715
|
-
* Add multiple flow trees in a single Redis roundtrip.
|
|
716
|
-
*
|
|
717
|
-
* @param flows - Flow tree definitions.
|
|
718
|
-
* @returns The root `JobNode` for each flow, in input order.
|
|
719
|
-
* A job whose custom `jobId` already exists is not added again; see {@link FlowService.add}.
|
|
720
|
-
* @throws {QueueException} `FLOW_DISABLED` (503) when flows are not enabled.
|
|
721
|
-
*/
|
|
722
543
|
async addBulk(flows) {
|
|
723
544
|
return this.requireProducer().addBulk([...flows]);
|
|
724
545
|
}
|
|
725
|
-
/**
|
|
726
|
-
* Escape hatch returning the underlying `FlowProducer` for advanced use cases.
|
|
727
|
-
* Prefer {@link FlowService.add} / {@link FlowService.addBulk} when possible.
|
|
728
|
-
*
|
|
729
|
-
* @returns The underlying BullMQ `FlowProducer`.
|
|
730
|
-
* @throws {QueueException} `FLOW_DISABLED` (503) when flows are not enabled.
|
|
731
|
-
*/
|
|
732
546
|
getProducer() {
|
|
733
547
|
return this.requireProducer();
|
|
734
548
|
}
|
|
735
|
-
/**
|
|
736
|
-
* Close the producer when active; a no-op when inactive. A failed `close()` is
|
|
737
|
-
* swallowed so it never aborts the shutdown sequence.
|
|
738
|
-
*
|
|
739
|
-
* Deliberately NOT named `onModuleDestroy`: NestJS invokes lifecycle hooks by
|
|
740
|
-
* method name, so a hook here would run in addition to the call the ordered
|
|
741
|
-
* sequence already makes, closing the producer twice. `QueueLifecycle` is the
|
|
742
|
-
* only caller.
|
|
743
|
-
*/
|
|
744
549
|
async close() {
|
|
745
550
|
if (this.producer) {
|
|
746
551
|
await this.producer.close().catch(() => void 0);
|
|
747
552
|
}
|
|
748
553
|
}
|
|
749
|
-
/**
|
|
750
|
-
* Return the active producer or throw when flows are disabled. The producer is
|
|
751
|
-
* present if and only if flows were enabled at construction time.
|
|
752
|
-
*
|
|
753
|
-
* @throws {QueueException} `FLOW_DISABLED` (503) when no producer exists.
|
|
754
|
-
*/
|
|
755
554
|
requireProducer() {
|
|
756
555
|
if (!this.producer) {
|
|
757
556
|
throw new QueueException(QUEUE_ERROR_CODES.FLOW_DISABLED, common.HttpStatus.SERVICE_UNAVAILABLE);
|
|
@@ -772,14 +571,6 @@ exports.MetricsService = class MetricsService {
|
|
|
772
571
|
enabled;
|
|
773
572
|
ttlMs;
|
|
774
573
|
cache = /* @__PURE__ */ new Map();
|
|
775
|
-
/**
|
|
776
|
-
* Return cached metrics for `queueName`, or fetch fresh (and cache them) on a
|
|
777
|
-
* miss or after expiry.
|
|
778
|
-
*
|
|
779
|
-
* @param queueName - Target queue.
|
|
780
|
-
* @returns The current metrics snapshot.
|
|
781
|
-
* @throws {QueueException} `METRICS_DISABLED` (503) when metrics are disabled.
|
|
782
|
-
*/
|
|
783
574
|
async get(queueName) {
|
|
784
575
|
this.ensureEnabled();
|
|
785
576
|
const entry = this.cache.get(queueName);
|
|
@@ -790,23 +581,11 @@ exports.MetricsService = class MetricsService {
|
|
|
790
581
|
this.cache.set(queueName, { metrics, expiresAt: Date.now() + this.ttlMs });
|
|
791
582
|
return metrics;
|
|
792
583
|
}
|
|
793
|
-
/**
|
|
794
|
-
* Return a metrics snapshot for every queue currently cached in `QueueService`.
|
|
795
|
-
*
|
|
796
|
-
* @returns Metrics for all known queues.
|
|
797
|
-
* @throws {QueueException} `METRICS_DISABLED` (503) when metrics are disabled.
|
|
798
|
-
*/
|
|
799
584
|
async getAll() {
|
|
800
585
|
this.ensureEnabled();
|
|
801
586
|
const names = Array.from(this.queueService.getCachedQueues().keys());
|
|
802
587
|
return Promise.all(names.map((name) => this.get(name)));
|
|
803
588
|
}
|
|
804
|
-
/**
|
|
805
|
-
* Invalidate the cache for `queueName`, or the whole cache when omitted.
|
|
806
|
-
*
|
|
807
|
-
* @param queueName - The queue entry to drop; omit to clear everything.
|
|
808
|
-
* @throws {QueueException} `METRICS_DISABLED` (503) when metrics are disabled.
|
|
809
|
-
*/
|
|
810
589
|
invalidate(queueName) {
|
|
811
590
|
this.ensureEnabled();
|
|
812
591
|
if (queueName === void 0) {
|
|
@@ -815,11 +594,6 @@ exports.MetricsService = class MetricsService {
|
|
|
815
594
|
}
|
|
816
595
|
this.cache.delete(queueName);
|
|
817
596
|
}
|
|
818
|
-
/**
|
|
819
|
-
* Guard every operation behind the opt-in flag.
|
|
820
|
-
*
|
|
821
|
-
* @throws {QueueException} `METRICS_DISABLED` (503) when metrics are disabled.
|
|
822
|
-
*/
|
|
823
597
|
ensureEnabled() {
|
|
824
598
|
if (!this.enabled) {
|
|
825
599
|
throw new QueueException(QUEUE_ERROR_CODES.METRICS_DISABLED, common.HttpStatus.SERVICE_UNAVAILABLE);
|
|
@@ -839,19 +613,6 @@ exports.WorkerRegistry = class WorkerRegistry {
|
|
|
839
613
|
}
|
|
840
614
|
connection;
|
|
841
615
|
options;
|
|
842
|
-
/**
|
|
843
|
-
* Register an in-process worker with a handler function. The BullMQ `Worker`
|
|
844
|
-
* constructor is synchronous — no Redis handshake is performed at registration
|
|
845
|
-
* time. The duplicated connection is disconnected immediately if construction
|
|
846
|
-
* fails, preventing a Redis connection leak.
|
|
847
|
-
*
|
|
848
|
-
* @param config - Queue name, handler, and optional worker options.
|
|
849
|
-
* @returns The created BullMQ `Worker` instance.
|
|
850
|
-
* @throws {QueueException} `DUPLICATE_PROCESSOR` when a worker for
|
|
851
|
-
* `queueName` already exists.
|
|
852
|
-
* @throws {QueueException} `WORKER_REGISTRATION_FAILED` when options are
|
|
853
|
-
* invalid or the BullMQ `Worker` constructor throws.
|
|
854
|
-
*/
|
|
855
616
|
register(config) {
|
|
856
617
|
const { queueName, handler, options } = config;
|
|
857
618
|
this.guardDuplicate(queueName);
|
|
@@ -876,21 +637,6 @@ exports.WorkerRegistry = class WorkerRegistry {
|
|
|
876
637
|
__privateGet(this, _entries).set(queueName, { worker, connection: conn });
|
|
877
638
|
return worker;
|
|
878
639
|
}
|
|
879
|
-
/**
|
|
880
|
-
* Register a sandboxed (file-based, out-of-process) worker. The processor
|
|
881
|
-
* file must be an absolute path or a `file:` URL pointing to a compiled
|
|
882
|
-
* `.js`/`.cjs`/`.mjs` artifact. NestJS DI is **not** available inside the
|
|
883
|
-
* processor. The BullMQ `Worker` constructor is synchronous.
|
|
884
|
-
*
|
|
885
|
-
* @param config - Queue name, processor file path/URL, and optional options.
|
|
886
|
-
* @returns The created BullMQ `Worker` instance.
|
|
887
|
-
* @throws {QueueException} `WORKER_REGISTRATION_FAILED` when `processorFile`
|
|
888
|
-
* is a relative path, has a disallowed extension, or uses a non-`file:` URL protocol.
|
|
889
|
-
* @throws {QueueException} `DUPLICATE_PROCESSOR` when a worker for
|
|
890
|
-
* `queueName` already exists.
|
|
891
|
-
* @throws {QueueException} `WORKER_REGISTRATION_FAILED` when options are
|
|
892
|
-
* invalid or the BullMQ `Worker` constructor throws.
|
|
893
|
-
*/
|
|
894
640
|
registerSandboxed(config) {
|
|
895
641
|
const { queueName, processorFile, options } = config;
|
|
896
642
|
this.validateProcessorFile(processorFile);
|
|
@@ -919,13 +665,6 @@ exports.WorkerRegistry = class WorkerRegistry {
|
|
|
919
665
|
__privateGet(this, _entries).set(queueName, { worker, connection: conn });
|
|
920
666
|
return worker;
|
|
921
667
|
}
|
|
922
|
-
/**
|
|
923
|
-
* Stop and remove the worker for the given queue, releasing the duplicated
|
|
924
|
-
* connection the library opened for it. If no worker exists for `queueName`
|
|
925
|
-
* this is a no-op.
|
|
926
|
-
*
|
|
927
|
-
* @param queueName - The queue whose worker should be removed.
|
|
928
|
-
*/
|
|
929
668
|
async unregister(queueName) {
|
|
930
669
|
const entry = __privateGet(this, _entries).get(queueName);
|
|
931
670
|
if (!entry) return;
|
|
@@ -933,44 +672,19 @@ exports.WorkerRegistry = class WorkerRegistry {
|
|
|
933
672
|
await this.closeConnection(entry.connection);
|
|
934
673
|
__privateGet(this, _entries).delete(queueName);
|
|
935
674
|
}
|
|
936
|
-
/**
|
|
937
|
-
* Returns the queue names of all currently registered workers.
|
|
938
|
-
*
|
|
939
|
-
* @returns An immutable array of queue names.
|
|
940
|
-
*/
|
|
941
675
|
list() {
|
|
942
676
|
return Array.from(__privateGet(this, _entries).keys());
|
|
943
677
|
}
|
|
944
|
-
/**
|
|
945
|
-
* Returns a read-only view of the registered workers keyed by queue name.
|
|
946
|
-
* Intended for the shutdown orchestrator only.
|
|
947
|
-
*
|
|
948
|
-
* @returns A read-only map of queue name to worker.
|
|
949
|
-
*/
|
|
950
678
|
getAll() {
|
|
951
679
|
return new Map(
|
|
952
680
|
Array.from(__privateGet(this, _entries), ([name, entry]) => [name, entry.worker])
|
|
953
681
|
);
|
|
954
682
|
}
|
|
955
|
-
/**
|
|
956
|
-
* Returns a read-only view of the duplicated connections the library opened
|
|
957
|
-
* for its workers, keyed by queue name. Intended for the shutdown orchestrator
|
|
958
|
-
* so it can close exactly the connections the library created (Mode A never
|
|
959
|
-
* exposes the consumer's shared client here).
|
|
960
|
-
*
|
|
961
|
-
* @returns A read-only map of queue name to duplicated connection.
|
|
962
|
-
*/
|
|
963
683
|
getConnections() {
|
|
964
684
|
return new Map(
|
|
965
685
|
Array.from(__privateGet(this, _entries), ([name, entry]) => [name, entry.connection])
|
|
966
686
|
);
|
|
967
687
|
}
|
|
968
|
-
/**
|
|
969
|
-
* Gracefully quit a duplicated connection, falling back to a hard disconnect
|
|
970
|
-
* if the quit handshake fails so a socket is never leaked.
|
|
971
|
-
*
|
|
972
|
-
* @param connection - The duplicated connection to release.
|
|
973
|
-
*/
|
|
974
688
|
async closeConnection(connection) {
|
|
975
689
|
try {
|
|
976
690
|
await connection.quit();
|
|
@@ -978,12 +692,6 @@ exports.WorkerRegistry = class WorkerRegistry {
|
|
|
978
692
|
connection.disconnect();
|
|
979
693
|
}
|
|
980
694
|
}
|
|
981
|
-
/**
|
|
982
|
-
* Guard against registering two workers for the same queue.
|
|
983
|
-
*
|
|
984
|
-
* @param queueName - Queue name to check.
|
|
985
|
-
* @throws {QueueException} `DUPLICATE_PROCESSOR` if the queue is already registered.
|
|
986
|
-
*/
|
|
987
695
|
guardDuplicate(queueName) {
|
|
988
696
|
if (__privateGet(this, _entries).has(queueName)) {
|
|
989
697
|
throw new QueueException(
|
|
@@ -993,14 +701,6 @@ exports.WorkerRegistry = class WorkerRegistry {
|
|
|
993
701
|
);
|
|
994
702
|
}
|
|
995
703
|
}
|
|
996
|
-
/**
|
|
997
|
-
* Validate tunable options before constructing a Worker.
|
|
998
|
-
* Enforces `concurrency` in `[1, MAX_WORKER_CONCURRENCY]` and
|
|
999
|
-
* `limiter.max`/`limiter.duration` `>= 1`.
|
|
1000
|
-
*
|
|
1001
|
-
* @param opts - The caller-supplied options (may be undefined).
|
|
1002
|
-
* @throws {QueueException} `WORKER_REGISTRATION_FAILED` when values are out of range.
|
|
1003
|
-
*/
|
|
1004
704
|
validateOptions(opts) {
|
|
1005
705
|
if (opts?.concurrency !== void 0) {
|
|
1006
706
|
if (opts.concurrency < 1) {
|
|
@@ -1047,15 +747,6 @@ exports.WorkerRegistry = class WorkerRegistry {
|
|
|
1047
747
|
}
|
|
1048
748
|
}
|
|
1049
749
|
}
|
|
1050
|
-
/**
|
|
1051
|
-
* Validate that `processorFile` is a safe, reachable artifact path. Accepts
|
|
1052
|
-
* absolute paths with `.js`/`.cjs`/`.mjs` extensions and `file:` URLs. Rejects
|
|
1053
|
-
* relative paths, non-JS extensions, and non-`file:` URL protocols to prevent
|
|
1054
|
-
* path-traversal and arbitrary-file-execution attacks.
|
|
1055
|
-
*
|
|
1056
|
-
* @param processorFile - The path or URL supplied by the caller.
|
|
1057
|
-
* @throws {QueueException} `WORKER_REGISTRATION_FAILED` when validation fails.
|
|
1058
|
-
*/
|
|
1059
750
|
validateProcessorFile(processorFile) {
|
|
1060
751
|
if (processorFile instanceof URL) {
|
|
1061
752
|
if (processorFile.protocol !== "file:") {
|
|
@@ -1081,13 +772,6 @@ exports.WorkerRegistry = class WorkerRegistry {
|
|
|
1081
772
|
}
|
|
1082
773
|
this.assertProcessorExtension(processorFile);
|
|
1083
774
|
}
|
|
1084
|
-
/**
|
|
1085
|
-
* Reject a processor path whose extension is not an executable JS module,
|
|
1086
|
-
* the last line of defense against loading an arbitrary file as a worker.
|
|
1087
|
-
*
|
|
1088
|
-
* @param filePath - The filesystem path (or file: URL pathname) to check.
|
|
1089
|
-
* @throws {QueueException} `WORKER_REGISTRATION_FAILED` for a disallowed extension.
|
|
1090
|
-
*/
|
|
1091
775
|
assertProcessorExtension(filePath) {
|
|
1092
776
|
const allowedExtensions = /* @__PURE__ */ new Set([".js", ".cjs", ".mjs"]);
|
|
1093
777
|
if (!allowedExtensions.has(path.extname(filePath))) {
|
|
@@ -1100,23 +784,11 @@ exports.WorkerRegistry = class WorkerRegistry {
|
|
|
1100
784
|
);
|
|
1101
785
|
}
|
|
1102
786
|
}
|
|
1103
|
-
/**
|
|
1104
|
-
* Build a BullMQ `WorkerOptions` object from the library's `WorkerOptions`
|
|
1105
|
-
* shape. Only forwards optional fields that are explicitly set to preserve
|
|
1106
|
-
* `exactOptionalPropertyTypes` correctness. Accepts the pre-created duplicated
|
|
1107
|
-
* connection so callers can close it on construction failure.
|
|
1108
|
-
*
|
|
1109
|
-
* @param opts - Library-level worker options.
|
|
1110
|
-
* @param conn - Pre-created duplicated Redis connection.
|
|
1111
|
-
* @returns A BullMQ-compatible options object.
|
|
1112
|
-
*/
|
|
1113
787
|
buildBullOptions(opts, conn) {
|
|
1114
788
|
const result = {
|
|
1115
789
|
connection: conn,
|
|
1116
790
|
concurrency: opts?.concurrency ?? DEFAULT_WORKER_CONCURRENCY,
|
|
1117
791
|
autorun: opts?.autorun ?? true,
|
|
1118
|
-
// Match the producer Queue's key prefix so the worker polls the same
|
|
1119
|
-
// keyspace; without this a non-default prefix leaves jobs unconsumed.
|
|
1120
792
|
prefix: this.options.prefix
|
|
1121
793
|
};
|
|
1122
794
|
if (opts?.limiter !== void 0) result.limiter = opts.limiter;
|
|
@@ -1143,18 +815,11 @@ exports.QueueEventsRegistry = class QueueEventsRegistry {
|
|
|
1143
815
|
}
|
|
1144
816
|
connection;
|
|
1145
817
|
options;
|
|
1146
|
-
/**
|
|
1147
|
-
* Returns the cached `QueueEvents` for `queueName`, creating one on first
|
|
1148
|
-
* call. Subsequent calls for the same name return the same instance without
|
|
1149
|
-
* opening a second Redis connection.
|
|
1150
|
-
*
|
|
1151
|
-
* @param queueName - The BullMQ queue to observe.
|
|
1152
|
-
* @returns The `QueueEvents` instance for the queue.
|
|
1153
|
-
*/
|
|
1154
818
|
getOrCreate(queueName) {
|
|
1155
819
|
const existing = __privateGet(this, _events).get(queueName);
|
|
1156
820
|
if (existing) return existing;
|
|
1157
821
|
const conn = duplicateConnection(this.connection.getClient());
|
|
822
|
+
attachClientErrorListener(conn, this.logger, `queue events: ${queueName}`);
|
|
1158
823
|
let qe;
|
|
1159
824
|
try {
|
|
1160
825
|
qe = new bullmq.QueueEvents(queueName, { connection: conn, prefix: this.options.prefix });
|
|
@@ -1167,35 +832,12 @@ exports.QueueEventsRegistry = class QueueEventsRegistry {
|
|
|
1167
832
|
__privateGet(this, _connections).set(queueName, conn);
|
|
1168
833
|
return qe;
|
|
1169
834
|
}
|
|
1170
|
-
/**
|
|
1171
|
-
* Returns the queue names for which a `QueueEvents` connection is open.
|
|
1172
|
-
*
|
|
1173
|
-
* @returns An immutable array of queue names.
|
|
1174
|
-
*/
|
|
1175
835
|
list() {
|
|
1176
836
|
return Array.from(__privateGet(this, _events).keys());
|
|
1177
837
|
}
|
|
1178
|
-
/**
|
|
1179
|
-
* Returns the live `QueueEvents` map. Intended for the shutdown orchestrator.
|
|
1180
|
-
*
|
|
1181
|
-
* @returns A read-only view of the internal map.
|
|
1182
|
-
*/
|
|
1183
838
|
getAll() {
|
|
1184
839
|
return __privateGet(this, _events);
|
|
1185
840
|
}
|
|
1186
|
-
/**
|
|
1187
|
-
* Returns a read-only view of the duplicated connections the library opened
|
|
1188
|
-
* for its `QueueEvents`, keyed by queue name. Intended for the shutdown
|
|
1189
|
-
* orchestrator so it can close exactly the connections the library created
|
|
1190
|
-
* (Mode A never exposes the consumer's shared client here).
|
|
1191
|
-
*
|
|
1192
|
-
* Returns a copy. `ReadonlyMap` is a compile-time claim only — handing back the
|
|
1193
|
-
* live registry lets any consumer cast it away and delete a connection the
|
|
1194
|
-
* shutdown sequence is about to close. The allocation costs nothing on a path
|
|
1195
|
-
* that runs once per shutdown.
|
|
1196
|
-
*
|
|
1197
|
-
* @returns A read-only snapshot of queue name to duplicated connection.
|
|
1198
|
-
*/
|
|
1199
841
|
getConnections() {
|
|
1200
842
|
return new Map(__privateGet(this, _connections));
|
|
1201
843
|
}
|
|
@@ -1227,10 +869,6 @@ var ProcessorDiscoveryService = class {
|
|
|
1227
869
|
events;
|
|
1228
870
|
logger = new common.Logger(ProcessorDiscoveryService.name);
|
|
1229
871
|
registeredQueues = /* @__PURE__ */ new Set();
|
|
1230
|
-
/**
|
|
1231
|
-
* Entry point called by NestJS after the module is fully initialized.
|
|
1232
|
-
* Discovers all `@Processor`-annotated providers and wires them.
|
|
1233
|
-
*/
|
|
1234
872
|
onModuleInit() {
|
|
1235
873
|
const providers = this.discovery.getProviders();
|
|
1236
874
|
for (const wrapper of providers) {
|
|
@@ -1242,15 +880,6 @@ var ProcessorDiscoveryService = class {
|
|
|
1242
880
|
this.wireProcessor(instance, processorMeta);
|
|
1243
881
|
}
|
|
1244
882
|
}
|
|
1245
|
-
/**
|
|
1246
|
-
* Build a dispatcher function that routes `job.name` to the correct handler.
|
|
1247
|
-
* Named handlers take priority; a catch-all (`@Process()`) handles everything
|
|
1248
|
-
* else. Throws if no matching handler is found at job-processing time.
|
|
1249
|
-
*
|
|
1250
|
-
* @param instance - The provider instance whose methods serve as handlers.
|
|
1251
|
-
* @param handlers - Accumulated `@Process` metadata entries for the class.
|
|
1252
|
-
* @returns A dispatcher `(job: Job) => Promise<unknown>`.
|
|
1253
|
-
*/
|
|
1254
883
|
buildDispatcher(instance, handlers) {
|
|
1255
884
|
const namedHandlers = /* @__PURE__ */ new Map();
|
|
1256
885
|
let catchAll;
|
|
@@ -1275,13 +904,6 @@ var ProcessorDiscoveryService = class {
|
|
|
1275
904
|
);
|
|
1276
905
|
};
|
|
1277
906
|
}
|
|
1278
|
-
/**
|
|
1279
|
-
* Wire a single discovered processor class: validate uniqueness, register a
|
|
1280
|
-
* Worker, bind worker-local and queue-global event listeners.
|
|
1281
|
-
*
|
|
1282
|
-
* @param instance - The resolved provider instance.
|
|
1283
|
-
* @param processorMeta - Metadata written by `@Processor`.
|
|
1284
|
-
*/
|
|
1285
907
|
wireProcessor(instance, processorMeta) {
|
|
1286
908
|
const { queueName, workerOptions, _warnedNoConcurrency } = processorMeta;
|
|
1287
909
|
const ctor = instance.constructor;
|
|
@@ -1339,20 +961,6 @@ var ProcessorDiscoveryService = class {
|
|
|
1339
961
|
}
|
|
1340
962
|
}
|
|
1341
963
|
}
|
|
1342
|
-
/**
|
|
1343
|
-
* Bind an event listener so a failing handler is reported instead of escaping.
|
|
1344
|
-
*
|
|
1345
|
-
* An `EventEmitter` neither awaits a listener nor catches its throw: a rejected promise becomes
|
|
1346
|
-
* an `unhandledRejection`, which ends the process by default, and BullMQ re-emits a synchronous
|
|
1347
|
-
* throw as `error`. A failing handler is the consumer's defect, never a reason to
|
|
1348
|
-
* stop the application, but it must still be visible, so the failure is logged with the handler
|
|
1349
|
-
* and a stable code and the process carries on.
|
|
1350
|
-
*
|
|
1351
|
-
* @param instance - The provider the handler belongs to (its `this`).
|
|
1352
|
-
* @param method - The handler method.
|
|
1353
|
-
* @param origin - Which decorator, event and method name the listener comes from.
|
|
1354
|
-
* @returns The listener to register on the emitter.
|
|
1355
|
-
*/
|
|
1356
964
|
guardListener(instance, method, origin) {
|
|
1357
965
|
const report = (cause) => {
|
|
1358
966
|
this.logger.error(
|
|
@@ -1392,29 +1000,23 @@ var QueueLifecycle = class {
|
|
|
1392
1000
|
connection;
|
|
1393
1001
|
resolved;
|
|
1394
1002
|
logger = new common.Logger(QueueLifecycle.name);
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
* context closes (e.g. on `SIGTERM` with shutdown hooks enabled).
|
|
1398
|
-
*/
|
|
1003
|
+
startedAt;
|
|
1004
|
+
forcedWorkers = 0;
|
|
1399
1005
|
async onModuleDestroy() {
|
|
1400
|
-
|
|
1401
|
-
|
|
1006
|
+
this.startedAt = Date.now();
|
|
1007
|
+
this.forcedWorkers = await this.closeWorkers();
|
|
1402
1008
|
await this.closeQueueEvents();
|
|
1403
1009
|
await this.closeDuplicatedConnections();
|
|
1010
|
+
}
|
|
1011
|
+
async onApplicationShutdown() {
|
|
1404
1012
|
await this.drainQueuesIfRequested();
|
|
1405
1013
|
await this.closeFlowProducer();
|
|
1406
1014
|
await this.closeQueues();
|
|
1407
1015
|
await this.teardownConnection();
|
|
1408
1016
|
this.logger.log(
|
|
1409
|
-
`Queue shutdown complete in ${String(Date.now() -
|
|
1017
|
+
`Queue shutdown complete in ${String(Date.now() - (this.startedAt ?? Date.now()))}ms (forced ${String(this.forcedWorkers)} worker(s))`
|
|
1410
1018
|
);
|
|
1411
1019
|
}
|
|
1412
|
-
/**
|
|
1413
|
-
* Close every worker with a bounded drain, force-closing any that exceed the
|
|
1414
|
-
* budget. Returns the number of workers that had to be force-closed.
|
|
1415
|
-
*
|
|
1416
|
-
* @returns The count of force-closed workers.
|
|
1417
|
-
*/
|
|
1418
1020
|
async closeWorkers() {
|
|
1419
1021
|
let forced = 0;
|
|
1420
1022
|
const drainTimeoutMs = this.resolved.shutdown.drainTimeoutMs;
|
|
@@ -1431,14 +1033,6 @@ var QueueLifecycle = class {
|
|
|
1431
1033
|
}
|
|
1432
1034
|
return forced;
|
|
1433
1035
|
}
|
|
1434
|
-
/**
|
|
1435
|
-
* Race a graceful `worker.close()` against a timer. `worker.close()` already
|
|
1436
|
-
* drains active jobs and takes no timeout argument, so the timer is the only
|
|
1437
|
-
* way to bound the wait. Rejects with a timeout error when the budget elapses.
|
|
1438
|
-
*
|
|
1439
|
-
* @param worker - The worker to close.
|
|
1440
|
-
* @param timeoutMs - The drain budget in milliseconds.
|
|
1441
|
-
*/
|
|
1442
1036
|
async closeWorkerWithTimeout(worker, timeoutMs) {
|
|
1443
1037
|
let timer;
|
|
1444
1038
|
const closed = worker.close();
|
|
@@ -1456,18 +1050,11 @@ var QueueLifecycle = class {
|
|
|
1456
1050
|
clearTimeout(timer);
|
|
1457
1051
|
}
|
|
1458
1052
|
}
|
|
1459
|
-
/** Close every cached `QueueEvents`, swallowing individual failures. */
|
|
1460
1053
|
async closeQueueEvents() {
|
|
1461
1054
|
for (const [, queueEvents] of this.events.getAll()) {
|
|
1462
1055
|
await queueEvents.close().catch(() => void 0);
|
|
1463
1056
|
}
|
|
1464
1057
|
}
|
|
1465
|
-
/**
|
|
1466
|
-
* Close the duplicated worker/queue-events connections the library opened.
|
|
1467
|
-
* BullMQ treats a passed-in client as shared and never closes it, so the
|
|
1468
|
-
* library must release these explicitly. In Mode A the consumer's shared
|
|
1469
|
-
* client is never among them.
|
|
1470
|
-
*/
|
|
1471
1058
|
async closeDuplicatedConnections() {
|
|
1472
1059
|
const connections = [
|
|
1473
1060
|
...this.workers.getConnections().values(),
|
|
@@ -1481,10 +1068,6 @@ var QueueLifecycle = class {
|
|
|
1481
1068
|
}
|
|
1482
1069
|
}
|
|
1483
1070
|
}
|
|
1484
|
-
/**
|
|
1485
|
-
* Drain every cached queue when `drainOnShutdown` is enabled (dev/test only).
|
|
1486
|
-
* Removes waiting jobs and leaves delayed and active ones; off by default to stay production-safe.
|
|
1487
|
-
*/
|
|
1488
1071
|
async drainQueuesIfRequested() {
|
|
1489
1072
|
if (!this.resolved.shutdown.drainOnShutdown) return;
|
|
1490
1073
|
for (const [name, queue] of this.queues.getCachedQueues()) {
|
|
@@ -1495,18 +1078,12 @@ var QueueLifecycle = class {
|
|
|
1495
1078
|
});
|
|
1496
1079
|
}
|
|
1497
1080
|
}
|
|
1498
|
-
/** Close the flow producer (idempotent), swallowing any failure. */
|
|
1499
1081
|
async closeFlowProducer() {
|
|
1500
1082
|
await this.flow.close().catch(() => void 0);
|
|
1501
1083
|
}
|
|
1502
|
-
/** Close every cached queue and clear the cache, swallowing individual failures. */
|
|
1503
1084
|
async closeQueues() {
|
|
1504
1085
|
await this.queues.closeAll().catch(() => void 0);
|
|
1505
1086
|
}
|
|
1506
|
-
/**
|
|
1507
|
-
* Tear down the Queue-role connection. Mode B quits the library-owned client;
|
|
1508
|
-
* Mode A is a no-op for the consumer's shared client.
|
|
1509
|
-
*/
|
|
1510
1087
|
async teardownConnection() {
|
|
1511
1088
|
await this.connection.teardown().catch(() => void 0);
|
|
1512
1089
|
}
|
|
@@ -1523,24 +1100,12 @@ QueueLifecycle = __decorateClass([
|
|
|
1523
1100
|
|
|
1524
1101
|
// src/server/bymax-queue.module.ts
|
|
1525
1102
|
var { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = (
|
|
1526
|
-
// The `moduleName` shapes the generated option token's identifier — `BYMAX_QUEUE_MODULE_OPTIONS`,
|
|
1527
|
-
// which the module spec asserts by value; emptying the name would degrade it to an opaque
|
|
1528
|
-
// `CONFIGURABLE_MODULE_OPTIONS[<hash>]`, so the argument is a caught regression, not a free label.
|
|
1529
1103
|
new common.ConfigurableModuleBuilder({ moduleName: "BymaxQueue" }).setClassMethodName("forRoot").setFactoryMethodName("createQueueOptions").setExtras({ isGlobal: true }, (definition, extras) => ({
|
|
1530
1104
|
...definition,
|
|
1531
1105
|
global: extras.isGlobal
|
|
1532
1106
|
})).build()
|
|
1533
1107
|
);
|
|
1534
1108
|
exports.BymaxQueueModule = class BymaxQueueModule extends ConfigurableModuleClass {
|
|
1535
|
-
/**
|
|
1536
|
-
* Synchronous registration. Validates options, resolves defaults, and wires the
|
|
1537
|
-
* connection resolver (initialized via async factory), the queue service, the
|
|
1538
|
-
* worker registry, the queue-events registry, processor discovery, and the
|
|
1539
|
-
* graceful-shutdown lifecycle.
|
|
1540
|
-
*
|
|
1541
|
-
* @param options - Static module options.
|
|
1542
|
-
* @returns The configured dynamic module.
|
|
1543
|
-
*/
|
|
1544
1109
|
static forRoot(options) {
|
|
1545
1110
|
validateOptions(options);
|
|
1546
1111
|
const resolved = applyDefaults(options);
|
|
@@ -1566,17 +1131,6 @@ exports.BymaxQueueModule = class BymaxQueueModule extends ConfigurableModuleClas
|
|
|
1566
1131
|
exports: exports.BymaxQueueModule.buildExports()
|
|
1567
1132
|
};
|
|
1568
1133
|
}
|
|
1569
|
-
/**
|
|
1570
|
-
* Asynchronous registration. Use when options depend on other modules
|
|
1571
|
-
* (ConfigService, BymaxCacheModule, etc.). Mirrors the NestJS standard async
|
|
1572
|
-
* dynamic-module pattern (`useFactory` | `useClass` | `useExisting` + `inject`)
|
|
1573
|
-
* and registers the same provider graph and exports as {@link forRoot}, with
|
|
1574
|
-
* `global` applied from the `isGlobal` extra. The resolved options and the
|
|
1575
|
-
* initialized connection are derived from the async-resolved options at runtime.
|
|
1576
|
-
*
|
|
1577
|
-
* @param options - Asynchronous module options.
|
|
1578
|
-
* @returns The configured dynamic module.
|
|
1579
|
-
*/
|
|
1580
1134
|
static forRootAsync(options) {
|
|
1581
1135
|
const base = super.forRootAsync(options);
|
|
1582
1136
|
return {
|
|
@@ -1607,14 +1161,6 @@ exports.BymaxQueueModule = class BymaxQueueModule extends ConfigurableModuleClas
|
|
|
1607
1161
|
exports: exports.BymaxQueueModule.buildExports()
|
|
1608
1162
|
};
|
|
1609
1163
|
}
|
|
1610
|
-
/**
|
|
1611
|
-
* Providers shared verbatim by both registration paths: the options alias, the
|
|
1612
|
-
* initialized connection resolver, the resolved client/mode tokens, and the
|
|
1613
|
-
* singleton services. `BYMAX_QUEUE_RESOLVED_OPTIONS`, `FlowService`, and
|
|
1614
|
-
* `MetricsService` are intentionally omitted — they differ per path.
|
|
1615
|
-
*
|
|
1616
|
-
* @returns The shared provider list.
|
|
1617
|
-
*/
|
|
1618
1164
|
static buildCoreProviders() {
|
|
1619
1165
|
return [
|
|
1620
1166
|
{ provide: BYMAX_QUEUE_OPTIONS, useExisting: MODULE_OPTIONS_TOKEN },
|
|
@@ -1644,11 +1190,6 @@ exports.BymaxQueueModule = class BymaxQueueModule extends ConfigurableModuleClas
|
|
|
1644
1190
|
QueueLifecycle
|
|
1645
1191
|
];
|
|
1646
1192
|
}
|
|
1647
|
-
/**
|
|
1648
|
-
* The public exports shared by both registration paths.
|
|
1649
|
-
*
|
|
1650
|
-
* @returns A fresh exports list (services and injection tokens).
|
|
1651
|
-
*/
|
|
1652
1193
|
static buildExports() {
|
|
1653
1194
|
return [
|
|
1654
1195
|
exports.QueueService,
|