@warlock.js/queue 5.33.0 → 5.34.1
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 +9 -54
- package/README.md +18 -3
- package/cjs/index.cjs +644 -168
- package/cjs/index.cjs.map +1 -1
- package/esm/dashboard-boot.mjs +1 -0
- package/esm/dashboard-boot.mjs.map +1 -1
- package/esm/dashboard.d.mts.map +1 -1
- package/esm/dashboard.mjs +2 -1
- package/esm/dashboard.mjs.map +1 -1
- package/esm/define-job.d.mts +0 -1
- package/esm/define-job.d.mts.map +1 -1
- package/esm/define-job.mjs +11 -64
- package/esm/define-job.mjs.map +1 -1
- package/esm/driver.d.mts +23 -0
- package/esm/driver.d.mts.map +1 -0
- package/esm/drivers/bullmq.mjs +132 -0
- package/esm/drivers/bullmq.mjs.map +1 -0
- package/esm/drivers/local-store.mjs +9 -0
- package/esm/drivers/local-store.mjs.map +1 -0
- package/esm/drivers/local.mjs +307 -0
- package/esm/drivers/local.mjs.map +1 -0
- package/esm/drivers/memory-store.mjs +48 -0
- package/esm/drivers/memory-store.mjs.map +1 -0
- package/esm/drivers/sqlite-store.mjs +89 -0
- package/esm/drivers/sqlite-store.mjs.map +1 -0
- package/esm/failed-jobs.d.mts +2 -8
- package/esm/failed-jobs.d.mts.map +1 -1
- package/esm/failed-jobs.mjs +5 -30
- package/esm/failed-jobs.mjs.map +1 -1
- package/esm/index.d.mts +4 -2
- package/esm/index.mjs +2 -1
- package/esm/job-options.d.mts +9 -0
- package/esm/job-options.d.mts.map +1 -0
- package/esm/job-options.mjs +20 -0
- package/esm/job-options.mjs.map +1 -0
- package/esm/queue-driver.d.mts +6 -0
- package/esm/queue-driver.d.mts.map +1 -0
- package/esm/queue-driver.mjs +37 -0
- package/esm/queue-driver.mjs.map +1 -0
- package/esm/queue-manager.d.mts +5 -26
- package/esm/queue-manager.d.mts.map +1 -1
- package/esm/queue-manager.mjs +28 -84
- package/esm/queue-manager.mjs.map +1 -1
- package/esm/types.d.mts +18 -5
- package/esm/types.d.mts.map +1 -1
- package/llms-full.txt +129 -3
- package/llms.txt +2 -1
- package/package.json +4 -4
- package/skills/configure-queue/SKILL.md +1 -1
- package/skills/index.md +4 -2
- package/skills/local-queues/SKILL.md +121 -0
- package/skills/overview/SKILL.md +3 -2
package/cjs/index.cjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
let _warlock_js_logger = require("@warlock.js/logger");
|
|
3
3
|
let bullmq = require("bullmq");
|
|
4
|
+
let node_crypto = require("node:crypto");
|
|
5
|
+
let node_fs = require("node:fs");
|
|
6
|
+
let node_module = require("node:module");
|
|
7
|
+
let node_path = require("node:path");
|
|
4
8
|
|
|
5
9
|
//#region ../../packages/queue/src/errors.ts
|
|
6
10
|
/**
|
|
@@ -278,111 +282,640 @@ async function processJob(job) {
|
|
|
278
282
|
}
|
|
279
283
|
|
|
280
284
|
//#endregion
|
|
281
|
-
//#region ../../packages/queue/src/
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
285
|
+
//#region ../../packages/queue/src/drivers/bullmq.ts
|
|
286
|
+
var BullMQDriver = class {
|
|
287
|
+
constructor(config) {
|
|
288
|
+
this.config = config;
|
|
289
|
+
this.queues = /* @__PURE__ */ new Map();
|
|
290
|
+
this.workers = /* @__PURE__ */ new Map();
|
|
291
|
+
if (!config.connection) throw new Error("The bullmq queue driver requires `connection`.");
|
|
292
|
+
}
|
|
293
|
+
getQueue(name) {
|
|
294
|
+
let queue = this.queues.get(name);
|
|
295
|
+
if (!queue) {
|
|
296
|
+
const connection = this.config.connection;
|
|
297
|
+
if (!connection) throw new Error("The bullmq queue driver requires `connection`.");
|
|
298
|
+
queue = new bullmq.Queue(name, {
|
|
299
|
+
connection,
|
|
300
|
+
prefix: this.config.prefix ?? "warlock"
|
|
301
|
+
});
|
|
302
|
+
queue.on("error", (error) => _warlock_js_logger.log.error("queue", "connection", error));
|
|
303
|
+
this.queues.set(name, queue);
|
|
304
|
+
}
|
|
305
|
+
return queue;
|
|
306
|
+
}
|
|
307
|
+
async dispatch(request) {
|
|
308
|
+
const { backoff, ...rest } = request.options;
|
|
309
|
+
const options = {
|
|
310
|
+
...rest,
|
|
311
|
+
backoff: typeof backoff === "number" ? {
|
|
312
|
+
type: "fixed",
|
|
313
|
+
delay: backoff
|
|
314
|
+
} : backoff
|
|
315
|
+
};
|
|
316
|
+
const job = await this.getQueue(request.queue).add(request.name, request.data, options);
|
|
317
|
+
return {
|
|
318
|
+
id: String(job.id),
|
|
319
|
+
name: request.name,
|
|
320
|
+
queue: request.queue
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
async find(queue, id) {
|
|
324
|
+
const source = this.getQueue(queue);
|
|
325
|
+
const initial = await source.getJob(id);
|
|
326
|
+
if (!initial) return void 0;
|
|
327
|
+
const state = await initial.getState();
|
|
328
|
+
return toSnapshot(await source.getJob(id) ?? initial, state);
|
|
329
|
+
}
|
|
330
|
+
async failed(queue, start, end) {
|
|
331
|
+
return (await this.getQueue(queue).getFailed(start, end)).map((job) => ({
|
|
332
|
+
id: String(job.id),
|
|
333
|
+
name: job.name,
|
|
334
|
+
queue,
|
|
335
|
+
payload: unwrapPayload(job.data).payload,
|
|
336
|
+
attemptsMade: job.attemptsMade,
|
|
337
|
+
failedReason: job.failedReason,
|
|
338
|
+
stacktrace: job.stacktrace ?? [],
|
|
339
|
+
failedAt: job.finishedOn ? new Date(job.finishedOn) : void 0,
|
|
340
|
+
retry: () => job.retry("failed")
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
async retry(queue, id) {
|
|
344
|
+
const job = await this.getQueue(queue).getJob(id);
|
|
345
|
+
if (!job || !await job.isFailed()) throw new FailedJobNotFoundError(id, queue);
|
|
346
|
+
await job.retry("failed");
|
|
347
|
+
}
|
|
348
|
+
async start(queue) {
|
|
349
|
+
let worker = this.workers.get(queue);
|
|
350
|
+
if (!worker) {
|
|
351
|
+
const connection = this.config.connection;
|
|
352
|
+
if (!connection) throw new Error("The bullmq queue driver requires `connection`.");
|
|
353
|
+
worker = new bullmq.Worker(queue, processJob, {
|
|
354
|
+
connection,
|
|
355
|
+
prefix: this.config.prefix ?? "warlock",
|
|
356
|
+
concurrency: this.config.workers?.concurrency ?? 1
|
|
357
|
+
});
|
|
358
|
+
worker.on("error", (error) => _warlock_js_logger.log.error("queue", "worker", error));
|
|
359
|
+
worker.on("failed", (job, error) => _warlock_js_logger.log.error("queue", "job.failed", `${job?.name ?? "unknown"} (${job?.id ?? "?"}): ${error.message}`));
|
|
360
|
+
this.workers.set(queue, worker);
|
|
361
|
+
}
|
|
362
|
+
await worker.waitUntilReady();
|
|
363
|
+
}
|
|
364
|
+
running() {
|
|
365
|
+
return [...this.workers.keys()];
|
|
366
|
+
}
|
|
367
|
+
async close(timeout) {
|
|
368
|
+
const workers = [...this.workers.values()];
|
|
369
|
+
this.workers.clear();
|
|
370
|
+
await Promise.all(workers.map((worker) => closeWorker(worker, timeout)));
|
|
371
|
+
const queues = [...this.queues.values()];
|
|
372
|
+
this.queues.clear();
|
|
373
|
+
await Promise.all(queues.map((queue) => queue.close()));
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
function toSnapshot(job, state) {
|
|
377
|
+
return {
|
|
378
|
+
id: String(job.id),
|
|
379
|
+
name: job.name,
|
|
380
|
+
queue: job.queueName,
|
|
381
|
+
state,
|
|
382
|
+
payload: unwrapPayload(job.data).payload,
|
|
383
|
+
progress: job.progress,
|
|
384
|
+
attemptsMade: job.attemptsMade,
|
|
385
|
+
result: job.returnvalue,
|
|
386
|
+
failedReason: job.failedReason || void 0,
|
|
387
|
+
createdAt: new Date(job.timestamp),
|
|
388
|
+
finishedAt: job.finishedOn ? new Date(job.finishedOn) : void 0
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
async function closeWorker(worker, timeout) {
|
|
392
|
+
let timer;
|
|
393
|
+
const timedOut = new Promise((resolve) => {
|
|
394
|
+
timer = setTimeout(() => resolve("timeout"), timeout);
|
|
395
|
+
});
|
|
396
|
+
const closing = worker.close().then(() => "closed");
|
|
397
|
+
closing.catch(() => void 0);
|
|
398
|
+
try {
|
|
399
|
+
if (await Promise.race([closing, timedOut]) === "timeout") {
|
|
400
|
+
_warlock_js_logger.log.warn("queue", "shutdown", `Worker for "${worker.name}" still had active jobs after ${timeout}ms; disconnecting. Those jobs are retried once their lock expires.`);
|
|
401
|
+
await worker.disconnect();
|
|
402
|
+
}
|
|
403
|
+
} finally {
|
|
404
|
+
clearTimeout(timer);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
//#endregion
|
|
409
|
+
//#region ../../packages/queue/src/drivers/local-store.ts
|
|
410
|
+
/** Match JSON transport on every backend, without sharing mutable objects. */
|
|
411
|
+
function jsonCopy(value) {
|
|
412
|
+
return JSON.parse(JSON.stringify({ value })).value;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
//#endregion
|
|
416
|
+
//#region ../../packages/queue/src/drivers/local.ts
|
|
417
|
+
/** Shared scheduling and lease semantics for the SQLite and memory stores. */
|
|
418
|
+
var LocalDriver = class {
|
|
419
|
+
constructor(store, config) {
|
|
420
|
+
this.store = store;
|
|
421
|
+
this.workers = /* @__PURE__ */ new Map();
|
|
422
|
+
this.active = /* @__PURE__ */ new Set();
|
|
423
|
+
this.closed = false;
|
|
424
|
+
this.stopping = false;
|
|
425
|
+
try {
|
|
426
|
+
this.leaseDuration = positiveInteger(config.local?.leaseDuration ?? 3e4, "local.leaseDuration", 30);
|
|
427
|
+
this.pollInterval = positiveInteger(config.local?.pollInterval ?? 100, "local.pollInterval");
|
|
428
|
+
this.concurrency = positiveInteger(config.workers?.concurrency ?? 1, "workers.concurrency");
|
|
429
|
+
} catch (error) {
|
|
430
|
+
store.close();
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async dispatch(request) {
|
|
435
|
+
this.assertOpen();
|
|
436
|
+
validateOptions(request);
|
|
437
|
+
const id = request.options.jobId ?? (0, node_crypto.randomUUID)();
|
|
438
|
+
const now = Date.now();
|
|
439
|
+
return {
|
|
440
|
+
id: this.store.transaction(() => {
|
|
441
|
+
const previous = this.store.get(request.queue, id);
|
|
442
|
+
if (previous) return previous;
|
|
443
|
+
const entry = {
|
|
444
|
+
id,
|
|
445
|
+
name: request.name,
|
|
446
|
+
queue: request.queue,
|
|
447
|
+
data: jsonCopy(request.data),
|
|
448
|
+
options: jsonCopy(request.options),
|
|
449
|
+
state: request.options.delay ? "delayed" : request.options.priority ? "prioritized" : "waiting",
|
|
450
|
+
createdAt: now,
|
|
451
|
+
readyAt: now + (request.options.delay ?? 0),
|
|
452
|
+
attemptsMade: 0,
|
|
453
|
+
priority: request.options.priority ?? 0,
|
|
454
|
+
progress: 0,
|
|
455
|
+
logs: [],
|
|
456
|
+
stacktrace: [],
|
|
457
|
+
leaseUntil: 0
|
|
458
|
+
};
|
|
459
|
+
this.store.put(entry);
|
|
460
|
+
return entry;
|
|
461
|
+
}).id,
|
|
462
|
+
name: request.name,
|
|
463
|
+
queue: request.queue
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
async find(queue, id) {
|
|
467
|
+
this.assertOpen();
|
|
468
|
+
const job = this.store.get(queue, id);
|
|
469
|
+
return job ? snapshot(job) : void 0;
|
|
470
|
+
}
|
|
471
|
+
async failed(queue, start, end) {
|
|
472
|
+
this.assertOpen();
|
|
473
|
+
const jobs = newestFirst(this.store.list(queue, "failed"));
|
|
474
|
+
const last = end < 0 ? jobs.length + end : end;
|
|
475
|
+
return jobs.slice(start, last + 1).map((job) => ({
|
|
476
|
+
id: job.id,
|
|
477
|
+
name: job.name,
|
|
478
|
+
queue,
|
|
479
|
+
payload: unwrapPayload(job.data).payload,
|
|
480
|
+
attemptsMade: job.attemptsMade,
|
|
481
|
+
failedReason: job.failedReason ?? "",
|
|
482
|
+
stacktrace: job.stacktrace,
|
|
483
|
+
failedAt: job.finishedAt === void 0 ? void 0 : new Date(job.finishedAt),
|
|
484
|
+
retry: () => this.retry(queue, job.id)
|
|
485
|
+
}));
|
|
486
|
+
}
|
|
487
|
+
async retry(queue, id) {
|
|
488
|
+
this.assertOpen();
|
|
489
|
+
this.store.transaction(() => {
|
|
490
|
+
const job = this.store.get(queue, id);
|
|
491
|
+
if (!job || job.state !== "failed") throw new FailedJobNotFoundError(id, queue);
|
|
492
|
+
job.state = job.priority ? "prioritized" : "waiting";
|
|
493
|
+
job.attemptsMade = 0;
|
|
494
|
+
job.readyAt = Date.now();
|
|
495
|
+
delete job.finishedAt;
|
|
496
|
+
delete job.failedReason;
|
|
497
|
+
delete job.result;
|
|
498
|
+
job.stacktrace = [];
|
|
499
|
+
this.store.put(job);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
async start(queue) {
|
|
503
|
+
this.assertOpen();
|
|
504
|
+
if (this.stopping) throw new Error("Queue workers are shutting down.");
|
|
505
|
+
if (this.workers.has(queue)) return;
|
|
506
|
+
const worker = { active: /* @__PURE__ */ new Set() };
|
|
507
|
+
this.workers.set(queue, worker);
|
|
508
|
+
this.schedule(queue, worker, 0);
|
|
509
|
+
}
|
|
510
|
+
running() {
|
|
511
|
+
return [...this.workers.keys()];
|
|
512
|
+
}
|
|
513
|
+
async close(timeout) {
|
|
514
|
+
if (this.closed) return;
|
|
515
|
+
this.stopping = true;
|
|
516
|
+
for (const worker of this.workers.values()) clearTimeout(worker.timer);
|
|
517
|
+
this.workers.clear();
|
|
518
|
+
let timer;
|
|
519
|
+
const timedOut = new Promise((resolve) => {
|
|
520
|
+
timer = setTimeout(resolve, timeout);
|
|
521
|
+
});
|
|
522
|
+
try {
|
|
523
|
+
await Promise.race([Promise.allSettled([...this.active].map((entry) => entry.done)), timedOut]);
|
|
524
|
+
} finally {
|
|
525
|
+
clearTimeout(timer);
|
|
526
|
+
try {
|
|
527
|
+
for (const entry of this.active) {
|
|
528
|
+
clearInterval(entry.heartbeat);
|
|
529
|
+
entry.controller.abort(/* @__PURE__ */ new Error("Queue shutdown timeout elapsed."));
|
|
530
|
+
this.mutate(entry.job, entry.token, (job) => {
|
|
531
|
+
job.state = job.priority ? "prioritized" : "waiting";
|
|
532
|
+
job.readyAt = Date.now();
|
|
533
|
+
job.leaseUntil = 0;
|
|
534
|
+
delete job.token;
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
} finally {
|
|
538
|
+
this.closed = true;
|
|
539
|
+
for (const entry of this.active) clearInterval(entry.heartbeat);
|
|
540
|
+
this.store.close();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
schedule(queue, worker, delay) {
|
|
545
|
+
if (this.stopping) return;
|
|
546
|
+
worker.timer = setTimeout(() => {
|
|
547
|
+
try {
|
|
548
|
+
while (!this.stopping && worker.active.size < this.concurrency) {
|
|
549
|
+
const token = (0, node_crypto.randomUUID)();
|
|
550
|
+
const job = this.store.transaction(() => {
|
|
551
|
+
const now = Date.now();
|
|
552
|
+
const candidate = this.store.ready(queue, now);
|
|
553
|
+
if (!candidate) return void 0;
|
|
554
|
+
candidate.state = "active";
|
|
555
|
+
candidate.token = token;
|
|
556
|
+
candidate.leaseUntil = now + this.leaseDuration;
|
|
557
|
+
this.store.put(candidate);
|
|
558
|
+
return candidate;
|
|
559
|
+
});
|
|
560
|
+
if (!job) break;
|
|
561
|
+
this.run(job, token, worker);
|
|
562
|
+
}
|
|
563
|
+
} catch (error) {
|
|
564
|
+
_warlock_js_logger.log.error("queue", "worker", error);
|
|
565
|
+
}
|
|
566
|
+
this.schedule(queue, worker, this.pollInterval);
|
|
567
|
+
}, delay);
|
|
568
|
+
}
|
|
569
|
+
run(job, token, worker) {
|
|
570
|
+
const controller = new AbortController();
|
|
571
|
+
const heartbeat = setInterval(() => {
|
|
572
|
+
try {
|
|
573
|
+
if (!this.mutate(job, token, (current) => {
|
|
574
|
+
current.leaseUntil = Date.now() + this.leaseDuration;
|
|
575
|
+
})) {
|
|
576
|
+
controller.abort(/* @__PURE__ */ new Error("Queue job lease expired or changed owner."));
|
|
577
|
+
clearInterval(heartbeat);
|
|
578
|
+
}
|
|
579
|
+
} catch (error) {
|
|
580
|
+
controller.abort(error);
|
|
581
|
+
clearInterval(heartbeat);
|
|
582
|
+
_warlock_js_logger.log.error("queue", "lease", error);
|
|
583
|
+
}
|
|
584
|
+
}, Math.floor(this.leaseDuration / 3));
|
|
585
|
+
const entry = {
|
|
586
|
+
job,
|
|
587
|
+
token,
|
|
588
|
+
controller,
|
|
589
|
+
heartbeat,
|
|
590
|
+
done: Promise.resolve()
|
|
591
|
+
};
|
|
592
|
+
this.active.add(entry);
|
|
593
|
+
worker.active.add(entry);
|
|
594
|
+
entry.done = this.execute(entry).catch((error) => {
|
|
595
|
+
_warlock_js_logger.log.error("queue", "job", error);
|
|
596
|
+
}).finally(() => {
|
|
597
|
+
clearInterval(heartbeat);
|
|
598
|
+
this.active.delete(entry);
|
|
599
|
+
worker.active.delete(entry);
|
|
297
600
|
});
|
|
298
|
-
|
|
299
|
-
|
|
601
|
+
}
|
|
602
|
+
async execute(entry) {
|
|
603
|
+
const { job, token, controller } = entry;
|
|
604
|
+
try {
|
|
605
|
+
const definition = findRegisteredJob(job.name);
|
|
606
|
+
if (!definition) throw new UnrecoverableJobError(`No job named "${job.name}" is defined in this process. Import its definition in the worker.`);
|
|
607
|
+
const { payload, context: captured } = unwrapPayload(job.data);
|
|
608
|
+
const context = {
|
|
609
|
+
id: job.id,
|
|
610
|
+
name: job.name,
|
|
611
|
+
queue: job.queue,
|
|
612
|
+
attempt: job.attemptsMade + 1,
|
|
613
|
+
maxAttempts: job.options.attempts ?? 1,
|
|
614
|
+
context: captured,
|
|
615
|
+
signal: controller.signal,
|
|
616
|
+
progress: async (value) => {
|
|
617
|
+
if (!this.mutate(job, token, (current) => {
|
|
618
|
+
current.progress = jsonCopy(value);
|
|
619
|
+
})) throw new Error("Queue job lease lost.");
|
|
620
|
+
},
|
|
621
|
+
log: async (line) => {
|
|
622
|
+
if (!this.mutate(job, token, (current) => {
|
|
623
|
+
current.logs.push(line);
|
|
624
|
+
})) throw new Error("Queue job lease lost.");
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
const run = async () => definition.handle(payload, context);
|
|
628
|
+
const queueContext = getQueueContext();
|
|
629
|
+
const result = jsonCopy(captured !== void 0 && queueContext ? await queueContext.restore(captured, run) : await run());
|
|
630
|
+
this.mutate(job, token, (current) => {
|
|
631
|
+
current.attemptsMade++;
|
|
632
|
+
current.result = result;
|
|
633
|
+
current.state = "completed";
|
|
634
|
+
current.finishedAt = Date.now();
|
|
635
|
+
current.leaseUntil = 0;
|
|
636
|
+
delete current.token;
|
|
637
|
+
}, true);
|
|
638
|
+
} catch (error) {
|
|
639
|
+
this.mutate(job, token, (current) => {
|
|
640
|
+
current.attemptsMade++;
|
|
641
|
+
current.failedReason = error instanceof Error ? error.message : String(error);
|
|
642
|
+
current.stacktrace.push(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
643
|
+
current.leaseUntil = 0;
|
|
644
|
+
delete current.token;
|
|
645
|
+
if (error instanceof UnrecoverableJobError || current.attemptsMade >= (current.options.attempts ?? 1)) {
|
|
646
|
+
current.state = "failed";
|
|
647
|
+
current.finishedAt = Date.now();
|
|
648
|
+
} else {
|
|
649
|
+
const backoff = current.options.backoff;
|
|
650
|
+
const base = typeof backoff === "number" ? backoff : backoff?.delay ?? 0;
|
|
651
|
+
const delay = typeof backoff === "object" && backoff.type === "exponential" ? base * 2 ** (current.attemptsMade - 1) : base;
|
|
652
|
+
current.readyAt = Math.min(Number.MAX_SAFE_INTEGER, Date.now() + delay);
|
|
653
|
+
current.state = delay > 0 ? "delayed" : current.priority ? "prioritized" : "waiting";
|
|
654
|
+
}
|
|
655
|
+
}, true);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/** Fencing: expired owners cannot renew, finish, or overwrite a new owner's job. */
|
|
659
|
+
mutate(job, token, update, retain = false) {
|
|
660
|
+
if (this.closed) return false;
|
|
661
|
+
return this.store.transaction(() => {
|
|
662
|
+
const current = this.store.get(job.queue, job.id);
|
|
663
|
+
if (!current || !ownsLease(current, token)) return false;
|
|
664
|
+
update(current);
|
|
665
|
+
this.store.put(current);
|
|
666
|
+
if (retain && (current.state === "completed" || current.state === "failed")) {
|
|
667
|
+
const retention = current.state === "completed" ? current.options.removeOnComplete : current.options.removeOnFail;
|
|
668
|
+
if (retention === true) this.store.remove(current.queue, current.id);
|
|
669
|
+
if (typeof retention === "number") for (const old of newestFirst(this.store.list(current.queue, current.state)).slice(retention)) this.store.remove(old.queue, old.id);
|
|
670
|
+
}
|
|
671
|
+
return true;
|
|
300
672
|
});
|
|
301
|
-
queues.set(name, queue);
|
|
302
673
|
}
|
|
303
|
-
|
|
674
|
+
assertOpen() {
|
|
675
|
+
if (this.closed) throw new Error("Queue driver is closed.");
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
function snapshot(job) {
|
|
679
|
+
return {
|
|
680
|
+
id: job.id,
|
|
681
|
+
name: job.name,
|
|
682
|
+
queue: job.queue,
|
|
683
|
+
state: job.state,
|
|
684
|
+
payload: unwrapPayload(job.data).payload,
|
|
685
|
+
progress: job.progress,
|
|
686
|
+
attemptsMade: job.attemptsMade,
|
|
687
|
+
result: job.result,
|
|
688
|
+
failedReason: job.failedReason,
|
|
689
|
+
createdAt: new Date(job.createdAt),
|
|
690
|
+
finishedAt: job.finishedAt === void 0 ? void 0 : new Date(job.finishedAt)
|
|
691
|
+
};
|
|
304
692
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
693
|
+
function newestFirst(jobs) {
|
|
694
|
+
return jobs.reverse().sort((a, b) => (b.finishedAt ?? 0) - (a.finishedAt ?? 0));
|
|
695
|
+
}
|
|
696
|
+
function positiveInteger(value, name, minimum = 1) {
|
|
697
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > 2147483647) throw new Error(`${name} must be an integer between ${minimum} and 2147483647.`);
|
|
698
|
+
return value;
|
|
699
|
+
}
|
|
700
|
+
function validateOptions({ options }) {
|
|
701
|
+
positiveInteger(options.attempts ?? 1, "attempts");
|
|
702
|
+
if (options.priority !== void 0 && (!Number.isInteger(options.priority) || options.priority < 0 || options.priority > 2097152)) throw new Error("priority must be an integer between 0 and 2097152.");
|
|
703
|
+
const backoff = options.backoff;
|
|
704
|
+
const delay = typeof backoff === "number" ? backoff : backoff?.delay ?? 0;
|
|
705
|
+
if (!Number.isFinite(delay) || delay < 0 || typeof backoff === "object" && !["fixed", "exponential"].includes(backoff.type)) throw new Error("backoff must have a non-negative finite delay and fixed or exponential type.");
|
|
706
|
+
if (options.delay !== void 0 && (!Number.isFinite(options.delay) || options.delay < 0)) throw new Error("delay must be non-negative and finite.");
|
|
707
|
+
for (const retention of [options.removeOnComplete, options.removeOnFail]) if (typeof retention === "number" && (!Number.isInteger(retention) || retention < 0)) throw new Error("Job retention must be a non-negative integer or boolean.");
|
|
708
|
+
}
|
|
709
|
+
function ownsLease(job, token) {
|
|
710
|
+
return job.state === "active" && job.token === token && job.leaseUntil > Date.now();
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
//#endregion
|
|
714
|
+
//#region ../../packages/queue/src/drivers/memory-store.ts
|
|
715
|
+
var MemoryStore = class {
|
|
716
|
+
constructor() {
|
|
717
|
+
this.jobs = /* @__PURE__ */ new Map();
|
|
718
|
+
}
|
|
719
|
+
transaction(run) {
|
|
720
|
+
const before = new Map(this.jobs);
|
|
721
|
+
try {
|
|
722
|
+
return run();
|
|
723
|
+
} catch (error) {
|
|
724
|
+
this.jobs = before;
|
|
725
|
+
throw error;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
key(queue, id) {
|
|
729
|
+
return JSON.stringify([queue, id]);
|
|
730
|
+
}
|
|
731
|
+
get(queue, id) {
|
|
732
|
+
return jsonCopy(this.jobs.get(this.key(queue, id)));
|
|
733
|
+
}
|
|
734
|
+
list(queue, state) {
|
|
735
|
+
return [...this.jobs.values()].filter((job) => job.queue === queue && (!state || job.state === state)).map(jsonCopy);
|
|
736
|
+
}
|
|
737
|
+
ready(queue, now) {
|
|
738
|
+
let candidate;
|
|
739
|
+
for (const job of this.list(queue)) if ((job.state === "active" && job.leaseUntil <= now || [
|
|
740
|
+
"waiting",
|
|
741
|
+
"delayed",
|
|
742
|
+
"prioritized"
|
|
743
|
+
].includes(job.state) && job.readyAt <= now) && (!candidate || job.priority < candidate.priority)) candidate = job;
|
|
744
|
+
return candidate;
|
|
745
|
+
}
|
|
746
|
+
put(job) {
|
|
747
|
+
this.jobs.set(this.key(job.queue, job.id), jsonCopy(job));
|
|
748
|
+
}
|
|
749
|
+
remove(queue, id) {
|
|
750
|
+
this.jobs.delete(this.key(queue, id));
|
|
751
|
+
}
|
|
752
|
+
close() {
|
|
753
|
+
this.jobs.clear();
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
//#endregion
|
|
758
|
+
//#region ../../packages/queue/src/drivers/sqlite-store.ts
|
|
759
|
+
/** node:sqlite is loaded only when the local backend is selected. */
|
|
760
|
+
var SqliteStore = class {
|
|
761
|
+
constructor(config) {
|
|
762
|
+
const path = (0, node_path.resolve)(config.local?.path ?? ".warlock/queue.sqlite");
|
|
763
|
+
if (config.local?.path === ":memory:") throw new Error("Use driver: 'memory' for non-durable queues.");
|
|
764
|
+
let sqlite;
|
|
765
|
+
try {
|
|
766
|
+
sqlite = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("node:sqlite");
|
|
767
|
+
} catch (cause) {
|
|
768
|
+
throw new Error("The local queue driver requires Node >= 22.15 with node:sqlite available without flags.", { cause });
|
|
769
|
+
}
|
|
770
|
+
if (typeof sqlite.DatabaseSync !== "function") throw new Error("node:sqlite.DatabaseSync is unavailable.");
|
|
771
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
|
|
772
|
+
this.db = new sqlite.DatabaseSync(path);
|
|
773
|
+
this.namespace = config.prefix ?? "warlock";
|
|
774
|
+
try {
|
|
775
|
+
this.db.exec(`
|
|
776
|
+
PRAGMA busy_timeout = 5000;
|
|
777
|
+
PRAGMA journal_mode = WAL;
|
|
778
|
+
PRAGMA synchronous = FULL;
|
|
779
|
+
CREATE TABLE IF NOT EXISTS warlock_queue_jobs (
|
|
780
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
781
|
+
namespace TEXT NOT NULL, queue TEXT NOT NULL, id TEXT NOT NULL,
|
|
782
|
+
state TEXT NOT NULL, ready_at REAL NOT NULL, lease_until REAL NOT NULL,
|
|
783
|
+
priority INTEGER NOT NULL, body TEXT NOT NULL,
|
|
784
|
+
UNIQUE(namespace, queue, id)
|
|
785
|
+
);
|
|
786
|
+
CREATE INDEX IF NOT EXISTS warlock_queue_ready
|
|
787
|
+
ON warlock_queue_jobs(namespace, queue, state, ready_at, lease_until);
|
|
788
|
+
`);
|
|
789
|
+
} catch (error) {
|
|
790
|
+
this.db.close();
|
|
791
|
+
throw error;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
transaction(run) {
|
|
795
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
796
|
+
try {
|
|
797
|
+
const result = run();
|
|
798
|
+
this.db.exec("COMMIT");
|
|
799
|
+
return result;
|
|
800
|
+
} catch (error) {
|
|
801
|
+
this.db.exec("ROLLBACK");
|
|
802
|
+
throw error;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
get(queue, id) {
|
|
806
|
+
return decode(this.db.prepare("SELECT body FROM warlock_queue_jobs WHERE namespace = ? AND queue = ? AND id = ?").get(this.namespace, queue, id));
|
|
807
|
+
}
|
|
808
|
+
list(queue, state) {
|
|
809
|
+
return (state ? this.db.prepare("SELECT body FROM warlock_queue_jobs WHERE namespace = ? AND queue = ? AND state = ? ORDER BY seq").all(this.namespace, queue, state) : this.db.prepare("SELECT body FROM warlock_queue_jobs WHERE namespace = ? AND queue = ? ORDER BY seq").all(this.namespace, queue)).flatMap((row) => {
|
|
810
|
+
const job = decode(row);
|
|
811
|
+
return job ? [job] : [];
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
ready(queue, now) {
|
|
815
|
+
return decode(this.db.prepare(`SELECT body FROM warlock_queue_jobs
|
|
816
|
+
WHERE namespace = ? AND queue = ? AND (
|
|
817
|
+
(state IN ('waiting', 'delayed', 'prioritized') AND ready_at <= ?) OR
|
|
818
|
+
(state = 'active' AND lease_until <= ?)
|
|
819
|
+
) ORDER BY priority, seq LIMIT 1`).get(this.namespace, queue, now, now));
|
|
820
|
+
}
|
|
821
|
+
put(job) {
|
|
822
|
+
this.db.prepare(`INSERT INTO warlock_queue_jobs
|
|
823
|
+
(namespace, queue, id, state, ready_at, lease_until, priority, body)
|
|
824
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
825
|
+
ON CONFLICT(namespace, queue, id) DO UPDATE SET
|
|
826
|
+
state = excluded.state, ready_at = excluded.ready_at,
|
|
827
|
+
lease_until = excluded.lease_until, priority = excluded.priority, body = excluded.body`).run(this.namespace, job.queue, job.id, job.state, job.readyAt, job.leaseUntil, job.priority, JSON.stringify(job));
|
|
828
|
+
}
|
|
829
|
+
remove(queue, id) {
|
|
830
|
+
this.db.prepare("DELETE FROM warlock_queue_jobs WHERE namespace = ? AND queue = ? AND id = ?").run(this.namespace, queue, id);
|
|
831
|
+
}
|
|
832
|
+
close() {
|
|
833
|
+
this.db.close();
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
function decode(row) {
|
|
837
|
+
return typeof row?.body === "string" ? JSON.parse(row.body) : void 0;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
//#endregion
|
|
841
|
+
//#region ../../packages/queue/src/queue-driver.ts
|
|
842
|
+
let activeDriver;
|
|
843
|
+
/** The configured backend, created on first use. Close it before changing config. */
|
|
844
|
+
function getQueueDriver() {
|
|
845
|
+
if (!activeDriver) {
|
|
846
|
+
const config = getQueueConfig();
|
|
847
|
+
switch (config.driver ?? "bullmq") {
|
|
848
|
+
case "bullmq":
|
|
849
|
+
activeDriver = new BullMQDriver(config);
|
|
850
|
+
break;
|
|
851
|
+
case "local":
|
|
852
|
+
activeDriver = new LocalDriver(new SqliteStore(config), config);
|
|
853
|
+
break;
|
|
854
|
+
case "memory":
|
|
855
|
+
activeDriver = new LocalDriver(new MemoryStore(), config);
|
|
856
|
+
break;
|
|
857
|
+
default: throw new Error(`Unknown queue driver: ${String(config.driver)}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return activeDriver;
|
|
861
|
+
}
|
|
862
|
+
function currentQueueDriver() {
|
|
863
|
+
return activeDriver;
|
|
864
|
+
}
|
|
865
|
+
function forgetQueueDriver() {
|
|
866
|
+
activeDriver = void 0;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
//#endregion
|
|
870
|
+
//#region ../../packages/queue/src/queue-manager.ts
|
|
871
|
+
let stopListening;
|
|
872
|
+
let closing;
|
|
873
|
+
/** Low-level BullMQ access. For portable operations use defineJob / failedJobs. */
|
|
874
|
+
function getQueue(name) {
|
|
875
|
+
const driver = getQueueDriver();
|
|
876
|
+
if (!(driver instanceof BullMQDriver)) throw new Error("getQueue() requires the bullmq queue driver.");
|
|
877
|
+
return driver.getQueue(name);
|
|
878
|
+
}
|
|
879
|
+
/** Start workers for registered queues, honouring process roles and workers.enabled. */
|
|
317
880
|
async function startWorkers() {
|
|
881
|
+
if (closing) await closing;
|
|
318
882
|
const { Application } = await import("@warlock.js/core");
|
|
319
883
|
if (!Application.hasRole("worker")) {
|
|
320
884
|
_warlock_js_logger.log.info("queue", "workers", `queue: workers not started (role: ${[...Application.roles].join(",")})`);
|
|
321
885
|
return [];
|
|
322
886
|
}
|
|
323
887
|
if (getQueueConfig().workers?.enabled === false) return [];
|
|
324
|
-
|
|
888
|
+
const driver = getQueueDriver();
|
|
325
889
|
stopListening ??= onJobRegistered((job) => {
|
|
326
|
-
|
|
890
|
+
driver.start(queueOf(job)).catch((error) => _warlock_js_logger.log.error("queue", "worker", error));
|
|
327
891
|
});
|
|
328
|
-
await Promise.all([...
|
|
329
|
-
return
|
|
892
|
+
await Promise.all([...new Set(registeredJobs().map(queueOf))].map((queue) => driver.start(queue)));
|
|
893
|
+
return driver.running();
|
|
330
894
|
}
|
|
331
|
-
/**
|
|
895
|
+
/** Names of queues with workers in this process. */
|
|
332
896
|
function runningWorkers() {
|
|
333
|
-
return
|
|
334
|
-
}
|
|
335
|
-
function ensureWorker(queueName) {
|
|
336
|
-
if (workers.has(queueName)) return;
|
|
337
|
-
const config = getQueueConfig();
|
|
338
|
-
const worker = new bullmq.Worker(queueName, processJob, {
|
|
339
|
-
connection: config.connection,
|
|
340
|
-
prefix: config.prefix ?? "warlock",
|
|
341
|
-
concurrency: config.workers?.concurrency ?? 1
|
|
342
|
-
});
|
|
343
|
-
worker.on("error", (error) => {
|
|
344
|
-
_warlock_js_logger.log.error("queue", "worker", error);
|
|
345
|
-
});
|
|
346
|
-
worker.on("failed", (job, error) => {
|
|
347
|
-
_warlock_js_logger.log.error("queue", "job.failed", `${job?.name ?? "unknown"} (${job?.id ?? "?"}): ${error.message}`);
|
|
348
|
-
});
|
|
349
|
-
workers.set(queueName, worker);
|
|
897
|
+
return currentQueueDriver()?.running() ?? [];
|
|
350
898
|
}
|
|
351
|
-
/**
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
* connection. Safe to call when nothing was started, and more than once.
|
|
355
|
-
*/
|
|
356
|
-
async function closeQueue(options = {}) {
|
|
899
|
+
/** Stop taking jobs, drain active handlers, then release leases and close storage. */
|
|
900
|
+
function closeQueue(options = {}) {
|
|
901
|
+
if (closing) return closing;
|
|
357
902
|
stopListening?.();
|
|
358
903
|
stopListening = void 0;
|
|
904
|
+
const driver = currentQueueDriver();
|
|
905
|
+
if (!driver) return Promise.resolve();
|
|
359
906
|
const timeout = options.timeout ?? configuredShutdownTimeout();
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
907
|
+
if (!Number.isFinite(timeout) || timeout < 0) return Promise.reject(/* @__PURE__ */ new Error("Queue shutdown timeout must be non-negative and finite."));
|
|
908
|
+
closing = driver.close(timeout).finally(() => {
|
|
909
|
+
forgetQueueDriver();
|
|
910
|
+
closing = void 0;
|
|
911
|
+
});
|
|
912
|
+
return closing;
|
|
366
913
|
}
|
|
367
914
|
function configuredShutdownTimeout() {
|
|
368
915
|
try {
|
|
369
|
-
return getQueueConfig().workers?.shutdownTimeout ??
|
|
916
|
+
return getQueueConfig().workers?.shutdownTimeout ?? 3e4;
|
|
370
917
|
} catch {
|
|
371
|
-
return
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
async function closeWorker(worker, timeout) {
|
|
375
|
-
let timer;
|
|
376
|
-
const timedOut = new Promise((resolve) => {
|
|
377
|
-
timer = setTimeout(() => resolve("timeout"), timeout);
|
|
378
|
-
});
|
|
379
|
-
const closing = worker.close().then(() => "closed");
|
|
380
|
-
closing.catch(() => void 0);
|
|
381
|
-
const outcome = await Promise.race([closing, timedOut]);
|
|
382
|
-
clearTimeout(timer);
|
|
383
|
-
if (outcome === "timeout") {
|
|
384
|
-
_warlock_js_logger.log.warn("queue", "shutdown", `Worker for "${worker.name}" still had active jobs after ${timeout}ms; disconnecting. Those jobs are retried once their lock expires.`);
|
|
385
|
-
await worker.disconnect();
|
|
918
|
+
return 3e4;
|
|
386
919
|
}
|
|
387
920
|
}
|
|
388
921
|
|
|
@@ -402,6 +935,7 @@ async function closeWorker(worker, timeout) {
|
|
|
402
935
|
* await queueDashboard(getHttpServer(), { basePath: "/admin/queues" });
|
|
403
936
|
*/
|
|
404
937
|
async function queueDashboard(server, options = {}) {
|
|
938
|
+
if ((getQueueConfig().driver ?? "bullmq") !== "bullmq") throw new Error("The bull-board dashboard requires the bullmq queue driver. Use job.find() and failedJobs() with local or memory queues.");
|
|
405
939
|
const basePath = options.basePath ?? "/admin/queues";
|
|
406
940
|
const { createBullBoard, BullMQAdapter, FastifyAdapter } = await loadBullBoard();
|
|
407
941
|
const queueNames = options.queues ?? [.../* @__PURE__ */ new Set([defaultQueueName(), ...registeredJobs().map((job) => queueOf(job))])];
|
|
@@ -473,6 +1007,7 @@ const DEFAULT_DASHBOARD_PATH = "/admin/queues";
|
|
|
473
1007
|
async function mountQueueDashboard(server, config) {
|
|
474
1008
|
const dashboard = config.dashboard;
|
|
475
1009
|
if (!dashboard?.enabled) return;
|
|
1010
|
+
if ((config.driver ?? "bullmq") !== "bullmq") throw new Error("The bull-board dashboard requires the bullmq queue driver.");
|
|
476
1011
|
const middlewareList = dashboard.middleware ?? [];
|
|
477
1012
|
if (middlewareList.length === 0) {
|
|
478
1013
|
if (process.env.NODE_ENV === "production") throw new QueueDashboardUnguardedError();
|
|
@@ -524,6 +1059,21 @@ function toMilliseconds(value) {
|
|
|
524
1059
|
return Math.round(Number(match[1]) * UNIT_MILLISECONDS[match[2]]);
|
|
525
1060
|
}
|
|
526
1061
|
|
|
1062
|
+
//#endregion
|
|
1063
|
+
//#region ../../packages/queue/src/job-options.ts
|
|
1064
|
+
function resolveJobOptions(definition, options) {
|
|
1065
|
+
const defaults = getQueueConfig().defaultJobOptions ?? {};
|
|
1066
|
+
return {
|
|
1067
|
+
attempts: options.attempts ?? definition.attempts ?? defaults.attempts,
|
|
1068
|
+
backoff: options.backoff ?? definition.backoff ?? defaults.backoff,
|
|
1069
|
+
removeOnComplete: definition.removeOnComplete ?? defaults.removeOnComplete,
|
|
1070
|
+
removeOnFail: definition.removeOnFail ?? defaults.removeOnFail,
|
|
1071
|
+
delay: options.delay === void 0 ? void 0 : toMilliseconds(options.delay),
|
|
1072
|
+
priority: options.priority,
|
|
1073
|
+
jobId: options.jobId
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
527
1077
|
//#endregion
|
|
528
1078
|
//#region ../../packages/queue/src/define-job.ts
|
|
529
1079
|
/**
|
|
@@ -562,12 +1112,12 @@ function defineJob(definition) {
|
|
|
562
1112
|
options
|
|
563
1113
|
});
|
|
564
1114
|
const captured = definition.context === false ? void 0 : getQueueContext()?.capture();
|
|
565
|
-
|
|
566
|
-
return {
|
|
567
|
-
id: String(job.id),
|
|
1115
|
+
return getQueueDriver().dispatch({
|
|
568
1116
|
name: definition.name,
|
|
569
|
-
queue: queueName
|
|
570
|
-
|
|
1117
|
+
queue: queueName,
|
|
1118
|
+
data: wrapPayload(payload, captured),
|
|
1119
|
+
options: resolveJobOptions(definition, options)
|
|
1120
|
+
});
|
|
571
1121
|
},
|
|
572
1122
|
async find(id) {
|
|
573
1123
|
const interceptor = getDispatchInterceptor();
|
|
@@ -576,11 +1126,8 @@ function defineJob(definition) {
|
|
|
576
1126
|
queue: queueOf(definition),
|
|
577
1127
|
id
|
|
578
1128
|
});
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
if (!initialJob || initialJob.name !== definition.name) return;
|
|
582
|
-
const state = await initialJob.getState();
|
|
583
|
-
return toSnapshot(await queue.getJob(id) ?? initialJob, state);
|
|
1129
|
+
const snapshot = await getQueueDriver().find(queueOf(definition), id);
|
|
1130
|
+
return snapshot?.name === definition.name ? snapshot : void 0;
|
|
584
1131
|
}
|
|
585
1132
|
};
|
|
586
1133
|
}
|
|
@@ -589,88 +1136,16 @@ function assertValidDefinition(definition) {
|
|
|
589
1136
|
if (typeof definition.handle !== "function") throw new InvalidJobDefinitionError(`defineJob("${definition.name}") requires a \`handle(payload, ctx)\` function.`);
|
|
590
1137
|
if (definition.attempts !== void 0 && !(Number.isInteger(definition.attempts) && definition.attempts >= 1)) throw new InvalidJobDefinitionError(`defineJob("${definition.name}"): \`attempts\` must be an integer >= 1, got ${definition.attempts}.`);
|
|
591
1138
|
}
|
|
592
|
-
/**
|
|
593
|
-
* Merge app defaults < job definition < dispatch options into BullMQ's shape.
|
|
594
|
-
*/
|
|
595
|
-
function toBullJobOptions(definition, options) {
|
|
596
|
-
const defaults = getQueueConfig().defaultJobOptions ?? {};
|
|
597
|
-
const attempts = options.attempts ?? definition.attempts ?? defaults.attempts;
|
|
598
|
-
const backoff = options.backoff ?? definition.backoff ?? defaults.backoff;
|
|
599
|
-
const removeOnComplete = definition.removeOnComplete ?? defaults.removeOnComplete;
|
|
600
|
-
const removeOnFail = definition.removeOnFail ?? defaults.removeOnFail;
|
|
601
|
-
const bullOptions = {};
|
|
602
|
-
if (attempts !== void 0) bullOptions.attempts = attempts;
|
|
603
|
-
if (backoff !== void 0) bullOptions.backoff = toBullBackoff(backoff);
|
|
604
|
-
if (removeOnComplete !== void 0) bullOptions.removeOnComplete = removeOnComplete;
|
|
605
|
-
if (removeOnFail !== void 0) bullOptions.removeOnFail = removeOnFail;
|
|
606
|
-
if (options.delay !== void 0) bullOptions.delay = toMilliseconds(options.delay);
|
|
607
|
-
if (options.priority !== void 0) bullOptions.priority = options.priority;
|
|
608
|
-
if (options.jobId !== void 0) bullOptions.jobId = options.jobId;
|
|
609
|
-
return bullOptions;
|
|
610
|
-
}
|
|
611
|
-
function toBullBackoff(backoff) {
|
|
612
|
-
return typeof backoff === "number" ? {
|
|
613
|
-
type: "fixed",
|
|
614
|
-
delay: backoff
|
|
615
|
-
} : backoff;
|
|
616
|
-
}
|
|
617
|
-
/**
|
|
618
|
-
* A plain view of a BullMQ job.
|
|
619
|
-
*
|
|
620
|
-
* @param job The job to read fields from.
|
|
621
|
-
* @param state The job's state; pass a state read *before* `job` was
|
|
622
|
-
* fetched (or re-fetched) so the returned snapshot's fields are consistent
|
|
623
|
-
* with it. If omitted, the state is read from `job` directly.
|
|
624
|
-
*/
|
|
625
|
-
async function toSnapshot(job, state) {
|
|
626
|
-
const resolvedState = state ?? await job.getState();
|
|
627
|
-
return {
|
|
628
|
-
id: String(job.id),
|
|
629
|
-
name: job.name,
|
|
630
|
-
queue: job.queueName,
|
|
631
|
-
state: resolvedState,
|
|
632
|
-
payload: unwrapPayload(job.data).payload,
|
|
633
|
-
progress: job.progress,
|
|
634
|
-
attemptsMade: job.attemptsMade,
|
|
635
|
-
result: job.returnvalue,
|
|
636
|
-
failedReason: job.failedReason || void 0,
|
|
637
|
-
createdAt: new Date(job.timestamp),
|
|
638
|
-
finishedAt: job.finishedOn ? new Date(job.finishedOn) : void 0
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
1139
|
|
|
642
1140
|
//#endregion
|
|
643
1141
|
//#region ../../packages/queue/src/failed-jobs.ts
|
|
644
|
-
/**
|
|
645
|
-
* List failed jobs, newest first — jobs that used up every attempt, or
|
|
646
|
-
* failed unrecoverably. Each entry can be retried.
|
|
647
|
-
*/
|
|
1142
|
+
/** List failed jobs, newest first, using the selected driver. */
|
|
648
1143
|
async function failedJobs(options = {}) {
|
|
649
|
-
|
|
650
|
-
return (await getQueue(queueName).getFailed(options.start ?? 0, options.end ?? 99)).map((job) => toFailedJob(job));
|
|
1144
|
+
return getQueueDriver().failed(options.queue ?? defaultQueueName(), options.start ?? 0, options.end ?? 99);
|
|
651
1145
|
}
|
|
652
|
-
/**
|
|
653
|
-
* Retry one failed job by id: it goes back to waiting with its attempts reset.
|
|
654
|
-
* Throws {@link FailedJobNotFoundError} when no FAILED job has that id.
|
|
655
|
-
*/
|
|
1146
|
+
/** Move a failed job back to waiting, resetting its attempts. */
|
|
656
1147
|
async function retryFailedJob(id, options = {}) {
|
|
657
|
-
|
|
658
|
-
const job = await getQueue(queueName).getJob(id);
|
|
659
|
-
if (!job || !await job.isFailed()) throw new FailedJobNotFoundError(id, queueName);
|
|
660
|
-
await job.retry("failed");
|
|
661
|
-
}
|
|
662
|
-
function toFailedJob(job) {
|
|
663
|
-
return {
|
|
664
|
-
id: String(job.id),
|
|
665
|
-
name: job.name,
|
|
666
|
-
queue: job.queueName,
|
|
667
|
-
payload: unwrapPayload(job.data).payload,
|
|
668
|
-
attemptsMade: job.attemptsMade,
|
|
669
|
-
failedReason: job.failedReason,
|
|
670
|
-
stacktrace: job.stacktrace ?? [],
|
|
671
|
-
failedAt: job.finishedOn ? new Date(job.finishedOn) : void 0,
|
|
672
|
-
retry: () => job.retry("failed")
|
|
673
|
-
};
|
|
1148
|
+
await getQueueDriver().retry(options.queue ?? defaultQueueName(), id);
|
|
674
1149
|
}
|
|
675
1150
|
|
|
676
1151
|
//#endregion
|
|
@@ -774,6 +1249,7 @@ exports.getDispatchInterceptor = getDispatchInterceptor;
|
|
|
774
1249
|
exports.getQueue = getQueue;
|
|
775
1250
|
exports.getQueueConfig = getQueueConfig;
|
|
776
1251
|
exports.getQueueContext = getQueueContext;
|
|
1252
|
+
exports.getQueueDriver = getQueueDriver;
|
|
777
1253
|
exports.loadBullBoard = loadBullBoard;
|
|
778
1254
|
exports.mountQueueDashboard = mountQueueDashboard;
|
|
779
1255
|
exports.queueConnector = queueConnector;
|