@basaltkit/queue 1.5.0 → 2.0.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/README.md +17 -4
- package/dist/index.d.ts +12 -2
- package/dist/index.js +39 -5
- package/package.json +17 -5
package/README.md
CHANGED
|
@@ -20,15 +20,20 @@ This module gives you three things you'd normally have to build by hand:
|
|
|
20
20
|
|
|
21
21
|
1. **Declarative, type-safe jobs** — you define each job once with `defineJob` (name, validation schema, number of attempts) and then call `MyJob.dispatch(data)` anywhere in the application. Data is validated with Zod *before* entering the queue, so invalid data never reaches the worker.
|
|
22
22
|
2. **Context propagation** — information from the current request (`requestId`, `tenantId`, `userId`, etc.) automatically travels along with the job and is restored inside the worker. Your logs and tenant checks work in the worker the same way they did in the HTTP request.
|
|
23
|
-
3. **
|
|
23
|
+
3. **Interchangeable drivers** — in production, use **BullMQ** (over Redis, with real retries and delays), or RabbitMQ/SQS/Kafka through a driver package; in development and tests, use the **sync** driver, which runs the job immediately, in the same process, without needing Redis installed. The core itself is backend-neutral: it depends on no broker client.
|
|
24
24
|
|
|
25
25
|
## Installation
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
pnpm add @basaltkit/queue
|
|
29
|
+
|
|
30
|
+
# only if you use the BullMQ/Redis driver (`connection` or `BullmqQueueDriver`):
|
|
31
|
+
pnpm add bullmq
|
|
29
32
|
```
|
|
30
33
|
|
|
31
|
-
The package depends on `@basaltkit/core` and `@basaltkit/events` (installed automatically). For
|
|
34
|
+
The package depends on `@basaltkit/core` and `@basaltkit/events` (installed automatically). For development and tests you need nothing else — the sync driver has no dependencies.
|
|
35
|
+
|
|
36
|
+
`bullmq` is an **optional peer dependency**, not a dependency: `@basaltkit/queue` is the driver-agnostic core, so an application running on [RabbitMQ](https://www.npmjs.com/package/@basaltkit/queue-rabbitmq), [SQS](https://www.npmjs.com/package/@basaltkit/queue-sqs), [Kafka](https://www.npmjs.com/package/@basaltkit/queue-kafka) or the sync driver never installs (or loads) BullMQ and its ioredis transitive weight. Install `bullmq` yourself the moment you pass `connection` or construct `BullmqQueueDriver`; forget it and boot fails with a `MissingQueueDriverPackageError` telling you exactly that. For production with BullMQ you also need an accessible **Redis** server (it stores the queues there).
|
|
32
37
|
|
|
33
38
|
## Get started in 5 minutes
|
|
34
39
|
|
|
@@ -399,7 +404,7 @@ Creates the event→job bridge. Returns the subscription cancel function.
|
|
|
399
404
|
### Drivers
|
|
400
405
|
|
|
401
406
|
- **`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.
|
|
402
|
-
- **`class BullmqQueueDriver`** — production over Redis; see the options table below.
|
|
407
|
+
- **`class BullmqQueueDriver`** — production over Redis; see the options table below. Imported from its own entry point, `@basaltkit/queue/bullmq` (one import path per backend, like the RabbitMQ/SQS/Kafka driver packages), and requires the optional `bullmq` peer.
|
|
403
408
|
- **`interface QueueDriver`** (Advanced) — contract for custom drivers: `setExecutor(executor)`, `add(queue, jobName, data, options: AddJobOptions)`, `startWorker(queue, { concurrency? })`, optional `stats(queue)` / `retryFailed(queue, { limit? })` / `list(queue, options)`, `close()`, plus the optional `name` and `capabilities` fields. Helper types: `AddJobOptions`, `JobExecutor`, `QueueStats`, `DriverCapabilities`, `JobState`, `JobSummary`, `JobEnvelope`, `ListJobsOptions`.
|
|
404
409
|
|
|
405
410
|
The three optional methods are a deliberate pattern: a driver **omits** what its
|
|
@@ -417,6 +422,12 @@ Creates the event→job bridge. Returns the subscription cancel function.
|
|
|
417
422
|
|
|
418
423
|
#### `new BullmqQueueDriver(options: BullmqDriverOptions)`
|
|
419
424
|
|
|
425
|
+
```ts
|
|
426
|
+
import { BullmqQueueDriver } from '@basaltkit/queue/bullmq' // needs `pnpm add bullmq`
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
(The `BullmqDriverOptions` **type** is also re-exported from `@basaltkit/queue` — types are erased at build, so they cost nothing.)
|
|
430
|
+
|
|
420
431
|
| Option | Type | Default | Purpose |
|
|
421
432
|
|---|---|---|---|
|
|
422
433
|
| `connection` | `string \| ConnectionOptions` | — (required) | Redis URL (`redis://…`, `rediss://…` → TLS) or ioredis options. |
|
|
@@ -477,7 +488,8 @@ Passing your own `driver` bypasses that forwarding — a supplied driver owns it
|
|
|
477
488
|
so set them in its constructor:
|
|
478
489
|
|
|
479
490
|
```ts
|
|
480
|
-
import { queuePlugin
|
|
491
|
+
import { queuePlugin } from '@basaltkit/queue'
|
|
492
|
+
import { BullmqQueueDriver } from '@basaltkit/queue/bullmq'
|
|
481
493
|
|
|
482
494
|
queuePlugin({
|
|
483
495
|
jobs: [SendWelcomeEmail],
|
|
@@ -496,6 +508,7 @@ queuePlugin({
|
|
|
496
508
|
| `JobNotRegisteredError` | `QUEUE_JOB_NOT_REGISTERED` | `job.dispatch()` was called before the job was registered in a `QueueManager`. |
|
|
497
509
|
| `UnknownJobError` | `QUEUE_UNKNOWN_JOB` | A job reached the worker but is not registered in that process — producer and worker registered different job lists. |
|
|
498
510
|
| `UnsupportedJobOptionError` | `QUEUE_UNSUPPORTED_OPTION` | With `onUnsupported: 'throw'`, a dispatch used an option the active driver's `capabilities` do not include. `status = 500`. |
|
|
511
|
+
| `MissingQueueDriverPackageError` | `QUEUE_MISSING_DRIVER_PACKAGE` | `queuePlugin({ connection })` selected the BullMQ driver but the optional `bullmq` peer is not installed. Thrown at boot, with the fix in the message; `.cause` holds the original resolution error. |
|
|
499
512
|
|
|
500
513
|
Errors thrown outside these classes come from the driver's client (ioredis, amqplib, kafkajs,
|
|
501
514
|
the AWS SDK) and reach you through that driver's `onError`.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
|
+
import { BasaltError } from '@basaltkit/core';
|
|
1
2
|
import { type QueueDriver } from './driver.js';
|
|
2
|
-
import {
|
|
3
|
+
import type { BullmqDriverOptions } from './drivers/bullmq.js';
|
|
3
4
|
import type { JobDefinition, JobRetention } from './job.js';
|
|
4
5
|
import { QueueManager, type UnsupportedPolicy } from './manager.js';
|
|
5
6
|
export { defineJob, JobValidationError, JobNotRegisteredError, type JobDefinition, type JobSchema, type JobBackoff, type JobRetention, type DispatchOptions, } from './job.js';
|
|
6
7
|
export { QueueManager, UnknownJobError, UnsupportedJobOptionError, type UnsupportedPolicy, type QueueManagerOptions, } from './manager.js';
|
|
7
8
|
export { queuedOn, type QueuedListenerOptions } from './bridge.js';
|
|
8
9
|
export { SyncQueueDriver } from './drivers/sync.js';
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The BullMQ driver's *types* stay on the barrel (erased at build, so they cost
|
|
12
|
+
* a consumer nothing). The CLASS lives at its own entry point:
|
|
13
|
+
* `import { BullmqQueueDriver } from '@basaltkit/queue/bullmq'` — the same
|
|
14
|
+
* shape as the RabbitMQ/SQS/Kafka driver packages, one import path per backend.
|
|
15
|
+
*/
|
|
16
|
+
export type { BullmqDriverOptions } from './drivers/bullmq.js';
|
|
10
17
|
export { readJobEnvelope, DEFAULT_LIST_STATES, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, type QueueDriver, type QueueStats, type AddJobOptions, type JobExecutor, type DriverCapabilities, type JobState, type JobSummary, type JobEnvelope, type ListJobsOptions, } from './driver.js';
|
|
11
18
|
export declare const QUEUE: import("@basaltkit/core").Token<QueueManager>;
|
|
19
|
+
export declare class MissingQueueDriverPackageError extends BasaltError {
|
|
20
|
+
constructor(options?: ErrorOptions);
|
|
21
|
+
}
|
|
12
22
|
export interface QueuePluginOptions {
|
|
13
23
|
/** Jobs known to this process (producer and/or worker). */
|
|
14
24
|
jobs?: JobDefinition<unknown>[];
|
package/dist/index.js
CHANGED
|
@@ -1,25 +1,59 @@
|
|
|
1
|
-
import { createToken, definePlugin, ensureMetadata } from '@basaltkit/core';
|
|
1
|
+
import { BasaltError, createToken, definePlugin, ensureMetadata } from '@basaltkit/core';
|
|
2
2
|
import { DEFAULT_LIST_LIMIT, DEFAULT_LIST_STATES, } from './driver.js';
|
|
3
|
-
import { BullmqQueueDriver } from './drivers/bullmq.js';
|
|
4
3
|
import { SyncQueueDriver } from './drivers/sync.js';
|
|
5
4
|
import { QueueManager } from './manager.js';
|
|
6
5
|
export { defineJob, JobValidationError, JobNotRegisteredError, } from './job.js';
|
|
7
6
|
export { QueueManager, UnknownJobError, UnsupportedJobOptionError, } from './manager.js';
|
|
8
7
|
export { queuedOn } from './bridge.js';
|
|
9
8
|
export { SyncQueueDriver } from './drivers/sync.js';
|
|
10
|
-
export { BullmqQueueDriver } from './drivers/bullmq.js';
|
|
11
9
|
export { readJobEnvelope, DEFAULT_LIST_STATES, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, } from './driver.js';
|
|
12
10
|
export const QUEUE = createToken('queue');
|
|
11
|
+
let bullmqDriverModule;
|
|
12
|
+
/** Actionable guidance instead of a bare ERR_MODULE_NOT_FOUND from deep inside the driver. */
|
|
13
|
+
const MISSING_BULLMQ = '`queuePlugin({ connection })` selects the BullMQ driver, which needs the `bullmq` package — ' +
|
|
14
|
+
'an optional peer dependency of @basaltkit/queue that is not installed. ' +
|
|
15
|
+
'Either install it (`pnpm add bullmq`), or pass an explicit `driver:` — ' +
|
|
16
|
+
'`@basaltkit/queue-rabbitmq`, `@basaltkit/queue-sqs`, `@basaltkit/queue-kafka`, ' +
|
|
17
|
+
'or `new SyncQueueDriver()` for dev/tests.';
|
|
18
|
+
export class MissingQueueDriverPackageError extends BasaltError {
|
|
19
|
+
constructor(options) {
|
|
20
|
+
super('QUEUE_MISSING_DRIVER_PACKAGE', MISSING_BULLMQ, options);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function loadBullmqDriver() {
|
|
24
|
+
if (bullmqDriverModule)
|
|
25
|
+
return bullmqDriverModule;
|
|
26
|
+
try {
|
|
27
|
+
bullmqDriverModule = await import('./drivers/bullmq.js');
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw new MissingQueueDriverPackageError({ cause: error });
|
|
31
|
+
}
|
|
32
|
+
return bullmqDriverModule;
|
|
33
|
+
}
|
|
13
34
|
export function queuePlugin(options = {}) {
|
|
14
35
|
return definePlugin({
|
|
15
36
|
name: 'basalt:queue',
|
|
16
|
-
register({ container }) {
|
|
37
|
+
async register({ container }) {
|
|
38
|
+
// Resolve the BullMQ driver MODULE here — `BasaltApp.boot()` awaits
|
|
39
|
+
// `register`, so the class is in hand before anything can resolve QUEUE,
|
|
40
|
+
// and the container factory below stays synchronous. This is still a
|
|
41
|
+
// bindings-only phase: loading a module is not I/O against the world —
|
|
42
|
+
// no Redis connection is opened until the singleton is first resolved.
|
|
43
|
+
// Doing it in `boot` instead would leave a hole: a plugin booting earlier
|
|
44
|
+
// that resolves QUEUE would hit an unloaded driver.
|
|
45
|
+
if (!options.driver && options.connection)
|
|
46
|
+
await loadBullmqDriver();
|
|
17
47
|
registerQueueCommands(container);
|
|
18
48
|
container.singleton(QUEUE, () => {
|
|
19
49
|
let driver = options.driver;
|
|
20
50
|
if (!driver) {
|
|
21
51
|
if (options.connection) {
|
|
22
|
-
|
|
52
|
+
// Defensive: only reachable if `register`'s promise was dropped by
|
|
53
|
+
// a non-standard host instead of awaited.
|
|
54
|
+
if (!bullmqDriverModule)
|
|
55
|
+
throw new MissingQueueDriverPackageError();
|
|
56
|
+
driver = new bullmqDriverModule.BullmqQueueDriver({
|
|
23
57
|
connection: options.connection,
|
|
24
58
|
...(options.onError !== undefined ? { onError: options.onError } : {}),
|
|
25
59
|
...(options.onJobFailed !== undefined ? { onJobFailed: options.onJobFailed } : {}),
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basaltkit/queue",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22.5.0"
|
|
6
6
|
},
|
|
7
|
-
"description": "Basalt queues
|
|
7
|
+
"description": "Driver-agnostic Basalt queues: declarative jobs with Zod payloads, context propagation (tenant/requestId) to workers, a BullMQ/Redis driver behind an optional peer, and a sync driver for tests.",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"sideEffects": false,
|
|
@@ -12,18 +12,30 @@
|
|
|
12
12
|
".": {
|
|
13
13
|
"types": "./dist/index.d.ts",
|
|
14
14
|
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./bullmq": {
|
|
17
|
+
"types": "./dist/drivers/bullmq.d.ts",
|
|
18
|
+
"import": "./dist/drivers/bullmq.js"
|
|
15
19
|
}
|
|
16
20
|
},
|
|
17
21
|
"files": [
|
|
18
22
|
"dist"
|
|
19
23
|
],
|
|
20
24
|
"dependencies": {
|
|
21
|
-
"
|
|
22
|
-
"@basaltkit/core": "^1.3.1"
|
|
23
|
-
|
|
25
|
+
"@basaltkit/events": "^1.1.1",
|
|
26
|
+
"@basaltkit/core": "^1.3.1"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"bullmq": "^6.2.1"
|
|
30
|
+
},
|
|
31
|
+
"peerDependenciesMeta": {
|
|
32
|
+
"bullmq": {
|
|
33
|
+
"optional": true
|
|
34
|
+
}
|
|
24
35
|
},
|
|
25
36
|
"devDependencies": {
|
|
26
37
|
"@types/node": "^26.3.0",
|
|
38
|
+
"bullmq": "^6.2.1",
|
|
27
39
|
"typescript": "^7.0.2",
|
|
28
40
|
"vitest": "^4.1.11",
|
|
29
41
|
"zod": "^3.24.0 || ^4.0.0",
|