@warlock.js/queue 5.19.1 → 5.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,20 @@
1
- # Changelog — @warlock.js/queue
1
+ # Changelog — @warlock.js/queue
2
2
 
3
3
  All notable changes to `@warlock.js/queue` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
+ ## 5.21.0 - 2026-09-25
8
+
9
+ ### Changed
10
+
11
+ - Lockstep release maintenance and dependency refresh.
12
+ ## 5.20.0 - 2026-09-24
13
+
14
+ ### Changed
15
+
16
+ - Added a package-level skill index and clearer discovery descriptions for grouped agent guidance.
17
+
7
18
  ## 5.19.0 - 2026-09-23
8
19
 
9
20
  ### Changed
package/llms-full.txt CHANGED
@@ -6,192 +6,192 @@
6
6
 
7
7
  ## configure-queue `@warlock.js/queue/configure-queue/SKILL.md`
8
8
 
9
- ---
10
- name: configure-queue
11
- description: "Configure the queue in @warlock.js/queue; use when you need to configure queue."
12
- ---
13
-
14
- # Configure the queue
15
-
16
- Redis is required. The config is declarative; the connector applies it.
17
-
18
- ```ts title="src/config/queue.ts"
19
- import type { QueueConfig } from "@warlock.js/queue";
20
-
21
- const queueConfig: QueueConfig = {
22
- connection: { host: "127.0.0.1", port: 6379 }, // any BullMQ connection option, or { url }
23
- prefix: "my-app", // Redis key prefix, default "warlock"
24
- defaultJobOptions: { attempts: 3, backoff: { type: "exponential", delay: 1000 } },
25
- workers: {
26
- enabled: true, // default true
27
- concurrency: 5, // jobs in parallel per queue, default 1
28
- shutdownTimeout: 30_000, // ms to wait for active jobs, default 30000
29
- },
30
- };
31
-
32
- export default queueConfig;
33
- ```
34
-
35
- ```ts title="warlock.config.ts"
36
- import { defineConfig } from "@warlock.js/core";
37
- import { queueConnector } from "@warlock.js/queue";
38
-
39
- export default defineConfig({
40
- connectors: [queueConnector()],
41
- });
42
- ```
43
-
44
- ## What the connector does
45
-
46
- - Starts in the **late** phase — after app code is imported — so every `defineJob` has registered. One worker per queue that has a job.
47
- - No `queue` config → logs a warning and does nothing.
48
- - On SIGINT/SIGTERM: workers stop taking jobs, active jobs get up to `shutdownTimeout` ms, then Redis connections close. A job still running after that is retried by another worker once its lock expires.
49
-
50
- ## Dispatch-only process
51
-
52
- Set `workers.enabled: false`. `dispatch()` still works; nothing is processed in that process. Run the workers elsewhere with the same job definitions:
53
-
54
- ```ts
55
- import { setQueueConfig, startWorkers, closeQueue } from "@warlock.js/queue";
56
- import "./jobs"; // modules that call defineJob
57
-
58
- setQueueConfig({ connection: { host: "127.0.0.1", port: 6379 } });
59
- await startWorkers();
60
- process.on("SIGTERM", () => closeQueue());
61
- ```
62
-
63
- Every process must use the same `prefix`, or they will not see each other's jobs.
64
-
65
- ## Dashboard
66
-
67
- `QueueConfig` also takes a `dashboard: { enabled, path, middleware }` block — see [`manage-failed-jobs`](../manage-failed-jobs/SKILL.md#dashboard-optional) for the `warlock add bull-board` setup and the production guard (`QueueDashboardUnguardedError`).
9
+ ---
10
+ name: configure-queue
11
+ description: 'Configure `@warlock.js/queue`: the declarative `src/config/queue.ts` (`QueueConfig` — `connection`, `prefix`, `defaultQueue`, `defaultJobOptions`, `workers: { enabled, concurrency, shutdownTimeout }`), registering `queueConnector()` in `warlock.config.ts > connectors`, running a dispatch-only process, and graceful shutdown. Programmatic `setQueueConfig` / `startWorkers` / `closeQueue` for scripts and tests. Triggers: `QueueConfig`, `queueConnector`, `setQueueConfig`, `startWorkers`, `closeQueue`, `workers.enabled`, `shutdownTimeout`; "configure the queue", "connect BullMQ to Redis", "disable workers in the web process", "graceful shutdown of jobs". Skip: writing jobs — `@warlock.js/queue/define-jobs/SKILL.md`.'
12
+ ---
13
+
14
+ # Configure the queue
15
+
16
+ Redis is required. The config is declarative; the connector applies it.
17
+
18
+ ```ts title="src/config/queue.ts"
19
+ import type { QueueConfig } from "@warlock.js/queue";
20
+
21
+ const queueConfig: QueueConfig = {
22
+ connection: { host: "127.0.0.1", port: 6379 }, // any BullMQ connection option, or { url }
23
+ prefix: "my-app", // Redis key prefix, default "warlock"
24
+ defaultJobOptions: { attempts: 3, backoff: { type: "exponential", delay: 1000 } },
25
+ workers: {
26
+ enabled: true, // default true
27
+ concurrency: 5, // jobs in parallel per queue, default 1
28
+ shutdownTimeout: 30_000, // ms to wait for active jobs, default 30000
29
+ },
30
+ };
31
+
32
+ export default queueConfig;
33
+ ```
34
+
35
+ ```ts title="warlock.config.ts"
36
+ import { defineConfig } from "@warlock.js/core";
37
+ import { queueConnector } from "@warlock.js/queue";
38
+
39
+ export default defineConfig({
40
+ connectors: [queueConnector()],
41
+ });
42
+ ```
43
+
44
+ ## What the connector does
45
+
46
+ - Starts in the **late** phase — after app code is imported — so every `defineJob` has registered. One worker per queue that has a job.
47
+ - No `queue` config → logs a warning and does nothing.
48
+ - On SIGINT/SIGTERM: workers stop taking jobs, active jobs get up to `shutdownTimeout` ms, then Redis connections close. A job still running after that is retried by another worker once its lock expires.
49
+
50
+ ## Dispatch-only process
51
+
52
+ Set `workers.enabled: false`. `dispatch()` still works; nothing is processed in that process. Run the workers elsewhere with the same job definitions:
53
+
54
+ ```ts
55
+ import { setQueueConfig, startWorkers, closeQueue } from "@warlock.js/queue";
56
+ import "./jobs"; // modules that call defineJob
57
+
58
+ setQueueConfig({ connection: { host: "127.0.0.1", port: 6379 } });
59
+ await startWorkers();
60
+ process.on("SIGTERM", () => closeQueue());
61
+ ```
62
+
63
+ Every process must use the same `prefix`, or they will not see each other's jobs.
64
+
65
+ ## Dashboard
66
+
67
+ `QueueConfig` also takes a `dashboard: { enabled, path, middleware }` block — see [`manage-failed-jobs`](../manage-failed-jobs/SKILL.md#dashboard-optional) for the `warlock add bull-board` setup and the production guard (`QueueDashboardUnguardedError`).
68
68
 
69
69
 
70
70
  ## define-jobs `@warlock.js/queue/define-jobs/SKILL.md`
71
71
 
72
- ---
73
- name: define-jobs
74
- description: "Define and dispatch jobs in @warlock.js/queue; use when you need to define jobs."
75
- ---
76
-
77
- # Define and dispatch jobs
78
-
79
- ```ts title="src/app/invoices/jobs/send-invoice.job.ts"
80
- import { defineJob } from "@warlock.js/queue";
81
-
82
- export const sendInvoice = defineJob({
83
- name: "invoices.send", // unique across the app
84
- attempts: 5, // total tries, default 1
85
- backoff: { type: "exponential", delay: 2000 }, // or a number = fixed ms
86
- async handle(payload: { invoiceId: string }, ctx) {
87
- await ctx.log(`attempt ${ctx.attempt} of ${ctx.maxAttempts}`);
88
- await ctx.progress(50);
89
- // throw to fail this attempt; it is retried until attempts run out
90
- return { sent: true }; // stored as the job result
91
- },
92
- });
93
- ```
94
-
95
- The job module must be imported by the process that runs workers — in a Warlock app, anything under `src/app` that is loaded at boot.
96
-
97
- ## Dispatch
98
-
99
- ```ts
100
- const { id } = await sendInvoice.dispatch({ invoiceId: "42" });
101
-
102
- await sendInvoice.dispatch({ invoiceId: "43" }, {
103
- delay: "10m", // ms number or "500ms" | "30s" | "10m" | "2h" | "1d"
104
- priority: 1, // 1 runs first; larger numbers later
105
- jobId: "invoice:43", // a second dispatch with a live id is ignored
106
- });
107
- ```
108
-
109
- Option precedence: dispatch options > `defineJob` > `queue.defaultJobOptions`.
110
-
111
- ## Read a job
112
-
113
- ```ts
114
- const job = await sendInvoice.find(id);
115
- // { id, name, queue, state, payload, progress, attemptsMade, result, failedReason, createdAt, finishedAt }
116
- ```
117
-
118
- ## Rules
119
-
120
- - A job name with no handler in the worker process fails at once, without retries.
121
- - Payloads are stored as JSON: pass ids, not model instances.
122
- - Defining the same name again replaces the handler (this is what keeps dev reloads working), so keep names unique.
72
+ ---
73
+ name: define-jobs
74
+ description: 'Define and dispatch background jobs with `@warlock.js/queue`: `defineJob({ name, queue?, attempts?, backoff?, removeOnComplete?, removeOnFail?, handle(payload, ctx) })` returns a typed job; `job.dispatch(payload, { delay, priority, jobId, attempts, backoff })`; the handler context (`id`, `attempt`, `maxAttempts`, `progress()`, `log()`); reading a job with `job.find(id)`. Triggers: `defineJob`, `.dispatch(`, `ctx.progress`, `JobContext`, `DispatchOptions`, `backoff`, `attempts`, `priority`, `jobId`; "run this in the background", "retry with backoff", "delay a job", "job progress", "idempotent dispatch". Skip: config and workers — `@warlock.js/queue/configure-queue/SKILL.md`; failed jobs — `@warlock.js/queue/manage-failed-jobs/SKILL.md`.'
75
+ ---
76
+
77
+ # Define and dispatch jobs
78
+
79
+ ```ts title="src/app/invoices/jobs/send-invoice.job.ts"
80
+ import { defineJob } from "@warlock.js/queue";
81
+
82
+ export const sendInvoice = defineJob({
83
+ name: "invoices.send", // unique across the app
84
+ attempts: 5, // total tries, default 1
85
+ backoff: { type: "exponential", delay: 2000 }, // or a number = fixed ms
86
+ async handle(payload: { invoiceId: string }, ctx) {
87
+ await ctx.log(`attempt ${ctx.attempt} of ${ctx.maxAttempts}`);
88
+ await ctx.progress(50);
89
+ // throw to fail this attempt; it is retried until attempts run out
90
+ return { sent: true }; // stored as the job result
91
+ },
92
+ });
93
+ ```
94
+
95
+ The job module must be imported by the process that runs workers — in a Warlock app, anything under `src/app` that is loaded at boot.
96
+
97
+ ## Dispatch
98
+
99
+ ```ts
100
+ const { id } = await sendInvoice.dispatch({ invoiceId: "42" });
101
+
102
+ await sendInvoice.dispatch({ invoiceId: "43" }, {
103
+ delay: "10m", // ms number or "500ms" | "30s" | "10m" | "2h" | "1d"
104
+ priority: 1, // 1 runs first; larger numbers later
105
+ jobId: "invoice:43", // a second dispatch with a live id is ignored
106
+ });
107
+ ```
108
+
109
+ Option precedence: dispatch options > `defineJob` > `queue.defaultJobOptions`.
110
+
111
+ ## Read a job
112
+
113
+ ```ts
114
+ const job = await sendInvoice.find(id);
115
+ // { id, name, queue, state, payload, progress, attemptsMade, result, failedReason, createdAt, finishedAt }
116
+ ```
117
+
118
+ ## Rules
119
+
120
+ - A job name with no handler in the worker process fails at once, without retries.
121
+ - Payloads are stored as JSON: pass ids, not model instances.
122
+ - Defining the same name again replaces the handler (this is what keeps dev reloads working), so keep names unique.
123
123
 
124
124
 
125
125
  ## manage-failed-jobs `@warlock.js/queue/manage-failed-jobs/SKILL.md`
126
126
 
127
- ---
128
- name: manage-failed-jobs
129
- description: "Failed jobs in @warlock.js/queue; use when you need to manage failed jobs."
130
- ---
131
-
132
- # Failed jobs
133
-
134
- A job is failed once it has used every attempt, or failed in a way that cannot be retried (for example, no handler for its name). Failed jobs are kept unless you set `removeOnFail`.
135
-
136
- ```ts
137
- import { failedJobs, retryFailedJob } from "@warlock.js/queue";
138
-
139
- const failed = await failedJobs({ queue: "default", start: 0, end: 49 }); // newest first
140
-
141
- for (const job of failed) {
142
- console.log(job.name, job.failedReason, job.attemptsMade);
143
- }
144
-
145
- await failed[0]?.retry(); // back to waiting
146
- await retryFailedJob("invoice:43"); // by id; throws FailedJobNotFoundError if not failed
147
- ```
148
-
149
- ## Dashboard (optional)
150
-
151
- ```sh
152
- warlock add bull-board
153
- ```
154
-
155
- This installs `@bull-board/api` + `@bull-board/fastify` and writes a `dashboard` block to `src/config/queue.ts`. It `requires: ["queue"]`, so it adds the `queue` feature first automatically when it isn't installed yet — no need to run `warlock add queue` yourself first:
156
-
157
- ```ts title="src/config/queue.ts"
158
- import type { QueueConfig } from "@warlock.js/queue";
159
- import { middleware } from "@warlock.js/core";
160
- import { authMiddleware } from "@warlock.js/auth";
161
-
162
- const queueConfig: QueueConfig = {
163
- connection: { host: "127.0.0.1", port: 6379 },
164
- dashboard: {
165
- enabled: true,
166
- path: "/admin/queues", // default
167
- middleware: [authMiddleware("admin")], // guards every dashboard route
168
- },
169
- };
170
-
171
- export default queueConfig;
172
- ```
173
-
174
- - The dashboard can retry and delete jobs, so it must be guarded. In production, an empty `middleware` list throws `QueueDashboardUnguardedError` at boot instead of mounting exposed. Outside production it logs a warning and mounts anyway.
175
- - `queueConnector()` mounts the dashboard at boot from this config — no extra wiring needed.
176
-
177
- ### Advanced: manual `queueDashboard()`
178
-
179
- For mounting outside the config-driven path (a custom server, a non-standard boot sequence), call `queueDashboard` yourself:
180
-
181
- ```ts
182
- import { getHttpServer } from "@warlock.js/core";
183
- import { queueDashboard } from "@warlock.js/queue";
184
- import { authMiddleware } from "@warlock.js/auth";
185
-
186
- await queueDashboard(getHttpServer(), {
187
- basePath: "/admin/queues",
188
- middleware: [authMiddleware("admin")],
189
- });
190
- ```
191
-
192
- - Call it before the HTTP server starts listening.
193
- - Shows every queue that has a job, plus the default queue, unless you pass `queues`.
194
- - It has no authentication of its own — pass `middleware` yourself; this path does not enforce the production guard that the `dashboard` config does.
127
+ ---
128
+ name: manage-failed-jobs
129
+ description: 'Inspect and retry failed `@warlock.js/queue` jobs: `failedJobs({ queue, start, end })` returns `FailedJob[]` (`id`, `name`, `payload`, `attemptsMade`, `failedReason`, `stacktrace`, `failedAt`, `retry()`), `retryFailedJob(id, { queue })` (throws `FailedJobNotFoundError`), and the bull-board UI — `warlock add bull-board` (requires: ["queue"], so it installs queue first when missing) writes a `dashboard: { enabled, path, middleware }` block to `src/config/queue.ts`; a guard middleware is required in production or boot throws `QueueDashboardUnguardedError`. Manual `queueDashboard(server, { basePath, queues, middleware })` also available — needs `@bull-board/api` + `@bull-board/fastify`, loaded only on call, missing ones throw `QueueDashboardDependencyError`. Triggers: `failedJobs`, `retryFailedJob`, `queueDashboard`, `bull-board`, `dashboard.enabled`, `QueueDashboardUnguardedError`; "list failed jobs", "retry a failed job", "queue dashboard", "job admin UI". Skip: defining retries — `@warlock.js/queue/define-jobs/SKILL.md`.'
130
+ ---
131
+
132
+ # Failed jobs
133
+
134
+ A job is failed once it has used every attempt, or failed in a way that cannot be retried (for example, no handler for its name). Failed jobs are kept unless you set `removeOnFail`.
135
+
136
+ ```ts
137
+ import { failedJobs, retryFailedJob } from "@warlock.js/queue";
138
+
139
+ const failed = await failedJobs({ queue: "default", start: 0, end: 49 }); // newest first
140
+
141
+ for (const job of failed) {
142
+ console.log(job.name, job.failedReason, job.attemptsMade);
143
+ }
144
+
145
+ await failed[0]?.retry(); // back to waiting
146
+ await retryFailedJob("invoice:43"); // by id; throws FailedJobNotFoundError if not failed
147
+ ```
148
+
149
+ ## Dashboard (optional)
150
+
151
+ ```sh
152
+ warlock add bull-board
153
+ ```
154
+
155
+ This installs `@bull-board/api` + `@bull-board/fastify` and writes a `dashboard` block to `src/config/queue.ts`. It `requires: ["queue"]`, so it adds the `queue` feature first automatically when it isn't installed yet — no need to run `warlock add queue` yourself first:
156
+
157
+ ```ts title="src/config/queue.ts"
158
+ import type { QueueConfig } from "@warlock.js/queue";
159
+ import { middleware } from "@warlock.js/core";
160
+ import { authMiddleware } from "@warlock.js/auth";
161
+
162
+ const queueConfig: QueueConfig = {
163
+ connection: { host: "127.0.0.1", port: 6379 },
164
+ dashboard: {
165
+ enabled: true,
166
+ path: "/admin/queues", // default
167
+ middleware: [authMiddleware("admin")], // guards every dashboard route
168
+ },
169
+ };
170
+
171
+ export default queueConfig;
172
+ ```
173
+
174
+ - The dashboard can retry and delete jobs, so it must be guarded. In production, an empty `middleware` list throws `QueueDashboardUnguardedError` at boot instead of mounting exposed. Outside production it logs a warning and mounts anyway.
175
+ - `queueConnector()` mounts the dashboard at boot from this config — no extra wiring needed.
176
+
177
+ ### Advanced: manual `queueDashboard()`
178
+
179
+ For mounting outside the config-driven path (a custom server, a non-standard boot sequence), call `queueDashboard` yourself:
180
+
181
+ ```ts
182
+ import { getHttpServer } from "@warlock.js/core";
183
+ import { queueDashboard } from "@warlock.js/queue";
184
+ import { authMiddleware } from "@warlock.js/auth";
185
+
186
+ await queueDashboard(getHttpServer(), {
187
+ basePath: "/admin/queues",
188
+ middleware: [authMiddleware("admin")],
189
+ });
190
+ ```
191
+
192
+ - Call it before the HTTP server starts listening.
193
+ - Shows every queue that has a job, plus the default queue, unless you pass `queues`.
194
+ - It has no authentication of its own — pass `middleware` yourself; this path does not enforce the production guard that the `dashboard` config does.
195
195
 
196
196
 
197
197
  ## overview `@warlock.js/queue/overview/SKILL.md`
@@ -226,40 +226,40 @@ Durable background jobs. Jobs are stored in **Redis** (required), retried on fai
226
226
 
227
227
  ## queue-notifications `@warlock.js/queue/queue-notifications/SKILL.md`
228
228
 
229
- ---
230
- name: queue-notifications
231
- description: "Queue notifications with BullMQ in @warlock.js/queue; use when you need to queue notifications."
232
- ---
233
-
234
- # Queue notifications with BullMQ
235
-
236
- ```ts title="src/config/notifications.ts"
237
- import { type NotificationConfig, bullmqQueue, mailChannel } from "@warlock.js/notifications";
238
-
239
- const config: NotificationConfig = {
240
- channels: { mail: mailChannel() },
241
- queue: bullmqQueue({ attempts: 3, backoff: { type: "exponential", delay: 5000 } }),
242
- };
243
-
244
- export default config;
245
- ```
246
-
247
- `bullmqQueue` ships inside `@warlock.js/notifications` itself and lazy-loads `@warlock.js/queue` (an OPTIONAL peer) the first time `.queue()` runs — install it with `warlock add queue` if it isn't there yet, otherwise the first `.queue()` throws `QueuePackageNotInstalledError`.
248
-
249
- Also configure the queue itself (`configure-queue`). No separate notifications worker is needed: the delivery is an ordinary job, run by the queue workers.
250
-
251
- ```ts
252
- await orderShipped.queue(user, { order }); // enqueued, delivered by a worker
253
- await orderShipped.queue(user, { order }, { delay: "10m" }); // delivered in 10 minutes
254
- ```
255
-
256
- ## Behaviour
257
-
258
- - The job carries the rendered payload and resolved route; the worker looks the channel up by name in its own notifications config and calls `channel.send`.
259
- - `delay`: a number is seconds (notifications' convention); a string is a duration.
260
- - `channel.send` throws → retried per `attempts` / `backoff`, then listed by `failedJobs()`.
261
- - Channel not configured in the worker → fails at once, no retries.
262
- - Deliveries run under the job name `warlock.notifications.deliver`.
263
-
229
+ ---
230
+ name: queue-notifications
231
+ description: 'Send `@warlock.js/notifications` `.queue()` deliveries through BullMQ with `bullmqQueue({ queue?, attempts?, backoff? })` from `@warlock.js/notifications` — put it in the `queue` slot of `src/config/notifications.ts`; the queue workers deliver. Honours `SendOptions.delay` (number = seconds, or "10m"), retries a failing `channel.send`, and fails at once for a channel missing from the worker config. Triggers: `bullmqQueue`, `NotificationConfig.queue`, `QueuePackageNotInstalledError`; "queue notifications with BullMQ", "retry notification delivery", "delayed notification". Skip: the herald backend — `@warlock.js/notifications/queue-notifications/SKILL.md`.'
232
+ ---
233
+
234
+ # Queue notifications with BullMQ
235
+
236
+ ```ts title="src/config/notifications.ts"
237
+ import { type NotificationConfig, bullmqQueue, mailChannel } from "@warlock.js/notifications";
238
+
239
+ const config: NotificationConfig = {
240
+ channels: { mail: mailChannel() },
241
+ queue: bullmqQueue({ attempts: 3, backoff: { type: "exponential", delay: 5000 } }),
242
+ };
243
+
244
+ export default config;
245
+ ```
246
+
247
+ `bullmqQueue` ships inside `@warlock.js/notifications` itself and lazy-loads `@warlock.js/queue` (an OPTIONAL peer) the first time `.queue()` runs — install it with `warlock add queue` if it isn't there yet, otherwise the first `.queue()` throws `QueuePackageNotInstalledError`.
248
+
249
+ Also configure the queue itself (`configure-queue`). No separate notifications worker is needed: the delivery is an ordinary job, run by the queue workers.
250
+
251
+ ```ts
252
+ await orderShipped.queue(user, { order }); // enqueued, delivered by a worker
253
+ await orderShipped.queue(user, { order }, { delay: "10m" }); // delivered in 10 minutes
254
+ ```
255
+
256
+ ## Behaviour
257
+
258
+ - The job carries the rendered payload and resolved route; the worker looks the channel up by name in its own notifications config and calls `channel.send`.
259
+ - `delay`: a number is seconds (notifications' convention); a string is a duration.
260
+ - `channel.send` throws → retried per `attempts` / `backoff`, then listed by `failedJobs()`.
261
+ - Channel not configured in the worker → fails at once, no retries.
262
+ - Deliveries run under the job name `warlock.notifications.deliver`.
263
+
264
264
 
265
265
 
package/llms.txt CHANGED
@@ -6,8 +6,8 @@
6
6
 
7
7
  ## Skills
8
8
 
9
- - [configure-queue](@warlock.js/queue/configure-queue/SKILL.md): Configure the queue in @warlock.js/queue; use when you need to configure queue.
10
- - [define-jobs](@warlock.js/queue/define-jobs/SKILL.md): Define and dispatch jobs in @warlock.js/queue; use when you need to define jobs.
11
- - [manage-failed-jobs](@warlock.js/queue/manage-failed-jobs/SKILL.md): Failed jobs in @warlock.js/queue; use when you need to manage failed jobs.
9
+ - [configure-queue](@warlock.js/queue/configure-queue/SKILL.md): (no description)
10
+ - [define-jobs](@warlock.js/queue/define-jobs/SKILL.md): (no description)
11
+ - [manage-failed-jobs](@warlock.js/queue/manage-failed-jobs/SKILL.md): (no description)
12
12
  - [overview](@warlock.js/queue/overview/SKILL.md): @warlock.js/queue overview for orientation and choosing the focused skill for the task.
13
- - [queue-notifications](@warlock.js/queue/queue-notifications/SKILL.md): Queue notifications with BullMQ in @warlock.js/queue; use when you need to queue notifications.
13
+ - [queue-notifications](@warlock.js/queue/queue-notifications/SKILL.md): (no description)
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.19.1",
15
- "@warlock.js/logger": "5.19.1"
14
+ "@warlock.js/core": "5.21.0",
15
+ "@warlock.js/logger": "5.21.0"
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.19.1",
40
+ "version": "5.21.0",
41
41
  "main": "./cjs/index.cjs",
42
42
  "module": "./esm/index.mjs",
43
43
  "types": "./esm/index.d.mts",
@@ -1,59 +1,59 @@
1
- ---
2
- name: configure-queue
3
- description: "Configure the queue in @warlock.js/queue; use when you need to configure queue."
4
- ---
5
-
6
- # Configure the queue
7
-
8
- Redis is required. The config is declarative; the connector applies it.
9
-
10
- ```ts title="src/config/queue.ts"
11
- import type { QueueConfig } from "@warlock.js/queue";
12
-
13
- const queueConfig: QueueConfig = {
14
- connection: { host: "127.0.0.1", port: 6379 }, // any BullMQ connection option, or { url }
15
- prefix: "my-app", // Redis key prefix, default "warlock"
16
- defaultJobOptions: { attempts: 3, backoff: { type: "exponential", delay: 1000 } },
17
- workers: {
18
- enabled: true, // default true
19
- concurrency: 5, // jobs in parallel per queue, default 1
20
- shutdownTimeout: 30_000, // ms to wait for active jobs, default 30000
21
- },
22
- };
23
-
24
- export default queueConfig;
25
- ```
26
-
27
- ```ts title="warlock.config.ts"
28
- import { defineConfig } from "@warlock.js/core";
29
- import { queueConnector } from "@warlock.js/queue";
30
-
31
- export default defineConfig({
32
- connectors: [queueConnector()],
33
- });
34
- ```
35
-
36
- ## What the connector does
37
-
38
- - Starts in the **late** phase — after app code is imported — so every `defineJob` has registered. One worker per queue that has a job.
39
- - No `queue` config → logs a warning and does nothing.
40
- - On SIGINT/SIGTERM: workers stop taking jobs, active jobs get up to `shutdownTimeout` ms, then Redis connections close. A job still running after that is retried by another worker once its lock expires.
41
-
42
- ## Dispatch-only process
43
-
44
- Set `workers.enabled: false`. `dispatch()` still works; nothing is processed in that process. Run the workers elsewhere with the same job definitions:
45
-
46
- ```ts
47
- import { setQueueConfig, startWorkers, closeQueue } from "@warlock.js/queue";
48
- import "./jobs"; // modules that call defineJob
49
-
50
- setQueueConfig({ connection: { host: "127.0.0.1", port: 6379 } });
51
- await startWorkers();
52
- process.on("SIGTERM", () => closeQueue());
53
- ```
54
-
55
- Every process must use the same `prefix`, or they will not see each other's jobs.
56
-
57
- ## Dashboard
58
-
59
- `QueueConfig` also takes a `dashboard: { enabled, path, middleware }` block — see [`manage-failed-jobs`](../manage-failed-jobs/SKILL.md#dashboard-optional) for the `warlock add bull-board` setup and the production guard (`QueueDashboardUnguardedError`).
1
+ ---
2
+ name: configure-queue
3
+ description: 'Configure `@warlock.js/queue`: the declarative `src/config/queue.ts` (`QueueConfig` — `connection`, `prefix`, `defaultQueue`, `defaultJobOptions`, `workers: { enabled, concurrency, shutdownTimeout }`), registering `queueConnector()` in `warlock.config.ts > connectors`, running a dispatch-only process, and graceful shutdown. Programmatic `setQueueConfig` / `startWorkers` / `closeQueue` for scripts and tests. Triggers: `QueueConfig`, `queueConnector`, `setQueueConfig`, `startWorkers`, `closeQueue`, `workers.enabled`, `shutdownTimeout`; "configure the queue", "connect BullMQ to Redis", "disable workers in the web process", "graceful shutdown of jobs". Skip: writing jobs — `@warlock.js/queue/define-jobs/SKILL.md`.'
4
+ ---
5
+
6
+ # Configure the queue
7
+
8
+ Redis is required. The config is declarative; the connector applies it.
9
+
10
+ ```ts title="src/config/queue.ts"
11
+ import type { QueueConfig } from "@warlock.js/queue";
12
+
13
+ const queueConfig: QueueConfig = {
14
+ connection: { host: "127.0.0.1", port: 6379 }, // any BullMQ connection option, or { url }
15
+ prefix: "my-app", // Redis key prefix, default "warlock"
16
+ defaultJobOptions: { attempts: 3, backoff: { type: "exponential", delay: 1000 } },
17
+ workers: {
18
+ enabled: true, // default true
19
+ concurrency: 5, // jobs in parallel per queue, default 1
20
+ shutdownTimeout: 30_000, // ms to wait for active jobs, default 30000
21
+ },
22
+ };
23
+
24
+ export default queueConfig;
25
+ ```
26
+
27
+ ```ts title="warlock.config.ts"
28
+ import { defineConfig } from "@warlock.js/core";
29
+ import { queueConnector } from "@warlock.js/queue";
30
+
31
+ export default defineConfig({
32
+ connectors: [queueConnector()],
33
+ });
34
+ ```
35
+
36
+ ## What the connector does
37
+
38
+ - Starts in the **late** phase — after app code is imported — so every `defineJob` has registered. One worker per queue that has a job.
39
+ - No `queue` config → logs a warning and does nothing.
40
+ - On SIGINT/SIGTERM: workers stop taking jobs, active jobs get up to `shutdownTimeout` ms, then Redis connections close. A job still running after that is retried by another worker once its lock expires.
41
+
42
+ ## Dispatch-only process
43
+
44
+ Set `workers.enabled: false`. `dispatch()` still works; nothing is processed in that process. Run the workers elsewhere with the same job definitions:
45
+
46
+ ```ts
47
+ import { setQueueConfig, startWorkers, closeQueue } from "@warlock.js/queue";
48
+ import "./jobs"; // modules that call defineJob
49
+
50
+ setQueueConfig({ connection: { host: "127.0.0.1", port: 6379 } });
51
+ await startWorkers();
52
+ process.on("SIGTERM", () => closeQueue());
53
+ ```
54
+
55
+ Every process must use the same `prefix`, or they will not see each other's jobs.
56
+
57
+ ## Dashboard
58
+
59
+ `QueueConfig` also takes a `dashboard: { enabled, path, middleware }` block — see [`manage-failed-jobs`](../manage-failed-jobs/SKILL.md#dashboard-optional) for the `warlock add bull-board` setup and the production guard (`QueueDashboardUnguardedError`).
@@ -1,51 +1,51 @@
1
- ---
2
- name: define-jobs
3
- description: "Define and dispatch jobs in @warlock.js/queue; use when you need to define jobs."
4
- ---
5
-
6
- # Define and dispatch jobs
7
-
8
- ```ts title="src/app/invoices/jobs/send-invoice.job.ts"
9
- import { defineJob } from "@warlock.js/queue";
10
-
11
- export const sendInvoice = defineJob({
12
- name: "invoices.send", // unique across the app
13
- attempts: 5, // total tries, default 1
14
- backoff: { type: "exponential", delay: 2000 }, // or a number = fixed ms
15
- async handle(payload: { invoiceId: string }, ctx) {
16
- await ctx.log(`attempt ${ctx.attempt} of ${ctx.maxAttempts}`);
17
- await ctx.progress(50);
18
- // throw to fail this attempt; it is retried until attempts run out
19
- return { sent: true }; // stored as the job result
20
- },
21
- });
22
- ```
23
-
24
- The job module must be imported by the process that runs workers — in a Warlock app, anything under `src/app` that is loaded at boot.
25
-
26
- ## Dispatch
27
-
28
- ```ts
29
- const { id } = await sendInvoice.dispatch({ invoiceId: "42" });
30
-
31
- await sendInvoice.dispatch({ invoiceId: "43" }, {
32
- delay: "10m", // ms number or "500ms" | "30s" | "10m" | "2h" | "1d"
33
- priority: 1, // 1 runs first; larger numbers later
34
- jobId: "invoice:43", // a second dispatch with a live id is ignored
35
- });
36
- ```
37
-
38
- Option precedence: dispatch options > `defineJob` > `queue.defaultJobOptions`.
39
-
40
- ## Read a job
41
-
42
- ```ts
43
- const job = await sendInvoice.find(id);
44
- // { id, name, queue, state, payload, progress, attemptsMade, result, failedReason, createdAt, finishedAt }
45
- ```
46
-
47
- ## Rules
48
-
49
- - A job name with no handler in the worker process fails at once, without retries.
50
- - Payloads are stored as JSON: pass ids, not model instances.
51
- - Defining the same name again replaces the handler (this is what keeps dev reloads working), so keep names unique.
1
+ ---
2
+ name: define-jobs
3
+ description: 'Define and dispatch background jobs with `@warlock.js/queue`: `defineJob({ name, queue?, attempts?, backoff?, removeOnComplete?, removeOnFail?, handle(payload, ctx) })` returns a typed job; `job.dispatch(payload, { delay, priority, jobId, attempts, backoff })`; the handler context (`id`, `attempt`, `maxAttempts`, `progress()`, `log()`); reading a job with `job.find(id)`. Triggers: `defineJob`, `.dispatch(`, `ctx.progress`, `JobContext`, `DispatchOptions`, `backoff`, `attempts`, `priority`, `jobId`; "run this in the background", "retry with backoff", "delay a job", "job progress", "idempotent dispatch". Skip: config and workers — `@warlock.js/queue/configure-queue/SKILL.md`; failed jobs — `@warlock.js/queue/manage-failed-jobs/SKILL.md`.'
4
+ ---
5
+
6
+ # Define and dispatch jobs
7
+
8
+ ```ts title="src/app/invoices/jobs/send-invoice.job.ts"
9
+ import { defineJob } from "@warlock.js/queue";
10
+
11
+ export const sendInvoice = defineJob({
12
+ name: "invoices.send", // unique across the app
13
+ attempts: 5, // total tries, default 1
14
+ backoff: { type: "exponential", delay: 2000 }, // or a number = fixed ms
15
+ async handle(payload: { invoiceId: string }, ctx) {
16
+ await ctx.log(`attempt ${ctx.attempt} of ${ctx.maxAttempts}`);
17
+ await ctx.progress(50);
18
+ // throw to fail this attempt; it is retried until attempts run out
19
+ return { sent: true }; // stored as the job result
20
+ },
21
+ });
22
+ ```
23
+
24
+ The job module must be imported by the process that runs workers — in a Warlock app, anything under `src/app` that is loaded at boot.
25
+
26
+ ## Dispatch
27
+
28
+ ```ts
29
+ const { id } = await sendInvoice.dispatch({ invoiceId: "42" });
30
+
31
+ await sendInvoice.dispatch({ invoiceId: "43" }, {
32
+ delay: "10m", // ms number or "500ms" | "30s" | "10m" | "2h" | "1d"
33
+ priority: 1, // 1 runs first; larger numbers later
34
+ jobId: "invoice:43", // a second dispatch with a live id is ignored
35
+ });
36
+ ```
37
+
38
+ Option precedence: dispatch options > `defineJob` > `queue.defaultJobOptions`.
39
+
40
+ ## Read a job
41
+
42
+ ```ts
43
+ const job = await sendInvoice.find(id);
44
+ // { id, name, queue, state, payload, progress, attemptsMade, result, failedReason, createdAt, finishedAt }
45
+ ```
46
+
47
+ ## Rules
48
+
49
+ - A job name with no handler in the worker process fails at once, without retries.
50
+ - Payloads are stored as JSON: pass ids, not model instances.
51
+ - Defining the same name again replaces the handler (this is what keeps dev reloads working), so keep names unique.
@@ -0,0 +1,21 @@
1
+ ---
2
+ description: "Durable background jobs on BullMQ + Redis: retries, delays, priorities, workers, failed-job recovery. Exports `defineJob`, `queueConnector`, `startWorkers`, `closeQueue`, `getQueue`, `failedJobs`, `retryFailedJob`, `mountQueueDashboard`, `QueueConfig`, `QueueJob`. Use for: \"run this in the background\", \"retry a failed job\", \"delay a job\", \"survive restarts\", \"job dashboard\". Not this package: cron or recurring schedules → @warlock.js/scheduler; broker pub/sub messaging → @warlock.js/herald; core `Queue` is an in-memory batcher."
3
+ ---
4
+ # @warlock.js/queue
5
+
6
+ Jobs are stored in Redis (required; not bundled), retried on failure, and run by workers, in the app process by default. `defineJob({ name, handle })` registers a handler and returns a typed job; `job.dispatch(payload, options)` enqueues it. Any process with the same definition and a worker can run it. Server-only.
7
+
8
+ ## The 80% path
9
+ 1. Orient with `overview.md`.
10
+ 2. Configure `src/config/queue.ts` and register `queueConnector()`: `configure-queue.md`.
11
+ 3. Define and dispatch jobs (attempts, backoff, delay, priority, progress): `define-jobs.md`.
12
+ 4. Inspect and recover failures, optionally the bull-board dashboard: `manage-failed-jobs.md`.
13
+ 5. Back `@warlock.js/notifications` `.queue()` with BullMQ: `queue-notifications.md`.
14
+
15
+ ## Conventions and pitfalls
16
+ - Job names are the routing key; a job with no matching `defineJob` in the worker process cannot run.
17
+ - Handlers must be idempotent: retries re-run them.
18
+ - Shutdown lets active jobs finish up to `workers.shutdownTimeout`, then closes connections.
19
+ - Guard the dashboard with middleware; an unguarded one is refused on purpose.
20
+ - Core `Queue` is an in-memory batcher, lost on exit; use this package when work must survive.
21
+ - Do not value-import from client code.
@@ -1,68 +1,68 @@
1
- ---
2
- name: manage-failed-jobs
3
- description: "Failed jobs in @warlock.js/queue; use when you need to manage failed jobs."
4
- ---
5
-
6
- # Failed jobs
7
-
8
- A job is failed once it has used every attempt, or failed in a way that cannot be retried (for example, no handler for its name). Failed jobs are kept unless you set `removeOnFail`.
9
-
10
- ```ts
11
- import { failedJobs, retryFailedJob } from "@warlock.js/queue";
12
-
13
- const failed = await failedJobs({ queue: "default", start: 0, end: 49 }); // newest first
14
-
15
- for (const job of failed) {
16
- console.log(job.name, job.failedReason, job.attemptsMade);
17
- }
18
-
19
- await failed[0]?.retry(); // back to waiting
20
- await retryFailedJob("invoice:43"); // by id; throws FailedJobNotFoundError if not failed
21
- ```
22
-
23
- ## Dashboard (optional)
24
-
25
- ```sh
26
- warlock add bull-board
27
- ```
28
-
29
- This installs `@bull-board/api` + `@bull-board/fastify` and writes a `dashboard` block to `src/config/queue.ts`. It `requires: ["queue"]`, so it adds the `queue` feature first automatically when it isn't installed yet — no need to run `warlock add queue` yourself first:
30
-
31
- ```ts title="src/config/queue.ts"
32
- import type { QueueConfig } from "@warlock.js/queue";
33
- import { middleware } from "@warlock.js/core";
34
- import { authMiddleware } from "@warlock.js/auth";
35
-
36
- const queueConfig: QueueConfig = {
37
- connection: { host: "127.0.0.1", port: 6379 },
38
- dashboard: {
39
- enabled: true,
40
- path: "/admin/queues", // default
41
- middleware: [authMiddleware("admin")], // guards every dashboard route
42
- },
43
- };
44
-
45
- export default queueConfig;
46
- ```
47
-
48
- - The dashboard can retry and delete jobs, so it must be guarded. In production, an empty `middleware` list throws `QueueDashboardUnguardedError` at boot instead of mounting exposed. Outside production it logs a warning and mounts anyway.
49
- - `queueConnector()` mounts the dashboard at boot from this config — no extra wiring needed.
50
-
51
- ### Advanced: manual `queueDashboard()`
52
-
53
- For mounting outside the config-driven path (a custom server, a non-standard boot sequence), call `queueDashboard` yourself:
54
-
55
- ```ts
56
- import { getHttpServer } from "@warlock.js/core";
57
- import { queueDashboard } from "@warlock.js/queue";
58
- import { authMiddleware } from "@warlock.js/auth";
59
-
60
- await queueDashboard(getHttpServer(), {
61
- basePath: "/admin/queues",
62
- middleware: [authMiddleware("admin")],
63
- });
64
- ```
65
-
66
- - Call it before the HTTP server starts listening.
67
- - Shows every queue that has a job, plus the default queue, unless you pass `queues`.
68
- - It has no authentication of its own — pass `middleware` yourself; this path does not enforce the production guard that the `dashboard` config does.
1
+ ---
2
+ name: manage-failed-jobs
3
+ description: 'Inspect and retry failed `@warlock.js/queue` jobs: `failedJobs({ queue, start, end })` returns `FailedJob[]` (`id`, `name`, `payload`, `attemptsMade`, `failedReason`, `stacktrace`, `failedAt`, `retry()`), `retryFailedJob(id, { queue })` (throws `FailedJobNotFoundError`), and the bull-board UI — `warlock add bull-board` (requires: ["queue"], so it installs queue first when missing) writes a `dashboard: { enabled, path, middleware }` block to `src/config/queue.ts`; a guard middleware is required in production or boot throws `QueueDashboardUnguardedError`. Manual `queueDashboard(server, { basePath, queues, middleware })` also available — needs `@bull-board/api` + `@bull-board/fastify`, loaded only on call, missing ones throw `QueueDashboardDependencyError`. Triggers: `failedJobs`, `retryFailedJob`, `queueDashboard`, `bull-board`, `dashboard.enabled`, `QueueDashboardUnguardedError`; "list failed jobs", "retry a failed job", "queue dashboard", "job admin UI". Skip: defining retries — `@warlock.js/queue/define-jobs/SKILL.md`.'
4
+ ---
5
+
6
+ # Failed jobs
7
+
8
+ A job is failed once it has used every attempt, or failed in a way that cannot be retried (for example, no handler for its name). Failed jobs are kept unless you set `removeOnFail`.
9
+
10
+ ```ts
11
+ import { failedJobs, retryFailedJob } from "@warlock.js/queue";
12
+
13
+ const failed = await failedJobs({ queue: "default", start: 0, end: 49 }); // newest first
14
+
15
+ for (const job of failed) {
16
+ console.log(job.name, job.failedReason, job.attemptsMade);
17
+ }
18
+
19
+ await failed[0]?.retry(); // back to waiting
20
+ await retryFailedJob("invoice:43"); // by id; throws FailedJobNotFoundError if not failed
21
+ ```
22
+
23
+ ## Dashboard (optional)
24
+
25
+ ```sh
26
+ warlock add bull-board
27
+ ```
28
+
29
+ This installs `@bull-board/api` + `@bull-board/fastify` and writes a `dashboard` block to `src/config/queue.ts`. It `requires: ["queue"]`, so it adds the `queue` feature first automatically when it isn't installed yet — no need to run `warlock add queue` yourself first:
30
+
31
+ ```ts title="src/config/queue.ts"
32
+ import type { QueueConfig } from "@warlock.js/queue";
33
+ import { middleware } from "@warlock.js/core";
34
+ import { authMiddleware } from "@warlock.js/auth";
35
+
36
+ const queueConfig: QueueConfig = {
37
+ connection: { host: "127.0.0.1", port: 6379 },
38
+ dashboard: {
39
+ enabled: true,
40
+ path: "/admin/queues", // default
41
+ middleware: [authMiddleware("admin")], // guards every dashboard route
42
+ },
43
+ };
44
+
45
+ export default queueConfig;
46
+ ```
47
+
48
+ - The dashboard can retry and delete jobs, so it must be guarded. In production, an empty `middleware` list throws `QueueDashboardUnguardedError` at boot instead of mounting exposed. Outside production it logs a warning and mounts anyway.
49
+ - `queueConnector()` mounts the dashboard at boot from this config — no extra wiring needed.
50
+
51
+ ### Advanced: manual `queueDashboard()`
52
+
53
+ For mounting outside the config-driven path (a custom server, a non-standard boot sequence), call `queueDashboard` yourself:
54
+
55
+ ```ts
56
+ import { getHttpServer } from "@warlock.js/core";
57
+ import { queueDashboard } from "@warlock.js/queue";
58
+ import { authMiddleware } from "@warlock.js/auth";
59
+
60
+ await queueDashboard(getHttpServer(), {
61
+ basePath: "/admin/queues",
62
+ middleware: [authMiddleware("admin")],
63
+ });
64
+ ```
65
+
66
+ - Call it before the HTTP server starts listening.
67
+ - Shows every queue that has a job, plus the default queue, unless you pass `queues`.
68
+ - It has no authentication of its own — pass `middleware` yourself; this path does not enforce the production guard that the `dashboard` config does.
@@ -1,35 +1,35 @@
1
- ---
2
- name: queue-notifications
3
- description: "Queue notifications with BullMQ in @warlock.js/queue; use when you need to queue notifications."
4
- ---
5
-
6
- # Queue notifications with BullMQ
7
-
8
- ```ts title="src/config/notifications.ts"
9
- import { type NotificationConfig, bullmqQueue, mailChannel } from "@warlock.js/notifications";
10
-
11
- const config: NotificationConfig = {
12
- channels: { mail: mailChannel() },
13
- queue: bullmqQueue({ attempts: 3, backoff: { type: "exponential", delay: 5000 } }),
14
- };
15
-
16
- export default config;
17
- ```
18
-
19
- `bullmqQueue` ships inside `@warlock.js/notifications` itself and lazy-loads `@warlock.js/queue` (an OPTIONAL peer) the first time `.queue()` runs — install it with `warlock add queue` if it isn't there yet, otherwise the first `.queue()` throws `QueuePackageNotInstalledError`.
20
-
21
- Also configure the queue itself (`configure-queue`). No separate notifications worker is needed: the delivery is an ordinary job, run by the queue workers.
22
-
23
- ```ts
24
- await orderShipped.queue(user, { order }); // enqueued, delivered by a worker
25
- await orderShipped.queue(user, { order }, { delay: "10m" }); // delivered in 10 minutes
26
- ```
27
-
28
- ## Behaviour
29
-
30
- - The job carries the rendered payload and resolved route; the worker looks the channel up by name in its own notifications config and calls `channel.send`.
31
- - `delay`: a number is seconds (notifications' convention); a string is a duration.
32
- - `channel.send` throws → retried per `attempts` / `backoff`, then listed by `failedJobs()`.
33
- - Channel not configured in the worker → fails at once, no retries.
34
- - Deliveries run under the job name `warlock.notifications.deliver`.
35
-
1
+ ---
2
+ name: queue-notifications
3
+ description: 'Send `@warlock.js/notifications` `.queue()` deliveries through BullMQ with `bullmqQueue({ queue?, attempts?, backoff? })` from `@warlock.js/notifications` — put it in the `queue` slot of `src/config/notifications.ts`; the queue workers deliver. Honours `SendOptions.delay` (number = seconds, or "10m"), retries a failing `channel.send`, and fails at once for a channel missing from the worker config. Triggers: `bullmqQueue`, `NotificationConfig.queue`, `QueuePackageNotInstalledError`; "queue notifications with BullMQ", "retry notification delivery", "delayed notification". Skip: the herald backend — `@warlock.js/notifications/queue-notifications/SKILL.md`.'
4
+ ---
5
+
6
+ # Queue notifications with BullMQ
7
+
8
+ ```ts title="src/config/notifications.ts"
9
+ import { type NotificationConfig, bullmqQueue, mailChannel } from "@warlock.js/notifications";
10
+
11
+ const config: NotificationConfig = {
12
+ channels: { mail: mailChannel() },
13
+ queue: bullmqQueue({ attempts: 3, backoff: { type: "exponential", delay: 5000 } }),
14
+ };
15
+
16
+ export default config;
17
+ ```
18
+
19
+ `bullmqQueue` ships inside `@warlock.js/notifications` itself and lazy-loads `@warlock.js/queue` (an OPTIONAL peer) the first time `.queue()` runs — install it with `warlock add queue` if it isn't there yet, otherwise the first `.queue()` throws `QueuePackageNotInstalledError`.
20
+
21
+ Also configure the queue itself (`configure-queue`). No separate notifications worker is needed: the delivery is an ordinary job, run by the queue workers.
22
+
23
+ ```ts
24
+ await orderShipped.queue(user, { order }); // enqueued, delivered by a worker
25
+ await orderShipped.queue(user, { order }, { delay: "10m" }); // delivered in 10 minutes
26
+ ```
27
+
28
+ ## Behaviour
29
+
30
+ - The job carries the rendered payload and resolved route; the worker looks the channel up by name in its own notifications config and calls `channel.send`.
31
+ - `delay`: a number is seconds (notifications' convention); a string is a duration.
32
+ - `channel.send` throws → retried per `attempts` / `backoff`, then listed by `failedJobs()`.
33
+ - Channel not configured in the worker → fails at once, no retries.
34
+ - Deliveries run under the job name `warlock.notifications.deliver`.
35
+