@warlock.js/queue 5.14.0 → 5.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +0 -3
- package/cjs/index.cjs +398 -29
- package/cjs/index.cjs.map +1 -1
- package/esm/dashboard-guard-plugin.mjs +1 -1
- package/esm/dashboard-guard-plugin.mjs.map +1 -1
- package/llms-full.txt +2 -2
- package/package.json +3 -17
- package/skills/manage-failed-jobs/SKILL.md +2 -1
- package/skills/queue-notifications/SKILL.md +0 -1
- package/cjs/define-job-33RNLEqL.cjs +0 -475
- package/cjs/define-job-33RNLEqL.cjs.map +0 -1
- package/cjs/notifications/index.cjs +0 -92
- package/cjs/notifications/index.cjs.map +0 -1
- package/esm/notifications/index.d.mts +0 -2
- package/esm/notifications/index.mjs +0 -3
- package/esm/notifications/queue-notification-dispatcher.d.mts +0 -42
- package/esm/notifications/queue-notification-dispatcher.d.mts.map +0 -1
- package/esm/notifications/queue-notification-dispatcher.mjs +0 -91
- package/esm/notifications/queue-notification-dispatcher.mjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to `@warlock.js/queue` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
6
|
|
|
7
|
+
## 5.16.0 - 2026-09-18
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- The failed-jobs dashboard skill sample now includes the connection config and the `authMiddleware` import.
|
|
12
|
+
|
|
13
|
+
## 5.15.0 - 2026-09-18
|
|
14
|
+
|
|
15
|
+
### BREAKING
|
|
16
|
+
|
|
17
|
+
- Removed `@warlock.js/queue/notifications` (`queueNotificationDispatcher`), deprecated in 5.14. Use `bullmqQueue()` from `@warlock.js/notifications` instead. The `@warlock.js/notifications` peer dependency is also dropped, since this was the only thing in the package that needed it.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- The dashboard guard now ends the request explicitly when a middleware short-circuits, instead of leaving it to Fastify noticing the reply was already sent. The adapter has always returned a "handled" boolean for this; the hook discarded it, so whether an unauthenticated caller reached the dashboard depended on write ordering — a guard answering asynchronously could lose that race.
|
|
22
|
+
|
|
7
23
|
## 5.14.0 - 2026-09-17
|
|
8
24
|
|
|
9
25
|
### Added
|
package/README.md
CHANGED
|
@@ -106,9 +106,6 @@ const config: NotificationConfig = {
|
|
|
106
106
|
`@warlock.js/queue` is dynamically imported the first time `.queue()` runs —
|
|
107
107
|
notifications never pays for it unless `bullmqQueue()` is configured.
|
|
108
108
|
|
|
109
|
-
> **Deprecated:** `queueNotificationDispatcher` from `@warlock.js/queue/notifications`
|
|
110
|
-
> still works for this release (it logs a one-time deprecation warning) and is
|
|
111
|
-
> removed in the next one. Switch to `bullmqQueue` above.
|
|
112
109
|
|
|
113
110
|
## Dashboard (optional)
|
|
114
111
|
|
package/cjs/index.cjs
CHANGED
|
@@ -1,7 +1,88 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_define_job = require('./define-job-33RNLEqL.cjs');
|
|
3
2
|
let _warlock_js_logger = require("@warlock.js/logger");
|
|
3
|
+
let bullmq = require("bullmq");
|
|
4
4
|
|
|
5
|
+
//#region ../queue/src/errors.ts
|
|
6
|
+
/**
|
|
7
|
+
* Thrown when the queue is used before `setQueueConfig` (or the queue
|
|
8
|
+
* connector) supplied a configuration.
|
|
9
|
+
*/
|
|
10
|
+
var QueueNotConfiguredError = class extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("@warlock.js/queue is not configured. Add src/config/queue.ts exporting a QueueConfig and register queueConnector() in warlock.config.ts > connectors, or call setQueueConfig() yourself.");
|
|
13
|
+
this.name = "QueueNotConfiguredError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Thrown for a malformed duration such as `"10 minutes"`.
|
|
18
|
+
*/
|
|
19
|
+
var InvalidDurationError = class extends Error {
|
|
20
|
+
constructor(value) {
|
|
21
|
+
super(`Invalid duration ${JSON.stringify(value)}: expected milliseconds as a non-negative number or a string like "500ms", "30s", "10m", "2h", "1d".`);
|
|
22
|
+
this.name = "InvalidDurationError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Thrown by `defineJob` for an invalid definition.
|
|
27
|
+
*/
|
|
28
|
+
var InvalidJobDefinitionError = class extends Error {
|
|
29
|
+
constructor(message) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "InvalidJobDefinitionError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Thrown by `retryFailedJob` when no failed job has the given id.
|
|
36
|
+
*/
|
|
37
|
+
var FailedJobNotFoundError = class extends Error {
|
|
38
|
+
constructor(id, queue) {
|
|
39
|
+
super(`No failed job with id "${id}" on queue "${queue}".`);
|
|
40
|
+
this.name = "FailedJobNotFoundError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Thrown by `queueDashboard` when an optional bull-board package is not
|
|
45
|
+
* installed.
|
|
46
|
+
*/
|
|
47
|
+
var QueueDashboardDependencyError = class extends Error {
|
|
48
|
+
constructor(missing) {
|
|
49
|
+
super(`The queue dashboard needs the optional package "${missing}", which is not installed.\nInstall both bull-board packages:
|
|
50
|
+
|
|
51
|
+
npm install @bull-board/api @bull-board/fastify
|
|
52
|
+
`);
|
|
53
|
+
this.name = "QueueDashboardDependencyError";
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region ../queue/src/config.ts
|
|
59
|
+
let activeConfig;
|
|
60
|
+
/**
|
|
61
|
+
* Set the active queue configuration. In a Warlock app the queue connector
|
|
62
|
+
* calls this at boot with `src/config/queue.ts`; scripts and tests may call
|
|
63
|
+
* it directly. Replaces (does not merge) any previous configuration.
|
|
64
|
+
*/
|
|
65
|
+
function setQueueConfig(config) {
|
|
66
|
+
activeConfig = config;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The active queue configuration. Throws {@link QueueNotConfiguredError}
|
|
70
|
+
* when none was set.
|
|
71
|
+
*/
|
|
72
|
+
function getQueueConfig() {
|
|
73
|
+
if (!activeConfig) throw new QueueNotConfiguredError();
|
|
74
|
+
return activeConfig;
|
|
75
|
+
}
|
|
76
|
+
/** Forget the active configuration. */
|
|
77
|
+
function resetQueueConfig() {
|
|
78
|
+
activeConfig = void 0;
|
|
79
|
+
}
|
|
80
|
+
/** The queue a job runs on when it names none. */
|
|
81
|
+
function defaultQueueName() {
|
|
82
|
+
return activeConfig?.defaultQueue ?? "default";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
5
86
|
//#region ../queue/src/dashboard-middleware-adapter.ts
|
|
6
87
|
/**
|
|
7
88
|
* Run a Warlock middleware list against a raw Fastify request/reply pair.
|
|
@@ -55,13 +136,176 @@ async function runDashboardMiddleware(middlewareList, fastifyRequest, fastifyRep
|
|
|
55
136
|
function buildDashboardGuardPlugin(middlewareList, bullBoardPlugin) {
|
|
56
137
|
return function dashboardGuardPlugin(instance, _options, done) {
|
|
57
138
|
if (middlewareList.length > 0) instance.addHook("onRequest", async (request, reply) => {
|
|
58
|
-
await runDashboardMiddleware(middlewareList, request, reply);
|
|
139
|
+
if (await runDashboardMiddleware(middlewareList, request, reply)) return reply;
|
|
59
140
|
});
|
|
60
141
|
instance.register(bullBoardPlugin);
|
|
61
142
|
done();
|
|
62
143
|
};
|
|
63
144
|
}
|
|
64
145
|
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region ../queue/src/job-registry.ts
|
|
148
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
149
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
150
|
+
/**
|
|
151
|
+
* Register a definition under its name.
|
|
152
|
+
*
|
|
153
|
+
* Re-registering a name REPLACES the previous definition: in development a
|
|
154
|
+
* job module is re-evaluated on every reload, and refusing the second
|
|
155
|
+
* evaluation would break the reload. Job names must therefore be unique
|
|
156
|
+
* across the app — two different modules using one name leave only the
|
|
157
|
+
* later handler active.
|
|
158
|
+
*/
|
|
159
|
+
function registerJob(job) {
|
|
160
|
+
jobs.set(job.name, job);
|
|
161
|
+
for (const listener of listeners) listener(job);
|
|
162
|
+
}
|
|
163
|
+
/** The definition registered under `name`, if any. */
|
|
164
|
+
function findRegisteredJob(name) {
|
|
165
|
+
return jobs.get(name);
|
|
166
|
+
}
|
|
167
|
+
/** Every registered definition. */
|
|
168
|
+
function registeredJobs() {
|
|
169
|
+
return [...jobs.values()];
|
|
170
|
+
}
|
|
171
|
+
/** The queue a definition runs on, resolved against the active config. */
|
|
172
|
+
function queueOf(job) {
|
|
173
|
+
return job.queue ?? defaultQueueName();
|
|
174
|
+
}
|
|
175
|
+
/** Be told whenever a job is registered. Returns an unsubscribe function. */
|
|
176
|
+
function onJobRegistered(listener) {
|
|
177
|
+
listeners.add(listener);
|
|
178
|
+
return () => listeners.delete(listener);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region ../queue/src/process-job.ts
|
|
183
|
+
/**
|
|
184
|
+
* The single BullMQ processor every worker runs: route the job to the
|
|
185
|
+
* handler registered under its name.
|
|
186
|
+
*
|
|
187
|
+
* A name with no handler in this process fails with `UnrecoverableError` —
|
|
188
|
+
* retrying cannot make a missing definition appear, so it must not burn
|
|
189
|
+
* through its attempts.
|
|
190
|
+
*/
|
|
191
|
+
async function processJob(job) {
|
|
192
|
+
const definition = findRegisteredJob(job.name);
|
|
193
|
+
if (!definition) throw new bullmq.UnrecoverableError(`No job named "${job.name}" is defined in this process. Make sure the module that calls defineJob() is imported by the worker process.`);
|
|
194
|
+
const context = {
|
|
195
|
+
id: String(job.id),
|
|
196
|
+
name: job.name,
|
|
197
|
+
queue: job.queueName,
|
|
198
|
+
attempt: job.attemptsMade + 1,
|
|
199
|
+
maxAttempts: job.opts.attempts ?? 1,
|
|
200
|
+
progress: (value) => job.updateProgress(value),
|
|
201
|
+
log: async (line) => {
|
|
202
|
+
await job.log(line);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
return definition.handle(job.data, context);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region ../queue/src/queue-manager.ts
|
|
210
|
+
const DEFAULT_SHUTDOWN_TIMEOUT = 3e4;
|
|
211
|
+
const queues = /* @__PURE__ */ new Map();
|
|
212
|
+
const workers = /* @__PURE__ */ new Map();
|
|
213
|
+
let stopListening;
|
|
214
|
+
/**
|
|
215
|
+
* The BullMQ queue for `name`, created on first use with the configured
|
|
216
|
+
* connection and prefix.
|
|
217
|
+
*/
|
|
218
|
+
function getQueue(name) {
|
|
219
|
+
let queue = queues.get(name);
|
|
220
|
+
if (!queue) {
|
|
221
|
+
const config = getQueueConfig();
|
|
222
|
+
queue = new bullmq.Queue(name, {
|
|
223
|
+
connection: config.connection,
|
|
224
|
+
prefix: config.prefix ?? "warlock"
|
|
225
|
+
});
|
|
226
|
+
queue.on("error", (error) => {
|
|
227
|
+
_warlock_js_logger.log.error("queue", "connection", error);
|
|
228
|
+
});
|
|
229
|
+
queues.set(name, queue);
|
|
230
|
+
}
|
|
231
|
+
return queue;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Start one worker per queue that has a registered job, and keep starting
|
|
235
|
+
* workers for queues whose first job is defined later.
|
|
236
|
+
*
|
|
237
|
+
* A no-op returning `[]` when `workers.enabled` is `false`. Calling it again
|
|
238
|
+
* while workers run starts only the missing ones.
|
|
239
|
+
*
|
|
240
|
+
* @returns the queue names that now have a worker in this process.
|
|
241
|
+
*/
|
|
242
|
+
async function startWorkers() {
|
|
243
|
+
if (getQueueConfig().workers?.enabled === false) return [];
|
|
244
|
+
for (const job of registeredJobs()) ensureWorker(queueOf(job));
|
|
245
|
+
stopListening ??= onJobRegistered((job) => {
|
|
246
|
+
ensureWorker(queueOf(job));
|
|
247
|
+
});
|
|
248
|
+
await Promise.all([...workers.values()].map((worker) => worker.waitUntilReady()));
|
|
249
|
+
return [...workers.keys()];
|
|
250
|
+
}
|
|
251
|
+
/** The queue names with a running worker in this process. */
|
|
252
|
+
function runningWorkers() {
|
|
253
|
+
return [...workers.keys()];
|
|
254
|
+
}
|
|
255
|
+
function ensureWorker(queueName) {
|
|
256
|
+
if (workers.has(queueName)) return;
|
|
257
|
+
const config = getQueueConfig();
|
|
258
|
+
const worker = new bullmq.Worker(queueName, processJob, {
|
|
259
|
+
connection: config.connection,
|
|
260
|
+
prefix: config.prefix ?? "warlock",
|
|
261
|
+
concurrency: config.workers?.concurrency ?? 1
|
|
262
|
+
});
|
|
263
|
+
worker.on("error", (error) => {
|
|
264
|
+
_warlock_js_logger.log.error("queue", "worker", error);
|
|
265
|
+
});
|
|
266
|
+
worker.on("failed", (job, error) => {
|
|
267
|
+
_warlock_js_logger.log.error("queue", "job.failed", `${job?.name ?? "unknown"} (${job?.id ?? "?"}): ${error.message}`);
|
|
268
|
+
});
|
|
269
|
+
workers.set(queueName, worker);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Graceful shutdown: stop workers taking new jobs and wait for active ones
|
|
273
|
+
* (bounded by `timeout`, then force-close), then close every queue
|
|
274
|
+
* connection. Safe to call when nothing was started, and more than once.
|
|
275
|
+
*/
|
|
276
|
+
async function closeQueue(options = {}) {
|
|
277
|
+
stopListening?.();
|
|
278
|
+
stopListening = void 0;
|
|
279
|
+
const timeout = options.timeout ?? configuredShutdownTimeout();
|
|
280
|
+
const closingWorkers = [...workers.values()];
|
|
281
|
+
workers.clear();
|
|
282
|
+
await Promise.all(closingWorkers.map((worker) => closeWorker(worker, timeout)));
|
|
283
|
+
const closingQueues = [...queues.values()];
|
|
284
|
+
queues.clear();
|
|
285
|
+
await Promise.all(closingQueues.map((queue) => queue.close()));
|
|
286
|
+
}
|
|
287
|
+
function configuredShutdownTimeout() {
|
|
288
|
+
try {
|
|
289
|
+
return getQueueConfig().workers?.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT;
|
|
290
|
+
} catch {
|
|
291
|
+
return DEFAULT_SHUTDOWN_TIMEOUT;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async function closeWorker(worker, timeout) {
|
|
295
|
+
let timer;
|
|
296
|
+
const timedOut = new Promise((resolve) => {
|
|
297
|
+
timer = setTimeout(() => resolve("timeout"), timeout);
|
|
298
|
+
});
|
|
299
|
+
const closing = worker.close().then(() => "closed");
|
|
300
|
+
closing.catch(() => void 0);
|
|
301
|
+
const outcome = await Promise.race([closing, timedOut]);
|
|
302
|
+
clearTimeout(timer);
|
|
303
|
+
if (outcome === "timeout") {
|
|
304
|
+
_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.`);
|
|
305
|
+
await worker.disconnect();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
65
309
|
//#endregion
|
|
66
310
|
//#region ../queue/src/dashboard.ts
|
|
67
311
|
/**
|
|
@@ -80,11 +324,11 @@ function buildDashboardGuardPlugin(middlewareList, bullBoardPlugin) {
|
|
|
80
324
|
async function queueDashboard(server, options = {}) {
|
|
81
325
|
const basePath = options.basePath ?? "/admin/queues";
|
|
82
326
|
const { createBullBoard, BullMQAdapter, FastifyAdapter } = await loadBullBoard();
|
|
83
|
-
const queueNames = options.queues ?? [...new Set([
|
|
327
|
+
const queueNames = options.queues ?? [...new Set([defaultQueueName(), ...registeredJobs().map((job) => queueOf(job))])];
|
|
84
328
|
const serverAdapter = new FastifyAdapter();
|
|
85
329
|
serverAdapter.setBasePath(basePath);
|
|
86
330
|
createBullBoard({
|
|
87
|
-
queues: queueNames.map((name) => new BullMQAdapter(
|
|
331
|
+
queues: queueNames.map((name) => new BullMQAdapter(getQueue(name))),
|
|
88
332
|
serverAdapter
|
|
89
333
|
});
|
|
90
334
|
const guardedPlugin = buildDashboardGuardPlugin(options.middleware ?? [], serverAdapter.registerPlugin());
|
|
@@ -108,7 +352,7 @@ async function importOptional(importer, specifier) {
|
|
|
108
352
|
try {
|
|
109
353
|
return await importer(specifier);
|
|
110
354
|
} catch (error) {
|
|
111
|
-
if (isModuleNotFound(error)) throw new
|
|
355
|
+
if (isModuleNotFound(error)) throw new QueueDashboardDependencyError(specifier.split("/").slice(0, 2).join("/"));
|
|
112
356
|
throw error;
|
|
113
357
|
}
|
|
114
358
|
}
|
|
@@ -164,6 +408,131 @@ async function mountQueueDashboard(server, config) {
|
|
|
164
408
|
});
|
|
165
409
|
}
|
|
166
410
|
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region ../queue/src/duration.ts
|
|
413
|
+
const UNIT_MILLISECONDS = {
|
|
414
|
+
ms: 1,
|
|
415
|
+
s: 1e3,
|
|
416
|
+
m: 6e4,
|
|
417
|
+
h: 36e5,
|
|
418
|
+
d: 864e5
|
|
419
|
+
};
|
|
420
|
+
const DURATION_PATTERN = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/;
|
|
421
|
+
/**
|
|
422
|
+
* Convert a {@link Duration} to milliseconds. Numbers are already
|
|
423
|
+
* milliseconds. Anything else is rejected loudly rather than guessed at.
|
|
424
|
+
*/
|
|
425
|
+
function toMilliseconds(value) {
|
|
426
|
+
if (typeof value === "number") {
|
|
427
|
+
if (!Number.isFinite(value) || value < 0) throw new InvalidDurationError(value);
|
|
428
|
+
return Math.round(value);
|
|
429
|
+
}
|
|
430
|
+
const match = typeof value === "string" ? DURATION_PATTERN.exec(value.trim()) : null;
|
|
431
|
+
if (!match) throw new InvalidDurationError(value);
|
|
432
|
+
return Math.round(Number(match[1]) * UNIT_MILLISECONDS[match[2]]);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region ../queue/src/define-job.ts
|
|
437
|
+
/**
|
|
438
|
+
* Define a background job.
|
|
439
|
+
*
|
|
440
|
+
* The definition is registered by name so any worker in the process can run
|
|
441
|
+
* it; the returned object dispatches it with a typed payload.
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* export const sendInvoice = defineJob({
|
|
445
|
+
* name: "invoices.send",
|
|
446
|
+
* attempts: 5,
|
|
447
|
+
* backoff: { type: "exponential", delay: 2000 },
|
|
448
|
+
* async handle(payload: { invoiceId: string }, ctx) {
|
|
449
|
+
* await ctx.progress(50);
|
|
450
|
+
* },
|
|
451
|
+
* });
|
|
452
|
+
*
|
|
453
|
+
* await sendInvoice.dispatch({ invoiceId: "42" }, { delay: "10m", priority: 1 });
|
|
454
|
+
*/
|
|
455
|
+
function defineJob(definition) {
|
|
456
|
+
assertValidDefinition(definition);
|
|
457
|
+
registerJob(definition);
|
|
458
|
+
return {
|
|
459
|
+
name: definition.name,
|
|
460
|
+
get queue() {
|
|
461
|
+
return queueOf(definition);
|
|
462
|
+
},
|
|
463
|
+
async dispatch(payload, options = {}) {
|
|
464
|
+
const queueName = queueOf(definition);
|
|
465
|
+
const job = await getQueue(queueName).add(definition.name, payload, toBullJobOptions(definition, options));
|
|
466
|
+
return {
|
|
467
|
+
id: String(job.id),
|
|
468
|
+
name: definition.name,
|
|
469
|
+
queue: queueName
|
|
470
|
+
};
|
|
471
|
+
},
|
|
472
|
+
async find(id) {
|
|
473
|
+
const queue = getQueue(queueOf(definition));
|
|
474
|
+
const initialJob = await queue.getJob(id);
|
|
475
|
+
if (!initialJob || initialJob.name !== definition.name) return;
|
|
476
|
+
const state = await initialJob.getState();
|
|
477
|
+
return toSnapshot(await queue.getJob(id) ?? initialJob, state);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function assertValidDefinition(definition) {
|
|
482
|
+
if (typeof definition.name !== "string" || definition.name.trim() === "") throw new InvalidJobDefinitionError("defineJob() requires a non-empty `name`.");
|
|
483
|
+
if (typeof definition.handle !== "function") throw new InvalidJobDefinitionError(`defineJob("${definition.name}") requires a \`handle(payload, ctx)\` function.`);
|
|
484
|
+
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}.`);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Merge app defaults < job definition < dispatch options into BullMQ's shape.
|
|
488
|
+
*/
|
|
489
|
+
function toBullJobOptions(definition, options) {
|
|
490
|
+
const defaults = getQueueConfig().defaultJobOptions ?? {};
|
|
491
|
+
const attempts = options.attempts ?? definition.attempts ?? defaults.attempts;
|
|
492
|
+
const backoff = options.backoff ?? definition.backoff ?? defaults.backoff;
|
|
493
|
+
const removeOnComplete = definition.removeOnComplete ?? defaults.removeOnComplete;
|
|
494
|
+
const removeOnFail = definition.removeOnFail ?? defaults.removeOnFail;
|
|
495
|
+
const bullOptions = {};
|
|
496
|
+
if (attempts !== void 0) bullOptions.attempts = attempts;
|
|
497
|
+
if (backoff !== void 0) bullOptions.backoff = toBullBackoff(backoff);
|
|
498
|
+
if (removeOnComplete !== void 0) bullOptions.removeOnComplete = removeOnComplete;
|
|
499
|
+
if (removeOnFail !== void 0) bullOptions.removeOnFail = removeOnFail;
|
|
500
|
+
if (options.delay !== void 0) bullOptions.delay = toMilliseconds(options.delay);
|
|
501
|
+
if (options.priority !== void 0) bullOptions.priority = options.priority;
|
|
502
|
+
if (options.jobId !== void 0) bullOptions.jobId = options.jobId;
|
|
503
|
+
return bullOptions;
|
|
504
|
+
}
|
|
505
|
+
function toBullBackoff(backoff) {
|
|
506
|
+
return typeof backoff === "number" ? {
|
|
507
|
+
type: "fixed",
|
|
508
|
+
delay: backoff
|
|
509
|
+
} : backoff;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* A plain view of a BullMQ job.
|
|
513
|
+
*
|
|
514
|
+
* @param job The job to read fields from.
|
|
515
|
+
* @param state The job's state; pass a state read *before* `job` was
|
|
516
|
+
* fetched (or re-fetched) so the returned snapshot's fields are consistent
|
|
517
|
+
* with it. If omitted, the state is read from `job` directly.
|
|
518
|
+
*/
|
|
519
|
+
async function toSnapshot(job, state) {
|
|
520
|
+
const resolvedState = state ?? await job.getState();
|
|
521
|
+
return {
|
|
522
|
+
id: String(job.id),
|
|
523
|
+
name: job.name,
|
|
524
|
+
queue: job.queueName,
|
|
525
|
+
state: resolvedState,
|
|
526
|
+
payload: job.data,
|
|
527
|
+
progress: job.progress,
|
|
528
|
+
attemptsMade: job.attemptsMade,
|
|
529
|
+
result: job.returnvalue,
|
|
530
|
+
failedReason: job.failedReason || void 0,
|
|
531
|
+
createdAt: new Date(job.timestamp),
|
|
532
|
+
finishedAt: job.finishedOn ? new Date(job.finishedOn) : void 0
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
167
536
|
//#endregion
|
|
168
537
|
//#region ../queue/src/failed-jobs.ts
|
|
169
538
|
/**
|
|
@@ -171,16 +540,16 @@ async function mountQueueDashboard(server, config) {
|
|
|
171
540
|
* failed unrecoverably. Each entry can be retried.
|
|
172
541
|
*/
|
|
173
542
|
async function failedJobs(options = {}) {
|
|
174
|
-
return (await
|
|
543
|
+
return (await getQueue(options.queue ?? defaultQueueName()).getFailed(options.start ?? 0, options.end ?? 99)).map((job) => toFailedJob(job));
|
|
175
544
|
}
|
|
176
545
|
/**
|
|
177
546
|
* Retry one failed job by id: it goes back to waiting with its attempts reset.
|
|
178
547
|
* Throws {@link FailedJobNotFoundError} when no FAILED job has that id.
|
|
179
548
|
*/
|
|
180
549
|
async function retryFailedJob(id, options = {}) {
|
|
181
|
-
const queueName = options.queue ??
|
|
182
|
-
const job = await
|
|
183
|
-
if (!job || !await job.isFailed()) throw new
|
|
550
|
+
const queueName = options.queue ?? defaultQueueName();
|
|
551
|
+
const job = await getQueue(queueName).getJob(id);
|
|
552
|
+
if (!job || !await job.isFailed()) throw new FailedJobNotFoundError(id, queueName);
|
|
184
553
|
await job.retry("failed");
|
|
185
554
|
}
|
|
186
555
|
function toFailedJob(job) {
|
|
@@ -240,7 +609,7 @@ function queueConnector(options = {}) {
|
|
|
240
609
|
async boot() {
|
|
241
610
|
const queueConfig = options.config ?? await readQueueConfig();
|
|
242
611
|
if (!queueConfig?.dashboard?.enabled) return;
|
|
243
|
-
|
|
612
|
+
setQueueConfig(queueConfig);
|
|
244
613
|
const { getHttpServer } = await import("@warlock.js/core");
|
|
245
614
|
await mountQueueDashboard(getHttpServer(), queueConfig);
|
|
246
615
|
},
|
|
@@ -250,8 +619,8 @@ function queueConnector(options = {}) {
|
|
|
250
619
|
_warlock_js_logger.log.warn("queue", "configured", "queueConnector() is registered but no `queue` config was found (src/config/queue.ts); queue not started");
|
|
251
620
|
return;
|
|
252
621
|
}
|
|
253
|
-
|
|
254
|
-
const started = await
|
|
622
|
+
setQueueConfig(queueConfig);
|
|
623
|
+
const started = await startWorkers();
|
|
255
624
|
active = true;
|
|
256
625
|
_warlock_js_logger.log.info("queue", "configured", started.length > 0 ? `Queue workers running for: ${started.join(", ")}` : "Queue configured (no in-process workers)");
|
|
257
626
|
},
|
|
@@ -261,8 +630,8 @@ function queueConnector(options = {}) {
|
|
|
261
630
|
},
|
|
262
631
|
async shutdown() {
|
|
263
632
|
if (!active) return;
|
|
264
|
-
await
|
|
265
|
-
|
|
633
|
+
await closeQueue();
|
|
634
|
+
resetQueueConfig();
|
|
266
635
|
active = false;
|
|
267
636
|
},
|
|
268
637
|
shouldRestart(changedFiles) {
|
|
@@ -281,27 +650,27 @@ async function readQueueConfig() {
|
|
|
281
650
|
|
|
282
651
|
//#endregion
|
|
283
652
|
exports.DEFAULT_DASHBOARD_PATH = DEFAULT_DASHBOARD_PATH;
|
|
284
|
-
exports.FailedJobNotFoundError =
|
|
285
|
-
exports.InvalidDurationError =
|
|
286
|
-
exports.InvalidJobDefinitionError =
|
|
653
|
+
exports.FailedJobNotFoundError = FailedJobNotFoundError;
|
|
654
|
+
exports.InvalidDurationError = InvalidDurationError;
|
|
655
|
+
exports.InvalidJobDefinitionError = InvalidJobDefinitionError;
|
|
287
656
|
exports.QUEUE_CONNECTOR_PRIORITY = QUEUE_CONNECTOR_PRIORITY;
|
|
288
|
-
exports.QueueDashboardDependencyError =
|
|
657
|
+
exports.QueueDashboardDependencyError = QueueDashboardDependencyError;
|
|
289
658
|
exports.QueueDashboardUnguardedError = QueueDashboardUnguardedError;
|
|
290
|
-
exports.QueueNotConfiguredError =
|
|
291
|
-
exports.closeQueue =
|
|
292
|
-
exports.defaultQueueName =
|
|
293
|
-
exports.defineJob =
|
|
659
|
+
exports.QueueNotConfiguredError = QueueNotConfiguredError;
|
|
660
|
+
exports.closeQueue = closeQueue;
|
|
661
|
+
exports.defaultQueueName = defaultQueueName;
|
|
662
|
+
exports.defineJob = defineJob;
|
|
294
663
|
exports.failedJobs = failedJobs;
|
|
295
|
-
exports.getQueue =
|
|
296
|
-
exports.getQueueConfig =
|
|
664
|
+
exports.getQueue = getQueue;
|
|
665
|
+
exports.getQueueConfig = getQueueConfig;
|
|
297
666
|
exports.loadBullBoard = loadBullBoard;
|
|
298
667
|
exports.mountQueueDashboard = mountQueueDashboard;
|
|
299
668
|
exports.queueConnector = queueConnector;
|
|
300
669
|
exports.queueDashboard = queueDashboard;
|
|
301
|
-
exports.resetQueueConfig =
|
|
670
|
+
exports.resetQueueConfig = resetQueueConfig;
|
|
302
671
|
exports.retryFailedJob = retryFailedJob;
|
|
303
|
-
exports.runningWorkers =
|
|
304
|
-
exports.setQueueConfig =
|
|
305
|
-
exports.startWorkers =
|
|
306
|
-
exports.toMilliseconds =
|
|
672
|
+
exports.runningWorkers = runningWorkers;
|
|
673
|
+
exports.setQueueConfig = setQueueConfig;
|
|
674
|
+
exports.startWorkers = startWorkers;
|
|
675
|
+
exports.toMilliseconds = toMilliseconds;
|
|
307
676
|
//# sourceMappingURL=index.cjs.map
|