@warlock.js/queue 5.24.0 → 5.25.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.
@@ -1,67 +1,67 @@
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
- ## 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
-
63
- ## Rules
64
-
65
- - A job name with no handler in the worker process fails at once, without retries.
66
- - Payloads are stored as JSON: pass ids, not model instances.
67
- - 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
+ ## 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
+
63
+ ## Rules
64
+
65
+ - A job name with no handler in the worker process fails at once, without retries.
66
+ - Payloads are stored as JSON: pass ids, not model instances.
67
+ - Defining the same name again replaces the handler (this is what keeps dev reloads working), so keep names unique.
@@ -1,68 +1,68 @@
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
+ ---
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: '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
-
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
+