@basaltkit/queue 1.3.1 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -10
- package/dist/index.d.ts +12 -0
- package/dist/index.js +5 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -119,7 +119,10 @@ await GenerateInvoice.dispatch({ orderId: 'o-1' }, { delay: '10m' })
|
|
|
119
119
|
await GenerateInvoice.dispatch({ orderId: 'o-2' }, { priority: 1 })
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
Note: with the sync driver, `delay`
|
|
122
|
+
Note: with the sync driver, `delay` and `priority` are ignored — the job runs immediately.
|
|
123
|
+
That is not silent: the sync driver declares `delayed: false` / `priority: false`, so the
|
|
124
|
+
`onUnsupported` policy fires (by default a one-off `console.warn` per job+feature). Set
|
|
125
|
+
`onUnsupported: 'throw'` in production if a delay is load-bearing.
|
|
123
126
|
|
|
124
127
|
### Context propagation (tenant, requestId…)
|
|
125
128
|
|
|
@@ -232,8 +235,15 @@ console.log(driver.executed) // [{ queue: 'default', jobName: 'demo', attempts:
|
|
|
232
235
|
| `queue` | `string` | No | `'default'` | Name of the queue the job goes into. |
|
|
233
236
|
| `attempts` | `number` | No | `1` | Maximum number of attempts on failure. |
|
|
234
237
|
| `backoff` | `JobBackoff` | No | — | Wait strategy between attempts. |
|
|
238
|
+
| `removeOnComplete` | `JobRetention` | No | the `queuePlugin` default | Redis retention for this job once it completes — overrides the plugin-wide default. |
|
|
239
|
+
| `removeOnFail` | `JobRetention` | No | the `queuePlugin` default | Redis retention for this job once it fails permanently — overrides the plugin-wide default. |
|
|
235
240
|
| `handle` | `(payload: T) => void \| Promise<void>` | Yes | — | The function that does the work (runs on the worker). |
|
|
236
241
|
|
|
242
|
+
`JobRetention` = `boolean | number | { age?: DurationInput; count?: number }`. `true` removes
|
|
243
|
+
the job as soon as it finishes, `false` keeps it forever, a number keeps that many most-recent,
|
|
244
|
+
and `{ age, count }` caps by both. Only the BullMQ driver stores finished jobs, so only it
|
|
245
|
+
honours retention; the sync driver stores nothing and ignores it.
|
|
246
|
+
|
|
237
247
|
The returned object (`JobDefinition<T>`) exposes:
|
|
238
248
|
|
|
239
249
|
- `dispatch(payload, options?)` — puts the job on the queue. Throws `JobNotRegisteredError` if the job hasn't yet been registered with a `QueueManager`.
|
|
@@ -264,6 +274,16 @@ Basalt plugin that registers a `QueueManager` in the container under the `QUEUE`
|
|
|
264
274
|
| `connection` | `string \| ConnectionOptions` | No | — | Redis URL (`redis://…` or `rediss://…`) or ioredis options. With a value → BullMQ driver; without one → sync driver. |
|
|
265
275
|
| `driver` | `QueueDriver` | No | — | Custom driver — takes precedence over `connection`. |
|
|
266
276
|
| `workers` | `{ queue: string; concurrency?: number }[]` | No | `[]` | Queues whose workers start in this process on boot. |
|
|
277
|
+
| `onUnsupported` | `'throw' \| 'warn' \| 'ignore'` | No | `'warn'` | What happens when a dispatch uses an option the active driver cannot honour (a delay on Kafka, a priority on SQS). `'warn'` logs once per job+feature and proceeds; `'throw'` raises `UnsupportedJobOptionError` — set it in production when the option is load-bearing; `'ignore'` is silent. |
|
|
278
|
+
| `removeOnComplete` | `JobRetention` | No | driver default (BullMQ keeps the last `1000`) | Default Redis retention for completed jobs. A job's own `removeOnComplete` wins. |
|
|
279
|
+
| `removeOnFail` | `JobRetention` | No | driver default (BullMQ `false` — keep all) | Default retention for failed jobs. Keeping all is deliberate (inspection and `queue:retry`); set e.g. `{ age: '14d' }` so failures don't grow unbounded. |
|
|
280
|
+
| `onError` | `(error, { queue, source }) => void` | No | `console.error` with context | Infra faults from the driver's broker client (Redis down). Forwarded to the driver built from `connection`; ignored when you pass your own `driver`. |
|
|
281
|
+
| `onJobFailed` | `({ queue, job, jobId?, error }) => void` | No | `console.error` with context | A job exhausted its retries. Forwarded to the driver built from `connection`; ignored when you pass your own `driver`. |
|
|
282
|
+
|
|
283
|
+
**Failure callbacks work on the shorthand path.** `queuePlugin({ connection, onError,
|
|
284
|
+
onJobFailed })` forwards both to the BullMQ driver it builds. They are ignored when you
|
|
285
|
+
supply your own `driver` — that driver owns its own callbacks, so configure them there.
|
|
286
|
+
See [Failure hooks](#failure-hooks) below.
|
|
267
287
|
|
|
268
288
|
```ts
|
|
269
289
|
import { QUEUE } from '@basaltkit/queue'
|
|
@@ -274,12 +294,23 @@ const manager = app.container.get(QUEUE) // get the QueueManager from the contai
|
|
|
274
294
|
|
|
275
295
|
| Method | Signature | Description |
|
|
276
296
|
|---|---|---|
|
|
277
|
-
| `constructor` | `new QueueManager(driver: QueueDriver)` | Creates the manager over a driver. |
|
|
297
|
+
| `constructor` | `new QueueManager(driver: QueueDriver, options?: QueueManagerOptions)` | Creates the manager over a driver. |
|
|
278
298
|
| `register` | `(job) => this` | Registers a job and wires its `dispatch`. |
|
|
279
|
-
| `dispatch` | `<T>(job, payload: T, options?: DispatchOptions) => Promise<void>` | Validates
|
|
299
|
+
| `dispatch` | `<T>(job, payload: T, options?: DispatchOptions) => Promise<void>` | Validates, snapshots the request context, checks the driver's capabilities, and enqueues. Auto-registers the job if not registered yet. |
|
|
280
300
|
| `work` | `(queue = 'default', { concurrency? }?) => void` | Starts a worker for the queue (no-op on the sync driver). |
|
|
301
|
+
| `stats` | `(queue = 'default') => Promise<QueueStats \| undefined>` | Job counts per state, or `undefined` when the driver can't introspect (sync). |
|
|
302
|
+
| `retryFailed` | `(queue = 'default', { limit? }?) => Promise<number \| undefined>` | Re-enqueues failed jobs; returns the count, or `undefined` when the driver doesn't support it. |
|
|
281
303
|
| `close` | `() => Promise<void>` | Closes workers and connections. |
|
|
282
304
|
|
|
305
|
+
`QueueManagerOptions`:
|
|
306
|
+
|
|
307
|
+
| Option | Type | Default | Purpose |
|
|
308
|
+
|---|---|---|---|
|
|
309
|
+
| `onUnsupported` | `'throw' \| 'warn' \| 'ignore'` | `'warn'` | Same policy as the plugin option. |
|
|
310
|
+
| `warn` | `(message: string) => void` | `console.warn` | Where `'warn'` diagnostics go — point it at your logger. |
|
|
311
|
+
| `removeOnComplete` | `JobRetention` | driver default | Default retention for completed jobs. |
|
|
312
|
+
| `removeOnFail` | `JobRetention` | driver default | Default retention for failed jobs. |
|
|
313
|
+
|
|
283
314
|
### `queuedOn<T>(bus, manager, event, handler, options?): () => void`
|
|
284
315
|
|
|
285
316
|
Creates the event→job bridge. Returns the subscription cancel function.
|
|
@@ -294,17 +325,94 @@ Creates the event→job bridge. Returns the subscription cancel function.
|
|
|
294
325
|
|
|
295
326
|
### Drivers
|
|
296
327
|
|
|
297
|
-
- **`class SyncQueueDriver`** — runs inline on `dispatch`, honors `attempts` (immediate retry). Public property `executed: { queue, jobName, attempts }[]` with the execution history. For testing and dev without Redis.
|
|
298
|
-
- **`class BullmqQueueDriver`** — production over Redis
|
|
299
|
-
- **`interface QueueDriver`** (Advanced) — contract for custom drivers: `setExecutor(executor)`, `add(queue, jobName, data, options: AddJobOptions)`, `startWorker(queue, { concurrency? })`, `close()
|
|
328
|
+
- **`class SyncQueueDriver`** — runs inline on `dispatch`, honors `attempts` (immediate retry). Public property `executed: { queue, jobName, attempts }[]` with the execution history (capped at 1000 entries). For testing and dev without Redis.
|
|
329
|
+
- **`class BullmqQueueDriver`** — production over Redis; see the options table below.
|
|
330
|
+
- **`interface QueueDriver`** (Advanced) — contract for custom drivers: `setExecutor(executor)`, `add(queue, jobName, data, options: AddJobOptions)`, `startWorker(queue, { concurrency? })`, optional `stats(queue)` / `retryFailed(queue, { limit? })`, `close()`, plus the optional `name` and `capabilities` fields. Helper types: `AddJobOptions`, `JobExecutor`, `QueueStats`, `DriverCapabilities`.
|
|
331
|
+
|
|
332
|
+
#### `new BullmqQueueDriver(options: BullmqDriverOptions)`
|
|
333
|
+
|
|
334
|
+
| Option | Type | Default | Purpose |
|
|
335
|
+
|---|---|---|---|
|
|
336
|
+
| `connection` | `string \| ConnectionOptions` | — (required) | Redis URL (`redis://…`, `rediss://…` → TLS) or ioredis options. |
|
|
337
|
+
| `onError` | `(error: unknown, info: { queue: string; source: 'worker' \| 'queue' }) => void` | `console.error` with the queue name and source | Infra errors from BullMQ's `Worker`/`Queue` EventEmitters (Redis down, connection reset). Node makes an unlistened `'error'` event **fatal**, so the driver always attaches a listener — override this to route the fault into your logger/alerting. |
|
|
338
|
+
| `onJobFailed` | `(info: { queue: string; job: string; jobId?: string; error: unknown }) => void` | `console.error` naming the job, its id and the queue | A job **exhausted its retries** (BullMQ's `'failed'` event). Without it, permanently-failed jobs are only visible by polling `queue:stats`. |
|
|
339
|
+
|
|
340
|
+
Retention defaults applied by this driver when neither the job nor the plugin sets one:
|
|
341
|
+
completed → keep the last `1000`; failed → keep all.
|
|
342
|
+
|
|
343
|
+
#### `DriverCapabilities`
|
|
344
|
+
|
|
345
|
+
Each driver declares what its backend honours — `{ delayed, priority, retries, backoff }`, all
|
|
346
|
+
`boolean`. `QueueManager.dispatch` compares a dispatch's options against them and applies
|
|
347
|
+
`onUnsupported`. A driver that omits `capabilities` is treated as fully capable.
|
|
348
|
+
|
|
349
|
+
| Driver | `delayed` | `priority` | `retries` | `backoff` |
|
|
350
|
+
|---|:---:|:---:|:---:|:---:|
|
|
351
|
+
| `bullmq` | ✅ | ✅ | ✅ | ✅ |
|
|
352
|
+
| `sync` | ❌ | ❌ | ✅ | ❌ |
|
|
353
|
+
| [`rabbitmq`](https://www.npmjs.com/package/@basaltkit/queue-rabbitmq) | ✅ | ✅ | ✅ | ✅ |
|
|
354
|
+
| [`sqs`](https://www.npmjs.com/package/@basaltkit/queue-sqs) | ✅ (≤ 15 min) | ❌ | ✅ | ✅ |
|
|
355
|
+
| [`kafka`](https://www.npmjs.com/package/@basaltkit/queue-kafka) | ❌ | ❌ | ✅ | ❌ |
|
|
356
|
+
|
|
357
|
+
### Failure hooks
|
|
358
|
+
|
|
359
|
+
The queue has no hook bus of its own — failures are reported through driver callbacks, and every
|
|
360
|
+
one of them has a **non-silent default**, so nothing disappears if you configure nothing.
|
|
361
|
+
|
|
362
|
+
| Hook | Where it lives | Receives | Default when unset |
|
|
363
|
+
|---|---|---|---|
|
|
364
|
+
| `onError` | `QueuePluginOptions` or `BullmqDriverOptions` | `(error, { queue, source: 'worker' \| 'queue' })` | `console.error` with queue + source |
|
|
365
|
+
| `onJobFailed` | `QueuePluginOptions` or `BullmqDriverOptions` | `({ queue, job, jobId?, error })` | `console.error` naming the job |
|
|
366
|
+
| `warn` | `QueueManagerOptions` | `(message: string)` | `console.warn`, once per job+feature |
|
|
367
|
+
| `onError` | [`RabbitmqDriverOptions`](https://www.npmjs.com/package/@basaltkit/queue-rabbitmq) | `(error, { source: 'connection' \| 'channel' })` | `console.error` |
|
|
368
|
+
| `onError` | [`KafkaDriverOptions`](https://www.npmjs.com/package/@basaltkit/queue-kafka) | `(error, { source: 'consumer' \| 'producer'; queue? })` | `console.error` |
|
|
369
|
+
| `onError` | [`SqsDriverOptions`](https://www.npmjs.com/package/@basaltkit/queue-sqs) | `(error, { queue })` | `console.error`, then an `errorPauseMs` pause |
|
|
370
|
+
|
|
371
|
+
Only the BullMQ driver has `onJobFailed`; the broker drivers route an exhausted job to their own
|
|
372
|
+
dead-letter destination (`q.dead`, `<topic>.dead`, `<queue>-dead`) instead.
|
|
373
|
+
|
|
374
|
+
Override the BullMQ callbacks right on the plugin — `queuePlugin({ connection })` forwards
|
|
375
|
+
them to the driver it builds:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import { queuePlugin } from '@basaltkit/queue'
|
|
379
|
+
|
|
380
|
+
queuePlugin({
|
|
381
|
+
connection: process.env.REDIS_URL!,
|
|
382
|
+
jobs: [SendWelcomeEmail],
|
|
383
|
+
workers: [{ queue: 'default', concurrency: 5 }],
|
|
384
|
+
onError: (error, { queue, source }) => logger.error({ err: error, queue, source }, 'queue infra error'),
|
|
385
|
+
onJobFailed: ({ queue, job, jobId, error }) =>
|
|
386
|
+
logger.error({ err: error, queue, job, jobId }, 'job failed permanently'),
|
|
387
|
+
})
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Passing your own `driver` bypasses that forwarding — a supplied driver owns its callbacks,
|
|
391
|
+
so set them in its constructor:
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
import { queuePlugin, BullmqQueueDriver } from '@basaltkit/queue'
|
|
395
|
+
|
|
396
|
+
queuePlugin({
|
|
397
|
+
jobs: [SendWelcomeEmail],
|
|
398
|
+
driver: new BullmqQueueDriver({
|
|
399
|
+
connection: process.env.REDIS_URL!,
|
|
400
|
+
onError: (error, { queue, source }) => logger.error({ err: error, queue, source }, 'queue infra error'),
|
|
401
|
+
}),
|
|
402
|
+
})
|
|
403
|
+
```
|
|
300
404
|
|
|
301
405
|
### Exported errors
|
|
302
406
|
|
|
303
|
-
|
|
|
407
|
+
| Error | Code | When |
|
|
304
408
|
|---|---|---|
|
|
305
|
-
| `JobValidationError` | `JOB_INVALID` |
|
|
306
|
-
| `JobNotRegisteredError` | `QUEUE_JOB_NOT_REGISTERED` | `dispatch` before
|
|
307
|
-
| `UnknownJobError` | `QUEUE_UNKNOWN_JOB` |
|
|
409
|
+
| `JobValidationError` | `JOB_INVALID` | The payload failed the job's `schema` — on `dispatch` and again on the worker. Carries `.job` and `.issues`. |
|
|
410
|
+
| `JobNotRegisteredError` | `QUEUE_JOB_NOT_REGISTERED` | `job.dispatch()` was called before the job was registered in a `QueueManager`. |
|
|
411
|
+
| `UnknownJobError` | `QUEUE_UNKNOWN_JOB` | A job reached the worker but is not registered in that process — producer and worker registered different job lists. |
|
|
412
|
+
| `UnsupportedJobOptionError` | `QUEUE_UNSUPPORTED_OPTION` | With `onUnsupported: 'throw'`, a dispatch used an option the active driver's `capabilities` do not include. `status = 500`. |
|
|
413
|
+
|
|
414
|
+
Errors thrown outside these classes come from the driver's client (ioredis, amqplib, kafkajs,
|
|
415
|
+
the AWS SDK) and reach you through that driver's `onError`.
|
|
308
416
|
|
|
309
417
|
### Token
|
|
310
418
|
|
|
@@ -327,6 +435,12 @@ The data passed to `dispatch` doesn't match the `schema`. The error includes `is
|
|
|
327
435
|
**In dev, `delay` doesn't work.**
|
|
328
436
|
The sync driver always runs immediately. Delays, timed backoff, and priority only have a real effect with the BullMQ driver (with `connection`).
|
|
329
437
|
|
|
438
|
+
**`[basalt/queue] The "sync" driver does not support …` in the logs.**
|
|
439
|
+
The `onUnsupported` policy caught a dispatch option the active driver can't honour. It warns once per job+feature. Either switch to a driver that supports it, drop the option, or set `onUnsupported: 'ignore'` if you accept the degradation.
|
|
440
|
+
|
|
441
|
+
**A job failed for good and nothing was logged.**
|
|
442
|
+
Only the BullMQ driver reports exhausted jobs, via `onJobFailed` (default `console.error`). If you replaced it with a no-op, you removed the only signal. On the broker drivers, inspect the dead-letter destination (`q.dead`, `<topic>.dead`, `<queue>-dead`).
|
|
443
|
+
|
|
330
444
|
**Do I need Redis to run the tests?**
|
|
331
445
|
No. Without `connection`, the plugin uses `SyncQueueDriver`. You can also instantiate the driver directly and inspect `driver.executed`.
|
|
332
446
|
|
package/dist/index.d.ts
CHANGED
|
@@ -38,5 +38,17 @@ export interface QueuePluginOptions {
|
|
|
38
38
|
* and retries) — set e.g. `{ age: '14d' }` so failures don't grow unbounded.
|
|
39
39
|
*/
|
|
40
40
|
removeOnFail?: JobRetention;
|
|
41
|
+
/**
|
|
42
|
+
* Infra errors from the driver's broker client (e.g. Redis down). Forwarded
|
|
43
|
+
* to the driver built from `connection`; default `console.error` with context.
|
|
44
|
+
* Ignored when you pass your own `driver` — configure it on the driver then.
|
|
45
|
+
*/
|
|
46
|
+
onError?: BullmqDriverOptions['onError'];
|
|
47
|
+
/**
|
|
48
|
+
* A job exhausted its retries. Forwarded to the driver built from
|
|
49
|
+
* `connection`; default `console.error` with context. Ignored when you pass
|
|
50
|
+
* your own `driver` — configure it on the driver then.
|
|
51
|
+
*/
|
|
52
|
+
onJobFailed?: BullmqDriverOptions['onJobFailed'];
|
|
41
53
|
}
|
|
42
54
|
export declare function queuePlugin(options?: QueuePluginOptions): import("@basaltkit/core").BasaltPlugin<unknown>;
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,11 @@ export function queuePlugin(options = {}) {
|
|
|
17
17
|
let driver = options.driver;
|
|
18
18
|
if (!driver) {
|
|
19
19
|
if (options.connection) {
|
|
20
|
-
driver = new BullmqQueueDriver({
|
|
20
|
+
driver = new BullmqQueueDriver({
|
|
21
|
+
connection: options.connection,
|
|
22
|
+
...(options.onError !== undefined ? { onError: options.onError } : {}),
|
|
23
|
+
...(options.onJobFailed !== undefined ? { onJobFailed: options.onJobFailed } : {}),
|
|
24
|
+
});
|
|
21
25
|
}
|
|
22
26
|
else {
|
|
23
27
|
driver = new SyncQueueDriver();
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basaltkit/queue",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": ">=22.5.0"
|
|
6
|
+
},
|
|
4
7
|
"description": "Basalt queues on top of BullMQ: declarative jobs with Zod payloads, context propagation (tenant/requestId) to workers and a sync driver for tests.",
|
|
5
8
|
"license": "MIT",
|
|
6
9
|
"type": "module",
|
|
10
|
+
"sideEffects": false,
|
|
7
11
|
"exports": {
|
|
8
12
|
".": {
|
|
9
13
|
"types": "./dist/index.d.ts",
|
|
@@ -15,8 +19,8 @@
|
|
|
15
19
|
],
|
|
16
20
|
"dependencies": {
|
|
17
21
|
"bullmq": "^6.2.1",
|
|
18
|
-
"@basaltkit/core": "^1.3.
|
|
19
|
-
"@basaltkit/events": "^1.1.
|
|
22
|
+
"@basaltkit/core": "^1.3.1",
|
|
23
|
+
"@basaltkit/events": "^1.1.1"
|
|
20
24
|
},
|
|
21
25
|
"devDependencies": {
|
|
22
26
|
"@types/node": "^26.3.0",
|