@warlock.js/queue 5.13.0 → 5.15.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +43 -7
  3. package/cjs/index.cjs +525 -29
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/dashboard-boot.d.mts +20 -0
  6. package/esm/dashboard-boot.d.mts.map +1 -0
  7. package/esm/dashboard-boot.mjs +38 -0
  8. package/esm/dashboard-boot.mjs.map +1 -0
  9. package/esm/dashboard-guard-plugin.mjs +28 -0
  10. package/esm/dashboard-guard-plugin.mjs.map +1 -0
  11. package/esm/dashboard-middleware-adapter.mjs +39 -0
  12. package/esm/dashboard-middleware-adapter.mjs.map +1 -0
  13. package/esm/dashboard.d.mts +8 -0
  14. package/esm/dashboard.d.mts.map +1 -1
  15. package/esm/dashboard.mjs +3 -1
  16. package/esm/dashboard.mjs.map +1 -1
  17. package/esm/define-job.mjs +13 -6
  18. package/esm/define-job.mjs.map +1 -1
  19. package/esm/index.d.mts +4 -2
  20. package/esm/index.mjs +3 -1
  21. package/esm/queue-connector.d.mts.map +1 -1
  22. package/esm/queue-connector.mjs +17 -1
  23. package/esm/queue-connector.mjs.map +1 -1
  24. package/esm/queue-dashboard-unguarded.error.d.mts +13 -0
  25. package/esm/queue-dashboard-unguarded.error.d.mts.map +1 -0
  26. package/esm/queue-dashboard-unguarded.error.mjs +17 -0
  27. package/esm/queue-dashboard-unguarded.error.mjs.map +1 -0
  28. package/esm/types.d.mts +19 -2
  29. package/esm/types.d.mts.map +1 -1
  30. package/llms-full.txt +45 -9
  31. package/llms.txt +2 -2
  32. package/package.json +3 -17
  33. package/skills/configure-queue/SKILL.md +4 -0
  34. package/skills/manage-failed-jobs/SKILL.md +33 -4
  35. package/skills/overview/SKILL.md +1 -1
  36. package/skills/queue-notifications/SKILL.md +6 -4
  37. package/cjs/define-job-DideGKQK.cjs +0 -468
  38. package/cjs/define-job-DideGKQK.cjs.map +0 -1
  39. package/cjs/notifications/index.cjs +0 -68
  40. package/cjs/notifications/index.cjs.map +0 -1
  41. package/esm/notifications/index.d.mts +0 -2
  42. package/esm/notifications/index.mjs +0 -3
  43. package/esm/notifications/queue-notification-dispatcher.d.mts +0 -34
  44. package/esm/notifications/queue-notification-dispatcher.d.mts.map +0 -1
  45. package/esm/notifications/queue-notification-dispatcher.mjs +0 -67
  46. package/esm/notifications/queue-notification-dispatcher.mjs.map +0 -1
package/cjs/index.cjs CHANGED
@@ -1,7 +1,312 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_define_job = require('./define-job-DideGKQK.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
86
+ //#region ../queue/src/dashboard-middleware-adapter.ts
87
+ /**
88
+ * Run a Warlock middleware list against a raw Fastify request/reply pair.
89
+ *
90
+ * Bull-board's `FastifyAdapter` hands back a plain Fastify plugin, not a
91
+ * Warlock route — there is no `Route`, no validation pipeline, and none of
92
+ * `createRequestStore`'s context wiring (CSP, tracing, `@warlock.js/context`
93
+ * stores). Reusing that full pipeline here would pull the whole request
94
+ * machinery into a place it was never meant to run. Instead this builds the
95
+ * minimal `Request`/`Response` pair — enough for guard-style middleware
96
+ * (`authMiddleware`, `ipFilterMiddleware`, a custom check) to read the
97
+ * request and short-circuit with a response, which covers every one of
98
+ * bull-board's routes because they all sit behind the same hook.
99
+ *
100
+ * `@warlock.js/core` is imported dynamically so this module never drags
101
+ * core's runtime graph into a process that never mounts the dashboard.
102
+ *
103
+ * @returns `true` when a middleware sent a response and the caller must not
104
+ * continue (bull-board's handler must not run); `false` to continue.
105
+ */
106
+ async function runDashboardMiddleware(middlewareList, fastifyRequest, fastifyReply) {
107
+ if (middlewareList.length === 0) return false;
108
+ const { Request, Response } = await import("@warlock.js/core");
109
+ const request = new Request();
110
+ const response = new Response();
111
+ response.setResponse(fastifyReply);
112
+ request.response = response;
113
+ response.request = request;
114
+ request.setRequest(fastifyRequest);
115
+ for (const middlewareFunction of middlewareList) if (await middlewareFunction({
116
+ request,
117
+ response
118
+ })) return true;
119
+ return false;
120
+ }
121
+
122
+ //#endregion
123
+ //#region ../queue/src/dashboard-guard-plugin.ts
124
+ /**
125
+ * Wrap bull-board's Fastify plugin in a scope that runs `middlewareList`
126
+ * on every request before bull-board's own routes see it.
127
+ *
128
+ * Bull-board's `FastifyAdapter.registerPlugin()` returns a plain Fastify
129
+ * plugin with no hook for Warlock middleware to run through, and Fastify
130
+ * only lets an `onRequest` hook be added to a plugin scope — never spliced
131
+ * into a plugin someone else wrote. So this builds ONE plugin that adds the
132
+ * hook to its own scope and then registers bull-board's plugin as a child of
133
+ * that scope; Fastify's encapsulation runs the hook for every route the
134
+ * child registers, which is every dashboard route.
135
+ */
136
+ function buildDashboardGuardPlugin(middlewareList, bullBoardPlugin) {
137
+ return function dashboardGuardPlugin(instance, _options, done) {
138
+ if (middlewareList.length > 0) instance.addHook("onRequest", async (request, reply) => {
139
+ if (await runDashboardMiddleware(middlewareList, request, reply)) return reply;
140
+ });
141
+ instance.register(bullBoardPlugin);
142
+ done();
143
+ };
144
+ }
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
+
309
+ //#endregion
5
310
  //#region ../queue/src/dashboard.ts
6
311
  /**
7
312
  * Mount the bull-board failed/active/delayed job UI on Warlock's Fastify
@@ -19,14 +324,15 @@ let _warlock_js_logger = require("@warlock.js/logger");
19
324
  async function queueDashboard(server, options = {}) {
20
325
  const basePath = options.basePath ?? "/admin/queues";
21
326
  const { createBullBoard, BullMQAdapter, FastifyAdapter } = await loadBullBoard();
22
- const queueNames = options.queues ?? [...new Set([require_define_job.defaultQueueName(), ...require_define_job.registeredJobs().map((job) => require_define_job.queueOf(job))])];
327
+ const queueNames = options.queues ?? [...new Set([defaultQueueName(), ...registeredJobs().map((job) => queueOf(job))])];
23
328
  const serverAdapter = new FastifyAdapter();
24
329
  serverAdapter.setBasePath(basePath);
25
330
  createBullBoard({
26
- queues: queueNames.map((name) => new BullMQAdapter(require_define_job.getQueue(name))),
331
+ queues: queueNames.map((name) => new BullMQAdapter(getQueue(name))),
27
332
  serverAdapter
28
333
  });
29
- await server.register(serverAdapter.registerPlugin(), { prefix: basePath });
334
+ const guardedPlugin = buildDashboardGuardPlugin(options.middleware ?? [], serverAdapter.registerPlugin());
335
+ await server.register(guardedPlugin, { prefix: basePath });
30
336
  }
31
337
  /**
32
338
  * Load the optional bull-board packages. Exported for tests of the missing
@@ -46,7 +352,7 @@ async function importOptional(importer, specifier) {
46
352
  try {
47
353
  return await importer(specifier);
48
354
  } catch (error) {
49
- if (isModuleNotFound(error)) throw new require_define_job.QueueDashboardDependencyError(specifier.split("/").slice(0, 2).join("/"));
355
+ if (isModuleNotFound(error)) throw new QueueDashboardDependencyError(specifier.split("/").slice(0, 2).join("/"));
50
356
  throw error;
51
357
  }
52
358
  }
@@ -55,6 +361,178 @@ function isModuleNotFound(error) {
55
361
  return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
56
362
  }
57
363
 
364
+ //#endregion
365
+ //#region ../queue/src/queue-dashboard-unguarded.error.ts
366
+ /**
367
+ * Thrown at boot when `queue.dashboard.enabled` is `true` in production with
368
+ * no guard middleware. The dashboard can retry and delete jobs; mounting it
369
+ * on the open internet without a guard is a production incident waiting to
370
+ * happen, so this fails the boot instead of shipping the hole.
371
+ */
372
+ var QueueDashboardUnguardedError = class extends Error {
373
+ constructor() {
374
+ super("queue.dashboard.enabled is true in production with no middleware. The dashboard can retry and delete jobs, so it must be guarded before it is exposed.\n\nAdd a guard middleware:\n\n import { middleware } from \"@warlock.js/core\";\n import { authMiddleware } from \"@warlock.js/auth\";\n\n const queueConfig: QueueConfig = {\n // ...\n dashboard: {\n enabled: true,\n middleware: [authMiddleware(\"admin\")],\n },\n };\n");
375
+ this.name = "QueueDashboardUnguardedError";
376
+ }
377
+ };
378
+
379
+ //#endregion
380
+ //#region ../queue/src/dashboard-boot.ts
381
+ /** Default path the dashboard mounts on when `dashboard.path` is not set. */
382
+ const DEFAULT_DASHBOARD_PATH = "/admin/queues";
383
+ /**
384
+ * Mount the bull-board dashboard for `queue.dashboard` in `config`, applying
385
+ * the safety rule: `enabled` in production with no middleware throws
386
+ * {@link QueueDashboardUnguardedError} instead of booting exposed; outside
387
+ * production with no middleware it logs one warning and mounts anyway.
388
+ *
389
+ * Called by `queueConnector()` at boot, once the HTTP server exists but
390
+ * before it starts listening — see `queue-connector.ts`. Exported so it can
391
+ * be unit-tested without going through the whole connector lifecycle.
392
+ */
393
+ async function mountQueueDashboard(server, config) {
394
+ const dashboard = config.dashboard;
395
+ if (!dashboard?.enabled) return;
396
+ const middlewareList = dashboard.middleware ?? [];
397
+ if (middlewareList.length === 0) {
398
+ if (process.env.NODE_ENV === "production") throw new QueueDashboardUnguardedError();
399
+ _warlock_js_logger.log.warn("queue", "dashboard", "queue.dashboard.enabled is true with no middleware. The dashboard can retry and delete jobs — add a guard middleware before this ships to production.");
400
+ }
401
+ if (!server) {
402
+ _warlock_js_logger.log.warn("queue", "dashboard", "queue.dashboard.enabled is true but no HTTP server was found; the dashboard was not mounted.");
403
+ return;
404
+ }
405
+ await queueDashboard(server, {
406
+ basePath: dashboard.path ?? "/admin/queues",
407
+ middleware: middlewareList
408
+ });
409
+ }
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
+
58
536
  //#endregion
59
537
  //#region ../queue/src/failed-jobs.ts
60
538
  /**
@@ -62,16 +540,16 @@ function isModuleNotFound(error) {
62
540
  * failed unrecoverably. Each entry can be retried.
63
541
  */
64
542
  async function failedJobs(options = {}) {
65
- return (await require_define_job.getQueue(options.queue ?? require_define_job.defaultQueueName()).getFailed(options.start ?? 0, options.end ?? 99)).map((job) => toFailedJob(job));
543
+ return (await getQueue(options.queue ?? defaultQueueName()).getFailed(options.start ?? 0, options.end ?? 99)).map((job) => toFailedJob(job));
66
544
  }
67
545
  /**
68
546
  * Retry one failed job by id: it goes back to waiting with its attempts reset.
69
547
  * Throws {@link FailedJobNotFoundError} when no FAILED job has that id.
70
548
  */
71
549
  async function retryFailedJob(id, options = {}) {
72
- const queueName = options.queue ?? require_define_job.defaultQueueName();
73
- const job = await require_define_job.getQueue(queueName).getJob(id);
74
- if (!job || !await job.isFailed()) throw new require_define_job.FailedJobNotFoundError(id, queueName);
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);
75
553
  await job.retry("failed");
76
554
  }
77
555
  function toFailedJob(job) {
@@ -119,15 +597,30 @@ function queueConnector(options = {}) {
119
597
  priority: 11,
120
598
  lifecyclePhase: "late",
121
599
  isActive: () => active,
122
- boot: () => void 0,
600
+ /**
601
+ * Mounts the dashboard, when configured, here rather than in `start()`:
602
+ * `boot()` runs for every late-phase connector, in priority order, before
603
+ * any of them `start()`s — so by the time this runs, the HTTP connector
604
+ * (priority 5, before queue's 11) has already built its Fastify instance
605
+ * and registered its own plugins, but has not yet called `listen()`.
606
+ * Fastify refuses new plugin registrations after `listen()`, so this is
607
+ * the only point in the boot sequence where mounting is possible.
608
+ */
609
+ async boot() {
610
+ const queueConfig = options.config ?? await readQueueConfig();
611
+ if (!queueConfig?.dashboard?.enabled) return;
612
+ setQueueConfig(queueConfig);
613
+ const { getHttpServer } = await import("@warlock.js/core");
614
+ await mountQueueDashboard(getHttpServer(), queueConfig);
615
+ },
123
616
  async start() {
124
617
  const queueConfig = options.config ?? await readQueueConfig();
125
618
  if (!queueConfig) {
126
619
  _warlock_js_logger.log.warn("queue", "configured", "queueConnector() is registered but no `queue` config was found (src/config/queue.ts); queue not started");
127
620
  return;
128
621
  }
129
- require_define_job.setQueueConfig(queueConfig);
130
- const started = await require_define_job.startWorkers();
622
+ setQueueConfig(queueConfig);
623
+ const started = await startWorkers();
131
624
  active = true;
132
625
  _warlock_js_logger.log.info("queue", "configured", started.length > 0 ? `Queue workers running for: ${started.join(", ")}` : "Queue configured (no in-process workers)");
133
626
  },
@@ -137,8 +630,8 @@ function queueConnector(options = {}) {
137
630
  },
138
631
  async shutdown() {
139
632
  if (!active) return;
140
- await require_define_job.closeQueue();
141
- require_define_job.resetQueueConfig();
633
+ await closeQueue();
634
+ resetQueueConfig();
142
635
  active = false;
143
636
  },
144
637
  shouldRestart(changedFiles) {
@@ -156,25 +649,28 @@ async function readQueueConfig() {
156
649
  }
157
650
 
158
651
  //#endregion
159
- exports.FailedJobNotFoundError = require_define_job.FailedJobNotFoundError;
160
- exports.InvalidDurationError = require_define_job.InvalidDurationError;
161
- exports.InvalidJobDefinitionError = require_define_job.InvalidJobDefinitionError;
652
+ exports.DEFAULT_DASHBOARD_PATH = DEFAULT_DASHBOARD_PATH;
653
+ exports.FailedJobNotFoundError = FailedJobNotFoundError;
654
+ exports.InvalidDurationError = InvalidDurationError;
655
+ exports.InvalidJobDefinitionError = InvalidJobDefinitionError;
162
656
  exports.QUEUE_CONNECTOR_PRIORITY = QUEUE_CONNECTOR_PRIORITY;
163
- exports.QueueDashboardDependencyError = require_define_job.QueueDashboardDependencyError;
164
- exports.QueueNotConfiguredError = require_define_job.QueueNotConfiguredError;
165
- exports.closeQueue = require_define_job.closeQueue;
166
- exports.defaultQueueName = require_define_job.defaultQueueName;
167
- exports.defineJob = require_define_job.defineJob;
657
+ exports.QueueDashboardDependencyError = QueueDashboardDependencyError;
658
+ exports.QueueDashboardUnguardedError = QueueDashboardUnguardedError;
659
+ exports.QueueNotConfiguredError = QueueNotConfiguredError;
660
+ exports.closeQueue = closeQueue;
661
+ exports.defaultQueueName = defaultQueueName;
662
+ exports.defineJob = defineJob;
168
663
  exports.failedJobs = failedJobs;
169
- exports.getQueue = require_define_job.getQueue;
170
- exports.getQueueConfig = require_define_job.getQueueConfig;
664
+ exports.getQueue = getQueue;
665
+ exports.getQueueConfig = getQueueConfig;
171
666
  exports.loadBullBoard = loadBullBoard;
667
+ exports.mountQueueDashboard = mountQueueDashboard;
172
668
  exports.queueConnector = queueConnector;
173
669
  exports.queueDashboard = queueDashboard;
174
- exports.resetQueueConfig = require_define_job.resetQueueConfig;
670
+ exports.resetQueueConfig = resetQueueConfig;
175
671
  exports.retryFailedJob = retryFailedJob;
176
- exports.runningWorkers = require_define_job.runningWorkers;
177
- exports.setQueueConfig = require_define_job.setQueueConfig;
178
- exports.startWorkers = require_define_job.startWorkers;
179
- exports.toMilliseconds = require_define_job.toMilliseconds;
672
+ exports.runningWorkers = runningWorkers;
673
+ exports.setQueueConfig = setQueueConfig;
674
+ exports.startWorkers = startWorkers;
675
+ exports.toMilliseconds = toMilliseconds;
180
676
  //# sourceMappingURL=index.cjs.map