@shaferllc/keel 0.59.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/AGENTS.md +167 -0
  2. package/README.md +30 -1
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/core/application.d.ts +5 -5
  5. package/dist/core/application.js +14 -2
  6. package/dist/core/auth.d.ts +47 -0
  7. package/dist/core/auth.js +77 -0
  8. package/dist/core/authorization.d.ts +9 -1
  9. package/dist/core/authorization.js +22 -2
  10. package/dist/core/cache.d.ts +82 -5
  11. package/dist/core/cache.js +181 -23
  12. package/dist/core/cli/stubs.d.ts +12 -0
  13. package/dist/core/cli/stubs.js +120 -0
  14. package/dist/core/container.d.ts +20 -0
  15. package/dist/core/container.js +52 -0
  16. package/dist/core/cors.d.ts +29 -0
  17. package/dist/core/cors.js +72 -0
  18. package/dist/core/crypto.d.ts +40 -4
  19. package/dist/core/crypto.js +66 -6
  20. package/dist/core/csrf.d.ts +25 -0
  21. package/dist/core/csrf.js +78 -0
  22. package/dist/core/database.d.ts +49 -4
  23. package/dist/core/database.js +89 -21
  24. package/dist/core/events.d.ts +129 -5
  25. package/dist/core/events.js +165 -7
  26. package/dist/core/health.d.ts +141 -0
  27. package/dist/core/health.js +226 -0
  28. package/dist/core/helpers.d.ts +15 -3
  29. package/dist/core/helpers.js +23 -3
  30. package/dist/core/index.d.ts +33 -18
  31. package/dist/core/index.js +16 -8
  32. package/dist/core/lock.d.ts +139 -0
  33. package/dist/core/lock.js +215 -0
  34. package/dist/core/logger.d.ts +82 -4
  35. package/dist/core/logger.js +141 -23
  36. package/dist/core/mail.d.ts +128 -7
  37. package/dist/core/mail.js +264 -16
  38. package/dist/core/model.d.ts +2 -0
  39. package/dist/core/model.js +16 -14
  40. package/dist/core/provider.d.ts +7 -0
  41. package/dist/core/provider.js +7 -0
  42. package/dist/core/queue.d.ts +134 -9
  43. package/dist/core/queue.js +304 -14
  44. package/dist/core/rate-limit.js +3 -0
  45. package/dist/core/relations.js +13 -13
  46. package/dist/core/request.d.ts +26 -0
  47. package/dist/core/request.js +77 -0
  48. package/dist/core/shield.d.ts +39 -0
  49. package/dist/core/shield.js +60 -0
  50. package/dist/core/social.d.ts +173 -0
  51. package/dist/core/social.js +337 -0
  52. package/dist/core/storage.d.ts +159 -6
  53. package/dist/core/storage.js +287 -7
  54. package/dist/core/tokens.d.ts +74 -0
  55. package/dist/core/tokens.js +155 -0
  56. package/dist/db/d1.d.ts +32 -0
  57. package/dist/db/d1.js +26 -0
  58. package/dist/db/libsql.d.ts +29 -0
  59. package/dist/db/libsql.js +32 -0
  60. package/dist/db/pg.d.ts +29 -0
  61. package/dist/db/pg.js +33 -0
  62. package/dist/mcp/server.d.ts +19 -0
  63. package/dist/mcp/server.js +355 -0
  64. package/docs/ai-manifest.json +2472 -0
  65. package/docs/ai.md +128 -0
  66. package/docs/architecture.md +331 -0
  67. package/docs/authentication.md +453 -0
  68. package/docs/authorization.md +167 -0
  69. package/docs/broadcasting.md +137 -0
  70. package/docs/broker.md +500 -0
  71. package/docs/cache.md +558 -0
  72. package/docs/configuration.md +311 -0
  73. package/docs/console.md +356 -0
  74. package/docs/container.md +467 -0
  75. package/docs/controllers.md +265 -0
  76. package/docs/cors.md +51 -0
  77. package/docs/database.md +530 -0
  78. package/docs/debugging.md +129 -0
  79. package/docs/decorators.md +127 -0
  80. package/docs/errors.md +395 -0
  81. package/docs/events.md +496 -0
  82. package/docs/examples/architecture-app.ts +27 -0
  83. package/docs/examples/authentication.ts +61 -0
  84. package/docs/examples/authorization.ts +79 -0
  85. package/docs/examples/broadcasting.ts +60 -0
  86. package/docs/examples/broker-cache-validate.ts +34 -0
  87. package/docs/examples/broker-fault-tolerance.ts +29 -0
  88. package/docs/examples/broker-middleware.ts +27 -0
  89. package/docs/examples/broker.ts +203 -0
  90. package/docs/examples/cache.ts +222 -0
  91. package/docs/examples/configuration.ts +81 -0
  92. package/docs/examples/container.ts +134 -0
  93. package/docs/examples/controllers.ts +86 -0
  94. package/docs/examples/database.ts +118 -0
  95. package/docs/examples/debugging.ts +41 -0
  96. package/docs/examples/decorators.ts +40 -0
  97. package/docs/examples/errors.ts +121 -0
  98. package/docs/examples/events.ts +204 -0
  99. package/docs/examples/factories.ts +84 -0
  100. package/docs/examples/hashing.ts +71 -0
  101. package/docs/examples/health.ts +94 -0
  102. package/docs/examples/helpers.ts +171 -0
  103. package/docs/examples/hooks.ts +54 -0
  104. package/docs/examples/inertia.ts +81 -0
  105. package/docs/examples/locks.ts +120 -0
  106. package/docs/examples/logger.ts +92 -0
  107. package/docs/examples/mail.ts +160 -0
  108. package/docs/examples/middleware.ts +119 -0
  109. package/docs/examples/migrations.ts +126 -0
  110. package/docs/examples/models.ts +239 -0
  111. package/docs/examples/notification.ts +124 -0
  112. package/docs/examples/providers.ts +123 -0
  113. package/docs/examples/queues.ts +254 -0
  114. package/docs/examples/rate-limiting.ts +42 -0
  115. package/docs/examples/redis.ts +99 -0
  116. package/docs/examples/request-response.ts +197 -0
  117. package/docs/examples/routing.ts +186 -0
  118. package/docs/examples/scheduling.ts +62 -0
  119. package/docs/examples/sessions.ts +102 -0
  120. package/docs/examples/static-files.ts +63 -0
  121. package/docs/examples/storage.ts +132 -0
  122. package/docs/examples/templates.ts +58 -0
  123. package/docs/examples/testing.ts +66 -0
  124. package/docs/examples/transformer.ts +141 -0
  125. package/docs/examples/transformers.ts +49 -0
  126. package/docs/examples/url-builder.ts +86 -0
  127. package/docs/examples/validation.ts +102 -0
  128. package/docs/examples/views.tsx +62 -0
  129. package/docs/examples/vite.ts +106 -0
  130. package/docs/factories.md +166 -0
  131. package/docs/getting-started.md +290 -0
  132. package/docs/hashing.md +259 -0
  133. package/docs/health.md +225 -0
  134. package/docs/helpers.md +347 -0
  135. package/docs/hono.md +186 -0
  136. package/docs/hooks.md +118 -0
  137. package/docs/inertia.md +241 -0
  138. package/docs/locks.md +323 -0
  139. package/docs/logger.md +290 -0
  140. package/docs/mail.md +678 -0
  141. package/docs/middleware.md +425 -0
  142. package/docs/migrations.md +476 -0
  143. package/docs/models.md +810 -0
  144. package/docs/notifications.md +474 -0
  145. package/docs/providers.md +363 -0
  146. package/docs/queues.md +679 -0
  147. package/docs/rate-limiting.md +155 -0
  148. package/docs/redis.md +178 -0
  149. package/docs/request-response.md +953 -0
  150. package/docs/routing.md +804 -0
  151. package/docs/scheduling.md +110 -0
  152. package/docs/security.md +85 -0
  153. package/docs/sessions.md +354 -0
  154. package/docs/social-auth.md +174 -0
  155. package/docs/static-files.md +211 -0
  156. package/docs/storage.md +450 -0
  157. package/docs/templates.md +315 -0
  158. package/docs/testing.md +125 -0
  159. package/docs/transformers.md +381 -0
  160. package/docs/url-builder.md +295 -0
  161. package/docs/validation.md +288 -0
  162. package/docs/views.md +267 -0
  163. package/docs/vite.md +434 -0
  164. package/llms-full.txt +17694 -0
  165. package/llms.txt +116 -0
  166. package/package.json +38 -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`.