@shaferllc/keel 0.66.0 → 0.74.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/AGENTS.md +169 -0
- package/README.md +44 -6
- package/bin/keel-mcp.mjs +9 -0
- package/dist/api/api.config.stub +9 -0
- package/dist/api/config.d.ts +13 -0
- package/dist/api/config.js +14 -0
- package/dist/api/index.d.ts +16 -0
- package/dist/api/index.js +13 -0
- package/dist/api/provider.d.ts +10 -0
- package/dist/api/provider.js +17 -0
- package/dist/api/query.d.ts +35 -0
- package/dist/api/query.js +42 -0
- package/dist/api/resource.d.ts +91 -0
- package/dist/api/resource.js +188 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +8 -2
- package/dist/core/cache.d.ts +81 -5
- package/dist/core/cache.js +188 -23
- package/dist/core/cli/stubs.d.ts +26 -0
- package/dist/core/cli/stubs.js +225 -0
- package/dist/core/console-prompt.d.ts +79 -0
- package/dist/core/console-prompt.js +239 -0
- package/dist/core/console-ui.d.ts +96 -0
- package/dist/core/console-ui.js +187 -0
- package/dist/core/console.d.ts +188 -0
- package/dist/core/console.js +395 -0
- package/dist/core/database.d.ts +70 -1
- package/dist/core/database.js +174 -15
- package/dist/core/env.d.ts +96 -0
- package/dist/core/env.js +140 -0
- package/dist/core/events.d.ts +129 -5
- package/dist/core/events.js +165 -7
- package/dist/core/health.d.ts +141 -0
- package/dist/core/health.js +226 -0
- package/dist/core/helpers.d.ts +9 -3
- package/dist/core/helpers.js +11 -3
- package/dist/core/http/kernel.d.ts +2 -0
- package/dist/core/http/kernel.js +48 -0
- package/dist/core/http/router.d.ts +5 -5
- package/dist/core/http/router.js +5 -5
- package/dist/core/i18n.d.ts +162 -0
- package/dist/core/i18n.js +472 -0
- package/dist/core/index.d.ts +39 -13
- package/dist/core/index.js +19 -6
- package/dist/core/instrumentation.d.ts +113 -0
- package/dist/core/instrumentation.js +52 -0
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +89 -4
- package/dist/core/logger.js +167 -22
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/notification.js +10 -1
- package/dist/core/package.d.ts +120 -0
- package/dist/core/package.js +169 -0
- package/dist/core/pages.d.ts +108 -0
- package/dist/core/pages.js +199 -0
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +325 -14
- package/dist/core/repl.d.ts +33 -0
- package/dist/core/repl.js +88 -0
- package/dist/core/scheduler.js +6 -0
- package/dist/core/social.d.ts +4 -4
- package/dist/core/social.js +4 -4
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +299 -7
- package/dist/core/telemetry.d.ts +208 -0
- package/dist/core/telemetry.js +383 -0
- package/dist/core/template.d.ts +2 -3
- package/dist/core/template.js +2 -3
- package/dist/core/testing.d.ts +170 -1
- package/dist/core/testing.js +504 -2
- package/dist/db/d1.js +13 -0
- package/dist/db/pg.d.ts +13 -0
- package/dist/db/pg.js +46 -4
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/dist/openapi/config.d.ts +28 -0
- package/dist/openapi/config.js +25 -0
- package/dist/openapi/doc.d.ts +40 -0
- package/dist/openapi/doc.js +20 -0
- package/dist/openapi/export.d.ts +8 -0
- package/dist/openapi/export.js +19 -0
- package/dist/openapi/gate.d.ts +15 -0
- package/dist/openapi/gate.js +27 -0
- package/dist/openapi/index.d.ts +19 -0
- package/dist/openapi/index.js +15 -0
- package/dist/openapi/openapi.config.stub +29 -0
- package/dist/openapi/provider.d.ts +18 -0
- package/dist/openapi/provider.js +35 -0
- package/dist/openapi/routes.d.ts +9 -0
- package/dist/openapi/routes.js +23 -0
- package/dist/openapi/spec.d.ts +23 -0
- package/dist/openapi/spec.js +132 -0
- package/dist/openapi/ui.d.ts +8 -0
- package/dist/openapi/ui.js +31 -0
- package/dist/openapi/zod.d.ts +12 -0
- package/dist/openapi/zod.js +46 -0
- package/dist/watch/config.d.ts +33 -0
- package/dist/watch/config.js +38 -0
- package/dist/watch/entry.d.ts +53 -0
- package/dist/watch/entry.js +105 -0
- package/dist/watch/gate.d.ts +20 -0
- package/dist/watch/gate.js +32 -0
- package/dist/watch/index.d.ts +21 -0
- package/dist/watch/index.js +17 -0
- package/dist/watch/migration.d.ts +7 -0
- package/dist/watch/migration.js +20 -0
- package/dist/watch/provider.d.ts +22 -0
- package/dist/watch/provider.js +58 -0
- package/dist/watch/prune.d.ts +11 -0
- package/dist/watch/prune.js +20 -0
- package/dist/watch/recorder.d.ts +24 -0
- package/dist/watch/recorder.js +39 -0
- package/dist/watch/routes.d.ts +13 -0
- package/dist/watch/routes.js +55 -0
- package/dist/watch/store.d.ts +54 -0
- package/dist/watch/store.js +158 -0
- package/dist/watch/ui/dist/watch.css +1 -0
- package/dist/watch/ui/dist/watch.js +555 -0
- package/dist/watch/ui-shell.d.ts +12 -0
- package/dist/watch/ui-shell.js +24 -0
- package/dist/watch/watch.config.stub +47 -0
- package/dist/watch/watchers.d.ts +12 -0
- package/dist/watch/watchers.js +156 -0
- package/docs/ai-manifest.json +3206 -0
- package/docs/ai.md +128 -0
- package/docs/api-resources.md +118 -0
- package/docs/architecture.md +331 -0
- package/docs/authentication.md +453 -0
- package/docs/authorization.md +167 -0
- package/docs/broadcasting.md +137 -0
- package/docs/broker.md +500 -0
- package/docs/cache.md +558 -0
- package/docs/configuration.md +385 -0
- package/docs/console.md +536 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +631 -0
- package/docs/debugging.md +129 -0
- package/docs/decorators.md +127 -0
- package/docs/errors.md +395 -0
- package/docs/events.md +496 -0
- package/docs/examples/architecture-app.ts +27 -0
- package/docs/examples/authentication.ts +61 -0
- package/docs/examples/authorization.ts +79 -0
- package/docs/examples/broadcasting.ts +60 -0
- package/docs/examples/broker-cache-validate.ts +34 -0
- package/docs/examples/broker-fault-tolerance.ts +29 -0
- package/docs/examples/broker-middleware.ts +27 -0
- package/docs/examples/broker.ts +203 -0
- package/docs/examples/cache.ts +222 -0
- package/docs/examples/configuration.ts +121 -0
- package/docs/examples/console.ts +134 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +208 -0
- package/docs/examples/debugging.ts +41 -0
- package/docs/examples/decorators.ts +40 -0
- package/docs/examples/errors.ts +121 -0
- package/docs/examples/events.ts +204 -0
- package/docs/examples/factories.ts +84 -0
- package/docs/examples/hashing.ts +71 -0
- package/docs/examples/health.ts +94 -0
- package/docs/examples/helpers.ts +171 -0
- package/docs/examples/hooks.ts +54 -0
- package/docs/examples/i18n.ts +117 -0
- package/docs/examples/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +166 -0
- package/docs/examples/mail.ts +263 -0
- package/docs/examples/middleware.ts +119 -0
- package/docs/examples/migrations.ts +126 -0
- package/docs/examples/models.ts +239 -0
- package/docs/examples/notification.ts +124 -0
- package/docs/examples/pages.ts +82 -0
- package/docs/examples/providers.ts +123 -0
- package/docs/examples/queues.ts +254 -0
- package/docs/examples/rate-limiting.ts +42 -0
- package/docs/examples/redis.ts +99 -0
- package/docs/examples/request-response.ts +197 -0
- package/docs/examples/routing.ts +186 -0
- package/docs/examples/scheduling.ts +62 -0
- package/docs/examples/sessions.ts +102 -0
- package/docs/examples/static-files.ts +63 -0
- package/docs/examples/storage.ts +132 -0
- package/docs/examples/telemetry.ts +127 -0
- package/docs/examples/templates.ts +58 -0
- package/docs/examples/testing.ts +215 -0
- package/docs/examples/transformer.ts +141 -0
- package/docs/examples/transformers.ts +49 -0
- package/docs/examples/url-builder.ts +86 -0
- package/docs/examples/validation.ts +102 -0
- package/docs/examples/views.tsx +62 -0
- package/docs/examples/vite.ts +106 -0
- package/docs/factories.md +166 -0
- package/docs/getting-started.md +290 -0
- package/docs/hashing.md +259 -0
- package/docs/health.md +225 -0
- package/docs/helpers.md +347 -0
- package/docs/hono.md +186 -0
- package/docs/hooks.md +118 -0
- package/docs/i18n.md +302 -0
- package/docs/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +436 -0
- package/docs/mail.md +751 -0
- package/docs/middleware.md +425 -0
- package/docs/migrations.md +476 -0
- package/docs/models.md +810 -0
- package/docs/notifications.md +474 -0
- package/docs/openapi.md +111 -0
- package/docs/packages.md +118 -0
- package/docs/pages.md +217 -0
- package/docs/providers.md +363 -0
- package/docs/queues.md +679 -0
- package/docs/rate-limiting.md +155 -0
- package/docs/redis.md +178 -0
- package/docs/request-response.md +953 -0
- package/docs/routing.md +804 -0
- package/docs/scheduling.md +110 -0
- package/docs/security.md +85 -0
- package/docs/sessions.md +354 -0
- package/docs/social-auth.md +174 -0
- package/docs/static-files.md +211 -0
- package/docs/storage.md +452 -0
- package/docs/telemetry.md +263 -0
- package/docs/templates.md +314 -0
- package/docs/testing.md +376 -0
- package/docs/transformers.md +381 -0
- package/docs/url-builder.md +295 -0
- package/docs/validation.md +288 -0
- package/docs/views.md +267 -0
- package/docs/vite.md +434 -0
- package/docs/watch.md +118 -0
- package/llms-full.txt +19816 -0
- package/llms.txt +127 -0
- package/package.json +42 -7
package/docs/queues.md
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
# Queues & Jobs
|
|
2
|
+
|
|
3
|
+
Move slow work — sending mail, calling an API, processing an upload — off the
|
|
4
|
+
request path. You `dispatch` a **job** and a pluggable **driver** decides when it
|
|
5
|
+
runs: immediately (the default), held in memory for a worker to drain, or handed
|
|
6
|
+
to a real broker. The API mirrors the database and mail layers (`setQueue` /
|
|
7
|
+
`dispatch` are to queues what `setConnection` / `db()` are to the database), and
|
|
8
|
+
the core imports no broker, so it stays edge-safe.
|
|
9
|
+
|
|
10
|
+
## Defining a job
|
|
11
|
+
|
|
12
|
+
A job is a class with a `handle()` method. Pass whatever data it needs through
|
|
13
|
+
the constructor:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Job, mail } from "@shaferllc/keel/core";
|
|
17
|
+
|
|
18
|
+
export class SendWelcome extends Job {
|
|
19
|
+
constructor(private email: string) {
|
|
20
|
+
super();
|
|
21
|
+
}
|
|
22
|
+
async handle() {
|
|
23
|
+
await mail().to(this.email).subject("Welcome").text("Glad you're here").send();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Generate one with `keel make:job SendWelcome` (→ `app/Jobs/SendWelcomeJob.ts`).
|
|
29
|
+
|
|
30
|
+
## Dispatching
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { dispatch } from "@shaferllc/keel/core";
|
|
34
|
+
|
|
35
|
+
await dispatch(new SendWelcome(user.email));
|
|
36
|
+
|
|
37
|
+
// options: delay (seconds) and a named lane — honored by drivers that support them
|
|
38
|
+
await dispatch(new SendWelcome(user.email), { delay: 60, queue: "emails" });
|
|
39
|
+
|
|
40
|
+
// a plain function works too, for one-off work
|
|
41
|
+
await dispatch(() => rebuildSearchIndex());
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Out of the box `dispatch` runs against a `SyncDriver`, so a fresh app executes
|
|
45
|
+
jobs inline — no setup, no worker. Call `setQueue` once to defer instead.
|
|
46
|
+
`dispatch` returns a promise that resolves when the driver has accepted the job
|
|
47
|
+
(for `SyncDriver` that means *after* the job has run; for a deferring driver, as
|
|
48
|
+
soon as it's enqueued).
|
|
49
|
+
|
|
50
|
+
`MemoryDriver` honors `delay` and `priority`; `SyncDriver` runs inline and so
|
|
51
|
+
ignores both. `queue` is a lane label that a real broker driver acts on.
|
|
52
|
+
|
|
53
|
+
## Drivers
|
|
54
|
+
|
|
55
|
+
Register the default driver once (typically in a service provider):
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { setQueue, SyncDriver, MemoryDriver } from "@shaferllc/keel/core";
|
|
59
|
+
|
|
60
|
+
setQueue(new SyncDriver()); // the default — runs jobs immediately
|
|
61
|
+
setQueue(new MemoryDriver()); // holds jobs; a worker drains them
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Driver | Behavior |
|
|
65
|
+
|--------|----------|
|
|
66
|
+
| `SyncDriver` | Runs each job the instant it's dispatched. The default; great for dev and tests. |
|
|
67
|
+
| `MemoryDriver` | Enqueues jobs in memory; `work()` runs them. Inspect `.jobs` / `.size`. |
|
|
68
|
+
|
|
69
|
+
With the sync driver, a job that throws surfaces the error to whoever called
|
|
70
|
+
`dispatch` — so failures are visible in development.
|
|
71
|
+
|
|
72
|
+
## Running queued jobs
|
|
73
|
+
|
|
74
|
+
When a driver defers work, drain it with `work()`:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { dispatch, work } from "@shaferllc/keel/core";
|
|
78
|
+
|
|
79
|
+
setQueue(new MemoryDriver());
|
|
80
|
+
await dispatch(new SendWelcome("a@x.com"));
|
|
81
|
+
await dispatch(new SendWelcome("b@x.com"));
|
|
82
|
+
|
|
83
|
+
const ran = await work(); // runs both; returns 2
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`work()` is a no-op (returns `0`) for immediate drivers like `SyncDriver` — it
|
|
87
|
+
only drains drivers that hold jobs locally (those implementing `Drainable`).
|
|
88
|
+
|
|
89
|
+
Jobs run one at a time, highest priority first and otherwise in dispatch order.
|
|
90
|
+
`work()` drains what's **due**: a job still waiting out a `delay` or a retry
|
|
91
|
+
backoff is left on the queue for a later drain, so a second `work()` isn't
|
|
92
|
+
necessarily a no-op.
|
|
93
|
+
|
|
94
|
+
A job that throws is **retried** and, once it runs out of retries, **failed** —
|
|
95
|
+
`work()` records it and keeps going rather than propagating the error. See
|
|
96
|
+
[Retries and backoff](#retries-and-backoff) and
|
|
97
|
+
[When a job finally fails](#when-a-job-finally-fails).
|
|
98
|
+
|
|
99
|
+
## A custom / edge driver
|
|
100
|
+
|
|
101
|
+
A driver is one method — `push`. That's the seam for a real broker. On
|
|
102
|
+
Cloudflare, forward to a Queue binding and reconstruct the job in the consumer:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import type { QueueDriver } from "@shaferllc/keel/core";
|
|
106
|
+
|
|
107
|
+
const cloudflareQueue = (binding: Queue): QueueDriver => ({
|
|
108
|
+
async push(job, options) {
|
|
109
|
+
await binding.send({ job: serialize(job), options });
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
setQueue(cloudflareQueue(env.MY_QUEUE));
|
|
113
|
+
// In the queue consumer, rebuild the job from the payload and call handle().
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
(`Queue` and `env` above are Cloudflare's binding types — illustrative, not Keel
|
|
117
|
+
exports.) The only method Keel requires is `push(job, options)`; return a promise
|
|
118
|
+
that resolves once the job is safely handed off. Because a `Dispatchable` can be a
|
|
119
|
+
class instance or a closure, a broker driver has to decide how to serialize it —
|
|
120
|
+
typically dispatch only plain-data jobs across the wire and reconstruct them in
|
|
121
|
+
the consumer.
|
|
122
|
+
|
|
123
|
+
Drivers that hold jobs locally can implement the `Drainable` interface
|
|
124
|
+
(`size` + `work()`) so `Queue.work()` can drive them — that's how `MemoryDriver`
|
|
125
|
+
works.
|
|
126
|
+
|
|
127
|
+
## Retries and backoff
|
|
128
|
+
|
|
129
|
+
Background work fails for boring reasons — a provider hiccups, a connection
|
|
130
|
+
drops. A job **retries** before it gives up, with a growing delay between
|
|
131
|
+
attempts. Declare the policy on the job class:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
class ChargeCard extends Job {
|
|
135
|
+
static maxRetries = 5;
|
|
136
|
+
static backoff = exponentialBackoff(1_000); // 1s, 2s, 4s, 8s, 16s
|
|
137
|
+
|
|
138
|
+
async handle() {
|
|
139
|
+
await stripe.charge(this.amount);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`maxRetries` defaults to **0** — a job that doesn't opt in fails on its first
|
|
145
|
+
throw, which is the safe default for work that isn't idempotent. The strategies:
|
|
146
|
+
|
|
147
|
+
| Backoff | Delays |
|
|
148
|
+
|---------|--------|
|
|
149
|
+
| `exponentialBackoff(baseMs?, maxMs?)` | 1s, 2s, 4s, 8s… (the default) |
|
|
150
|
+
| `linearBackoff(stepMs?, maxMs?)` | 5s, 10s, 15s… |
|
|
151
|
+
| `fixedBackoff(delayMs?)` | the same delay every time |
|
|
152
|
+
| `noBackoff` | retry immediately |
|
|
153
|
+
|
|
154
|
+
Both cap at `maxMs` (default 60s) so a long-lived job can't back off into next
|
|
155
|
+
week. Per-dispatch overrides win over the class:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
await dispatch(new ChargeCard(id), { maxRetries: 1, backoff: noBackoff });
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**A retry's delay is honored, not slept through.** `work()` drains what's *due*;
|
|
162
|
+
a job waiting out its backoff stays on the queue for a later drain. The
|
|
163
|
+
`SyncDriver` is the exception — it runs inline, so it retries immediately and
|
|
164
|
+
ignores the delay, because blocking a request for a 30-second backoff would be
|
|
165
|
+
worse than useless.
|
|
166
|
+
|
|
167
|
+
## When a job finally fails
|
|
168
|
+
|
|
169
|
+
Once the retries are exhausted the job is **failed**. Three things happen, in
|
|
170
|
+
order:
|
|
171
|
+
|
|
172
|
+
1. It's **logged** at `error` level, with the job name, id, and attempt count.
|
|
173
|
+
2. Its `failed(error)` hook runs — the last chance to alert or compensate.
|
|
174
|
+
3. It lands in the driver's **dead-letter list** (`driver.failed`) rather than
|
|
175
|
+
vanishing.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
class ChargeCard extends Job {
|
|
179
|
+
static maxRetries = 3;
|
|
180
|
+
|
|
181
|
+
async handle() { … }
|
|
182
|
+
|
|
183
|
+
async failed(error: unknown) {
|
|
184
|
+
await notifyBilling(this.orderId, error);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**A failed job does not take down the worker.** `work()` records it and carries on
|
|
190
|
+
with the rest of the queue — one bad job can't stop the others. That's why the
|
|
191
|
+
failure is logged loudly: a worker that keeps running past a failure must not do
|
|
192
|
+
so silently.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
await work();
|
|
196
|
+
|
|
197
|
+
for (const failure of getQueue().failed) {
|
|
198
|
+
console.error(failure.id, failure.attempts, failure.error);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
A throw inside `failed()` is logged and swallowed too — failing to handle a
|
|
203
|
+
failure must not itself crash the worker.
|
|
204
|
+
|
|
205
|
+
The `SyncDriver` is again the exception: it ran the job *inline*, so the caller is
|
|
206
|
+
right there and gets the error thrown at them.
|
|
207
|
+
|
|
208
|
+
## Priority
|
|
209
|
+
|
|
210
|
+
Lower numbers run first. Default is `0`, so a negative priority jumps the queue:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
await dispatch(new SendReceipt(id), { priority: -10 }); // ahead of normal work
|
|
214
|
+
await dispatch(new RebuildSearchIndex(), { priority: 10 }); // whenever
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
A job class can declare its own default lane and priority:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
class ChargeCard extends Job {
|
|
221
|
+
static queue = "billing";
|
|
222
|
+
static priority = -5;
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## What a job knows about itself
|
|
227
|
+
|
|
228
|
+
While `handle()` runs, `this.context` carries the job's id, which attempt this is,
|
|
229
|
+
and the lane it's on — useful for logging, and for making a retry behave
|
|
230
|
+
differently from a first run:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
class ImportFile extends Job {
|
|
234
|
+
static maxRetries = 3;
|
|
235
|
+
|
|
236
|
+
async handle() {
|
|
237
|
+
const { jobId, attempt, queue } = this.context!;
|
|
238
|
+
if (attempt > 1) logger().warn("retrying import", { jobId, attempt });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## In tests
|
|
244
|
+
|
|
245
|
+
`fakeQueue()` records dispatches **without running them**, so a test can assert a
|
|
246
|
+
job was queued without paying for it to run — no email sent, no card charged.
|
|
247
|
+
`restoreQueue()` puts the real queue back.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import { fakeQueue, restoreQueue } from "@shaferllc/keel/core";
|
|
251
|
+
|
|
252
|
+
const queue = fakeQueue();
|
|
253
|
+
|
|
254
|
+
await registerUser(); // internally dispatches SendWelcome
|
|
255
|
+
|
|
256
|
+
queue.assertPushed(SendWelcome);
|
|
257
|
+
queue.assertPushed(SendWelcome, (job) => job.userId === user.id); // with a predicate
|
|
258
|
+
queue.assertPushedCount(1, SendWelcome);
|
|
259
|
+
queue.assertNotPushed(ChargeCard);
|
|
260
|
+
queue.assertNothingPushed();
|
|
261
|
+
|
|
262
|
+
restoreQueue();
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`queue.pushedJobs(SendWelcome)` returns the queued entries, so you can assert on a
|
|
266
|
+
dispatch's `delay`, `queue`, or `priority`.
|
|
267
|
+
|
|
268
|
+
When you want the job to actually *run*, use the `MemoryDriver` instead and drain
|
|
269
|
+
it:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
const driver = new MemoryDriver();
|
|
273
|
+
setQueue(driver);
|
|
274
|
+
|
|
275
|
+
await registerUser();
|
|
276
|
+
|
|
277
|
+
assert.equal(driver.size, 1);
|
|
278
|
+
await work(); // now run it and assert on the side effects
|
|
279
|
+
assert.equal(driver.failed.length, 0);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## API reference
|
|
285
|
+
|
|
286
|
+
### Top-level functions
|
|
287
|
+
|
|
288
|
+
The module keeps one process-wide default `Queue`. These four functions are the
|
|
289
|
+
everyday surface — you rarely touch `Queue` or a driver directly.
|
|
290
|
+
|
|
291
|
+
#### `dispatch(job, options?)`
|
|
292
|
+
|
|
293
|
+
`dispatch(job: Dispatchable, options?: JobOptions): Promise<void>`
|
|
294
|
+
|
|
295
|
+
Places a job (a `Job` instance or a plain function) on the default queue.
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
await dispatch(new SendWelcome("a@x.com"));
|
|
299
|
+
await dispatch(() => rebuildSearchIndex(), { delay: 60, queue: "emails" });
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Notes:** delegates to `getQueue().dispatch`. With the default `SyncDriver` the
|
|
303
|
+
returned promise resolves *after* the job has run, and a throwing job rejects it.
|
|
304
|
+
`options` defaults to `{}`; `delay`/`queue` are honored only by drivers that
|
|
305
|
+
support them.
|
|
306
|
+
|
|
307
|
+
#### `work()`
|
|
308
|
+
|
|
309
|
+
`work(): Promise<number>`
|
|
310
|
+
|
|
311
|
+
Drains the default queue's pending jobs and resolves with how many ran.
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
const ran = await work();
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Notes:** returns `0` for drivers that don't hold jobs locally (e.g.
|
|
318
|
+
`SyncDriver`). Runs jobs FIFO; a throwing job propagates and halts the drain.
|
|
319
|
+
|
|
320
|
+
#### `setQueue(driver)`
|
|
321
|
+
|
|
322
|
+
`setQueue(driver: QueueDriver): Queue`
|
|
323
|
+
|
|
324
|
+
Replaces the default queue with a new `Queue` wrapping `driver`, and returns it.
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
setQueue(new MemoryDriver());
|
|
328
|
+
const q = setQueue(new SyncDriver()); // returns the new Queue
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Notes:** global — the last call wins, and it rebinds what `dispatch`/`work`/
|
|
332
|
+
`getQueue` operate on. Before the first `setQueue`, the default is a `SyncDriver`.
|
|
333
|
+
|
|
334
|
+
#### `getQueue()`
|
|
335
|
+
|
|
336
|
+
`getQueue(): Queue`
|
|
337
|
+
|
|
338
|
+
Returns the current default `Queue` instance.
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
const driver = getQueue().driver; // inspect the active driver
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Notes:** the instance changes identity after each `setQueue` call.
|
|
345
|
+
|
|
346
|
+
### `Job`
|
|
347
|
+
|
|
348
|
+
`abstract class Job`
|
|
349
|
+
|
|
350
|
+
The base class for a unit of background work. Subclass it, pass data through the
|
|
351
|
+
constructor, and implement `handle()`.
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
class SendWelcome extends Job {
|
|
355
|
+
constructor(private email: string) { super(); }
|
|
356
|
+
async handle() { await mail().to(this.email).subject("Hi").text("Welcome").send(); }
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Notes:** dispatching a non-`Job` function is also allowed (see `Dispatchable`) —
|
|
361
|
+
`Job` exists for jobs that carry state or that you want to assert on by type in
|
|
362
|
+
tests (`job instanceof SendWelcome`).
|
|
363
|
+
|
|
364
|
+
#### `handle()`
|
|
365
|
+
|
|
366
|
+
`abstract handle(): void | Promise<void>`
|
|
367
|
+
|
|
368
|
+
The work the job performs. Called once when the driver runs the job.
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
async handle() { await rebuildSearchIndex(); }
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**Notes:** may be sync or async — the runner wraps the result in
|
|
375
|
+
`Promise.resolve`, so either is awaited. Throwing surfaces the error to whoever
|
|
376
|
+
drains the queue.
|
|
377
|
+
|
|
378
|
+
### `Queue`
|
|
379
|
+
|
|
380
|
+
`class Queue`
|
|
381
|
+
|
|
382
|
+
Pairs a `QueueDriver` with the `dispatch`/`work` API. The module manages a default
|
|
383
|
+
instance for you; construct one yourself only if you want a second, independent
|
|
384
|
+
queue.
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
const q = new Queue(new MemoryDriver());
|
|
388
|
+
await q.dispatch(new SendWelcome("a@x.com"));
|
|
389
|
+
await q.work();
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
#### `new Queue(driver)`
|
|
393
|
+
|
|
394
|
+
`constructor(driver: QueueDriver)`
|
|
395
|
+
|
|
396
|
+
Wraps a driver. The driver is exposed as the readonly `driver` property.
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
const q = new Queue(new SyncDriver());
|
|
400
|
+
q.driver; // the SyncDriver you passed
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
#### `dispatch(job, options?)`
|
|
404
|
+
|
|
405
|
+
`dispatch(job: Dispatchable, options?: JobOptions): Promise<void>`
|
|
406
|
+
|
|
407
|
+
Hands the job to the driver's `push`. The driver decides when it runs.
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
await q.dispatch(new SendWelcome("a@x.com"), { queue: "emails" });
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
**Notes:** `options` defaults to `{}`. Resolves when `push` resolves.
|
|
414
|
+
|
|
415
|
+
#### `work()`
|
|
416
|
+
|
|
417
|
+
`work(): Promise<number>`
|
|
418
|
+
|
|
419
|
+
Drains the driver if it holds jobs locally, returning how many ran.
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
const ran = await q.work();
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Notes:** feature-detects `Drainable` — if the driver has no `work` method, this
|
|
426
|
+
returns `0` without touching it. That's why the same call is safe against any
|
|
427
|
+
driver.
|
|
428
|
+
|
|
429
|
+
#### `driver`
|
|
430
|
+
|
|
431
|
+
`readonly driver: QueueDriver`
|
|
432
|
+
|
|
433
|
+
The driver this queue wraps. Useful for inspection (e.g. casting to
|
|
434
|
+
`MemoryDriver` to read `.jobs` in a test).
|
|
435
|
+
|
|
436
|
+
### Drivers
|
|
437
|
+
|
|
438
|
+
Both built-in drivers implement `QueueDriver`. You register one with `setQueue`;
|
|
439
|
+
you don't usually call their methods directly.
|
|
440
|
+
|
|
441
|
+
#### `SyncDriver`
|
|
442
|
+
|
|
443
|
+
`class SyncDriver implements QueueDriver`
|
|
444
|
+
|
|
445
|
+
Runs each job the instant it's pushed. The default driver — ideal for dev and
|
|
446
|
+
tests where you want failures to surface immediately.
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
setQueue(new SyncDriver());
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
##### `push(job, options)`
|
|
453
|
+
|
|
454
|
+
`push(job: Dispatchable, options: JobOptions): Promise<void>`
|
|
455
|
+
|
|
456
|
+
Runs the job right away and resolves when it finishes.
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
await new SyncDriver().push(() => doWork(), {});
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**Notes:** ignores `options` entirely (no deferral). A throwing job rejects the
|
|
463
|
+
promise. It is *not* `Drainable`, so `work()` against it returns `0`.
|
|
464
|
+
|
|
465
|
+
#### `MemoryDriver`
|
|
466
|
+
|
|
467
|
+
`class MemoryDriver implements QueueDriver, Drainable`
|
|
468
|
+
|
|
469
|
+
Holds pushed jobs in an in-memory array until you `work()` them. The go-to driver
|
|
470
|
+
for tests: dispatch, assert on `.jobs`/`.size`, then drain.
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
const driver = new MemoryDriver();
|
|
474
|
+
setQueue(driver);
|
|
475
|
+
await dispatch(new SendWelcome("a@x.com"));
|
|
476
|
+
driver.size; // 1
|
|
477
|
+
driver.jobs[0].job; // the queued Dispatchable
|
|
478
|
+
await work(); // runs it; size back to 0
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
##### `push(job, options)`
|
|
482
|
+
|
|
483
|
+
`push(job: Dispatchable, options: JobOptions): Promise<void>`
|
|
484
|
+
|
|
485
|
+
Appends `{ job, options }` to `.jobs` without running anything.
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
await driver.push(() => doWork(), { queue: "default" });
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
##### `work()`
|
|
492
|
+
|
|
493
|
+
`work(): Promise<number>`
|
|
494
|
+
|
|
495
|
+
Runs every pending job in FIFO order and returns how many ran.
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
const ran = await driver.work();
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**Notes:** removes each job before running it, so a throwing job halts the drain
|
|
502
|
+
and is not retried. After a successful drain `.jobs` is empty and a repeat call
|
|
503
|
+
returns `0`.
|
|
504
|
+
|
|
505
|
+
##### `size`
|
|
506
|
+
|
|
507
|
+
`get size(): number`
|
|
508
|
+
|
|
509
|
+
The number of jobs currently waiting — `this.jobs.length`.
|
|
510
|
+
|
|
511
|
+
##### `jobs`
|
|
512
|
+
|
|
513
|
+
`readonly jobs: QueuedJob[]`
|
|
514
|
+
|
|
515
|
+
The live backlog of `{ job, options }` entries, in insertion order. Read it in
|
|
516
|
+
tests to assert what was queued and with which options.
|
|
517
|
+
|
|
518
|
+
### Interfaces & types
|
|
519
|
+
|
|
520
|
+
#### `Dispatchable`
|
|
521
|
+
|
|
522
|
+
`type Dispatchable = Job | (() => void | Promise<void>)`
|
|
523
|
+
|
|
524
|
+
What `dispatch`/`push` accept: a `Job` subclass instance or a zero-arg function.
|
|
525
|
+
Use a function for quick one-offs, a `Job` when the work carries data or you want
|
|
526
|
+
to identify it by type.
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
const a: Dispatchable = new SendWelcome("a@x.com");
|
|
530
|
+
const b: Dispatchable = () => rebuildSearchIndex();
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
#### `JobOptions`
|
|
534
|
+
|
|
535
|
+
```ts
|
|
536
|
+
interface JobOptions {
|
|
537
|
+
delay?: number; // seconds before the job becomes available
|
|
538
|
+
queue?: string; // named lane to place the job on
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Per-dispatch hints. Both are optional and advisory — the built-in drivers ignore
|
|
543
|
+
them; a broker driver interprets them.
|
|
544
|
+
|
|
545
|
+
#### `QueueDriver`
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
interface QueueDriver {
|
|
549
|
+
push(job: Dispatchable, options: JobOptions): Promise<void>;
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
The seam to your backend — implement it to target any broker. `push` is the only
|
|
554
|
+
required method: accept the job and resolve once it's safely handed off.
|
|
555
|
+
|
|
556
|
+
```ts
|
|
557
|
+
const logDriver: QueueDriver = {
|
|
558
|
+
async push(job, options) {
|
|
559
|
+
console.log("queued", options.queue ?? "default");
|
|
560
|
+
// forward `job` to your broker here
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
setQueue(logDriver);
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
#### `Drainable`
|
|
567
|
+
|
|
568
|
+
```ts
|
|
569
|
+
interface Drainable {
|
|
570
|
+
readonly size: number;
|
|
571
|
+
work(): Promise<number>;
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
Implement this *in addition to* `QueueDriver` when your driver holds jobs locally
|
|
576
|
+
and can run them on demand — that's what lets `Queue.work()` drive it. Feature
|
|
577
|
+
detection is by the presence of `work`, so a driver that omits `Drainable` is
|
|
578
|
+
simply never drained.
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
class ArrayDriver implements QueueDriver, Drainable {
|
|
582
|
+
private q: Dispatchable[] = [];
|
|
583
|
+
get size() { return this.q.length; }
|
|
584
|
+
async push(job: Dispatchable) { this.q.push(job); }
|
|
585
|
+
async work() {
|
|
586
|
+
let n = 0;
|
|
587
|
+
for (const job of this.q.splice(0)) { await (job instanceof Job ? job.handle() : job()); n++; }
|
|
588
|
+
return n;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
#### `QueuedJob`
|
|
594
|
+
|
|
595
|
+
```ts
|
|
596
|
+
interface QueuedJob {
|
|
597
|
+
job: Dispatchable;
|
|
598
|
+
options: JobOptions;
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
An entry in `MemoryDriver.jobs`: the dispatched job paired with the options it was
|
|
603
|
+
dispatched with. What you assert on in tests.
|
|
604
|
+
|
|
605
|
+
```ts
|
|
606
|
+
const entry: QueuedJob = driver.jobs[0];
|
|
607
|
+
entry.options.queue; // string | undefined
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### Retries & backoff
|
|
611
|
+
|
|
612
|
+
#### `Job.maxRetries`
|
|
613
|
+
|
|
614
|
+
`static maxRetries: number` — retries after the first failure. Default `0`.
|
|
615
|
+
|
|
616
|
+
#### `Job.backoff`
|
|
617
|
+
|
|
618
|
+
`static backoff: Backoff` — how long to wait before each retry. Default
|
|
619
|
+
`exponentialBackoff(1_000)`.
|
|
620
|
+
|
|
621
|
+
#### `Job.failed(error)`
|
|
622
|
+
|
|
623
|
+
`failed(error: unknown): void | Promise<void>` — runs once the retries are
|
|
624
|
+
exhausted. A throw in here is logged and swallowed.
|
|
625
|
+
|
|
626
|
+
#### `Job.context`
|
|
627
|
+
|
|
628
|
+
`context?: JobContext` — `{ jobId, attempt, queue }`, set by the driver before
|
|
629
|
+
`handle()` runs. `attempt` is 1 on the first run.
|
|
630
|
+
|
|
631
|
+
#### Backoff strategies
|
|
632
|
+
|
|
633
|
+
| Function | Signature |
|
|
634
|
+
|----------|-----------|
|
|
635
|
+
| `exponentialBackoff` | `(baseMs = 1000, maxMs = 60000) => Backoff` — 1s, 2s, 4s… |
|
|
636
|
+
| `linearBackoff` | `(stepMs = 1000, maxMs = 60000) => Backoff` — 1s, 2s, 3s… |
|
|
637
|
+
| `fixedBackoff` | `(delayMs = 1000) => Backoff` |
|
|
638
|
+
| `noBackoff` | `Backoff` — retry immediately |
|
|
639
|
+
|
|
640
|
+
`type Backoff = (attempt: number) => number` — milliseconds before `attempt`
|
|
641
|
+
(1 = the first retry).
|
|
642
|
+
|
|
643
|
+
### Testing
|
|
644
|
+
|
|
645
|
+
#### `fakeQueue()` / `restoreQueue()`
|
|
646
|
+
|
|
647
|
+
`fakeQueue(): FakeQueue` swaps the queue for one that records dispatches without
|
|
648
|
+
running them. `restoreQueue()` puts the real one back.
|
|
649
|
+
|
|
650
|
+
`FakeQueue`:
|
|
651
|
+
|
|
652
|
+
| Method | Signature |
|
|
653
|
+
|--------|-----------|
|
|
654
|
+
| `assertPushed` | `(type, where?) => void` |
|
|
655
|
+
| `assertNotPushed` | `(type, where?) => void` |
|
|
656
|
+
| `assertPushedCount` | `(count, type?) => void` |
|
|
657
|
+
| `assertNothingPushed` | `() => void` |
|
|
658
|
+
| `pushedJobs` | `(type) => QueuedJob[]` — to assert on delay/lane/priority |
|
|
659
|
+
|
|
660
|
+
### Interfaces & types
|
|
661
|
+
|
|
662
|
+
#### `JobOptions`
|
|
663
|
+
|
|
664
|
+
`{ delay?, queue?, priority?, maxRetries?, backoff? }` — `delay` in seconds;
|
|
665
|
+
`priority` lower runs first; `maxRetries`/`backoff` override the job class.
|
|
666
|
+
|
|
667
|
+
#### `JobContext`
|
|
668
|
+
|
|
669
|
+
`{ jobId: string; attempt: number; queue: string }`.
|
|
670
|
+
|
|
671
|
+
#### `QueuedJob`
|
|
672
|
+
|
|
673
|
+
`{ id, job, options, attempts, availableAt }` — a job sitting on a queue.
|
|
674
|
+
`availableAt` is the epoch-ms before which it must not run (delay or backoff).
|
|
675
|
+
|
|
676
|
+
#### `FailedJob`
|
|
677
|
+
|
|
678
|
+
`{ id, job, options, attempts, error }` — a job that exhausted its retries. Read
|
|
679
|
+
them from `driver.failed` or `getQueue().failed`.
|