@warlock.js/queue 5.22.1 → 5.23.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 CHANGED
@@ -1,84 +1,97 @@
1
- # Changelog — @warlock.js/queue
2
-
3
- All notable changes to `@warlock.js/queue` are documented in this file.
4
-
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
-
7
- ## 5.22.1 - 2026-09-25
1
+ # Changelog — @warlock.js/queue
2
+
3
+ All notable changes to `@warlock.js/queue` are documented in this file.
4
+
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
+
7
+ ## 5.23.1 - 2026-09-26
8
8
 
9
9
  ### Changed
10
10
 
11
11
  - Lockstep patch release; package APIs are unchanged.
12
12
 
13
- ## 5.22.0 - 2026-09-25
14
-
15
- ### Changed
16
-
17
- - Lockstep release maintenance and dependency refresh.
18
-
19
- ## 5.21.0 - 2026-09-25
20
-
21
- ### Changed
22
-
23
- - Lockstep release maintenance and dependency refresh.
24
- ## 5.20.0 - 2026-09-24
25
-
26
- ### Changed
27
-
28
- - Added a package-level skill index and clearer discovery descriptions for grouped agent guidance.
29
-
30
- ## 5.19.0 - 2026-09-23
31
-
32
- ### Changed
33
-
34
- - Refined package skill-discovery descriptions and regenerated the llms projections.
35
-
36
- ## 5.16.0 - 2026-09-18
37
-
38
- ### Fixed
39
-
40
- - The failed-jobs dashboard skill sample now includes the connection config and the `authMiddleware` import.
41
-
42
- ## 5.15.0 - 2026-09-18
43
-
44
- ### BREAKING
45
-
46
- - 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.
47
-
48
- ### Fixed
49
-
50
- - 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.
51
-
52
- ## 5.14.0 - 2026-09-17
53
-
54
- ### Added
55
-
56
- - `dashboard: { enabled, path, middleware }` in `src/config/queue.ts`: the queue connector mounts Bull Board at boot, guarded by the given middleware.
57
- - `QueueDashboardUnguardedError`: in production the dashboard refuses to mount without guard middleware, since it can retry and delete jobs.
58
- - `queueDashboard()` accepts a `middleware` option.
59
-
60
- ### Deprecated
61
-
62
- - `@warlock.js/queue/notifications` (`queueNotificationDispatcher`): use `bullmqQueue()` from `@warlock.js/notifications`. It still works in 5.14, warns once, and is removed in the next release.
63
-
64
- ### Fixed
65
-
66
- - The connector registers the queue config when it mounts the dashboard at boot, so an app whose only queue usage is the dashboard no longer fails to start with `QueueNotConfiguredError`.
67
- - `find(id)` no longer returns a completed job with `result: null` and `attemptsMade: 0` when the job finishes mid-read.
68
-
69
- ## 5.13.0 - 2026-09-17
70
-
71
- ### Added
72
-
73
- - New package: durable background jobs on BullMQ + Redis. Redis is required.
74
- - `defineJob({ name, queue, attempts, backoff, removeOnComplete, removeOnFail, handle(payload, ctx) })` returns a typed job with `dispatch(payload, { delay, priority, jobId, attempts, backoff })` and `find(id)`.
75
- - Job context: `id`, `name`, `queue`, `attempt`, `maxAttempts`, `progress(value)`, `log(line)`.
76
- - `failedJobs({ queue, start, end })` and `retryFailedJob(id, { queue })`.
77
- - `queueConnector()` for `warlock.config.ts > connectors`: reads the `queue` config key, starts workers in the app process (turn off with `workers.enabled: false`), and on shutdown waits for active jobs up to `workers.shutdownTimeout` before closing.
78
- - `setQueueConfig`, `startWorkers`, `closeQueue` for scripts, tests and worker-only processes.
79
- - `@warlock.js/queue/notifications`: `queueNotificationDispatcher()` sends `@warlock.js/notifications` `.queue()` deliveries through BullMQ, with retries and `delay` support.
80
- - `queueDashboard(server, { basePath })`: mounts bull-board on Warlock's Fastify server. `@bull-board/api` and `@bull-board/fastify` are optional peers, loaded only when it is called; a missing one throws `QueueDashboardDependencyError`.
81
-
82
- ### Fixed
83
-
84
- - Portable `typecheck` script: runs against this package's own `typescript` devDependency instead of relying on a hoisted binary from elsewhere in the workspace.
13
+ ## 5.23.0 - 2026-09-25
14
+
15
+ ### Added
16
+
17
+ - `defineQueueContext()` / `setQueueContext()`: capture ambient state (tenant, request id) at `dispatch` and restore it around the job handler. The captured value travels in a versioned envelope; jobs without one still run. Exposed as `ctx.context`. Opt a job out with `context: false`.
18
+ - `UnrecoverableJobError`: throw it from a handler or `restore` to fail the job permanently (mapped to BullMQ's `UnrecoverableError`).
19
+
20
+ ## 5.22.1 - 2026-09-25
21
+
22
+ ### Changed
23
+
24
+ - Lockstep patch release; package APIs are unchanged.
25
+
26
+ ## 5.22.0 - 2026-09-25
27
+
28
+ ### Changed
29
+
30
+ - Lockstep release maintenance and dependency refresh.
31
+
32
+ ## 5.21.0 - 2026-09-25
33
+
34
+ ### Changed
35
+
36
+ - Lockstep release maintenance and dependency refresh.
37
+ ## 5.20.0 - 2026-09-24
38
+
39
+ ### Changed
40
+
41
+ - Added a package-level skill index and clearer discovery descriptions for grouped agent guidance.
42
+
43
+ ## 5.19.0 - 2026-09-23
44
+
45
+ ### Changed
46
+
47
+ - Refined package skill-discovery descriptions and regenerated the llms projections.
48
+
49
+ ## 5.16.0 - 2026-09-18
50
+
51
+ ### Fixed
52
+
53
+ - The failed-jobs dashboard skill sample now includes the connection config and the `authMiddleware` import.
54
+
55
+ ## 5.15.0 - 2026-09-18
56
+
57
+ ### BREAKING
58
+
59
+ - 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.
60
+
61
+ ### Fixed
62
+
63
+ - 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.
64
+
65
+ ## 5.14.0 - 2026-09-17
66
+
67
+ ### Added
68
+
69
+ - `dashboard: { enabled, path, middleware }` in `src/config/queue.ts`: the queue connector mounts Bull Board at boot, guarded by the given middleware.
70
+ - `QueueDashboardUnguardedError`: in production the dashboard refuses to mount without guard middleware, since it can retry and delete jobs.
71
+ - `queueDashboard()` accepts a `middleware` option.
72
+
73
+ ### Deprecated
74
+
75
+ - `@warlock.js/queue/notifications` (`queueNotificationDispatcher`): use `bullmqQueue()` from `@warlock.js/notifications`. It still works in 5.14, warns once, and is removed in the next release.
76
+
77
+ ### Fixed
78
+
79
+ - The connector registers the queue config when it mounts the dashboard at boot, so an app whose only queue usage is the dashboard no longer fails to start with `QueueNotConfiguredError`.
80
+ - `find(id)` no longer returns a completed job with `result: null` and `attemptsMade: 0` when the job finishes mid-read.
81
+
82
+ ## 5.13.0 - 2026-09-17
83
+
84
+ ### Added
85
+
86
+ - New package: durable background jobs on BullMQ + Redis. Redis is required.
87
+ - `defineJob({ name, queue, attempts, backoff, removeOnComplete, removeOnFail, handle(payload, ctx) })` returns a typed job with `dispatch(payload, { delay, priority, jobId, attempts, backoff })` and `find(id)`.
88
+ - Job context: `id`, `name`, `queue`, `attempt`, `maxAttempts`, `progress(value)`, `log(line)`.
89
+ - `failedJobs({ queue, start, end })` and `retryFailedJob(id, { queue })`.
90
+ - `queueConnector()` for `warlock.config.ts > connectors`: reads the `queue` config key, starts workers in the app process (turn off with `workers.enabled: false`), and on shutdown waits for active jobs up to `workers.shutdownTimeout` before closing.
91
+ - `setQueueConfig`, `startWorkers`, `closeQueue` for scripts, tests and worker-only processes.
92
+ - `@warlock.js/queue/notifications`: `queueNotificationDispatcher()` sends `@warlock.js/notifications` `.queue()` deliveries through BullMQ, with retries and `delay` support.
93
+ - `queueDashboard(server, { basePath })`: mounts bull-board on Warlock's Fastify server. `@bull-board/api` and `@bull-board/fastify` are optional peers, loaded only when it is called; a missing one throws `QueueDashboardDependencyError`.
94
+
95
+ ### Fixed
96
+
97
+ - Portable `typecheck` script: runs against this package's own `typescript` devDependency instead of relying on a hoisted binary from elsewhere in the workspace.
package/cjs/index.cjs CHANGED
@@ -53,6 +53,16 @@ var QueueDashboardDependencyError = class extends Error {
53
53
  this.name = "QueueDashboardDependencyError";
54
54
  }
55
55
  };
56
+ /**
57
+ * Throw from a job handler (or a queue context's `restore`) to fail the job
58
+ * permanently — it is not retried, whatever `attempts` allows.
59
+ */
60
+ var UnrecoverableJobError = class extends Error {
61
+ constructor(message, options) {
62
+ super(message, options);
63
+ this.name = "UnrecoverableJobError";
64
+ }
65
+ };
56
66
 
57
67
  //#endregion
58
68
  //#region ../queue/src/config.ts
@@ -178,6 +188,59 @@ function onJobRegistered(listener) {
178
188
  return () => listeners.delete(listener);
179
189
  }
180
190
 
191
+ //#endregion
192
+ //#region ../queue/src/queue-context.ts
193
+ /**
194
+ * Define a queue context. `restore`'s `captured` is inferred from `capture`.
195
+ *
196
+ * @example
197
+ * setQueueContext(
198
+ * defineQueueContext({
199
+ * capture: () => tenantStorage.getStore(),
200
+ * restore: (tenant, run) => tenantStorage.run(tenant, run),
201
+ * }),
202
+ * );
203
+ */
204
+ function defineQueueContext(context) {
205
+ return context;
206
+ }
207
+ let activeContext;
208
+ /** Register the queue context used by every job dispatch and run. */
209
+ function setQueueContext(context) {
210
+ activeContext = context;
211
+ }
212
+ /** The registered queue context, if any. */
213
+ function getQueueContext() {
214
+ return activeContext;
215
+ }
216
+ /** Forget the registered queue context. */
217
+ function resetQueueContext() {
218
+ activeContext = void 0;
219
+ }
220
+ /** Wrap a payload with the captured context; no context leaves it bare. */
221
+ function wrapPayload(payload, context) {
222
+ if (context === void 0) return payload;
223
+ return {
224
+ __warlock: {
225
+ v: 1,
226
+ context
227
+ },
228
+ payload
229
+ };
230
+ }
231
+ /** Split job data into payload and captured context; bare data passes through. */
232
+ function unwrapPayload(data) {
233
+ const envelope = data;
234
+ if (envelope !== null && typeof envelope === "object" && typeof envelope.__warlock === "object" && envelope.__warlock !== null && envelope.__warlock.v === 1 && "payload" in envelope) return {
235
+ payload: envelope.payload,
236
+ context: envelope.__warlock.context
237
+ };
238
+ return {
239
+ payload: data,
240
+ context: void 0
241
+ };
242
+ }
243
+
181
244
  //#endregion
182
245
  //#region ../queue/src/process-job.ts
183
246
  /**
@@ -191,6 +254,7 @@ function onJobRegistered(listener) {
191
254
  async function processJob(job) {
192
255
  const definition = findRegisteredJob(job.name);
193
256
  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.`);
257
+ const { payload, context: captured } = unwrapPayload(job.data);
194
258
  const context = {
195
259
  id: String(job.id),
196
260
  name: job.name,
@@ -200,9 +264,17 @@ async function processJob(job) {
200
264
  progress: (value) => job.updateProgress(value),
201
265
  log: async (line) => {
202
266
  await job.log(line);
203
- }
267
+ },
268
+ context: captured
204
269
  };
205
- return definition.handle(job.data, context);
270
+ const run = async () => definition.handle(payload, context);
271
+ const queueContext = getQueueContext();
272
+ try {
273
+ return captured !== void 0 && queueContext ? await queueContext.restore(captured, run) : await run();
274
+ } catch (error) {
275
+ if (error instanceof UnrecoverableJobError) throw new bullmq.UnrecoverableError(error.message);
276
+ throw error;
277
+ }
206
278
  }
207
279
 
208
280
  //#endregion
@@ -462,7 +534,8 @@ function defineJob(definition) {
462
534
  },
463
535
  async dispatch(payload, options = {}) {
464
536
  const queueName = queueOf(definition);
465
- const job = await getQueue(queueName).add(definition.name, payload, toBullJobOptions(definition, options));
537
+ const captured = definition.context === false ? void 0 : getQueueContext()?.capture();
538
+ const job = await getQueue(queueName).add(definition.name, wrapPayload(payload, captured), toBullJobOptions(definition, options));
466
539
  return {
467
540
  id: String(job.id),
468
541
  name: definition.name,
@@ -523,7 +596,7 @@ async function toSnapshot(job, state) {
523
596
  name: job.name,
524
597
  queue: job.queueName,
525
598
  state: resolvedState,
526
- payload: job.data,
599
+ payload: unwrapPayload(job.data).payload,
527
600
  progress: job.progress,
528
601
  attemptsMade: job.attemptsMade,
529
602
  result: job.returnvalue,
@@ -557,7 +630,7 @@ function toFailedJob(job) {
557
630
  id: String(job.id),
558
631
  name: job.name,
559
632
  queue: job.queueName,
560
- payload: job.data,
633
+ payload: unwrapPayload(job.data).payload,
561
634
  attemptsMade: job.attemptsMade,
562
635
  failedReason: job.failedReason,
563
636
  stacktrace: job.stacktrace ?? [],
@@ -657,20 +730,25 @@ exports.QUEUE_CONNECTOR_PRIORITY = QUEUE_CONNECTOR_PRIORITY;
657
730
  exports.QueueDashboardDependencyError = QueueDashboardDependencyError;
658
731
  exports.QueueDashboardUnguardedError = QueueDashboardUnguardedError;
659
732
  exports.QueueNotConfiguredError = QueueNotConfiguredError;
733
+ exports.UnrecoverableJobError = UnrecoverableJobError;
660
734
  exports.closeQueue = closeQueue;
661
735
  exports.defaultQueueName = defaultQueueName;
662
736
  exports.defineJob = defineJob;
737
+ exports.defineQueueContext = defineQueueContext;
663
738
  exports.failedJobs = failedJobs;
664
739
  exports.getQueue = getQueue;
665
740
  exports.getQueueConfig = getQueueConfig;
741
+ exports.getQueueContext = getQueueContext;
666
742
  exports.loadBullBoard = loadBullBoard;
667
743
  exports.mountQueueDashboard = mountQueueDashboard;
668
744
  exports.queueConnector = queueConnector;
669
745
  exports.queueDashboard = queueDashboard;
670
746
  exports.resetQueueConfig = resetQueueConfig;
747
+ exports.resetQueueContext = resetQueueContext;
671
748
  exports.retryFailedJob = retryFailedJob;
672
749
  exports.runningWorkers = runningWorkers;
673
750
  exports.setQueueConfig = setQueueConfig;
751
+ exports.setQueueContext = setQueueContext;
674
752
  exports.startWorkers = startWorkers;
675
753
  exports.toMilliseconds = toMilliseconds;
676
754
  //# sourceMappingURL=index.cjs.map
package/cjs/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["UnrecoverableError","Queue","Worker"],"sources":["../../../../../../queue/src/errors.ts","../../../../../../queue/src/config.ts","../../../../../../queue/src/dashboard-middleware-adapter.ts","../../../../../../queue/src/dashboard-guard-plugin.ts","../../../../../../queue/src/job-registry.ts","../../../../../../queue/src/process-job.ts","../../../../../../queue/src/queue-manager.ts","../../../../../../queue/src/dashboard.ts","../../../../../../queue/src/queue-dashboard-unguarded.error.ts","../../../../../../queue/src/dashboard-boot.ts","../../../../../../queue/src/duration.ts","../../../../../../queue/src/define-job.ts","../../../../../../queue/src/failed-jobs.ts","../../../../../../queue/src/queue-connector.ts"],"sourcesContent":["/**\n * Thrown when the queue is used before `setQueueConfig` (or the queue\n * connector) supplied a configuration.\n */\nexport class QueueNotConfiguredError extends Error {\n public constructor() {\n super(\n \"@warlock.js/queue is not configured. Add src/config/queue.ts exporting a QueueConfig \" +\n \"and register queueConnector() in warlock.config.ts > connectors, \" +\n \"or call setQueueConfig() yourself.\",\n );\n this.name = \"QueueNotConfiguredError\";\n }\n}\n\n/**\n * Thrown for a malformed duration such as `\"10 minutes\"`.\n */\nexport class InvalidDurationError extends Error {\n public constructor(value: unknown) {\n super(\n `Invalid duration ${JSON.stringify(value)}: expected milliseconds as a non-negative number ` +\n `or a string like \"500ms\", \"30s\", \"10m\", \"2h\", \"1d\".`,\n );\n this.name = \"InvalidDurationError\";\n }\n}\n\n/**\n * Thrown by `defineJob` for an invalid definition.\n */\nexport class InvalidJobDefinitionError extends Error {\n public constructor(message: string) {\n super(message);\n this.name = \"InvalidJobDefinitionError\";\n }\n}\n\n/**\n * Thrown by `retryFailedJob` when no failed job has the given id.\n */\nexport class FailedJobNotFoundError extends Error {\n public constructor(id: string, queue: string) {\n super(`No failed job with id \"${id}\" on queue \"${queue}\".`);\n this.name = \"FailedJobNotFoundError\";\n }\n}\n\n/**\n * Thrown by `queueDashboard` when an optional bull-board package is not\n * installed.\n */\nexport class QueueDashboardDependencyError extends Error {\n public constructor(missing: string) {\n super(\n `The queue dashboard needs the optional package \"${missing}\", which is not installed.\\n` +\n \"Install both bull-board packages:\\n\\n\" +\n \" npm install @bull-board/api @bull-board/fastify\\n\",\n );\n this.name = \"QueueDashboardDependencyError\";\n }\n}\n","import { QueueNotConfiguredError } from \"./errors\";\nimport type { QueueConfig } from \"./types\";\n\nlet activeConfig: QueueConfig | undefined;\n\n/**\n * Set the active queue configuration. In a Warlock app the queue connector\n * calls this at boot with `src/config/queue.ts`; scripts and tests may call\n * it directly. Replaces (does not merge) any previous configuration.\n */\nexport function setQueueConfig(config: QueueConfig): void {\n activeConfig = config;\n}\n\n/**\n * The active queue configuration. Throws {@link QueueNotConfiguredError}\n * when none was set.\n */\nexport function getQueueConfig(): QueueConfig {\n if (!activeConfig) {\n throw new QueueNotConfiguredError();\n }\n\n return activeConfig;\n}\n\n/** Forget the active configuration. */\nexport function resetQueueConfig(): void {\n activeConfig = undefined;\n}\n\n/** The queue a job runs on when it names none. */\nexport function defaultQueueName(): string {\n return activeConfig?.defaultQueue ?? \"default\";\n}\n","import type { Middleware } from \"@warlock.js/core\";\nimport type { FastifyReply, FastifyRequest } from \"fastify\";\n\n/**\n * Run a Warlock middleware list against a raw Fastify request/reply pair.\n *\n * Bull-board's `FastifyAdapter` hands back a plain Fastify plugin, not a\n * Warlock route — there is no `Route`, no validation pipeline, and none of\n * `createRequestStore`'s context wiring (CSP, tracing, `@warlock.js/context`\n * stores). Reusing that full pipeline here would pull the whole request\n * machinery into a place it was never meant to run. Instead this builds the\n * minimal `Request`/`Response` pair — enough for guard-style middleware\n * (`authMiddleware`, `ipFilterMiddleware`, a custom check) to read the\n * request and short-circuit with a response, which covers every one of\n * bull-board's routes because they all sit behind the same hook.\n *\n * `@warlock.js/core` is imported dynamically so this module never drags\n * core's runtime graph into a process that never mounts the dashboard.\n *\n * @returns `true` when a middleware sent a response and the caller must not\n * continue (bull-board's handler must not run); `false` to continue.\n */\nexport async function runDashboardMiddleware(\n middlewareList: Middleware[],\n fastifyRequest: FastifyRequest,\n fastifyReply: FastifyReply,\n): Promise<boolean> {\n if (middlewareList.length === 0) {\n return false;\n }\n\n const { Request, Response } = await import(\"@warlock.js/core\");\n\n const request = new Request();\n const response = new Response();\n\n response.setResponse(fastifyReply);\n request.response = response;\n response.request = request;\n request.setRequest(fastifyRequest);\n\n for (const middlewareFunction of middlewareList) {\n const result = await middlewareFunction({ request, response });\n\n if (result) {\n return true;\n }\n }\n\n return false;\n}\n","import type { Middleware } from \"@warlock.js/core\";\nimport type { FastifyInstance, FastifyPluginCallback } from \"fastify\";\nimport { runDashboardMiddleware } from \"./dashboard-middleware-adapter\";\n\n/**\n * Wrap bull-board's Fastify plugin in a scope that runs `middlewareList`\n * on every request before bull-board's own routes see it.\n *\n * Bull-board's `FastifyAdapter.registerPlugin()` returns a plain Fastify\n * plugin with no hook for Warlock middleware to run through, and Fastify\n * only lets an `onRequest` hook be added to a plugin scope — never spliced\n * into a plugin someone else wrote. So this builds ONE plugin that adds the\n * hook to its own scope and then registers bull-board's plugin as a child of\n * that scope; Fastify's encapsulation runs the hook for every route the\n * child registers, which is every dashboard route.\n */\nexport function buildDashboardGuardPlugin(\n middlewareList: Middleware[],\n bullBoardPlugin: unknown,\n): FastifyPluginCallback {\n return function dashboardGuardPlugin(instance: FastifyInstance, _options, done) {\n if (middlewareList.length > 0) {\n instance.addHook(\"onRequest\", async (request, reply) => {\n const handled = await runDashboardMiddleware(middlewareList, request, reply);\n\n if (handled) {\n // Returning the reply is Fastify's own way for an `onRequest` hook to\n // END the lifecycle, so bull-board's handler never runs. Relying\n // instead on Fastify noticing the reply was already sent leaves the\n // outcome to write ORDERING: a guard answering asynchronously can lose\n // that race and let the dashboard render to an unauthenticated caller.\n // The adapter returns this boolean precisely so the decision is\n // explicit rather than emergent.\n return reply;\n }\n });\n }\n\n instance.register(bullBoardPlugin as never);\n done();\n };\n}\n","import { defaultQueueName } from \"./config\";\nimport type { JobDefinition } from \"./types\";\n\n/** A registered definition, payload/result erased for storage. */\nexport type RegisteredJob = JobDefinition<unknown, unknown>;\n\ntype RegistryListener = (job: RegisteredJob) => void;\n\nconst jobs = new Map<string, RegisteredJob>();\nconst listeners = new Set<RegistryListener>();\n\n/**\n * Register a definition under its name.\n *\n * Re-registering a name REPLACES the previous definition: in development a\n * job module is re-evaluated on every reload, and refusing the second\n * evaluation would break the reload. Job names must therefore be unique\n * across the app — two different modules using one name leave only the\n * later handler active.\n */\nexport function registerJob(job: RegisteredJob): void {\n jobs.set(job.name, job);\n\n for (const listener of listeners) {\n listener(job);\n }\n}\n\n/** The definition registered under `name`, if any. */\nexport function findRegisteredJob(name: string): RegisteredJob | undefined {\n return jobs.get(name);\n}\n\n/** Every registered definition. */\nexport function registeredJobs(): RegisteredJob[] {\n return [...jobs.values()];\n}\n\n/** The queue a definition runs on, resolved against the active config. */\nexport function queueOf(job: Pick<RegisteredJob, \"queue\">): string {\n return job.queue ?? defaultQueueName();\n}\n\n/** Be told whenever a job is registered. Returns an unsubscribe function. */\nexport function onJobRegistered(listener: RegistryListener): () => void {\n listeners.add(listener);\n\n return () => listeners.delete(listener);\n}\n","import { type Job, UnrecoverableError } from \"bullmq\";\nimport { findRegisteredJob } from \"./job-registry\";\nimport type { JobContext } from \"./types\";\n\n/**\n * The single BullMQ processor every worker runs: route the job to the\n * handler registered under its name.\n *\n * A name with no handler in this process fails with `UnrecoverableError` —\n * retrying cannot make a missing definition appear, so it must not burn\n * through its attempts.\n */\nexport async function processJob(job: Job): Promise<unknown> {\n const definition = findRegisteredJob(job.name);\n\n if (!definition) {\n throw new UnrecoverableError(\n `No job named \"${job.name}\" is defined in this process. ` +\n \"Make sure the module that calls defineJob() is imported by the worker process.\",\n );\n }\n\n const context: JobContext = {\n id: String(job.id),\n name: job.name,\n queue: job.queueName,\n attempt: job.attemptsMade + 1,\n maxAttempts: job.opts.attempts ?? 1,\n progress: (value) => job.updateProgress(value),\n log: async (line) => {\n await job.log(line);\n },\n };\n\n return definition.handle(job.data, context);\n}\n","import { log } from \"@warlock.js/logger\";\nimport { Queue, Worker } from \"bullmq\";\nimport { getQueueConfig } from \"./config\";\nimport { onJobRegistered, queueOf, registeredJobs } from \"./job-registry\";\nimport { processJob } from \"./process-job\";\n\nconst DEFAULT_SHUTDOWN_TIMEOUT = 30_000;\n\nconst queues = new Map<string, Queue>();\nconst workers = new Map<string, Worker>();\nlet stopListening: (() => void) | undefined;\n\n/**\n * The BullMQ queue for `name`, created on first use with the configured\n * connection and prefix.\n */\nexport function getQueue(name: string): Queue {\n let queue = queues.get(name);\n\n if (!queue) {\n const config = getQueueConfig();\n\n queue = new Queue(name, {\n connection: config.connection,\n prefix: config.prefix ?? \"warlock\",\n });\n\n queue.on(\"error\", (error) => {\n log.error(\"queue\", \"connection\", error);\n });\n\n queues.set(name, queue);\n }\n\n return queue;\n}\n\n/**\n * Start one worker per queue that has a registered job, and keep starting\n * workers for queues whose first job is defined later.\n *\n * A no-op returning `[]` when `workers.enabled` is `false`. Calling it again\n * while workers run starts only the missing ones.\n *\n * @returns the queue names that now have a worker in this process.\n */\nexport async function startWorkers(): Promise<string[]> {\n const config = getQueueConfig();\n\n if (config.workers?.enabled === false) {\n return [];\n }\n\n for (const job of registeredJobs()) {\n ensureWorker(queueOf(job));\n }\n\n stopListening ??= onJobRegistered((job) => {\n ensureWorker(queueOf(job));\n });\n\n await Promise.all([...workers.values()].map((worker) => worker.waitUntilReady()));\n\n return [...workers.keys()];\n}\n\n/** The queue names with a running worker in this process. */\nexport function runningWorkers(): string[] {\n return [...workers.keys()];\n}\n\nfunction ensureWorker(queueName: string): void {\n if (workers.has(queueName)) {\n return;\n }\n\n const config = getQueueConfig();\n\n const worker = new Worker(queueName, processJob, {\n connection: config.connection,\n prefix: config.prefix ?? \"warlock\",\n concurrency: config.workers?.concurrency ?? 1,\n });\n\n worker.on(\"error\", (error) => {\n log.error(\"queue\", \"worker\", error);\n });\n\n worker.on(\"failed\", (job, error) => {\n log.error(\"queue\", \"job.failed\", `${job?.name ?? \"unknown\"} (${job?.id ?? \"?\"}): ${error.message}`);\n });\n\n workers.set(queueName, worker);\n}\n\nexport type CloseQueueOptions = {\n /**\n * How long to wait for active jobs before force-closing workers, in\n * milliseconds. Default: `workers.shutdownTimeout`, else `30000`.\n */\n timeout?: number;\n};\n\n/**\n * Graceful shutdown: stop workers taking new jobs and wait for active ones\n * (bounded by `timeout`, then force-close), then close every queue\n * connection. Safe to call when nothing was started, and more than once.\n */\nexport async function closeQueue(options: CloseQueueOptions = {}): Promise<void> {\n stopListening?.();\n stopListening = undefined;\n\n const timeout = options.timeout ?? configuredShutdownTimeout();\n const closingWorkers = [...workers.values()];\n workers.clear();\n\n await Promise.all(closingWorkers.map((worker) => closeWorker(worker, timeout)));\n\n const closingQueues = [...queues.values()];\n queues.clear();\n\n await Promise.all(closingQueues.map((queue) => queue.close()));\n}\n\nfunction configuredShutdownTimeout(): number {\n try {\n return getQueueConfig().workers?.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT;\n } catch {\n return DEFAULT_SHUTDOWN_TIMEOUT;\n }\n}\n\nasync function closeWorker(worker: Worker, timeout: number): Promise<void> {\n let timer: NodeJS.Timeout | undefined;\n\n const timedOut = new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), timeout);\n });\n\n // A close already in progress cannot be upgraded to a forced one (BullMQ\n // returns the pending promise), so the timeout path drops the connections\n // instead and lets the graceful close settle in the background.\n const closing = worker.close().then(() => \"closed\" as const);\n closing.catch(() => undefined);\n\n const outcome = await Promise.race([closing, timedOut]);\n\n clearTimeout(timer);\n\n if (outcome === \"timeout\") {\n log.warn(\n \"queue\",\n \"shutdown\",\n `Worker for \"${worker.name}\" still had active jobs after ${timeout}ms; disconnecting. ` +\n \"Those jobs are retried once their lock expires.\",\n );\n\n await worker.disconnect();\n }\n}\n","import type { Middleware } from \"@warlock.js/core\";\nimport { defaultQueueName } from \"./config\";\nimport { buildDashboardGuardPlugin } from \"./dashboard-guard-plugin\";\nimport { QueueDashboardDependencyError } from \"./errors\";\nimport { queueOf, registeredJobs } from \"./job-registry\";\nimport { getQueue } from \"./queue-manager\";\n\n/**\n * The part of a Fastify instance the dashboard needs. Warlock's HTTP server\n * (`getHttpServer()` from `@warlock.js/core`) satisfies it.\n */\nexport type DashboardServer = {\n register(plugin: never, options: { prefix: string }): unknown;\n};\n\nexport type QueueDashboardOptions = {\n /** URL prefix the dashboard is mounted on. Default `\"/admin/queues\"`. */\n basePath?: string;\n /** Queues to show. Default: every queue with a defined job, plus the default queue. */\n queues?: string[];\n /**\n * Run before every dashboard route. Applied via a wrapping Fastify plugin\n * scope, since bull-board's own plugin has no hook to splice Warlock\n * middleware into — see `dashboard-guard-plugin.ts`.\n */\n middleware?: Middleware[];\n};\n\ntype BullBoardModules = {\n createBullBoard: (options: { queues: unknown[]; serverAdapter: unknown }) => unknown;\n BullMQAdapter: new (queue: unknown) => unknown;\n FastifyAdapter: new () => {\n setBasePath(path: string): unknown;\n registerPlugin(): unknown;\n };\n};\n\n/**\n * Mount the bull-board failed/active/delayed job UI on Warlock's Fastify\n * server. Requires the optional packages `@bull-board/api` and\n * `@bull-board/fastify`; they are loaded only when this is called, and a\n * missing one throws {@link QueueDashboardDependencyError}.\n *\n * Call it before the HTTP server starts listening, and put it behind your\n * own authentication — the dashboard can retry and delete jobs.\n *\n * @example\n * import { getHttpServer } from \"@warlock.js/core\";\n * await queueDashboard(getHttpServer(), { basePath: \"/admin/queues\" });\n */\nexport async function queueDashboard(\n server: DashboardServer,\n options: QueueDashboardOptions = {},\n): Promise<void> {\n const basePath = options.basePath ?? \"/admin/queues\";\n const { createBullBoard, BullMQAdapter, FastifyAdapter } = await loadBullBoard();\n const queueNames =\n options.queues ?? [...new Set([defaultQueueName(), ...registeredJobs().map((job) => queueOf(job))])];\n\n const serverAdapter = new FastifyAdapter();\n serverAdapter.setBasePath(basePath);\n\n createBullBoard({\n queues: queueNames.map((name) => new BullMQAdapter(getQueue(name))),\n serverAdapter,\n });\n\n const guardedPlugin = buildDashboardGuardPlugin(options.middleware ?? [], serverAdapter.registerPlugin());\n\n await server.register(guardedPlugin as never, { prefix: basePath });\n}\n\n/**\n * Load the optional bull-board packages. Exported for tests of the missing\n * dependency path; `importer` defaults to a real dynamic import.\n */\nexport async function loadBullBoard(\n importer: (specifier: string) => Promise<Record<string, unknown>> = (specifier) => import(specifier),\n): Promise<BullBoardModules> {\n const api = await importOptional(importer, \"@bull-board/api\");\n const adapter = await importOptional(importer, \"@bull-board/api/bullMQAdapter\");\n const fastify = await importOptional(importer, \"@bull-board/fastify\");\n\n return {\n createBullBoard: api.createBullBoard as BullBoardModules[\"createBullBoard\"],\n BullMQAdapter: adapter.BullMQAdapter as BullBoardModules[\"BullMQAdapter\"],\n FastifyAdapter: fastify.FastifyAdapter as BullBoardModules[\"FastifyAdapter\"],\n };\n}\n\nasync function importOptional(\n importer: (specifier: string) => Promise<Record<string, unknown>>,\n specifier: string,\n): Promise<Record<string, unknown>> {\n try {\n return await importer(specifier);\n } catch (error) {\n if (isModuleNotFound(error)) {\n throw new QueueDashboardDependencyError(specifier.split(\"/\").slice(0, 2).join(\"/\"));\n }\n\n throw error;\n }\n}\n\nfunction isModuleNotFound(error: unknown): boolean {\n const code = (error as { code?: unknown } | undefined)?.code;\n\n return code === \"ERR_MODULE_NOT_FOUND\" || code === \"MODULE_NOT_FOUND\";\n}\n","/**\n * Thrown at boot when `queue.dashboard.enabled` is `true` in production with\n * no guard middleware. The dashboard can retry and delete jobs; mounting it\n * on the open internet without a guard is a production incident waiting to\n * happen, so this fails the boot instead of shipping the hole.\n */\nexport class QueueDashboardUnguardedError extends Error {\n public constructor() {\n super(\n \"queue.dashboard.enabled is true in production with no middleware. The dashboard can \" +\n \"retry and delete jobs, so it must be guarded before it is exposed.\\n\\n\" +\n \"Add a guard middleware:\\n\\n\" +\n \" import { middleware } from \\\"@warlock.js/core\\\";\\n\" +\n \" import { authMiddleware } from \\\"@warlock.js/auth\\\";\\n\\n\" +\n \" const queueConfig: QueueConfig = {\\n\" +\n \" // ...\\n\" +\n \" dashboard: {\\n\" +\n \" enabled: true,\\n\" +\n \" middleware: [authMiddleware(\\\"admin\\\")],\\n\" +\n \" },\\n\" +\n \" };\\n\",\n );\n this.name = \"QueueDashboardUnguardedError\";\n }\n}\n","import { log } from \"@warlock.js/logger\";\nimport { type DashboardServer, queueDashboard } from \"./dashboard\";\nimport { QueueDashboardUnguardedError } from \"./queue-dashboard-unguarded.error\";\nimport type { QueueConfig } from \"./types\";\n\n/** Default path the dashboard mounts on when `dashboard.path` is not set. */\nexport const DEFAULT_DASHBOARD_PATH = \"/admin/queues\";\n\n/**\n * Mount the bull-board dashboard for `queue.dashboard` in `config`, applying\n * the safety rule: `enabled` in production with no middleware throws\n * {@link QueueDashboardUnguardedError} instead of booting exposed; outside\n * production with no middleware it logs one warning and mounts anyway.\n *\n * Called by `queueConnector()` at boot, once the HTTP server exists but\n * before it starts listening — see `queue-connector.ts`. Exported so it can\n * be unit-tested without going through the whole connector lifecycle.\n */\nexport async function mountQueueDashboard(\n server: DashboardServer | undefined,\n config: QueueConfig,\n): Promise<void> {\n const dashboard = config.dashboard;\n\n if (!dashboard?.enabled) {\n return;\n }\n\n const middlewareList = dashboard.middleware ?? [];\n\n if (middlewareList.length === 0) {\n if (process.env.NODE_ENV === \"production\") {\n throw new QueueDashboardUnguardedError();\n }\n\n log.warn(\n \"queue\",\n \"dashboard\",\n \"queue.dashboard.enabled is true with no middleware. The dashboard can retry and delete \" +\n \"jobs — add a guard middleware before this ships to production.\",\n );\n }\n\n if (!server) {\n log.warn(\n \"queue\",\n \"dashboard\",\n \"queue.dashboard.enabled is true but no HTTP server was found; the dashboard was not mounted.\",\n );\n\n return;\n }\n\n await queueDashboard(server, {\n basePath: dashboard.path ?? DEFAULT_DASHBOARD_PATH,\n middleware: middlewareList,\n });\n}\n","import { InvalidDurationError } from \"./errors\";\nimport type { Duration } from \"./types\";\n\nconst UNIT_MILLISECONDS: Record<string, number> = {\n ms: 1,\n s: 1_000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n};\n\nconst DURATION_PATTERN = /^(\\d+(?:\\.\\d+)?)(ms|s|m|h|d)$/;\n\n/**\n * Convert a {@link Duration} to milliseconds. Numbers are already\n * milliseconds. Anything else is rejected loudly rather than guessed at.\n */\nexport function toMilliseconds(value: Duration | string): number {\n if (typeof value === \"number\") {\n if (!Number.isFinite(value) || value < 0) {\n throw new InvalidDurationError(value);\n }\n\n return Math.round(value);\n }\n\n const match = typeof value === \"string\" ? DURATION_PATTERN.exec(value.trim()) : null;\n\n if (!match) {\n throw new InvalidDurationError(value);\n }\n\n return Math.round(Number(match[1]) * UNIT_MILLISECONDS[match[2]!]!);\n}\n","import type { Job, JobsOptions } from \"bullmq\";\r\nimport { getQueueConfig } from \"./config\";\r\nimport { toMilliseconds } from \"./duration\";\r\nimport { InvalidJobDefinitionError } from \"./errors\";\r\nimport { queueOf, registerJob, type RegisteredJob } from \"./job-registry\";\r\nimport { getQueue } from \"./queue-manager\";\r\nimport type {\r\n DispatchOptions,\r\n JobBackoff,\r\n JobDefinition,\r\n JobOptions,\r\n JobSnapshot,\r\n JobState,\r\n QueueJob,\r\n} from \"./types\";\r\n\r\n/**\r\n * Define a background job.\r\n *\r\n * The definition is registered by name so any worker in the process can run\r\n * it; the returned object dispatches it with a typed payload.\r\n *\r\n * @example\r\n * export const sendInvoice = defineJob({\r\n * name: \"invoices.send\",\r\n * attempts: 5,\r\n * backoff: { type: \"exponential\", delay: 2000 },\r\n * async handle(payload: { invoiceId: string }, ctx) {\r\n * await ctx.progress(50);\r\n * },\r\n * });\r\n *\r\n * await sendInvoice.dispatch({ invoiceId: \"42\" }, { delay: \"10m\", priority: 1 });\r\n */\r\nexport function defineJob<TPayload, TResult = unknown>(\r\n definition: JobDefinition<TPayload, TResult>,\r\n): QueueJob<TPayload, TResult> {\r\n assertValidDefinition(definition);\r\n registerJob(definition as RegisteredJob);\r\n\r\n return {\r\n name: definition.name,\r\n get queue() {\r\n return queueOf(definition);\r\n },\r\n async dispatch(payload, options = {}) {\r\n const queueName = queueOf(definition);\r\n const job = await getQueue(queueName).add(\r\n definition.name,\r\n payload,\r\n toBullJobOptions(definition, options),\r\n );\r\n\r\n return { id: String(job.id), name: definition.name, queue: queueName };\r\n },\r\n async find(id) {\r\n const queue = getQueue(queueOf(definition));\r\n const initialJob = await queue.getJob(id);\r\n\r\n if (!initialJob || initialJob.name !== definition.name) {\r\n return undefined;\r\n }\r\n\r\n // Read the state first, then (re)fetch the job. BullMQ writes a job's\r\n // result/attemptsMade/finishedOn fields *before* it becomes visible\r\n // under a new state, so re-reading the job after the state is known\r\n // guarantees those fields are consistent with the reported state\r\n // (rather than reflecting a moment before the job finished).\r\n const state = (await initialJob.getState()) as JobState;\r\n const job = (await queue.getJob(id)) ?? initialJob;\r\n\r\n return toSnapshot<TPayload, TResult>(job, state);\r\n },\r\n };\r\n}\r\n\r\nfunction assertValidDefinition(definition: JobDefinition<unknown, unknown>): void {\r\n if (typeof definition.name !== \"string\" || definition.name.trim() === \"\") {\r\n throw new InvalidJobDefinitionError(\"defineJob() requires a non-empty `name`.\");\r\n }\r\n\r\n if (typeof definition.handle !== \"function\") {\r\n throw new InvalidJobDefinitionError(\r\n `defineJob(\"${definition.name}\") requires a \\`handle(payload, ctx)\\` function.`,\r\n );\r\n }\r\n\r\n if (definition.attempts !== undefined && !(Number.isInteger(definition.attempts) && definition.attempts >= 1)) {\r\n throw new InvalidJobDefinitionError(\r\n `defineJob(\"${definition.name}\"): \\`attempts\\` must be an integer >= 1, got ${definition.attempts}.`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Merge app defaults < job definition < dispatch options into BullMQ's shape.\r\n */\r\nfunction toBullJobOptions(definition: JobOptions, options: DispatchOptions): JobsOptions {\r\n const defaults = getQueueConfig().defaultJobOptions ?? {};\r\n const attempts = options.attempts ?? definition.attempts ?? defaults.attempts;\r\n const backoff = options.backoff ?? definition.backoff ?? defaults.backoff;\r\n const removeOnComplete = definition.removeOnComplete ?? defaults.removeOnComplete;\r\n const removeOnFail = definition.removeOnFail ?? defaults.removeOnFail;\r\n\r\n const bullOptions: JobsOptions = {};\r\n\r\n if (attempts !== undefined) bullOptions.attempts = attempts;\r\n if (backoff !== undefined) bullOptions.backoff = toBullBackoff(backoff);\r\n if (removeOnComplete !== undefined) bullOptions.removeOnComplete = removeOnComplete;\r\n if (removeOnFail !== undefined) bullOptions.removeOnFail = removeOnFail;\r\n if (options.delay !== undefined) bullOptions.delay = toMilliseconds(options.delay);\r\n if (options.priority !== undefined) bullOptions.priority = options.priority;\r\n if (options.jobId !== undefined) bullOptions.jobId = options.jobId;\r\n\r\n return bullOptions;\r\n}\r\n\r\nfunction toBullBackoff(backoff: JobBackoff): JobsOptions[\"backoff\"] {\r\n return typeof backoff === \"number\" ? { type: \"fixed\", delay: backoff } : backoff;\r\n}\r\n\r\n/**\r\n * A plain view of a BullMQ job.\r\n *\r\n * @param job The job to read fields from.\r\n * @param state The job's state; pass a state read *before* `job` was\r\n * fetched (or re-fetched) so the returned snapshot's fields are consistent\r\n * with it. If omitted, the state is read from `job` directly.\r\n */\r\nexport async function toSnapshot<TPayload, TResult>(\r\n job: Job,\r\n state?: JobState,\r\n): Promise<JobSnapshot<TPayload, TResult>> {\r\n const resolvedState = state ?? ((await job.getState()) as JobState);\r\n\r\n return {\r\n id: String(job.id),\r\n name: job.name,\r\n queue: job.queueName,\r\n state: resolvedState,\r\n payload: job.data as TPayload,\r\n progress: job.progress as JobSnapshot[\"progress\"],\r\n attemptsMade: job.attemptsMade,\r\n result: job.returnvalue as TResult | undefined,\r\n failedReason: job.failedReason || undefined,\r\n createdAt: new Date(job.timestamp),\r\n finishedAt: job.finishedOn ? new Date(job.finishedOn) : undefined,\r\n };\r\n}\r\n","import type { Job } from \"bullmq\";\nimport { defaultQueueName } from \"./config\";\nimport { FailedJobNotFoundError } from \"./errors\";\nimport { getQueue } from \"./queue-manager\";\nimport type { FailedJob } from \"./types\";\n\nexport type FailedJobsOptions = {\n /** Queue to read. Default: the default queue. */\n queue?: string;\n /** First index (newest first). Default `0`. */\n start?: number;\n /** Last index, inclusive. Default `99`. */\n end?: number;\n};\n\n/**\n * List failed jobs, newest first — jobs that used up every attempt, or\n * failed unrecoverably. Each entry can be retried.\n */\nexport async function failedJobs(options: FailedJobsOptions = {}): Promise<FailedJob[]> {\n const queueName = options.queue ?? defaultQueueName();\n const jobs = await getQueue(queueName).getFailed(options.start ?? 0, options.end ?? 99);\n\n return jobs.map((job) => toFailedJob(job));\n}\n\n/**\n * Retry one failed job by id: it goes back to waiting with its attempts reset.\n * Throws {@link FailedJobNotFoundError} when no FAILED job has that id.\n */\nexport async function retryFailedJob(id: string, options: { queue?: string } = {}): Promise<void> {\n const queueName = options.queue ?? defaultQueueName();\n const job = await getQueue(queueName).getJob(id);\n\n if (!job || !(await job.isFailed())) {\n throw new FailedJobNotFoundError(id, queueName);\n }\n\n await job.retry(\"failed\");\n}\n\nfunction toFailedJob(job: Job): FailedJob {\n return {\n id: String(job.id),\n name: job.name,\n queue: job.queueName,\n payload: job.data,\n attemptsMade: job.attemptsMade,\n failedReason: job.failedReason,\n stacktrace: job.stacktrace ?? [],\n failedAt: job.finishedOn ? new Date(job.finishedOn) : undefined,\n retry: () => job.retry(\"failed\"),\n };\n}\n","/**\r\n * The queue's connector for `warlock.config.ts > connectors`.\r\n *\r\n * Deliberately a plain object with TYPE-ONLY imports from core: the config\r\n * file that constructs it must not drag core's runtime graph (or BullMQ) in\r\n * at config-load time. Core is imported lazily inside `start()`, where the\r\n * app has already loaded it.\r\n */\r\nimport type { Connector, ConnectorLifecyclePhase } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { resetQueueConfig, setQueueConfig } from \"./config\";\r\nimport { mountQueueDashboard } from \"./dashboard-boot\";\r\nimport { closeQueue, startWorkers } from \"./queue-manager\";\r\nimport type { QueueConfig } from \"./types\";\r\n\r\n/**\r\n * Boots after every built-in connector (`ConnectorPriority.AI` is `10`), so\r\n * the logger is up and anything a job handler needs is already connected;\r\n * shuts down before them for the same reason.\r\n */\r\nexport const QUEUE_CONNECTOR_PRIORITY = 11;\r\n\r\nconst WATCHED_FILES = [\"src/config/queue.ts\"];\r\n\r\nexport type QueueConnectorOptions = {\r\n /**\r\n * Supply the configuration directly instead of reading the `queue` config\r\n * key (`src/config/queue.ts`).\r\n */\r\n config?: QueueConfig;\r\n};\r\n\r\n/**\r\n * Construct the queue connector.\r\n *\r\n * Runs in the `late` lifecycle phase — after app code is imported — so every\r\n * `defineJob` in the app has registered before workers start. At start it\r\n * reads the `queue` config and starts in-process workers unless\r\n * `workers.enabled` is `false`; at shutdown it closes workers (waiting for\r\n * active jobs, bounded by `workers.shutdownTimeout`) and then the queues.\r\n *\r\n * @example\r\n * // warlock.config.ts\r\n * import { queueConnector } from \"@warlock.js/queue\";\r\n *\r\n * export default defineConfig({ connectors: [queueConnector()] });\r\n */\r\nexport function queueConnector(options: QueueConnectorOptions = {}): Connector {\r\n let active = false;\r\n\r\n const connector: Connector = {\r\n name: \"queue\",\r\n priority: QUEUE_CONNECTOR_PRIORITY,\r\n // Core's `ConnectorLifecyclePhase.Late`; the value is spelled out so this\r\n // module stays free of a runtime import of core.\r\n lifecyclePhase: \"late\" as ConnectorLifecyclePhase,\r\n isActive: () => active,\r\n /**\r\n * Mounts the dashboard, when configured, here rather than in `start()`:\r\n * `boot()` runs for every late-phase connector, in priority order, before\r\n * any of them `start()`s — so by the time this runs, the HTTP connector\r\n * (priority 5, before queue's 11) has already built its Fastify instance\r\n * and registered its own plugins, but has not yet called `listen()`.\r\n * Fastify refuses new plugin registrations after `listen()`, so this is\r\n * the only point in the boot sequence where mounting is possible.\r\n */\r\n async boot() {\r\n const queueConfig = options.config ?? (await readQueueConfig());\r\n\r\n if (!queueConfig?.dashboard?.enabled) {\r\n return;\r\n }\r\n\r\n // The dashboard resolves queues through the active config, so it has to\r\n // be registered here rather than only in `start()`, which runs after\r\n // every late connector has booted. `start()` sets it again; the setter\r\n // is idempotent for the same object.\r\n setQueueConfig(queueConfig);\r\n\r\n const { getHttpServer } = await import(\"@warlock.js/core\");\r\n\r\n await mountQueueDashboard(getHttpServer(), queueConfig);\r\n },\r\n async start() {\r\n const queueConfig = options.config ?? (await readQueueConfig());\r\n\r\n if (!queueConfig) {\r\n log.warn(\r\n \"queue\",\r\n \"configured\",\r\n \"queueConnector() is registered but no `queue` config was found (src/config/queue.ts); queue not started\",\r\n );\r\n return;\r\n }\r\n\r\n setQueueConfig(queueConfig);\r\n const started = await startWorkers();\r\n active = true;\r\n\r\n log.info(\r\n \"queue\",\r\n \"configured\",\r\n started.length > 0\r\n ? `Queue workers running for: ${started.join(\", \")}`\r\n : \"Queue configured (no in-process workers)\",\r\n );\r\n },\r\n async restart() {\r\n await connector.shutdown();\r\n await connector.start();\r\n },\r\n async shutdown() {\r\n if (!active) {\r\n return;\r\n }\r\n\r\n await closeQueue();\r\n resetQueueConfig();\r\n active = false;\r\n },\r\n shouldRestart(changedFiles) {\r\n return changedFiles.some((file) => {\r\n const normalized = file.replace(/\\\\/g, \"/\");\r\n\r\n return WATCHED_FILES.some((watched) => normalized === watched || normalized.endsWith(`/${watched}`));\r\n });\r\n },\r\n };\r\n\r\n return connector;\r\n}\r\n\r\nasync function readQueueConfig(): Promise<QueueConfig | undefined> {\r\n const { config } = await import(\"@warlock.js/core\");\r\n\r\n return config.get<QueueConfig | undefined>(\"queue\");\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["UnrecoverableError","Queue","Worker"],"sources":["../../../../../../queue/src/errors.ts","../../../../../../queue/src/config.ts","../../../../../../queue/src/dashboard-middleware-adapter.ts","../../../../../../queue/src/dashboard-guard-plugin.ts","../../../../../../queue/src/job-registry.ts","../../../../../../queue/src/queue-context.ts","../../../../../../queue/src/process-job.ts","../../../../../../queue/src/queue-manager.ts","../../../../../../queue/src/dashboard.ts","../../../../../../queue/src/queue-dashboard-unguarded.error.ts","../../../../../../queue/src/dashboard-boot.ts","../../../../../../queue/src/duration.ts","../../../../../../queue/src/define-job.ts","../../../../../../queue/src/failed-jobs.ts","../../../../../../queue/src/queue-connector.ts"],"sourcesContent":["/**\n * Thrown when the queue is used before `setQueueConfig` (or the queue\n * connector) supplied a configuration.\n */\nexport class QueueNotConfiguredError extends Error {\n public constructor() {\n super(\n \"@warlock.js/queue is not configured. Add src/config/queue.ts exporting a QueueConfig \" +\n \"and register queueConnector() in warlock.config.ts > connectors, \" +\n \"or call setQueueConfig() yourself.\",\n );\n this.name = \"QueueNotConfiguredError\";\n }\n}\n\n/**\n * Thrown for a malformed duration such as `\"10 minutes\"`.\n */\nexport class InvalidDurationError extends Error {\n public constructor(value: unknown) {\n super(\n `Invalid duration ${JSON.stringify(value)}: expected milliseconds as a non-negative number ` +\n `or a string like \"500ms\", \"30s\", \"10m\", \"2h\", \"1d\".`,\n );\n this.name = \"InvalidDurationError\";\n }\n}\n\n/**\n * Thrown by `defineJob` for an invalid definition.\n */\nexport class InvalidJobDefinitionError extends Error {\n public constructor(message: string) {\n super(message);\n this.name = \"InvalidJobDefinitionError\";\n }\n}\n\n/**\n * Thrown by `retryFailedJob` when no failed job has the given id.\n */\nexport class FailedJobNotFoundError extends Error {\n public constructor(id: string, queue: string) {\n super(`No failed job with id \"${id}\" on queue \"${queue}\".`);\n this.name = \"FailedJobNotFoundError\";\n }\n}\n\n/**\n * Thrown by `queueDashboard` when an optional bull-board package is not\n * installed.\n */\nexport class QueueDashboardDependencyError extends Error {\n public constructor(missing: string) {\n super(\n `The queue dashboard needs the optional package \"${missing}\", which is not installed.\\n` +\n \"Install both bull-board packages:\\n\\n\" +\n \" npm install @bull-board/api @bull-board/fastify\\n\",\n );\n this.name = \"QueueDashboardDependencyError\";\n }\n}\n\n/**\n * Throw from a job handler (or a queue context's `restore`) to fail the job\n * permanently — it is not retried, whatever `attempts` allows.\n */\nexport class UnrecoverableJobError extends Error {\n public constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"UnrecoverableJobError\";\n }\n}\n","import { QueueNotConfiguredError } from \"./errors\";\nimport type { QueueConfig } from \"./types\";\n\nlet activeConfig: QueueConfig | undefined;\n\n/**\n * Set the active queue configuration. In a Warlock app the queue connector\n * calls this at boot with `src/config/queue.ts`; scripts and tests may call\n * it directly. Replaces (does not merge) any previous configuration.\n */\nexport function setQueueConfig(config: QueueConfig): void {\n activeConfig = config;\n}\n\n/**\n * The active queue configuration. Throws {@link QueueNotConfiguredError}\n * when none was set.\n */\nexport function getQueueConfig(): QueueConfig {\n if (!activeConfig) {\n throw new QueueNotConfiguredError();\n }\n\n return activeConfig;\n}\n\n/** Forget the active configuration. */\nexport function resetQueueConfig(): void {\n activeConfig = undefined;\n}\n\n/** The queue a job runs on when it names none. */\nexport function defaultQueueName(): string {\n return activeConfig?.defaultQueue ?? \"default\";\n}\n","import type { Middleware } from \"@warlock.js/core\";\nimport type { FastifyReply, FastifyRequest } from \"fastify\";\n\n/**\n * Run a Warlock middleware list against a raw Fastify request/reply pair.\n *\n * Bull-board's `FastifyAdapter` hands back a plain Fastify plugin, not a\n * Warlock route — there is no `Route`, no validation pipeline, and none of\n * `createRequestStore`'s context wiring (CSP, tracing, `@warlock.js/context`\n * stores). Reusing that full pipeline here would pull the whole request\n * machinery into a place it was never meant to run. Instead this builds the\n * minimal `Request`/`Response` pair — enough for guard-style middleware\n * (`authMiddleware`, `ipFilterMiddleware`, a custom check) to read the\n * request and short-circuit with a response, which covers every one of\n * bull-board's routes because they all sit behind the same hook.\n *\n * `@warlock.js/core` is imported dynamically so this module never drags\n * core's runtime graph into a process that never mounts the dashboard.\n *\n * @returns `true` when a middleware sent a response and the caller must not\n * continue (bull-board's handler must not run); `false` to continue.\n */\nexport async function runDashboardMiddleware(\n middlewareList: Middleware[],\n fastifyRequest: FastifyRequest,\n fastifyReply: FastifyReply,\n): Promise<boolean> {\n if (middlewareList.length === 0) {\n return false;\n }\n\n const { Request, Response } = await import(\"@warlock.js/core\");\n\n const request = new Request();\n const response = new Response();\n\n response.setResponse(fastifyReply);\n request.response = response;\n response.request = request;\n request.setRequest(fastifyRequest);\n\n for (const middlewareFunction of middlewareList) {\n const result = await middlewareFunction({ request, response });\n\n if (result) {\n return true;\n }\n }\n\n return false;\n}\n","import type { Middleware } from \"@warlock.js/core\";\nimport type { FastifyInstance, FastifyPluginCallback } from \"fastify\";\nimport { runDashboardMiddleware } from \"./dashboard-middleware-adapter\";\n\n/**\n * Wrap bull-board's Fastify plugin in a scope that runs `middlewareList`\n * on every request before bull-board's own routes see it.\n *\n * Bull-board's `FastifyAdapter.registerPlugin()` returns a plain Fastify\n * plugin with no hook for Warlock middleware to run through, and Fastify\n * only lets an `onRequest` hook be added to a plugin scope — never spliced\n * into a plugin someone else wrote. So this builds ONE plugin that adds the\n * hook to its own scope and then registers bull-board's plugin as a child of\n * that scope; Fastify's encapsulation runs the hook for every route the\n * child registers, which is every dashboard route.\n */\nexport function buildDashboardGuardPlugin(\n middlewareList: Middleware[],\n bullBoardPlugin: unknown,\n): FastifyPluginCallback {\n return function dashboardGuardPlugin(instance: FastifyInstance, _options, done) {\n if (middlewareList.length > 0) {\n instance.addHook(\"onRequest\", async (request, reply) => {\n const handled = await runDashboardMiddleware(middlewareList, request, reply);\n\n if (handled) {\n // Returning the reply is Fastify's own way for an `onRequest` hook to\n // END the lifecycle, so bull-board's handler never runs. Relying\n // instead on Fastify noticing the reply was already sent leaves the\n // outcome to write ORDERING: a guard answering asynchronously can lose\n // that race and let the dashboard render to an unauthenticated caller.\n // The adapter returns this boolean precisely so the decision is\n // explicit rather than emergent.\n return reply;\n }\n });\n }\n\n instance.register(bullBoardPlugin as never);\n done();\n };\n}\n","import { defaultQueueName } from \"./config\";\nimport type { JobDefinition } from \"./types\";\n\n/** A registered definition, payload/result erased for storage. */\nexport type RegisteredJob = JobDefinition<unknown, unknown>;\n\ntype RegistryListener = (job: RegisteredJob) => void;\n\nconst jobs = new Map<string, RegisteredJob>();\nconst listeners = new Set<RegistryListener>();\n\n/**\n * Register a definition under its name.\n *\n * Re-registering a name REPLACES the previous definition: in development a\n * job module is re-evaluated on every reload, and refusing the second\n * evaluation would break the reload. Job names must therefore be unique\n * across the app — two different modules using one name leave only the\n * later handler active.\n */\nexport function registerJob(job: RegisteredJob): void {\n jobs.set(job.name, job);\n\n for (const listener of listeners) {\n listener(job);\n }\n}\n\n/** The definition registered under `name`, if any. */\nexport function findRegisteredJob(name: string): RegisteredJob | undefined {\n return jobs.get(name);\n}\n\n/** Every registered definition. */\nexport function registeredJobs(): RegisteredJob[] {\n return [...jobs.values()];\n}\n\n/** The queue a definition runs on, resolved against the active config. */\nexport function queueOf(job: Pick<RegisteredJob, \"queue\">): string {\n return job.queue ?? defaultQueueName();\n}\n\n/** Be told whenever a job is registered. Returns an unsubscribe function. */\nexport function onJobRegistered(listener: RegistryListener): () => void {\n listeners.add(listener);\n\n return () => listeners.delete(listener);\n}\n","/**\n * Carries ambient state (tenant, request id, …) from the code that\n * dispatches a job into the worker that runs it.\n */\nexport type QueueContext<T> = {\n /** Read the ambient state at dispatch time; `undefined` means nothing to carry. */\n capture: () => T | undefined;\n /** Run `run` with the captured state re-established. */\n restore: (captured: T, run: () => Promise<unknown>) => Promise<unknown>;\n};\n\n/**\n * Define a queue context. `restore`'s `captured` is inferred from `capture`.\n *\n * @example\n * setQueueContext(\n * defineQueueContext({\n * capture: () => tenantStorage.getStore(),\n * restore: (tenant, run) => tenantStorage.run(tenant, run),\n * }),\n * );\n */\nexport function defineQueueContext<T>(context: QueueContext<T>): QueueContext<T> {\n return context;\n}\n\nlet activeContext: QueueContext<any> | undefined;\n\n/** Register the queue context used by every job dispatch and run. */\nexport function setQueueContext(context: QueueContext<any>): void {\n activeContext = context;\n}\n\n/** The registered queue context, if any. */\nexport function getQueueContext(): QueueContext<any> | undefined {\n return activeContext;\n}\n\n/** Forget the registered queue context. */\nexport function resetQueueContext(): void {\n activeContext = undefined;\n}\n\n/** The job-data envelope written when a context was captured. */\nexport type QueueEnvelope = {\n __warlock: { v: 1; context?: unknown };\n payload: unknown;\n};\n\n/** Wrap a payload with the captured context; no context leaves it bare. */\nexport function wrapPayload(payload: unknown, context: unknown): unknown {\n if (context === undefined) return payload;\n\n return { __warlock: { v: 1, context }, payload } satisfies QueueEnvelope;\n}\n\n/** Split job data into payload and captured context; bare data passes through. */\nexport function unwrapPayload(data: unknown): { payload: unknown; context: unknown } {\n const envelope = data as Partial<QueueEnvelope> | null;\n\n if (\n envelope !== null &&\n typeof envelope === \"object\" &&\n typeof envelope.__warlock === \"object\" &&\n envelope.__warlock !== null &&\n envelope.__warlock.v === 1 &&\n \"payload\" in envelope\n ) {\n return { payload: envelope.payload, context: envelope.__warlock.context };\n }\n\n return { payload: data, context: undefined };\n}\n","import { type Job, UnrecoverableError } from \"bullmq\";\nimport { UnrecoverableJobError } from \"./errors\";\nimport { findRegisteredJob } from \"./job-registry\";\nimport { getQueueContext, unwrapPayload } from \"./queue-context\";\nimport type { JobContext } from \"./types\";\n\n/**\n * The single BullMQ processor every worker runs: route the job to the\n * handler registered under its name.\n *\n * A name with no handler in this process fails with `UnrecoverableError` —\n * retrying cannot make a missing definition appear, so it must not burn\n * through its attempts.\n */\nexport async function processJob(job: Job): Promise<unknown> {\n const definition = findRegisteredJob(job.name);\n\n if (!definition) {\n throw new UnrecoverableError(\n `No job named \"${job.name}\" is defined in this process. ` +\n \"Make sure the module that calls defineJob() is imported by the worker process.\",\n );\n }\n\n const { payload, context: captured } = unwrapPayload(job.data);\n\n const context: JobContext = {\n id: String(job.id),\n name: job.name,\n queue: job.queueName,\n attempt: job.attemptsMade + 1,\n maxAttempts: job.opts.attempts ?? 1,\n progress: (value) => job.updateProgress(value),\n log: async (line) => {\n await job.log(line);\n },\n context: captured,\n };\n\n const run = async () => definition.handle(payload, context);\n const queueContext = getQueueContext();\n\n try {\n return captured !== undefined && queueContext ? await queueContext.restore(captured, run) : await run();\n } catch (error) {\n if (error instanceof UnrecoverableJobError) {\n throw new UnrecoverableError(error.message);\n }\n\n throw error;\n }\n}\n","import { log } from \"@warlock.js/logger\";\nimport { Queue, Worker } from \"bullmq\";\nimport { getQueueConfig } from \"./config\";\nimport { onJobRegistered, queueOf, registeredJobs } from \"./job-registry\";\nimport { processJob } from \"./process-job\";\n\nconst DEFAULT_SHUTDOWN_TIMEOUT = 30_000;\n\nconst queues = new Map<string, Queue>();\nconst workers = new Map<string, Worker>();\nlet stopListening: (() => void) | undefined;\n\n/**\n * The BullMQ queue for `name`, created on first use with the configured\n * connection and prefix.\n */\nexport function getQueue(name: string): Queue {\n let queue = queues.get(name);\n\n if (!queue) {\n const config = getQueueConfig();\n\n queue = new Queue(name, {\n connection: config.connection,\n prefix: config.prefix ?? \"warlock\",\n });\n\n queue.on(\"error\", (error) => {\n log.error(\"queue\", \"connection\", error);\n });\n\n queues.set(name, queue);\n }\n\n return queue;\n}\n\n/**\n * Start one worker per queue that has a registered job, and keep starting\n * workers for queues whose first job is defined later.\n *\n * A no-op returning `[]` when `workers.enabled` is `false`. Calling it again\n * while workers run starts only the missing ones.\n *\n * @returns the queue names that now have a worker in this process.\n */\nexport async function startWorkers(): Promise<string[]> {\n const config = getQueueConfig();\n\n if (config.workers?.enabled === false) {\n return [];\n }\n\n for (const job of registeredJobs()) {\n ensureWorker(queueOf(job));\n }\n\n stopListening ??= onJobRegistered((job) => {\n ensureWorker(queueOf(job));\n });\n\n await Promise.all([...workers.values()].map((worker) => worker.waitUntilReady()));\n\n return [...workers.keys()];\n}\n\n/** The queue names with a running worker in this process. */\nexport function runningWorkers(): string[] {\n return [...workers.keys()];\n}\n\nfunction ensureWorker(queueName: string): void {\n if (workers.has(queueName)) {\n return;\n }\n\n const config = getQueueConfig();\n\n const worker = new Worker(queueName, processJob, {\n connection: config.connection,\n prefix: config.prefix ?? \"warlock\",\n concurrency: config.workers?.concurrency ?? 1,\n });\n\n worker.on(\"error\", (error) => {\n log.error(\"queue\", \"worker\", error);\n });\n\n worker.on(\"failed\", (job, error) => {\n log.error(\"queue\", \"job.failed\", `${job?.name ?? \"unknown\"} (${job?.id ?? \"?\"}): ${error.message}`);\n });\n\n workers.set(queueName, worker);\n}\n\nexport type CloseQueueOptions = {\n /**\n * How long to wait for active jobs before force-closing workers, in\n * milliseconds. Default: `workers.shutdownTimeout`, else `30000`.\n */\n timeout?: number;\n};\n\n/**\n * Graceful shutdown: stop workers taking new jobs and wait for active ones\n * (bounded by `timeout`, then force-close), then close every queue\n * connection. Safe to call when nothing was started, and more than once.\n */\nexport async function closeQueue(options: CloseQueueOptions = {}): Promise<void> {\n stopListening?.();\n stopListening = undefined;\n\n const timeout = options.timeout ?? configuredShutdownTimeout();\n const closingWorkers = [...workers.values()];\n workers.clear();\n\n await Promise.all(closingWorkers.map((worker) => closeWorker(worker, timeout)));\n\n const closingQueues = [...queues.values()];\n queues.clear();\n\n await Promise.all(closingQueues.map((queue) => queue.close()));\n}\n\nfunction configuredShutdownTimeout(): number {\n try {\n return getQueueConfig().workers?.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT;\n } catch {\n return DEFAULT_SHUTDOWN_TIMEOUT;\n }\n}\n\nasync function closeWorker(worker: Worker, timeout: number): Promise<void> {\n let timer: NodeJS.Timeout | undefined;\n\n const timedOut = new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), timeout);\n });\n\n // A close already in progress cannot be upgraded to a forced one (BullMQ\n // returns the pending promise), so the timeout path drops the connections\n // instead and lets the graceful close settle in the background.\n const closing = worker.close().then(() => \"closed\" as const);\n closing.catch(() => undefined);\n\n const outcome = await Promise.race([closing, timedOut]);\n\n clearTimeout(timer);\n\n if (outcome === \"timeout\") {\n log.warn(\n \"queue\",\n \"shutdown\",\n `Worker for \"${worker.name}\" still had active jobs after ${timeout}ms; disconnecting. ` +\n \"Those jobs are retried once their lock expires.\",\n );\n\n await worker.disconnect();\n }\n}\n","import type { Middleware } from \"@warlock.js/core\";\nimport { defaultQueueName } from \"./config\";\nimport { buildDashboardGuardPlugin } from \"./dashboard-guard-plugin\";\nimport { QueueDashboardDependencyError } from \"./errors\";\nimport { queueOf, registeredJobs } from \"./job-registry\";\nimport { getQueue } from \"./queue-manager\";\n\n/**\n * The part of a Fastify instance the dashboard needs. Warlock's HTTP server\n * (`getHttpServer()` from `@warlock.js/core`) satisfies it.\n */\nexport type DashboardServer = {\n register(plugin: never, options: { prefix: string }): unknown;\n};\n\nexport type QueueDashboardOptions = {\n /** URL prefix the dashboard is mounted on. Default `\"/admin/queues\"`. */\n basePath?: string;\n /** Queues to show. Default: every queue with a defined job, plus the default queue. */\n queues?: string[];\n /**\n * Run before every dashboard route. Applied via a wrapping Fastify plugin\n * scope, since bull-board's own plugin has no hook to splice Warlock\n * middleware into — see `dashboard-guard-plugin.ts`.\n */\n middleware?: Middleware[];\n};\n\ntype BullBoardModules = {\n createBullBoard: (options: { queues: unknown[]; serverAdapter: unknown }) => unknown;\n BullMQAdapter: new (queue: unknown) => unknown;\n FastifyAdapter: new () => {\n setBasePath(path: string): unknown;\n registerPlugin(): unknown;\n };\n};\n\n/**\n * Mount the bull-board failed/active/delayed job UI on Warlock's Fastify\n * server. Requires the optional packages `@bull-board/api` and\n * `@bull-board/fastify`; they are loaded only when this is called, and a\n * missing one throws {@link QueueDashboardDependencyError}.\n *\n * Call it before the HTTP server starts listening, and put it behind your\n * own authentication — the dashboard can retry and delete jobs.\n *\n * @example\n * import { getHttpServer } from \"@warlock.js/core\";\n * await queueDashboard(getHttpServer(), { basePath: \"/admin/queues\" });\n */\nexport async function queueDashboard(\n server: DashboardServer,\n options: QueueDashboardOptions = {},\n): Promise<void> {\n const basePath = options.basePath ?? \"/admin/queues\";\n const { createBullBoard, BullMQAdapter, FastifyAdapter } = await loadBullBoard();\n const queueNames =\n options.queues ?? [...new Set([defaultQueueName(), ...registeredJobs().map((job) => queueOf(job))])];\n\n const serverAdapter = new FastifyAdapter();\n serverAdapter.setBasePath(basePath);\n\n createBullBoard({\n queues: queueNames.map((name) => new BullMQAdapter(getQueue(name))),\n serverAdapter,\n });\n\n const guardedPlugin = buildDashboardGuardPlugin(options.middleware ?? [], serverAdapter.registerPlugin());\n\n await server.register(guardedPlugin as never, { prefix: basePath });\n}\n\n/**\n * Load the optional bull-board packages. Exported for tests of the missing\n * dependency path; `importer` defaults to a real dynamic import.\n */\nexport async function loadBullBoard(\n importer: (specifier: string) => Promise<Record<string, unknown>> = (specifier) => import(specifier),\n): Promise<BullBoardModules> {\n const api = await importOptional(importer, \"@bull-board/api\");\n const adapter = await importOptional(importer, \"@bull-board/api/bullMQAdapter\");\n const fastify = await importOptional(importer, \"@bull-board/fastify\");\n\n return {\n createBullBoard: api.createBullBoard as BullBoardModules[\"createBullBoard\"],\n BullMQAdapter: adapter.BullMQAdapter as BullBoardModules[\"BullMQAdapter\"],\n FastifyAdapter: fastify.FastifyAdapter as BullBoardModules[\"FastifyAdapter\"],\n };\n}\n\nasync function importOptional(\n importer: (specifier: string) => Promise<Record<string, unknown>>,\n specifier: string,\n): Promise<Record<string, unknown>> {\n try {\n return await importer(specifier);\n } catch (error) {\n if (isModuleNotFound(error)) {\n throw new QueueDashboardDependencyError(specifier.split(\"/\").slice(0, 2).join(\"/\"));\n }\n\n throw error;\n }\n}\n\nfunction isModuleNotFound(error: unknown): boolean {\n const code = (error as { code?: unknown } | undefined)?.code;\n\n return code === \"ERR_MODULE_NOT_FOUND\" || code === \"MODULE_NOT_FOUND\";\n}\n","/**\n * Thrown at boot when `queue.dashboard.enabled` is `true` in production with\n * no guard middleware. The dashboard can retry and delete jobs; mounting it\n * on the open internet without a guard is a production incident waiting to\n * happen, so this fails the boot instead of shipping the hole.\n */\nexport class QueueDashboardUnguardedError extends Error {\n public constructor() {\n super(\n \"queue.dashboard.enabled is true in production with no middleware. The dashboard can \" +\n \"retry and delete jobs, so it must be guarded before it is exposed.\\n\\n\" +\n \"Add a guard middleware:\\n\\n\" +\n \" import { middleware } from \\\"@warlock.js/core\\\";\\n\" +\n \" import { authMiddleware } from \\\"@warlock.js/auth\\\";\\n\\n\" +\n \" const queueConfig: QueueConfig = {\\n\" +\n \" // ...\\n\" +\n \" dashboard: {\\n\" +\n \" enabled: true,\\n\" +\n \" middleware: [authMiddleware(\\\"admin\\\")],\\n\" +\n \" },\\n\" +\n \" };\\n\",\n );\n this.name = \"QueueDashboardUnguardedError\";\n }\n}\n","import { log } from \"@warlock.js/logger\";\nimport { type DashboardServer, queueDashboard } from \"./dashboard\";\nimport { QueueDashboardUnguardedError } from \"./queue-dashboard-unguarded.error\";\nimport type { QueueConfig } from \"./types\";\n\n/** Default path the dashboard mounts on when `dashboard.path` is not set. */\nexport const DEFAULT_DASHBOARD_PATH = \"/admin/queues\";\n\n/**\n * Mount the bull-board dashboard for `queue.dashboard` in `config`, applying\n * the safety rule: `enabled` in production with no middleware throws\n * {@link QueueDashboardUnguardedError} instead of booting exposed; outside\n * production with no middleware it logs one warning and mounts anyway.\n *\n * Called by `queueConnector()` at boot, once the HTTP server exists but\n * before it starts listening — see `queue-connector.ts`. Exported so it can\n * be unit-tested without going through the whole connector lifecycle.\n */\nexport async function mountQueueDashboard(\n server: DashboardServer | undefined,\n config: QueueConfig,\n): Promise<void> {\n const dashboard = config.dashboard;\n\n if (!dashboard?.enabled) {\n return;\n }\n\n const middlewareList = dashboard.middleware ?? [];\n\n if (middlewareList.length === 0) {\n if (process.env.NODE_ENV === \"production\") {\n throw new QueueDashboardUnguardedError();\n }\n\n log.warn(\n \"queue\",\n \"dashboard\",\n \"queue.dashboard.enabled is true with no middleware. The dashboard can retry and delete \" +\n \"jobs — add a guard middleware before this ships to production.\",\n );\n }\n\n if (!server) {\n log.warn(\n \"queue\",\n \"dashboard\",\n \"queue.dashboard.enabled is true but no HTTP server was found; the dashboard was not mounted.\",\n );\n\n return;\n }\n\n await queueDashboard(server, {\n basePath: dashboard.path ?? DEFAULT_DASHBOARD_PATH,\n middleware: middlewareList,\n });\n}\n","import { InvalidDurationError } from \"./errors\";\nimport type { Duration } from \"./types\";\n\nconst UNIT_MILLISECONDS: Record<string, number> = {\n ms: 1,\n s: 1_000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n};\n\nconst DURATION_PATTERN = /^(\\d+(?:\\.\\d+)?)(ms|s|m|h|d)$/;\n\n/**\n * Convert a {@link Duration} to milliseconds. Numbers are already\n * milliseconds. Anything else is rejected loudly rather than guessed at.\n */\nexport function toMilliseconds(value: Duration | string): number {\n if (typeof value === \"number\") {\n if (!Number.isFinite(value) || value < 0) {\n throw new InvalidDurationError(value);\n }\n\n return Math.round(value);\n }\n\n const match = typeof value === \"string\" ? DURATION_PATTERN.exec(value.trim()) : null;\n\n if (!match) {\n throw new InvalidDurationError(value);\n }\n\n return Math.round(Number(match[1]) * UNIT_MILLISECONDS[match[2]!]!);\n}\n","import type { Job, JobsOptions } from \"bullmq\";\r\nimport { getQueueConfig } from \"./config\";\r\nimport { toMilliseconds } from \"./duration\";\r\nimport { InvalidJobDefinitionError } from \"./errors\";\r\nimport { getQueueContext, unwrapPayload, wrapPayload } from \"./queue-context\";\r\nimport { queueOf, registerJob, type RegisteredJob } from \"./job-registry\";\r\nimport { getQueue } from \"./queue-manager\";\r\nimport type {\r\n DispatchOptions,\r\n JobBackoff,\r\n JobDefinition,\r\n JobOptions,\r\n JobSnapshot,\r\n JobState,\r\n QueueJob,\r\n} from \"./types\";\r\n\r\n/**\r\n * Define a background job.\r\n *\r\n * The definition is registered by name so any worker in the process can run\r\n * it; the returned object dispatches it with a typed payload.\r\n *\r\n * @example\r\n * export const sendInvoice = defineJob({\r\n * name: \"invoices.send\",\r\n * attempts: 5,\r\n * backoff: { type: \"exponential\", delay: 2000 },\r\n * async handle(payload: { invoiceId: string }, ctx) {\r\n * await ctx.progress(50);\r\n * },\r\n * });\r\n *\r\n * await sendInvoice.dispatch({ invoiceId: \"42\" }, { delay: \"10m\", priority: 1 });\r\n */\r\nexport function defineJob<TPayload, TResult = unknown>(\r\n definition: JobDefinition<TPayload, TResult>,\r\n): QueueJob<TPayload, TResult> {\r\n assertValidDefinition(definition);\r\n registerJob(definition as RegisteredJob);\r\n\r\n return {\r\n name: definition.name,\r\n get queue() {\r\n return queueOf(definition);\r\n },\r\n async dispatch(payload, options = {}) {\r\n const queueName = queueOf(definition);\r\n // Capture synchronously, in the caller's async context.\r\n const captured = definition.context === false ? undefined : getQueueContext()?.capture();\r\n const job = await getQueue(queueName).add(\r\n definition.name,\r\n wrapPayload(payload, captured),\r\n toBullJobOptions(definition, options),\r\n );\r\n\r\n return { id: String(job.id), name: definition.name, queue: queueName };\r\n },\r\n async find(id) {\r\n const queue = getQueue(queueOf(definition));\r\n const initialJob = await queue.getJob(id);\r\n\r\n if (!initialJob || initialJob.name !== definition.name) {\r\n return undefined;\r\n }\r\n\r\n // Read the state first, then (re)fetch the job. BullMQ writes a job's\r\n // result/attemptsMade/finishedOn fields *before* it becomes visible\r\n // under a new state, so re-reading the job after the state is known\r\n // guarantees those fields are consistent with the reported state\r\n // (rather than reflecting a moment before the job finished).\r\n const state = (await initialJob.getState()) as JobState;\r\n const job = (await queue.getJob(id)) ?? initialJob;\r\n\r\n return toSnapshot<TPayload, TResult>(job, state);\r\n },\r\n };\r\n}\r\n\r\nfunction assertValidDefinition(definition: JobDefinition<unknown, unknown>): void {\r\n if (typeof definition.name !== \"string\" || definition.name.trim() === \"\") {\r\n throw new InvalidJobDefinitionError(\"defineJob() requires a non-empty `name`.\");\r\n }\r\n\r\n if (typeof definition.handle !== \"function\") {\r\n throw new InvalidJobDefinitionError(\r\n `defineJob(\"${definition.name}\") requires a \\`handle(payload, ctx)\\` function.`,\r\n );\r\n }\r\n\r\n if (definition.attempts !== undefined && !(Number.isInteger(definition.attempts) && definition.attempts >= 1)) {\r\n throw new InvalidJobDefinitionError(\r\n `defineJob(\"${definition.name}\"): \\`attempts\\` must be an integer >= 1, got ${definition.attempts}.`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Merge app defaults < job definition < dispatch options into BullMQ's shape.\r\n */\r\nfunction toBullJobOptions(definition: JobOptions, options: DispatchOptions): JobsOptions {\r\n const defaults = getQueueConfig().defaultJobOptions ?? {};\r\n const attempts = options.attempts ?? definition.attempts ?? defaults.attempts;\r\n const backoff = options.backoff ?? definition.backoff ?? defaults.backoff;\r\n const removeOnComplete = definition.removeOnComplete ?? defaults.removeOnComplete;\r\n const removeOnFail = definition.removeOnFail ?? defaults.removeOnFail;\r\n\r\n const bullOptions: JobsOptions = {};\r\n\r\n if (attempts !== undefined) bullOptions.attempts = attempts;\r\n if (backoff !== undefined) bullOptions.backoff = toBullBackoff(backoff);\r\n if (removeOnComplete !== undefined) bullOptions.removeOnComplete = removeOnComplete;\r\n if (removeOnFail !== undefined) bullOptions.removeOnFail = removeOnFail;\r\n if (options.delay !== undefined) bullOptions.delay = toMilliseconds(options.delay);\r\n if (options.priority !== undefined) bullOptions.priority = options.priority;\r\n if (options.jobId !== undefined) bullOptions.jobId = options.jobId;\r\n\r\n return bullOptions;\r\n}\r\n\r\nfunction toBullBackoff(backoff: JobBackoff): JobsOptions[\"backoff\"] {\r\n return typeof backoff === \"number\" ? { type: \"fixed\", delay: backoff } : backoff;\r\n}\r\n\r\n/**\r\n * A plain view of a BullMQ job.\r\n *\r\n * @param job The job to read fields from.\r\n * @param state The job's state; pass a state read *before* `job` was\r\n * fetched (or re-fetched) so the returned snapshot's fields are consistent\r\n * with it. If omitted, the state is read from `job` directly.\r\n */\r\nexport async function toSnapshot<TPayload, TResult>(\r\n job: Job,\r\n state?: JobState,\r\n): Promise<JobSnapshot<TPayload, TResult>> {\r\n const resolvedState = state ?? ((await job.getState()) as JobState);\r\n\r\n return {\r\n id: String(job.id),\r\n name: job.name,\r\n queue: job.queueName,\r\n state: resolvedState,\r\n payload: unwrapPayload(job.data).payload as TPayload,\r\n progress: job.progress as JobSnapshot[\"progress\"],\r\n attemptsMade: job.attemptsMade,\r\n result: job.returnvalue as TResult | undefined,\r\n failedReason: job.failedReason || undefined,\r\n createdAt: new Date(job.timestamp),\r\n finishedAt: job.finishedOn ? new Date(job.finishedOn) : undefined,\r\n };\r\n}\r\n","import { unwrapPayload } from \"./queue-context\";\nimport type { Job } from \"bullmq\";\nimport { defaultQueueName } from \"./config\";\nimport { FailedJobNotFoundError } from \"./errors\";\nimport { getQueue } from \"./queue-manager\";\nimport type { FailedJob } from \"./types\";\n\nexport type FailedJobsOptions = {\n /** Queue to read. Default: the default queue. */\n queue?: string;\n /** First index (newest first). Default `0`. */\n start?: number;\n /** Last index, inclusive. Default `99`. */\n end?: number;\n};\n\n/**\n * List failed jobs, newest first — jobs that used up every attempt, or\n * failed unrecoverably. Each entry can be retried.\n */\nexport async function failedJobs(options: FailedJobsOptions = {}): Promise<FailedJob[]> {\n const queueName = options.queue ?? defaultQueueName();\n const jobs = await getQueue(queueName).getFailed(options.start ?? 0, options.end ?? 99);\n\n return jobs.map((job) => toFailedJob(job));\n}\n\n/**\n * Retry one failed job by id: it goes back to waiting with its attempts reset.\n * Throws {@link FailedJobNotFoundError} when no FAILED job has that id.\n */\nexport async function retryFailedJob(id: string, options: { queue?: string } = {}): Promise<void> {\n const queueName = options.queue ?? defaultQueueName();\n const job = await getQueue(queueName).getJob(id);\n\n if (!job || !(await job.isFailed())) {\n throw new FailedJobNotFoundError(id, queueName);\n }\n\n await job.retry(\"failed\");\n}\n\nfunction toFailedJob(job: Job): FailedJob {\n return {\n id: String(job.id),\n name: job.name,\n queue: job.queueName,\n payload: unwrapPayload(job.data).payload,\n attemptsMade: job.attemptsMade,\n failedReason: job.failedReason,\n stacktrace: job.stacktrace ?? [],\n failedAt: job.finishedOn ? new Date(job.finishedOn) : undefined,\n retry: () => job.retry(\"failed\"),\n };\n}\n","/**\r\n * The queue's connector for `warlock.config.ts > connectors`.\r\n *\r\n * Deliberately a plain object with TYPE-ONLY imports from core: the config\r\n * file that constructs it must not drag core's runtime graph (or BullMQ) in\r\n * at config-load time. Core is imported lazily inside `start()`, where the\r\n * app has already loaded it.\r\n */\r\nimport type { Connector, ConnectorLifecyclePhase } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { resetQueueConfig, setQueueConfig } from \"./config\";\r\nimport { mountQueueDashboard } from \"./dashboard-boot\";\r\nimport { closeQueue, startWorkers } from \"./queue-manager\";\r\nimport type { QueueConfig } from \"./types\";\r\n\r\n/**\r\n * Boots after every built-in connector (`ConnectorPriority.AI` is `10`), so\r\n * the logger is up and anything a job handler needs is already connected;\r\n * shuts down before them for the same reason.\r\n */\r\nexport const QUEUE_CONNECTOR_PRIORITY = 11;\r\n\r\nconst WATCHED_FILES = [\"src/config/queue.ts\"];\r\n\r\nexport type QueueConnectorOptions = {\r\n /**\r\n * Supply the configuration directly instead of reading the `queue` config\r\n * key (`src/config/queue.ts`).\r\n */\r\n config?: QueueConfig;\r\n};\r\n\r\n/**\r\n * Construct the queue connector.\r\n *\r\n * Runs in the `late` lifecycle phase — after app code is imported — so every\r\n * `defineJob` in the app has registered before workers start. At start it\r\n * reads the `queue` config and starts in-process workers unless\r\n * `workers.enabled` is `false`; at shutdown it closes workers (waiting for\r\n * active jobs, bounded by `workers.shutdownTimeout`) and then the queues.\r\n *\r\n * @example\r\n * // warlock.config.ts\r\n * import { queueConnector } from \"@warlock.js/queue\";\r\n *\r\n * export default defineConfig({ connectors: [queueConnector()] });\r\n */\r\nexport function queueConnector(options: QueueConnectorOptions = {}): Connector {\r\n let active = false;\r\n\r\n const connector: Connector = {\r\n name: \"queue\",\r\n priority: QUEUE_CONNECTOR_PRIORITY,\r\n // Core's `ConnectorLifecyclePhase.Late`; the value is spelled out so this\r\n // module stays free of a runtime import of core.\r\n lifecyclePhase: \"late\" as ConnectorLifecyclePhase,\r\n isActive: () => active,\r\n /**\r\n * Mounts the dashboard, when configured, here rather than in `start()`:\r\n * `boot()` runs for every late-phase connector, in priority order, before\r\n * any of them `start()`s — so by the time this runs, the HTTP connector\r\n * (priority 5, before queue's 11) has already built its Fastify instance\r\n * and registered its own plugins, but has not yet called `listen()`.\r\n * Fastify refuses new plugin registrations after `listen()`, so this is\r\n * the only point in the boot sequence where mounting is possible.\r\n */\r\n async boot() {\r\n const queueConfig = options.config ?? (await readQueueConfig());\r\n\r\n if (!queueConfig?.dashboard?.enabled) {\r\n return;\r\n }\r\n\r\n // The dashboard resolves queues through the active config, so it has to\r\n // be registered here rather than only in `start()`, which runs after\r\n // every late connector has booted. `start()` sets it again; the setter\r\n // is idempotent for the same object.\r\n setQueueConfig(queueConfig);\r\n\r\n const { getHttpServer } = await import(\"@warlock.js/core\");\r\n\r\n await mountQueueDashboard(getHttpServer(), queueConfig);\r\n },\r\n async start() {\r\n const queueConfig = options.config ?? (await readQueueConfig());\r\n\r\n if (!queueConfig) {\r\n log.warn(\r\n \"queue\",\r\n \"configured\",\r\n \"queueConnector() is registered but no `queue` config was found (src/config/queue.ts); queue not started\",\r\n );\r\n return;\r\n }\r\n\r\n setQueueConfig(queueConfig);\r\n const started = await startWorkers();\r\n active = true;\r\n\r\n log.info(\r\n \"queue\",\r\n \"configured\",\r\n started.length > 0\r\n ? `Queue workers running for: ${started.join(\", \")}`\r\n : \"Queue configured (no in-process workers)\",\r\n );\r\n },\r\n async restart() {\r\n await connector.shutdown();\r\n await connector.start();\r\n },\r\n async shutdown() {\r\n if (!active) {\r\n return;\r\n }\r\n\r\n await closeQueue();\r\n resetQueueConfig();\r\n active = false;\r\n },\r\n shouldRestart(changedFiles) {\r\n return changedFiles.some((file) => {\r\n const normalized = file.replace(/\\\\/g, \"/\");\r\n\r\n return WATCHED_FILES.some((watched) => normalized === watched || normalized.endsWith(`/${watched}`));\r\n });\r\n },\r\n };\r\n\r\n return connector;\r\n}\r\n\r\nasync function readQueueConfig(): Promise<QueueConfig | undefined> {\r\n const { config } = await import(\"@warlock.js/core\");\r\n\r\n return config.get<QueueConfig | undefined>(\"queue\");\r\n}\r\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"define-job.d.mts","names":[],"sources":["../../../../../../queue/src/define-job.ts"],"mappings":";;;;AAkCA;;;;;;;;;;;;;;;;iBAAgB,SAAA,8BACd,UAAA,EAAY,aAAA,CAAc,QAAA,EAAU,OAAA,IACnC,QAAA,CAAS,QAAA,EAAU,OAAA"}
1
+ {"version":3,"file":"define-job.d.mts","names":[],"sources":["../../../../../../queue/src/define-job.ts"],"mappings":";;;;AAmCA;;;;;;;;;;;;;;;;iBAAgB,SAAA,8BACd,UAAA,EAAY,aAAA,CAAc,QAAA,EAAU,OAAA,IACnC,QAAA,CAAS,QAAA,EAAU,OAAA"}
@@ -1,6 +1,7 @@
1
1
  import { InvalidJobDefinitionError } from "./errors.mjs";
2
2
  import { getQueueConfig } from "./config.mjs";
3
3
  import { queueOf, registerJob } from "./job-registry.mjs";
4
+ import { getQueueContext, unwrapPayload, wrapPayload } from "./queue-context.mjs";
4
5
  import { getQueue } from "./queue-manager.mjs";
5
6
  import { toMilliseconds } from "./duration.mjs";
6
7
 
@@ -33,7 +34,8 @@ function defineJob(definition) {
33
34
  },
34
35
  async dispatch(payload, options = {}) {
35
36
  const queueName = queueOf(definition);
36
- const job = await getQueue(queueName).add(definition.name, payload, toBullJobOptions(definition, options));
37
+ const captured = definition.context === false ? void 0 : getQueueContext()?.capture();
38
+ const job = await getQueue(queueName).add(definition.name, wrapPayload(payload, captured), toBullJobOptions(definition, options));
37
39
  return {
38
40
  id: String(job.id),
39
41
  name: definition.name,
@@ -94,7 +96,7 @@ async function toSnapshot(job, state) {
94
96
  name: job.name,
95
97
  queue: job.queueName,
96
98
  state: resolvedState,
97
- payload: job.data,
99
+ payload: unwrapPayload(job.data).payload,
98
100
  progress: job.progress,
99
101
  attemptsMade: job.attemptsMade,
100
102
  result: job.returnvalue,
@@ -1 +1 @@
1
- {"version":3,"file":"define-job.mjs","names":[],"sources":["../../../../../../queue/src/define-job.ts"],"sourcesContent":["import type { Job, JobsOptions } from \"bullmq\";\r\nimport { getQueueConfig } from \"./config\";\r\nimport { toMilliseconds } from \"./duration\";\r\nimport { InvalidJobDefinitionError } from \"./errors\";\r\nimport { queueOf, registerJob, type RegisteredJob } from \"./job-registry\";\r\nimport { getQueue } from \"./queue-manager\";\r\nimport type {\r\n DispatchOptions,\r\n JobBackoff,\r\n JobDefinition,\r\n JobOptions,\r\n JobSnapshot,\r\n JobState,\r\n QueueJob,\r\n} from \"./types\";\r\n\r\n/**\r\n * Define a background job.\r\n *\r\n * The definition is registered by name so any worker in the process can run\r\n * it; the returned object dispatches it with a typed payload.\r\n *\r\n * @example\r\n * export const sendInvoice = defineJob({\r\n * name: \"invoices.send\",\r\n * attempts: 5,\r\n * backoff: { type: \"exponential\", delay: 2000 },\r\n * async handle(payload: { invoiceId: string }, ctx) {\r\n * await ctx.progress(50);\r\n * },\r\n * });\r\n *\r\n * await sendInvoice.dispatch({ invoiceId: \"42\" }, { delay: \"10m\", priority: 1 });\r\n */\r\nexport function defineJob<TPayload, TResult = unknown>(\r\n definition: JobDefinition<TPayload, TResult>,\r\n): QueueJob<TPayload, TResult> {\r\n assertValidDefinition(definition);\r\n registerJob(definition as RegisteredJob);\r\n\r\n return {\r\n name: definition.name,\r\n get queue() {\r\n return queueOf(definition);\r\n },\r\n async dispatch(payload, options = {}) {\r\n const queueName = queueOf(definition);\r\n const job = await getQueue(queueName).add(\r\n definition.name,\r\n payload,\r\n toBullJobOptions(definition, options),\r\n );\r\n\r\n return { id: String(job.id), name: definition.name, queue: queueName };\r\n },\r\n async find(id) {\r\n const queue = getQueue(queueOf(definition));\r\n const initialJob = await queue.getJob(id);\r\n\r\n if (!initialJob || initialJob.name !== definition.name) {\r\n return undefined;\r\n }\r\n\r\n // Read the state first, then (re)fetch the job. BullMQ writes a job's\r\n // result/attemptsMade/finishedOn fields *before* it becomes visible\r\n // under a new state, so re-reading the job after the state is known\r\n // guarantees those fields are consistent with the reported state\r\n // (rather than reflecting a moment before the job finished).\r\n const state = (await initialJob.getState()) as JobState;\r\n const job = (await queue.getJob(id)) ?? initialJob;\r\n\r\n return toSnapshot<TPayload, TResult>(job, state);\r\n },\r\n };\r\n}\r\n\r\nfunction assertValidDefinition(definition: JobDefinition<unknown, unknown>): void {\r\n if (typeof definition.name !== \"string\" || definition.name.trim() === \"\") {\r\n throw new InvalidJobDefinitionError(\"defineJob() requires a non-empty `name`.\");\r\n }\r\n\r\n if (typeof definition.handle !== \"function\") {\r\n throw new InvalidJobDefinitionError(\r\n `defineJob(\"${definition.name}\") requires a \\`handle(payload, ctx)\\` function.`,\r\n );\r\n }\r\n\r\n if (definition.attempts !== undefined && !(Number.isInteger(definition.attempts) && definition.attempts >= 1)) {\r\n throw new InvalidJobDefinitionError(\r\n `defineJob(\"${definition.name}\"): \\`attempts\\` must be an integer >= 1, got ${definition.attempts}.`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Merge app defaults < job definition < dispatch options into BullMQ's shape.\r\n */\r\nfunction toBullJobOptions(definition: JobOptions, options: DispatchOptions): JobsOptions {\r\n const defaults = getQueueConfig().defaultJobOptions ?? {};\r\n const attempts = options.attempts ?? definition.attempts ?? defaults.attempts;\r\n const backoff = options.backoff ?? definition.backoff ?? defaults.backoff;\r\n const removeOnComplete = definition.removeOnComplete ?? defaults.removeOnComplete;\r\n const removeOnFail = definition.removeOnFail ?? defaults.removeOnFail;\r\n\r\n const bullOptions: JobsOptions = {};\r\n\r\n if (attempts !== undefined) bullOptions.attempts = attempts;\r\n if (backoff !== undefined) bullOptions.backoff = toBullBackoff(backoff);\r\n if (removeOnComplete !== undefined) bullOptions.removeOnComplete = removeOnComplete;\r\n if (removeOnFail !== undefined) bullOptions.removeOnFail = removeOnFail;\r\n if (options.delay !== undefined) bullOptions.delay = toMilliseconds(options.delay);\r\n if (options.priority !== undefined) bullOptions.priority = options.priority;\r\n if (options.jobId !== undefined) bullOptions.jobId = options.jobId;\r\n\r\n return bullOptions;\r\n}\r\n\r\nfunction toBullBackoff(backoff: JobBackoff): JobsOptions[\"backoff\"] {\r\n return typeof backoff === \"number\" ? { type: \"fixed\", delay: backoff } : backoff;\r\n}\r\n\r\n/**\r\n * A plain view of a BullMQ job.\r\n *\r\n * @param job The job to read fields from.\r\n * @param state The job's state; pass a state read *before* `job` was\r\n * fetched (or re-fetched) so the returned snapshot's fields are consistent\r\n * with it. If omitted, the state is read from `job` directly.\r\n */\r\nexport async function toSnapshot<TPayload, TResult>(\r\n job: Job,\r\n state?: JobState,\r\n): Promise<JobSnapshot<TPayload, TResult>> {\r\n const resolvedState = state ?? ((await job.getState()) as JobState);\r\n\r\n return {\r\n id: String(job.id),\r\n name: job.name,\r\n queue: job.queueName,\r\n state: resolvedState,\r\n payload: job.data as TPayload,\r\n progress: job.progress as JobSnapshot[\"progress\"],\r\n attemptsMade: job.attemptsMade,\r\n result: job.returnvalue as TResult | undefined,\r\n failedReason: job.failedReason || undefined,\r\n createdAt: new Date(job.timestamp),\r\n finishedAt: job.finishedOn ? new Date(job.finishedOn) : undefined,\r\n };\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,UACd,YAC6B;CAC7B,sBAAsB,UAAU;CAChC,YAAY,UAA2B;CAEvC,OAAO;EACL,MAAM,WAAW;EACjB,IAAI,QAAQ;GACV,OAAO,QAAQ,UAAU;EAC3B;EACA,MAAM,SAAS,SAAS,UAAU,CAAC,GAAG;GACpC,MAAM,YAAY,QAAQ,UAAU;GACpC,MAAM,MAAM,MAAM,SAAS,SAAS,CAAC,CAAC,IACpC,WAAW,MACX,SACA,iBAAiB,YAAY,OAAO,CACtC;GAEA,OAAO;IAAE,IAAI,OAAO,IAAI,EAAE;IAAG,MAAM,WAAW;IAAM,OAAO;GAAU;EACvE;EACA,MAAM,KAAK,IAAI;GACb,MAAM,QAAQ,SAAS,QAAQ,UAAU,CAAC;GAC1C,MAAM,aAAa,MAAM,MAAM,OAAO,EAAE;GAExC,IAAI,CAAC,cAAc,WAAW,SAAS,WAAW,MAChD;GAQF,MAAM,QAAS,MAAM,WAAW,SAAS;GAGzC,OAAO,WAFM,MAAM,MAAM,OAAO,EAAE,KAAM,YAEE,KAAK;EACjD;CACF;AACF;AAEA,SAAS,sBAAsB,YAAmD;CAChF,IAAI,OAAO,WAAW,SAAS,YAAY,WAAW,KAAK,KAAK,MAAM,IACpE,MAAM,IAAI,0BAA0B,0CAA0C;CAGhF,IAAI,OAAO,WAAW,WAAW,YAC/B,MAAM,IAAI,0BACR,cAAc,WAAW,KAAK,iDAChC;CAGF,IAAI,WAAW,aAAa,UAAa,EAAE,OAAO,UAAU,WAAW,QAAQ,KAAK,WAAW,YAAY,IACzG,MAAM,IAAI,0BACR,cAAc,WAAW,KAAK,gDAAgD,WAAW,SAAS,EACpG;AAEJ;;;;AAKA,SAAS,iBAAiB,YAAwB,SAAuC;CACvF,MAAM,WAAW,eAAe,CAAC,CAAC,qBAAqB,CAAC;CACxD,MAAM,WAAW,QAAQ,YAAY,WAAW,YAAY,SAAS;CACrE,MAAM,UAAU,QAAQ,WAAW,WAAW,WAAW,SAAS;CAClE,MAAM,mBAAmB,WAAW,oBAAoB,SAAS;CACjE,MAAM,eAAe,WAAW,gBAAgB,SAAS;CAEzD,MAAM,cAA2B,CAAC;CAElC,IAAI,aAAa,QAAW,YAAY,WAAW;CACnD,IAAI,YAAY,QAAW,YAAY,UAAU,cAAc,OAAO;CACtE,IAAI,qBAAqB,QAAW,YAAY,mBAAmB;CACnE,IAAI,iBAAiB,QAAW,YAAY,eAAe;CAC3D,IAAI,QAAQ,UAAU,QAAW,YAAY,QAAQ,eAAe,QAAQ,KAAK;CACjF,IAAI,QAAQ,aAAa,QAAW,YAAY,WAAW,QAAQ;CACnE,IAAI,QAAQ,UAAU,QAAW,YAAY,QAAQ,QAAQ;CAE7D,OAAO;AACT;AAEA,SAAS,cAAc,SAA6C;CAClE,OAAO,OAAO,YAAY,WAAW;EAAE,MAAM;EAAS,OAAO;CAAQ,IAAI;AAC3E;;;;;;;;;AAUA,eAAsB,WACpB,KACA,OACyC;CACzC,MAAM,gBAAgB,SAAW,MAAM,IAAI,SAAS;CAEpD,OAAO;EACL,IAAI,OAAO,IAAI,EAAE;EACjB,MAAM,IAAI;EACV,OAAO,IAAI;EACX,OAAO;EACP,SAAS,IAAI;EACb,UAAU,IAAI;EACd,cAAc,IAAI;EAClB,QAAQ,IAAI;EACZ,cAAc,IAAI,gBAAgB;EAClC,WAAW,IAAI,KAAK,IAAI,SAAS;EACjC,YAAY,IAAI,aAAa,IAAI,KAAK,IAAI,UAAU,IAAI;CAC1D;AACF"}
1
+ {"version":3,"file":"define-job.mjs","names":[],"sources":["../../../../../../queue/src/define-job.ts"],"sourcesContent":["import type { Job, JobsOptions } from \"bullmq\";\r\nimport { getQueueConfig } from \"./config\";\r\nimport { toMilliseconds } from \"./duration\";\r\nimport { InvalidJobDefinitionError } from \"./errors\";\r\nimport { getQueueContext, unwrapPayload, wrapPayload } from \"./queue-context\";\r\nimport { queueOf, registerJob, type RegisteredJob } from \"./job-registry\";\r\nimport { getQueue } from \"./queue-manager\";\r\nimport type {\r\n DispatchOptions,\r\n JobBackoff,\r\n JobDefinition,\r\n JobOptions,\r\n JobSnapshot,\r\n JobState,\r\n QueueJob,\r\n} from \"./types\";\r\n\r\n/**\r\n * Define a background job.\r\n *\r\n * The definition is registered by name so any worker in the process can run\r\n * it; the returned object dispatches it with a typed payload.\r\n *\r\n * @example\r\n * export const sendInvoice = defineJob({\r\n * name: \"invoices.send\",\r\n * attempts: 5,\r\n * backoff: { type: \"exponential\", delay: 2000 },\r\n * async handle(payload: { invoiceId: string }, ctx) {\r\n * await ctx.progress(50);\r\n * },\r\n * });\r\n *\r\n * await sendInvoice.dispatch({ invoiceId: \"42\" }, { delay: \"10m\", priority: 1 });\r\n */\r\nexport function defineJob<TPayload, TResult = unknown>(\r\n definition: JobDefinition<TPayload, TResult>,\r\n): QueueJob<TPayload, TResult> {\r\n assertValidDefinition(definition);\r\n registerJob(definition as RegisteredJob);\r\n\r\n return {\r\n name: definition.name,\r\n get queue() {\r\n return queueOf(definition);\r\n },\r\n async dispatch(payload, options = {}) {\r\n const queueName = queueOf(definition);\r\n // Capture synchronously, in the caller's async context.\r\n const captured = definition.context === false ? undefined : getQueueContext()?.capture();\r\n const job = await getQueue(queueName).add(\r\n definition.name,\r\n wrapPayload(payload, captured),\r\n toBullJobOptions(definition, options),\r\n );\r\n\r\n return { id: String(job.id), name: definition.name, queue: queueName };\r\n },\r\n async find(id) {\r\n const queue = getQueue(queueOf(definition));\r\n const initialJob = await queue.getJob(id);\r\n\r\n if (!initialJob || initialJob.name !== definition.name) {\r\n return undefined;\r\n }\r\n\r\n // Read the state first, then (re)fetch the job. BullMQ writes a job's\r\n // result/attemptsMade/finishedOn fields *before* it becomes visible\r\n // under a new state, so re-reading the job after the state is known\r\n // guarantees those fields are consistent with the reported state\r\n // (rather than reflecting a moment before the job finished).\r\n const state = (await initialJob.getState()) as JobState;\r\n const job = (await queue.getJob(id)) ?? initialJob;\r\n\r\n return toSnapshot<TPayload, TResult>(job, state);\r\n },\r\n };\r\n}\r\n\r\nfunction assertValidDefinition(definition: JobDefinition<unknown, unknown>): void {\r\n if (typeof definition.name !== \"string\" || definition.name.trim() === \"\") {\r\n throw new InvalidJobDefinitionError(\"defineJob() requires a non-empty `name`.\");\r\n }\r\n\r\n if (typeof definition.handle !== \"function\") {\r\n throw new InvalidJobDefinitionError(\r\n `defineJob(\"${definition.name}\") requires a \\`handle(payload, ctx)\\` function.`,\r\n );\r\n }\r\n\r\n if (definition.attempts !== undefined && !(Number.isInteger(definition.attempts) && definition.attempts >= 1)) {\r\n throw new InvalidJobDefinitionError(\r\n `defineJob(\"${definition.name}\"): \\`attempts\\` must be an integer >= 1, got ${definition.attempts}.`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Merge app defaults < job definition < dispatch options into BullMQ's shape.\r\n */\r\nfunction toBullJobOptions(definition: JobOptions, options: DispatchOptions): JobsOptions {\r\n const defaults = getQueueConfig().defaultJobOptions ?? {};\r\n const attempts = options.attempts ?? definition.attempts ?? defaults.attempts;\r\n const backoff = options.backoff ?? definition.backoff ?? defaults.backoff;\r\n const removeOnComplete = definition.removeOnComplete ?? defaults.removeOnComplete;\r\n const removeOnFail = definition.removeOnFail ?? defaults.removeOnFail;\r\n\r\n const bullOptions: JobsOptions = {};\r\n\r\n if (attempts !== undefined) bullOptions.attempts = attempts;\r\n if (backoff !== undefined) bullOptions.backoff = toBullBackoff(backoff);\r\n if (removeOnComplete !== undefined) bullOptions.removeOnComplete = removeOnComplete;\r\n if (removeOnFail !== undefined) bullOptions.removeOnFail = removeOnFail;\r\n if (options.delay !== undefined) bullOptions.delay = toMilliseconds(options.delay);\r\n if (options.priority !== undefined) bullOptions.priority = options.priority;\r\n if (options.jobId !== undefined) bullOptions.jobId = options.jobId;\r\n\r\n return bullOptions;\r\n}\r\n\r\nfunction toBullBackoff(backoff: JobBackoff): JobsOptions[\"backoff\"] {\r\n return typeof backoff === \"number\" ? { type: \"fixed\", delay: backoff } : backoff;\r\n}\r\n\r\n/**\r\n * A plain view of a BullMQ job.\r\n *\r\n * @param job The job to read fields from.\r\n * @param state The job's state; pass a state read *before* `job` was\r\n * fetched (or re-fetched) so the returned snapshot's fields are consistent\r\n * with it. If omitted, the state is read from `job` directly.\r\n */\r\nexport async function toSnapshot<TPayload, TResult>(\r\n job: Job,\r\n state?: JobState,\r\n): Promise<JobSnapshot<TPayload, TResult>> {\r\n const resolvedState = state ?? ((await job.getState()) as JobState);\r\n\r\n return {\r\n id: String(job.id),\r\n name: job.name,\r\n queue: job.queueName,\r\n state: resolvedState,\r\n payload: unwrapPayload(job.data).payload as TPayload,\r\n progress: job.progress as JobSnapshot[\"progress\"],\r\n attemptsMade: job.attemptsMade,\r\n result: job.returnvalue as TResult | undefined,\r\n failedReason: job.failedReason || undefined,\r\n createdAt: new Date(job.timestamp),\r\n finishedAt: job.finishedOn ? new Date(job.finishedOn) : undefined,\r\n };\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,UACd,YAC6B;CAC7B,sBAAsB,UAAU;CAChC,YAAY,UAA2B;CAEvC,OAAO;EACL,MAAM,WAAW;EACjB,IAAI,QAAQ;GACV,OAAO,QAAQ,UAAU;EAC3B;EACA,MAAM,SAAS,SAAS,UAAU,CAAC,GAAG;GACpC,MAAM,YAAY,QAAQ,UAAU;GAEpC,MAAM,WAAW,WAAW,YAAY,QAAQ,SAAY,gBAAgB,CAAC,EAAE,QAAQ;GACvF,MAAM,MAAM,MAAM,SAAS,SAAS,CAAC,CAAC,IACpC,WAAW,MACX,YAAY,SAAS,QAAQ,GAC7B,iBAAiB,YAAY,OAAO,CACtC;GAEA,OAAO;IAAE,IAAI,OAAO,IAAI,EAAE;IAAG,MAAM,WAAW;IAAM,OAAO;GAAU;EACvE;EACA,MAAM,KAAK,IAAI;GACb,MAAM,QAAQ,SAAS,QAAQ,UAAU,CAAC;GAC1C,MAAM,aAAa,MAAM,MAAM,OAAO,EAAE;GAExC,IAAI,CAAC,cAAc,WAAW,SAAS,WAAW,MAChD;GAQF,MAAM,QAAS,MAAM,WAAW,SAAS;GAGzC,OAAO,WAFM,MAAM,MAAM,OAAO,EAAE,KAAM,YAEE,KAAK;EACjD;CACF;AACF;AAEA,SAAS,sBAAsB,YAAmD;CAChF,IAAI,OAAO,WAAW,SAAS,YAAY,WAAW,KAAK,KAAK,MAAM,IACpE,MAAM,IAAI,0BAA0B,0CAA0C;CAGhF,IAAI,OAAO,WAAW,WAAW,YAC/B,MAAM,IAAI,0BACR,cAAc,WAAW,KAAK,iDAChC;CAGF,IAAI,WAAW,aAAa,UAAa,EAAE,OAAO,UAAU,WAAW,QAAQ,KAAK,WAAW,YAAY,IACzG,MAAM,IAAI,0BACR,cAAc,WAAW,KAAK,gDAAgD,WAAW,SAAS,EACpG;AAEJ;;;;AAKA,SAAS,iBAAiB,YAAwB,SAAuC;CACvF,MAAM,WAAW,eAAe,CAAC,CAAC,qBAAqB,CAAC;CACxD,MAAM,WAAW,QAAQ,YAAY,WAAW,YAAY,SAAS;CACrE,MAAM,UAAU,QAAQ,WAAW,WAAW,WAAW,SAAS;CAClE,MAAM,mBAAmB,WAAW,oBAAoB,SAAS;CACjE,MAAM,eAAe,WAAW,gBAAgB,SAAS;CAEzD,MAAM,cAA2B,CAAC;CAElC,IAAI,aAAa,QAAW,YAAY,WAAW;CACnD,IAAI,YAAY,QAAW,YAAY,UAAU,cAAc,OAAO;CACtE,IAAI,qBAAqB,QAAW,YAAY,mBAAmB;CACnE,IAAI,iBAAiB,QAAW,YAAY,eAAe;CAC3D,IAAI,QAAQ,UAAU,QAAW,YAAY,QAAQ,eAAe,QAAQ,KAAK;CACjF,IAAI,QAAQ,aAAa,QAAW,YAAY,WAAW,QAAQ;CACnE,IAAI,QAAQ,UAAU,QAAW,YAAY,QAAQ,QAAQ;CAE7D,OAAO;AACT;AAEA,SAAS,cAAc,SAA6C;CAClE,OAAO,OAAO,YAAY,WAAW;EAAE,MAAM;EAAS,OAAO;CAAQ,IAAI;AAC3E;;;;;;;;;AAUA,eAAsB,WACpB,KACA,OACyC;CACzC,MAAM,gBAAgB,SAAW,MAAM,IAAI,SAAS;CAEpD,OAAO;EACL,IAAI,OAAO,IAAI,EAAE;EACjB,MAAM,IAAI;EACV,OAAO,IAAI;EACX,OAAO;EACP,SAAS,cAAc,IAAI,IAAI,CAAC,CAAC;EACjC,UAAU,IAAI;EACd,cAAc,IAAI;EAClB,QAAQ,IAAI;EACZ,cAAc,IAAI,gBAAgB;EAClC,WAAW,IAAI,KAAK,IAAI,SAAS;EACjC,YAAY,IAAI,aAAa,IAAI,KAAK,IAAI,UAAU,IAAI;CAC1D;AACF"}
package/esm/errors.d.mts CHANGED
@@ -31,6 +31,15 @@ declare class FailedJobNotFoundError extends Error {
31
31
  declare class QueueDashboardDependencyError extends Error {
32
32
  constructor(missing: string);
33
33
  }
34
+ /**
35
+ * Throw from a job handler (or a queue context's `restore`) to fail the job
36
+ * permanently — it is not retried, whatever `attempts` allows.
37
+ */
38
+ declare class UnrecoverableJobError extends Error {
39
+ constructor(message: string, options?: {
40
+ cause?: unknown;
41
+ });
42
+ }
34
43
  //#endregion
35
- export { FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError, QueueDashboardDependencyError, QueueNotConfiguredError };
44
+ export { FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError, QueueDashboardDependencyError, QueueNotConfiguredError, UnrecoverableJobError };
36
45
  //# sourceMappingURL=errors.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../queue/src/errors.ts"],"mappings":";;AAIA;;;cAAa,uBAAA,SAAgC,KAAK;EAAL,WAAA;AAAA;;;;cAchC,oBAAA,SAA6B,KAAK;cAC1B,KAAA;AAAA;;AAAc;AAYnC;cAAa,yBAAA,SAAkC,KAAK;cAC/B,OAAA;AAAA;;;;cASR,sBAAA,SAA+B,KAAK;cAC5B,EAAA,UAAY,KAAA;AAAA;;;;;cAUpB,6BAAA,SAAsC,KAAK;cACnC,OAAA;AAAA"}
1
+ {"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../queue/src/errors.ts"],"mappings":";;AAIA;;;cAAa,uBAAA,SAAgC,KAAK;EAAL,WAAA;AAAA;;;;cAchC,oBAAA,SAA6B,KAAK;cAC1B,KAAA;AAAA;;AAAc;AAYnC;cAAa,yBAAA,SAAkC,KAAK;cAC/B,OAAA;AAAA;;;;cASR,sBAAA,SAA+B,KAAK;cAC5B,EAAA,UAAY,KAAA;AAAA;;;;;cAUpB,6BAAA,SAAsC,KAAK;cACnC,OAAA;AAAA;;AAXyB;AAU9C;;cAea,qBAAA,SAA8B,KAAK;cAC3B,OAAA,UAAiB,OAAA;IAAY,KAAA;EAAA;AAAA"}
package/esm/errors.mjs CHANGED
@@ -49,7 +49,17 @@ var QueueDashboardDependencyError = class extends Error {
49
49
  this.name = "QueueDashboardDependencyError";
50
50
  }
51
51
  };
52
+ /**
53
+ * Throw from a job handler (or a queue context's `restore`) to fail the job
54
+ * permanently — it is not retried, whatever `attempts` allows.
55
+ */
56
+ var UnrecoverableJobError = class extends Error {
57
+ constructor(message, options) {
58
+ super(message, options);
59
+ this.name = "UnrecoverableJobError";
60
+ }
61
+ };
52
62
 
53
63
  //#endregion
54
- export { FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError, QueueDashboardDependencyError, QueueNotConfiguredError };
64
+ export { FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError, QueueDashboardDependencyError, QueueNotConfiguredError, UnrecoverableJobError };
55
65
  //# sourceMappingURL=errors.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../queue/src/errors.ts"],"sourcesContent":["/**\n * Thrown when the queue is used before `setQueueConfig` (or the queue\n * connector) supplied a configuration.\n */\nexport class QueueNotConfiguredError extends Error {\n public constructor() {\n super(\n \"@warlock.js/queue is not configured. Add src/config/queue.ts exporting a QueueConfig \" +\n \"and register queueConnector() in warlock.config.ts > connectors, \" +\n \"or call setQueueConfig() yourself.\",\n );\n this.name = \"QueueNotConfiguredError\";\n }\n}\n\n/**\n * Thrown for a malformed duration such as `\"10 minutes\"`.\n */\nexport class InvalidDurationError extends Error {\n public constructor(value: unknown) {\n super(\n `Invalid duration ${JSON.stringify(value)}: expected milliseconds as a non-negative number ` +\n `or a string like \"500ms\", \"30s\", \"10m\", \"2h\", \"1d\".`,\n );\n this.name = \"InvalidDurationError\";\n }\n}\n\n/**\n * Thrown by `defineJob` for an invalid definition.\n */\nexport class InvalidJobDefinitionError extends Error {\n public constructor(message: string) {\n super(message);\n this.name = \"InvalidJobDefinitionError\";\n }\n}\n\n/**\n * Thrown by `retryFailedJob` when no failed job has the given id.\n */\nexport class FailedJobNotFoundError extends Error {\n public constructor(id: string, queue: string) {\n super(`No failed job with id \"${id}\" on queue \"${queue}\".`);\n this.name = \"FailedJobNotFoundError\";\n }\n}\n\n/**\n * Thrown by `queueDashboard` when an optional bull-board package is not\n * installed.\n */\nexport class QueueDashboardDependencyError extends Error {\n public constructor(missing: string) {\n super(\n `The queue dashboard needs the optional package \"${missing}\", which is not installed.\\n` +\n \"Install both bull-board packages:\\n\\n\" +\n \" npm install @bull-board/api @bull-board/fastify\\n\",\n );\n this.name = \"QueueDashboardDependencyError\";\n }\n}\n"],"mappings":";;;;;AAIA,IAAa,0BAAb,cAA6C,MAAM;CACjD,AAAO,cAAc;EACnB,MACE,0LAGF;EACA,KAAK,OAAO;CACd;AACF;;;;AAKA,IAAa,uBAAb,cAA0C,MAAM;CAC9C,AAAO,YAAY,OAAgB;EACjC,MACE,oBAAoB,KAAK,UAAU,KAAK,EAAE,qGAE5C;EACA,KAAK,OAAO;CACd;AACF;;;;AAKA,IAAa,4BAAb,cAA+C,MAAM;CACnD,AAAO,YAAY,SAAiB;EAClC,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;;;;AAKA,IAAa,yBAAb,cAA4C,MAAM;CAChD,AAAO,YAAY,IAAY,OAAe;EAC5C,MAAM,0BAA0B,GAAG,cAAc,MAAM,GAAG;EAC1D,KAAK,OAAO;CACd;AACF;;;;;AAMA,IAAa,gCAAb,cAAmD,MAAM;CACvD,AAAO,YAAY,SAAiB;EAClC,MACE,mDAAmD,QAAQ;;;CAG7D;EACA,KAAK,OAAO;CACd;AACF"}
1
+ {"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../queue/src/errors.ts"],"sourcesContent":["/**\n * Thrown when the queue is used before `setQueueConfig` (or the queue\n * connector) supplied a configuration.\n */\nexport class QueueNotConfiguredError extends Error {\n public constructor() {\n super(\n \"@warlock.js/queue is not configured. Add src/config/queue.ts exporting a QueueConfig \" +\n \"and register queueConnector() in warlock.config.ts > connectors, \" +\n \"or call setQueueConfig() yourself.\",\n );\n this.name = \"QueueNotConfiguredError\";\n }\n}\n\n/**\n * Thrown for a malformed duration such as `\"10 minutes\"`.\n */\nexport class InvalidDurationError extends Error {\n public constructor(value: unknown) {\n super(\n `Invalid duration ${JSON.stringify(value)}: expected milliseconds as a non-negative number ` +\n `or a string like \"500ms\", \"30s\", \"10m\", \"2h\", \"1d\".`,\n );\n this.name = \"InvalidDurationError\";\n }\n}\n\n/**\n * Thrown by `defineJob` for an invalid definition.\n */\nexport class InvalidJobDefinitionError extends Error {\n public constructor(message: string) {\n super(message);\n this.name = \"InvalidJobDefinitionError\";\n }\n}\n\n/**\n * Thrown by `retryFailedJob` when no failed job has the given id.\n */\nexport class FailedJobNotFoundError extends Error {\n public constructor(id: string, queue: string) {\n super(`No failed job with id \"${id}\" on queue \"${queue}\".`);\n this.name = \"FailedJobNotFoundError\";\n }\n}\n\n/**\n * Thrown by `queueDashboard` when an optional bull-board package is not\n * installed.\n */\nexport class QueueDashboardDependencyError extends Error {\n public constructor(missing: string) {\n super(\n `The queue dashboard needs the optional package \"${missing}\", which is not installed.\\n` +\n \"Install both bull-board packages:\\n\\n\" +\n \" npm install @bull-board/api @bull-board/fastify\\n\",\n );\n this.name = \"QueueDashboardDependencyError\";\n }\n}\n\n/**\n * Throw from a job handler (or a queue context's `restore`) to fail the job\n * permanently — it is not retried, whatever `attempts` allows.\n */\nexport class UnrecoverableJobError extends Error {\n public constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"UnrecoverableJobError\";\n }\n}\n"],"mappings":";;;;;AAIA,IAAa,0BAAb,cAA6C,MAAM;CACjD,AAAO,cAAc;EACnB,MACE,0LAGF;EACA,KAAK,OAAO;CACd;AACF;;;;AAKA,IAAa,uBAAb,cAA0C,MAAM;CAC9C,AAAO,YAAY,OAAgB;EACjC,MACE,oBAAoB,KAAK,UAAU,KAAK,EAAE,qGAE5C;EACA,KAAK,OAAO;CACd;AACF;;;;AAKA,IAAa,4BAAb,cAA+C,MAAM;CACnD,AAAO,YAAY,SAAiB;EAClC,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;;;;AAKA,IAAa,yBAAb,cAA4C,MAAM;CAChD,AAAO,YAAY,IAAY,OAAe;EAC5C,MAAM,0BAA0B,GAAG,cAAc,MAAM,GAAG;EAC1D,KAAK,OAAO;CACd;AACF;;;;;AAMA,IAAa,gCAAb,cAAmD,MAAM;CACvD,AAAO,YAAY,SAAiB;EAClC,MACE,mDAAmD,QAAQ;;;CAG7D;EACA,KAAK,OAAO;CACd;AACF;;;;;AAMA,IAAa,wBAAb,cAA2C,MAAM;CAC/C,AAAO,YAAY,SAAiB,SAA+B;EACjE,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;CACd;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"failed-jobs.d.mts","names":[],"sources":["../../../../../../queue/src/failed-jobs.ts"],"mappings":";;;KAMY,iBAAA;mDAEV,KAAA,WAF2B;EAI3B,KAAA,WAJ2B;EAM3B,GAAA;AAAA;;;AAAG;AAOL;iBAAsB,UAAA,CAAW,OAAA,GAAS,iBAAA,GAAyB,OAAA,CAAQ,SAAA;;;;;iBAWrD,cAAA,CAAe,EAAA,UAAY,OAAA;EAAW,KAAA;AAAA,IAAwB,OAAO"}
1
+ {"version":3,"file":"failed-jobs.d.mts","names":[],"sources":["../../../../../../queue/src/failed-jobs.ts"],"mappings":";;;KAOY,iBAAA;mDAEV,KAAA,WAF2B;EAI3B,KAAA,WAJ2B;EAM3B,GAAA;AAAA;;;AAAG;AAOL;iBAAsB,UAAA,CAAW,OAAA,GAAS,iBAAA,GAAyB,OAAA,CAAQ,SAAA;;;;;iBAWrD,cAAA,CAAe,EAAA,UAAY,OAAA;EAAW,KAAA;AAAA,IAAwB,OAAO"}
@@ -1,5 +1,6 @@
1
1
  import { FailedJobNotFoundError } from "./errors.mjs";
2
2
  import { defaultQueueName } from "./config.mjs";
3
+ import { unwrapPayload } from "./queue-context.mjs";
3
4
  import { getQueue } from "./queue-manager.mjs";
4
5
 
5
6
  //#region ../queue/src/failed-jobs.ts
@@ -25,7 +26,7 @@ function toFailedJob(job) {
25
26
  id: String(job.id),
26
27
  name: job.name,
27
28
  queue: job.queueName,
28
- payload: job.data,
29
+ payload: unwrapPayload(job.data).payload,
29
30
  attemptsMade: job.attemptsMade,
30
31
  failedReason: job.failedReason,
31
32
  stacktrace: job.stacktrace ?? [],
@@ -1 +1 @@
1
- {"version":3,"file":"failed-jobs.mjs","names":[],"sources":["../../../../../../queue/src/failed-jobs.ts"],"sourcesContent":["import type { Job } from \"bullmq\";\nimport { defaultQueueName } from \"./config\";\nimport { FailedJobNotFoundError } from \"./errors\";\nimport { getQueue } from \"./queue-manager\";\nimport type { FailedJob } from \"./types\";\n\nexport type FailedJobsOptions = {\n /** Queue to read. Default: the default queue. */\n queue?: string;\n /** First index (newest first). Default `0`. */\n start?: number;\n /** Last index, inclusive. Default `99`. */\n end?: number;\n};\n\n/**\n * List failed jobs, newest first — jobs that used up every attempt, or\n * failed unrecoverably. Each entry can be retried.\n */\nexport async function failedJobs(options: FailedJobsOptions = {}): Promise<FailedJob[]> {\n const queueName = options.queue ?? defaultQueueName();\n const jobs = await getQueue(queueName).getFailed(options.start ?? 0, options.end ?? 99);\n\n return jobs.map((job) => toFailedJob(job));\n}\n\n/**\n * Retry one failed job by id: it goes back to waiting with its attempts reset.\n * Throws {@link FailedJobNotFoundError} when no FAILED job has that id.\n */\nexport async function retryFailedJob(id: string, options: { queue?: string } = {}): Promise<void> {\n const queueName = options.queue ?? defaultQueueName();\n const job = await getQueue(queueName).getJob(id);\n\n if (!job || !(await job.isFailed())) {\n throw new FailedJobNotFoundError(id, queueName);\n }\n\n await job.retry(\"failed\");\n}\n\nfunction toFailedJob(job: Job): FailedJob {\n return {\n id: String(job.id),\n name: job.name,\n queue: job.queueName,\n payload: job.data,\n attemptsMade: job.attemptsMade,\n failedReason: job.failedReason,\n stacktrace: job.stacktrace ?? [],\n failedAt: job.finishedOn ? new Date(job.finishedOn) : undefined,\n retry: () => job.retry(\"failed\"),\n };\n}\n"],"mappings":";;;;;;;;;AAmBA,eAAsB,WAAW,UAA6B,CAAC,GAAyB;CAItF,QAAO,MAFY,SADD,QAAQ,SAAS,iBAAiB,CACf,CAAC,CAAC,UAAU,QAAQ,SAAS,GAAG,QAAQ,OAAO,EAAE,EAE3E,CAAC,KAAK,QAAQ,YAAY,GAAG,CAAC;AAC3C;;;;;AAMA,eAAsB,eAAe,IAAY,UAA8B,CAAC,GAAkB;CAChG,MAAM,YAAY,QAAQ,SAAS,iBAAiB;CACpD,MAAM,MAAM,MAAM,SAAS,SAAS,CAAC,CAAC,OAAO,EAAE;CAE/C,IAAI,CAAC,OAAO,CAAE,MAAM,IAAI,SAAS,GAC/B,MAAM,IAAI,uBAAuB,IAAI,SAAS;CAGhD,MAAM,IAAI,MAAM,QAAQ;AAC1B;AAEA,SAAS,YAAY,KAAqB;CACxC,OAAO;EACL,IAAI,OAAO,IAAI,EAAE;EACjB,MAAM,IAAI;EACV,OAAO,IAAI;EACX,SAAS,IAAI;EACb,cAAc,IAAI;EAClB,cAAc,IAAI;EAClB,YAAY,IAAI,cAAc,CAAC;EAC/B,UAAU,IAAI,aAAa,IAAI,KAAK,IAAI,UAAU,IAAI;EACtD,aAAa,IAAI,MAAM,QAAQ;CACjC;AACF"}
1
+ {"version":3,"file":"failed-jobs.mjs","names":[],"sources":["../../../../../../queue/src/failed-jobs.ts"],"sourcesContent":["import { unwrapPayload } from \"./queue-context\";\nimport type { Job } from \"bullmq\";\nimport { defaultQueueName } from \"./config\";\nimport { FailedJobNotFoundError } from \"./errors\";\nimport { getQueue } from \"./queue-manager\";\nimport type { FailedJob } from \"./types\";\n\nexport type FailedJobsOptions = {\n /** Queue to read. Default: the default queue. */\n queue?: string;\n /** First index (newest first). Default `0`. */\n start?: number;\n /** Last index, inclusive. Default `99`. */\n end?: number;\n};\n\n/**\n * List failed jobs, newest first — jobs that used up every attempt, or\n * failed unrecoverably. Each entry can be retried.\n */\nexport async function failedJobs(options: FailedJobsOptions = {}): Promise<FailedJob[]> {\n const queueName = options.queue ?? defaultQueueName();\n const jobs = await getQueue(queueName).getFailed(options.start ?? 0, options.end ?? 99);\n\n return jobs.map((job) => toFailedJob(job));\n}\n\n/**\n * Retry one failed job by id: it goes back to waiting with its attempts reset.\n * Throws {@link FailedJobNotFoundError} when no FAILED job has that id.\n */\nexport async function retryFailedJob(id: string, options: { queue?: string } = {}): Promise<void> {\n const queueName = options.queue ?? defaultQueueName();\n const job = await getQueue(queueName).getJob(id);\n\n if (!job || !(await job.isFailed())) {\n throw new FailedJobNotFoundError(id, queueName);\n }\n\n await job.retry(\"failed\");\n}\n\nfunction toFailedJob(job: Job): FailedJob {\n return {\n id: String(job.id),\n name: job.name,\n queue: job.queueName,\n payload: unwrapPayload(job.data).payload,\n attemptsMade: job.attemptsMade,\n failedReason: job.failedReason,\n stacktrace: job.stacktrace ?? [],\n failedAt: job.finishedOn ? new Date(job.finishedOn) : undefined,\n retry: () => job.retry(\"failed\"),\n };\n}\n"],"mappings":";;;;;;;;;;AAoBA,eAAsB,WAAW,UAA6B,CAAC,GAAyB;CAItF,QAAO,MAFY,SADD,QAAQ,SAAS,iBAAiB,CACf,CAAC,CAAC,UAAU,QAAQ,SAAS,GAAG,QAAQ,OAAO,EAAE,EAE3E,CAAC,KAAK,QAAQ,YAAY,GAAG,CAAC;AAC3C;;;;;AAMA,eAAsB,eAAe,IAAY,UAA8B,CAAC,GAAkB;CAChG,MAAM,YAAY,QAAQ,SAAS,iBAAiB;CACpD,MAAM,MAAM,MAAM,SAAS,SAAS,CAAC,CAAC,OAAO,EAAE;CAE/C,IAAI,CAAC,OAAO,CAAE,MAAM,IAAI,SAAS,GAC/B,MAAM,IAAI,uBAAuB,IAAI,SAAS;CAGhD,MAAM,IAAI,MAAM,QAAQ;AAC1B;AAEA,SAAS,YAAY,KAAqB;CACxC,OAAO;EACL,IAAI,OAAO,IAAI,EAAE;EACjB,MAAM,IAAI;EACV,OAAO,IAAI;EACX,SAAS,cAAc,IAAI,IAAI,CAAC,CAAC;EACjC,cAAc,IAAI;EAClB,cAAc,IAAI;EAClB,YAAY,IAAI,cAAc,CAAC;EAC/B,UAAU,IAAI,aAAa,IAAI,KAAK,IAAI,UAAU,IAAI;EACtD,aAAa,IAAI,MAAM,QAAQ;CACjC;AACF"}
package/esm/index.d.mts CHANGED
@@ -4,9 +4,10 @@ import { DashboardServer, QueueDashboardOptions, loadBullBoard, queueDashboard }
4
4
  import { DEFAULT_DASHBOARD_PATH, mountQueueDashboard } from "./dashboard-boot.mjs";
5
5
  import { defineJob } from "./define-job.mjs";
6
6
  import { toMilliseconds } from "./duration.mjs";
7
- import { FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError, QueueDashboardDependencyError, QueueNotConfiguredError } from "./errors.mjs";
7
+ import { FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError, QueueDashboardDependencyError, QueueNotConfiguredError, UnrecoverableJobError } from "./errors.mjs";
8
8
  import { FailedJobsOptions, failedJobs, retryFailedJob } from "./failed-jobs.mjs";
9
9
  import { QUEUE_CONNECTOR_PRIORITY, QueueConnectorOptions, queueConnector } from "./queue-connector.mjs";
10
+ import { QueueContext, defineQueueContext, getQueueContext, resetQueueContext, setQueueContext } from "./queue-context.mjs";
10
11
  import { QueueDashboardUnguardedError } from "./queue-dashboard-unguarded.error.mjs";
11
12
  import { CloseQueueOptions, closeQueue, getQueue, runningWorkers, startWorkers } from "./queue-manager.mjs";
12
- export { type CloseQueueOptions, DEFAULT_DASHBOARD_PATH, DashboardServer, DispatchOptions, DispatchedJob, Duration, DurationUnit, FailedJob, FailedJobNotFoundError, FailedJobsOptions, InvalidDurationError, InvalidJobDefinitionError, JobBackoff, JobContext, JobDefinition, JobOptions, JobProgress, JobRetention, JobSnapshot, JobState, QUEUE_CONNECTOR_PRIORITY, QueueConfig, QueueConnectorOptions, QueueDashboardConfig, QueueDashboardDependencyError, QueueDashboardOptions, QueueDashboardUnguardedError, QueueJob, QueueNotConfiguredError, QueueWorkersConfig, closeQueue, defaultQueueName, defineJob, failedJobs, getQueue, getQueueConfig, loadBullBoard, mountQueueDashboard, queueConnector, queueDashboard, resetQueueConfig, retryFailedJob, runningWorkers, setQueueConfig, startWorkers, toMilliseconds };
13
+ export { type CloseQueueOptions, DEFAULT_DASHBOARD_PATH, DashboardServer, DispatchOptions, DispatchedJob, Duration, DurationUnit, FailedJob, FailedJobNotFoundError, FailedJobsOptions, InvalidDurationError, InvalidJobDefinitionError, JobBackoff, JobContext, JobDefinition, JobOptions, JobProgress, JobRetention, JobSnapshot, JobState, QUEUE_CONNECTOR_PRIORITY, QueueConfig, QueueConnectorOptions, type QueueContext, QueueDashboardConfig, QueueDashboardDependencyError, QueueDashboardOptions, QueueDashboardUnguardedError, QueueJob, QueueNotConfiguredError, QueueWorkersConfig, UnrecoverableJobError, closeQueue, defaultQueueName, defineJob, defineQueueContext, failedJobs, getQueue, getQueueConfig, getQueueContext, loadBullBoard, mountQueueDashboard, queueConnector, queueDashboard, resetQueueConfig, resetQueueContext, retryFailedJob, runningWorkers, setQueueConfig, setQueueContext, startWorkers, toMilliseconds };
package/esm/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError, QueueDashboardDependencyError, QueueNotConfiguredError } from "./errors.mjs";
1
+ import { FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError, QueueDashboardDependencyError, QueueNotConfiguredError, UnrecoverableJobError } from "./errors.mjs";
2
2
  import { defaultQueueName, getQueueConfig, resetQueueConfig, setQueueConfig } from "./config.mjs";
3
+ import { defineQueueContext, getQueueContext, resetQueueContext, setQueueContext } from "./queue-context.mjs";
3
4
  import { closeQueue, getQueue, runningWorkers, startWorkers } from "./queue-manager.mjs";
4
5
  import { loadBullBoard, queueDashboard } from "./dashboard.mjs";
5
6
  import { QueueDashboardUnguardedError } from "./queue-dashboard-unguarded.error.mjs";
@@ -9,4 +10,4 @@ import { defineJob } from "./define-job.mjs";
9
10
  import { failedJobs, retryFailedJob } from "./failed-jobs.mjs";
10
11
  import { QUEUE_CONNECTOR_PRIORITY, queueConnector } from "./queue-connector.mjs";
11
12
 
12
- export { DEFAULT_DASHBOARD_PATH, FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError, QUEUE_CONNECTOR_PRIORITY, QueueDashboardDependencyError, QueueDashboardUnguardedError, QueueNotConfiguredError, closeQueue, defaultQueueName, defineJob, failedJobs, getQueue, getQueueConfig, loadBullBoard, mountQueueDashboard, queueConnector, queueDashboard, resetQueueConfig, retryFailedJob, runningWorkers, setQueueConfig, startWorkers, toMilliseconds };
13
+ export { DEFAULT_DASHBOARD_PATH, FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError, QUEUE_CONNECTOR_PRIORITY, QueueDashboardDependencyError, QueueDashboardUnguardedError, QueueNotConfiguredError, UnrecoverableJobError, closeQueue, defaultQueueName, defineJob, defineQueueContext, failedJobs, getQueue, getQueueConfig, getQueueContext, loadBullBoard, mountQueueDashboard, queueConnector, queueDashboard, resetQueueConfig, resetQueueContext, retryFailedJob, runningWorkers, setQueueConfig, setQueueContext, startWorkers, toMilliseconds };
@@ -1,4 +1,6 @@
1
+ import { UnrecoverableJobError } from "./errors.mjs";
1
2
  import { findRegisteredJob } from "./job-registry.mjs";
3
+ import { getQueueContext, unwrapPayload } from "./queue-context.mjs";
2
4
  import { UnrecoverableError } from "bullmq";
3
5
 
4
6
  //#region ../queue/src/process-job.ts
@@ -13,6 +15,7 @@ import { UnrecoverableError } from "bullmq";
13
15
  async function processJob(job) {
14
16
  const definition = findRegisteredJob(job.name);
15
17
  if (!definition) throw new UnrecoverableError(`No job named "${job.name}" is defined in this process. Make sure the module that calls defineJob() is imported by the worker process.`);
18
+ const { payload, context: captured } = unwrapPayload(job.data);
16
19
  const context = {
17
20
  id: String(job.id),
18
21
  name: job.name,
@@ -22,9 +25,17 @@ async function processJob(job) {
22
25
  progress: (value) => job.updateProgress(value),
23
26
  log: async (line) => {
24
27
  await job.log(line);
25
- }
28
+ },
29
+ context: captured
26
30
  };
27
- return definition.handle(job.data, context);
31
+ const run = async () => definition.handle(payload, context);
32
+ const queueContext = getQueueContext();
33
+ try {
34
+ return captured !== void 0 && queueContext ? await queueContext.restore(captured, run) : await run();
35
+ } catch (error) {
36
+ if (error instanceof UnrecoverableJobError) throw new UnrecoverableError(error.message);
37
+ throw error;
38
+ }
28
39
  }
29
40
 
30
41
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"process-job.mjs","names":[],"sources":["../../../../../../queue/src/process-job.ts"],"sourcesContent":["import { type Job, UnrecoverableError } from \"bullmq\";\nimport { findRegisteredJob } from \"./job-registry\";\nimport type { JobContext } from \"./types\";\n\n/**\n * The single BullMQ processor every worker runs: route the job to the\n * handler registered under its name.\n *\n * A name with no handler in this process fails with `UnrecoverableError` —\n * retrying cannot make a missing definition appear, so it must not burn\n * through its attempts.\n */\nexport async function processJob(job: Job): Promise<unknown> {\n const definition = findRegisteredJob(job.name);\n\n if (!definition) {\n throw new UnrecoverableError(\n `No job named \"${job.name}\" is defined in this process. ` +\n \"Make sure the module that calls defineJob() is imported by the worker process.\",\n );\n }\n\n const context: JobContext = {\n id: String(job.id),\n name: job.name,\n queue: job.queueName,\n attempt: job.attemptsMade + 1,\n maxAttempts: job.opts.attempts ?? 1,\n progress: (value) => job.updateProgress(value),\n log: async (line) => {\n await job.log(line);\n },\n };\n\n return definition.handle(job.data, context);\n}\n"],"mappings":";;;;;;;;;;;;AAYA,eAAsB,WAAW,KAA4B;CAC3D,MAAM,aAAa,kBAAkB,IAAI,IAAI;CAE7C,IAAI,CAAC,YACH,MAAM,IAAI,mBACR,iBAAiB,IAAI,KAAK,6GAE5B;CAGF,MAAM,UAAsB;EAC1B,IAAI,OAAO,IAAI,EAAE;EACjB,MAAM,IAAI;EACV,OAAO,IAAI;EACX,SAAS,IAAI,eAAe;EAC5B,aAAa,IAAI,KAAK,YAAY;EAClC,WAAW,UAAU,IAAI,eAAe,KAAK;EAC7C,KAAK,OAAO,SAAS;GACnB,MAAM,IAAI,IAAI,IAAI;EACpB;CACF;CAEA,OAAO,WAAW,OAAO,IAAI,MAAM,OAAO;AAC5C"}
1
+ {"version":3,"file":"process-job.mjs","names":[],"sources":["../../../../../../queue/src/process-job.ts"],"sourcesContent":["import { type Job, UnrecoverableError } from \"bullmq\";\nimport { UnrecoverableJobError } from \"./errors\";\nimport { findRegisteredJob } from \"./job-registry\";\nimport { getQueueContext, unwrapPayload } from \"./queue-context\";\nimport type { JobContext } from \"./types\";\n\n/**\n * The single BullMQ processor every worker runs: route the job to the\n * handler registered under its name.\n *\n * A name with no handler in this process fails with `UnrecoverableError` —\n * retrying cannot make a missing definition appear, so it must not burn\n * through its attempts.\n */\nexport async function processJob(job: Job): Promise<unknown> {\n const definition = findRegisteredJob(job.name);\n\n if (!definition) {\n throw new UnrecoverableError(\n `No job named \"${job.name}\" is defined in this process. ` +\n \"Make sure the module that calls defineJob() is imported by the worker process.\",\n );\n }\n\n const { payload, context: captured } = unwrapPayload(job.data);\n\n const context: JobContext = {\n id: String(job.id),\n name: job.name,\n queue: job.queueName,\n attempt: job.attemptsMade + 1,\n maxAttempts: job.opts.attempts ?? 1,\n progress: (value) => job.updateProgress(value),\n log: async (line) => {\n await job.log(line);\n },\n context: captured,\n };\n\n const run = async () => definition.handle(payload, context);\n const queueContext = getQueueContext();\n\n try {\n return captured !== undefined && queueContext ? await queueContext.restore(captured, run) : await run();\n } catch (error) {\n if (error instanceof UnrecoverableJobError) {\n throw new UnrecoverableError(error.message);\n }\n\n throw error;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,eAAsB,WAAW,KAA4B;CAC3D,MAAM,aAAa,kBAAkB,IAAI,IAAI;CAE7C,IAAI,CAAC,YACH,MAAM,IAAI,mBACR,iBAAiB,IAAI,KAAK,6GAE5B;CAGF,MAAM,EAAE,SAAS,SAAS,aAAa,cAAc,IAAI,IAAI;CAE7D,MAAM,UAAsB;EAC1B,IAAI,OAAO,IAAI,EAAE;EACjB,MAAM,IAAI;EACV,OAAO,IAAI;EACX,SAAS,IAAI,eAAe;EAC5B,aAAa,IAAI,KAAK,YAAY;EAClC,WAAW,UAAU,IAAI,eAAe,KAAK;EAC7C,KAAK,OAAO,SAAS;GACnB,MAAM,IAAI,IAAI,IAAI;EACpB;EACA,SAAS;CACX;CAEA,MAAM,MAAM,YAAY,WAAW,OAAO,SAAS,OAAO;CAC1D,MAAM,eAAe,gBAAgB;CAErC,IAAI;EACF,OAAO,aAAa,UAAa,eAAe,MAAM,aAAa,QAAQ,UAAU,GAAG,IAAI,MAAM,IAAI;CACxG,SAAS,OAAO;EACd,IAAI,iBAAiB,uBACnB,MAAM,IAAI,mBAAmB,MAAM,OAAO;EAG5C,MAAM;CACR;AACF"}
@@ -0,0 +1,30 @@
1
+ //#region ../queue/src/queue-context.d.ts
2
+ /**
3
+ * Carries ambient state (tenant, request id, …) from the code that
4
+ * dispatches a job into the worker that runs it.
5
+ */
6
+ type QueueContext<T> = {
7
+ /** Read the ambient state at dispatch time; `undefined` means nothing to carry. */capture: () => T | undefined; /** Run `run` with the captured state re-established. */
8
+ restore: (captured: T, run: () => Promise<unknown>) => Promise<unknown>;
9
+ };
10
+ /**
11
+ * Define a queue context. `restore`'s `captured` is inferred from `capture`.
12
+ *
13
+ * @example
14
+ * setQueueContext(
15
+ * defineQueueContext({
16
+ * capture: () => tenantStorage.getStore(),
17
+ * restore: (tenant, run) => tenantStorage.run(tenant, run),
18
+ * }),
19
+ * );
20
+ */
21
+ declare function defineQueueContext<T>(context: QueueContext<T>): QueueContext<T>;
22
+ /** Register the queue context used by every job dispatch and run. */
23
+ declare function setQueueContext(context: QueueContext<any>): void;
24
+ /** The registered queue context, if any. */
25
+ declare function getQueueContext(): QueueContext<any> | undefined;
26
+ /** Forget the registered queue context. */
27
+ declare function resetQueueContext(): void;
28
+ //#endregion
29
+ export { QueueContext, defineQueueContext, getQueueContext, resetQueueContext, setQueueContext };
30
+ //# sourceMappingURL=queue-context.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue-context.d.mts","names":[],"sources":["../../../../../../queue/src/queue-context.ts"],"mappings":";;AAIA;;;KAAY,YAAA;EAIU,mFAFpB,OAAA,QAAe,CAAA,cAEwC;EAAvD,OAAA,GAAU,QAAA,EAAU,CAAA,EAAG,GAAA,QAAW,OAAA,cAAqB,OAAA;AAAA;;;;;;;;;;;AAAO;iBAchD,kBAAA,IAAsB,OAAA,EAAS,YAAA,CAAa,CAAA,IAAK,YAAA,CAAa,CAAA;;iBAO9D,eAAA,CAAgB,OAA0B,EAAjB,YAAY;;iBAKrC,eAAA,IAAmB,YAAY;;iBAK/B,iBAAA"}
@@ -0,0 +1,55 @@
1
+ //#region ../queue/src/queue-context.ts
2
+ /**
3
+ * Define a queue context. `restore`'s `captured` is inferred from `capture`.
4
+ *
5
+ * @example
6
+ * setQueueContext(
7
+ * defineQueueContext({
8
+ * capture: () => tenantStorage.getStore(),
9
+ * restore: (tenant, run) => tenantStorage.run(tenant, run),
10
+ * }),
11
+ * );
12
+ */
13
+ function defineQueueContext(context) {
14
+ return context;
15
+ }
16
+ let activeContext;
17
+ /** Register the queue context used by every job dispatch and run. */
18
+ function setQueueContext(context) {
19
+ activeContext = context;
20
+ }
21
+ /** The registered queue context, if any. */
22
+ function getQueueContext() {
23
+ return activeContext;
24
+ }
25
+ /** Forget the registered queue context. */
26
+ function resetQueueContext() {
27
+ activeContext = void 0;
28
+ }
29
+ /** Wrap a payload with the captured context; no context leaves it bare. */
30
+ function wrapPayload(payload, context) {
31
+ if (context === void 0) return payload;
32
+ return {
33
+ __warlock: {
34
+ v: 1,
35
+ context
36
+ },
37
+ payload
38
+ };
39
+ }
40
+ /** Split job data into payload and captured context; bare data passes through. */
41
+ function unwrapPayload(data) {
42
+ const envelope = data;
43
+ if (envelope !== null && typeof envelope === "object" && typeof envelope.__warlock === "object" && envelope.__warlock !== null && envelope.__warlock.v === 1 && "payload" in envelope) return {
44
+ payload: envelope.payload,
45
+ context: envelope.__warlock.context
46
+ };
47
+ return {
48
+ payload: data,
49
+ context: void 0
50
+ };
51
+ }
52
+
53
+ //#endregion
54
+ export { defineQueueContext, getQueueContext, resetQueueContext, setQueueContext, unwrapPayload, wrapPayload };
55
+ //# sourceMappingURL=queue-context.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue-context.mjs","names":[],"sources":["../../../../../../queue/src/queue-context.ts"],"sourcesContent":["/**\n * Carries ambient state (tenant, request id, …) from the code that\n * dispatches a job into the worker that runs it.\n */\nexport type QueueContext<T> = {\n /** Read the ambient state at dispatch time; `undefined` means nothing to carry. */\n capture: () => T | undefined;\n /** Run `run` with the captured state re-established. */\n restore: (captured: T, run: () => Promise<unknown>) => Promise<unknown>;\n};\n\n/**\n * Define a queue context. `restore`'s `captured` is inferred from `capture`.\n *\n * @example\n * setQueueContext(\n * defineQueueContext({\n * capture: () => tenantStorage.getStore(),\n * restore: (tenant, run) => tenantStorage.run(tenant, run),\n * }),\n * );\n */\nexport function defineQueueContext<T>(context: QueueContext<T>): QueueContext<T> {\n return context;\n}\n\nlet activeContext: QueueContext<any> | undefined;\n\n/** Register the queue context used by every job dispatch and run. */\nexport function setQueueContext(context: QueueContext<any>): void {\n activeContext = context;\n}\n\n/** The registered queue context, if any. */\nexport function getQueueContext(): QueueContext<any> | undefined {\n return activeContext;\n}\n\n/** Forget the registered queue context. */\nexport function resetQueueContext(): void {\n activeContext = undefined;\n}\n\n/** The job-data envelope written when a context was captured. */\nexport type QueueEnvelope = {\n __warlock: { v: 1; context?: unknown };\n payload: unknown;\n};\n\n/** Wrap a payload with the captured context; no context leaves it bare. */\nexport function wrapPayload(payload: unknown, context: unknown): unknown {\n if (context === undefined) return payload;\n\n return { __warlock: { v: 1, context }, payload } satisfies QueueEnvelope;\n}\n\n/** Split job data into payload and captured context; bare data passes through. */\nexport function unwrapPayload(data: unknown): { payload: unknown; context: unknown } {\n const envelope = data as Partial<QueueEnvelope> | null;\n\n if (\n envelope !== null &&\n typeof envelope === \"object\" &&\n typeof envelope.__warlock === \"object\" &&\n envelope.__warlock !== null &&\n envelope.__warlock.v === 1 &&\n \"payload\" in envelope\n ) {\n return { payload: envelope.payload, context: envelope.__warlock.context };\n }\n\n return { payload: data, context: undefined };\n}\n"],"mappings":";;;;;;;;;;;;AAsBA,SAAgB,mBAAsB,SAA2C;CAC/E,OAAO;AACT;AAEA,IAAI;;AAGJ,SAAgB,gBAAgB,SAAkC;CAChE,gBAAgB;AAClB;;AAGA,SAAgB,kBAAiD;CAC/D,OAAO;AACT;;AAGA,SAAgB,oBAA0B;CACxC,gBAAgB;AAClB;;AASA,SAAgB,YAAY,SAAkB,SAA2B;CACvE,IAAI,YAAY,QAAW,OAAO;CAElC,OAAO;EAAE,WAAW;GAAE,GAAG;GAAG;EAAQ;EAAG;CAAQ;AACjD;;AAGA,SAAgB,cAAc,MAAuD;CACnF,MAAM,WAAW;CAEjB,IACE,aAAa,QACb,OAAO,aAAa,YACpB,OAAO,SAAS,cAAc,YAC9B,SAAS,cAAc,QACvB,SAAS,UAAU,MAAM,KACzB,aAAa,UAEb,OAAO;EAAE,SAAS,SAAS;EAAS,SAAS,SAAS,UAAU;CAAQ;CAG1E,OAAO;EAAE,SAAS;EAAM,SAAS;CAAU;AAC7C"}
package/esm/types.d.mts CHANGED
@@ -90,13 +90,15 @@ type JobContext = {
90
90
  attempt: number; /** Total attempts allowed. */
91
91
  maxAttempts: number; /** Record progress; readable through `job.find(id)`. */
92
92
  progress(value: JobProgress): Promise<void>; /** Append a line to the job's log. */
93
- log(line: string): Promise<void>;
93
+ log(line: string): Promise<void>; /** State captured at dispatch by the registered queue context, if any. */
94
+ context: unknown;
94
95
  };
95
96
  /**
96
97
  * The definition passed to `defineJob`.
97
98
  */
98
99
  type JobDefinition<TPayload, TResult> = JobOptions & {
99
- /** Unique job name, e.g. `"invoices.send"`. */name: string; /** Queue to run on. Default: `queue.defaultQueue` (`"default"`). */
100
+ /** Set `false` to skip capturing the queue context for this job. */context?: false; /** Unique job name, e.g. `"invoices.send"`. */
101
+ name: string; /** Queue to run on. Default: `queue.defaultQueue` (`"default"`). */
100
102
  queue?: string; /** The work. Throw to fail the attempt; the return value is stored as the job result. */
101
103
  handle(payload: TPayload, context: JobContext): Promise<TResult> | TResult;
102
104
  };
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../queue/src/types.ts"],"mappings":";;;;;KAIY,YAAA;AAAZ;;;;AAAA,KAMY,QAAA,wBAAgC,YAAY;AAAxD;;;;AAAA,KAMY,UAAA;EAAA,uFAIN,IAAA;EAEA,KAAK;AAAA;AAOX;;;;AAAA,KAAY,YAAA;AAMZ;;;;AAAA,KAAY,UAAA;EAQK,kEANf,QAAA,WAM2B;EAJ3B,OAAA,GAAU,UAAA,EAAV;EAEA,gBAAA,GAAmB,YAAA,EAAnB;EAEA,YAAA,GAAe,YAAA;AAAA;;;AAAY;KAMjB,kBAAA;EAAkB;;;;EAK5B,OAAA,YAOA;EALA,WAAA;EAKe;AAQjB;;;EARE,eAAA;AAAA;;;;;AAkBuB;KAVb,oBAAA;EAgBW,oDAdrB,OAAA,YAmBY;EAjBZ,IAAA;EAyBU;;;;;EAnBV,UAAA,GAAa,UAAU;AAAA;;;;KAMb,WAAA;EAaA;;;;EARV,UAAA,EAAY,iBAAA,EAcF;EAZV,MAAA;EAEA,YAAA,WAUuC;EARvC,iBAAA,GAAoB,UAAA,EAaA;EAXpB,OAAA,GAAU,kBAAA,EAuBM;EArBhB,SAAA,GAAY,oBAAA;AAAA;;KAIF,WAAA,YAAuB,MAAM;;;;KAK7B,UAAA;EAUV,kBARA,EAAA,UAUgB;EARhB,IAAA,UAQ8B;EAN9B,KAAA,UAQI;EANJ,OAAA,UAM0B;EAJ1B,WAAA,UAUU;EARV,QAAA,CAAS,KAAA,EAAO,WAAA,GAAc,OAAA,QAQP;EANvB,GAAA,CAAI,IAAA,WAAe,OAAA;AAAA;;;;KAMT,aAAA,sBAAmC,UAAA;EAM6B,+CAJ1E,IAAA,UAFwB;EAIxB,KAAA,WAJ6C;EAM7C,MAAA,CAAO,OAAA,EAAS,QAAA,EAAU,OAAA,EAAS,UAAA,GAAa,OAAA,CAAQ,OAAA,IAAW,OAAA;AAAA;;;;KAMzD,eAAA;EANgB,6CAQ1B,KAAA,GAAQ,QAAA;EARgD;;;AAAkB;EAa1E,QAAA;EAPyB;;;;EAYzB,KAAA,WALA;EAOA,QAAA;EAEA,OAAA,GAAU,UAAU;AAAA;;KAIV,aAAA;EACV,EAAA;EACA,IAAA;EACA,KAAA;AAAA;;KAIU,QAAA;;KAWA,WAAA;EACV,EAAA;EACA,IAAA;EACA,KAAA;EACA,KAAA,EAAO,QAAA;EACP,OAAA,EAAS,QAAA;EACT,QAAA,EAAU,WAAA;EACV,YAAA;EACA,MAAA,GAAS,OAAA;EACT,YAAA;EACA,SAAA,EAAW,IAAA;EACX,UAAA,GAAa,IAAA;AAAA;;KAIH,SAAA;EACV,EAAA;EACA,IAAA;EACA,KAAA;EACA,OAAA,EAAS,QAAA;EACT,YAAA;EACA,YAAA;EACA,UAAA;EACA,QAAA,GAAW,IAAA,EAnBX;EAqBA,KAAA,IAAS,OAAA;AAAA;;;;KAMC,QAAA;EAAA,SACD,IAAA;EAAA,SACA,KAAA,UAxBT;EA0BA,QAAA,CAAS,OAAA,EAAS,QAAA,EAAU,OAAA,GAAU,eAAA,GAAkB,OAAA,CAAQ,aAAA,GAzBrD;EA2BX,IAAA,CAAK,EAAA,WAAa,OAAA,CAAQ,WAAA,CAAY,QAAA,EAAU,OAAA;AAAA"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../queue/src/types.ts"],"mappings":";;;;;KAIY,YAAA;AAAZ;;;;AAAA,KAMY,QAAA,wBAAgC,YAAY;AAAxD;;;;AAAA,KAMY,UAAA;EAAA,uFAIN,IAAA;EAEA,KAAK;AAAA;AAOX;;;;AAAA,KAAY,YAAA;AAMZ;;;;AAAA,KAAY,UAAA;EAQK,kEANf,QAAA,WAM2B;EAJ3B,OAAA,GAAU,UAAA,EAAV;EAEA,gBAAA,GAAmB,YAAA,EAAnB;EAEA,YAAA,GAAe,YAAA;AAAA;;;AAAY;KAMjB,kBAAA;EAAkB;;;;EAK5B,OAAA,YAOA;EALA,WAAA;EAKe;AAQjB;;;EARE,eAAA;AAAA;;;;;AAkBuB;KAVb,oBAAA;EAgBW,oDAdrB,OAAA,YAmBY;EAjBZ,IAAA;EAyBU;;;;;EAnBV,UAAA,GAAa,UAAU;AAAA;;;;KAMb,WAAA;EAaA;;;;EARV,UAAA,EAAY,iBAAA,EAcF;EAZV,MAAA;EAEA,YAAA,WAUuC;EARvC,iBAAA,GAAoB,UAAA,EAaA;EAXpB,OAAA,GAAU,kBAAA,EAuBM;EArBhB,SAAA,GAAY,oBAAA;AAAA;;KAIF,WAAA,YAAuB,MAAM;;;;KAK7B,UAAA;EAUV,kBARA,EAAA,UAUgB;EARhB,IAAA,UAQ8B;EAN9B,KAAA,UAQI;EANJ,OAAA,UAQA;EANA,WAAA,UAMO;EAJP,QAAA,CAAS,KAAA,EAAO,WAAA,GAAc,OAAA,QAUP;EARvB,GAAA,CAAI,IAAA,WAAe,OAAA,QAQ0B;EAN7C,OAAA;AAAA;;;;KAMU,aAAA,sBAAmC,UAAA;EAQ6B,oEAN1E,OAAA,UAFkC;EAIlC,IAAA,UAFA;EAIA,KAAA;EAEA,MAAA,CAAO,OAAA,EAAS,QAAA,EAAU,OAAA,EAAS,UAAA,GAAa,OAAA,CAAQ,OAAA,IAAW,OAAA;AAAA;;;;KAMzD,eAAA;EAN8C,6CAQxD,KAAA,GAAQ,QAAA;EARkE;AAAA;AAM5E;;EAOE,QAAA;EASoB;;;;EAJpB,KAAA,WAEA;EAAA,QAAA,WAEU;EAAV,OAAA,GAAU,UAAU;AAAA;AAItB;AAAA,KAAY,aAAA;EACV,EAAA;EACA,IAAA;EACA,KAAA;AAAA;;KAIU,QAAA;AAJL;AAAA,KAeK,WAAA;EACV,EAAA;EACA,IAAA;EACA,KAAA;EACA,KAAA,EAAO,QAAA;EACP,OAAA,EAAS,QAAA;EACT,QAAA,EAAU,WAAA;EACV,YAAA;EACA,MAAA,GAAS,OAAA;EACT,YAAA;EACA,SAAA,EAAW,IAAA;EACX,UAAA,GAAa,IAAA;AAAA;;KAIH,SAAA;EACV,EAAA;EACA,IAAA;EACA,KAAA;EACA,OAAA,EAAS,QAAA;EACT,YAAA;EACA,YAAA;EACA,UAAA;EACA,QAAA,GAAW,IAAA,EAlBX;EAoBA,KAAA,IAAS,OAAA;AAAA;;;;KAMC,QAAA;EAAA,SACD,IAAA;EAAA,SACA,KAAA,UAvBE;EAyBX,QAAA,CAAS,OAAA,EAAS,QAAA,EAAU,OAAA,GAAU,eAAA,GAAkB,OAAA,CAAQ,aAAA,GAxBnD;EA0Bb,IAAA,CAAK,EAAA,WAAa,OAAA,CAAQ,WAAA,CAAY,QAAA,EAAU,OAAA;AAAA"}
package/package.json CHANGED
@@ -11,8 +11,8 @@
11
11
  "peerDependencies": {
12
12
  "@bull-board/api": "^9.0.0",
13
13
  "@bull-board/fastify": "^9.0.0",
14
- "@warlock.js/core": "5.22.1",
15
- "@warlock.js/logger": "5.22.1"
14
+ "@warlock.js/core": "5.23.1",
15
+ "@warlock.js/logger": "5.23.1"
16
16
  },
17
17
  "peerDependenciesMeta": {
18
18
  "@bull-board/api": {
@@ -37,7 +37,7 @@
37
37
  ],
38
38
  "author": "hassanzohdy",
39
39
  "license": "MIT",
40
- "version": "5.22.1",
40
+ "version": "5.23.1",
41
41
  "main": "./cjs/index.cjs",
42
42
  "module": "./esm/index.mjs",
43
43
  "types": "./esm/index.d.mts",
@@ -44,6 +44,22 @@ const job = await sendInvoice.find(id);
44
44
  // { id, name, queue, state, payload, progress, attemptsMade, result, failedReason, createdAt, finishedAt }
45
45
  ```
46
46
 
47
+ ## Carry request context into jobs
48
+
49
+ ```ts
50
+ setQueueContext(
51
+ defineQueueContext({
52
+ capture: () => tenantStorage.getStore(), // runs inside dispatch()
53
+ restore: (tenant, run) => tenantStorage.run(tenant, run), // wraps handle()
54
+ }),
55
+ );
56
+ ```
57
+
58
+ - `undefined` from `capture` carries nothing; the value must be JSON-safe. Read it in a handler as `ctx.context` (`unknown`).
59
+ - Jobs dispatched from a restored handler capture the same context.
60
+ - `defineJob({ context: false })` opts a job out.
61
+ - Throw `UnrecoverableJobError` from a handler or `restore` to fail the job without retries.
62
+
47
63
  ## Rules
48
64
 
49
65
  - A job name with no handler in the worker process fails at once, without retries.