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