@basaltkit/queue 1.2.0 → 1.3.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 +6 -0
- package/dist/bridge.d.ts +18 -0
- package/dist/bridge.js +24 -0
- package/dist/driver.d.ts +72 -0
- package/dist/driver.js +1 -0
- package/dist/drivers/bullmq.d.ts +53 -0
- package/dist/drivers/bullmq.js +113 -0
- package/dist/drivers/sync.d.ts +26 -0
- package/dist/drivers/sync.js +36 -0
- package/dist/index.d.ts +13 -289
- package/dist/index.js +84 -426
- package/dist/job.d.ts +76 -0
- package/dist/job.js +59 -0
- package/dist/manager.d.ts +61 -0
- package/dist/manager.js +161 -0
- package/package.json +12 -13
package/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://basaltkit-docs.pages.dev">
|
|
3
|
+
<img src="https://basaltkit-docs.pages.dev/social-card.png" alt="Basalt" width="440">
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
1
7
|
# @basaltkit/queue
|
|
2
8
|
|
|
3
9
|
Job queues for Basalt applications: define declarative "jobs" with Zod validation, run them in the background with BullMQ/Redis in production, and synchronously in development and tests — without changing a line of code.
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EventBus, BasaltEvent } from '@basaltkit/events';
|
|
2
|
+
import { type JobBackoff } from './job.js';
|
|
3
|
+
import type { QueueManager } from './manager.js';
|
|
4
|
+
export interface QueuedListenerOptions {
|
|
5
|
+
queue?: string;
|
|
6
|
+
attempts?: number;
|
|
7
|
+
backoff?: JobBackoff;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The events→queue bridge: the listener becomes a job — emit only enqueues,
|
|
11
|
+
* and the handler runs in the worker with the driver's retry/backoff and the
|
|
12
|
+
* context (tenant/requestId) restored.
|
|
13
|
+
*
|
|
14
|
+
* queuedOn(bus, queue, OrderCreated, async ({ orderId }) => { ... })
|
|
15
|
+
*
|
|
16
|
+
* Returns the listener's unsubscribe function.
|
|
17
|
+
*/
|
|
18
|
+
export declare function queuedOn<T>(bus: EventBus, manager: QueueManager, event: BasaltEvent<T>, handler: (payload: T) => void | Promise<void>, options?: QueuedListenerOptions): () => void;
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineJob } from './job.js';
|
|
2
|
+
/**
|
|
3
|
+
* The events→queue bridge: the listener becomes a job — emit only enqueues,
|
|
4
|
+
* and the handler runs in the worker with the driver's retry/backoff and the
|
|
5
|
+
* context (tenant/requestId) restored.
|
|
6
|
+
*
|
|
7
|
+
* queuedOn(bus, queue, OrderCreated, async ({ orderId }) => { ... })
|
|
8
|
+
*
|
|
9
|
+
* Returns the listener's unsubscribe function.
|
|
10
|
+
*/
|
|
11
|
+
export function queuedOn(bus, manager, event, handler, options = {}) {
|
|
12
|
+
const job = defineJob({
|
|
13
|
+
name: `listener:${event.name}`,
|
|
14
|
+
...(event.schema ? { schema: event.schema } : {}),
|
|
15
|
+
...(options.queue ? { queue: options.queue } : {}),
|
|
16
|
+
...(options.attempts !== undefined ? { attempts: options.attempts } : {}),
|
|
17
|
+
...(options.backoff ? { backoff: options.backoff } : {}),
|
|
18
|
+
handle: handler,
|
|
19
|
+
});
|
|
20
|
+
manager.register(job);
|
|
21
|
+
return bus.on(event, async (payload) => {
|
|
22
|
+
await job.dispatch(payload);
|
|
23
|
+
});
|
|
24
|
+
}
|
package/dist/driver.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Driver-neutral retention: `true`/`false`, a count, or `{ ageMs, count }`. */
|
|
2
|
+
export type RetentionOption = boolean | number | {
|
|
3
|
+
ageMs?: number;
|
|
4
|
+
count?: number;
|
|
5
|
+
};
|
|
6
|
+
export interface AddJobOptions {
|
|
7
|
+
attempts: number;
|
|
8
|
+
backoff?: {
|
|
9
|
+
type: 'exponential' | 'fixed';
|
|
10
|
+
delayMs: number;
|
|
11
|
+
} | undefined;
|
|
12
|
+
delayMs?: number | undefined;
|
|
13
|
+
priority?: number | undefined;
|
|
14
|
+
/** Retention for completed jobs. Undefined → the driver's default. */
|
|
15
|
+
removeOnComplete?: RetentionOption | undefined;
|
|
16
|
+
/** Retention for failed jobs. Undefined → the driver's default. */
|
|
17
|
+
removeOnFail?: RetentionOption | undefined;
|
|
18
|
+
}
|
|
19
|
+
export type JobExecutor = (jobName: string, data: unknown) => Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* What a driver's backend honors. Backends differ: RabbitMQ needs a
|
|
22
|
+
* dead-letter setup for delayed jobs, Kafka has no message priority, etc. The
|
|
23
|
+
* QueueManager checks a dispatch's options against these and reacts per the
|
|
24
|
+
* `onUnsupported` policy instead of silently dropping them. A driver that omits
|
|
25
|
+
* `capabilities` is assumed fully capable (back-compat for existing drivers).
|
|
26
|
+
*/
|
|
27
|
+
export interface DriverCapabilities {
|
|
28
|
+
/** Honors delayed delivery (`delay`). */
|
|
29
|
+
delayed: boolean;
|
|
30
|
+
/** Honors message priority. */
|
|
31
|
+
priority: boolean;
|
|
32
|
+
/** Re-runs a failed job up to `attempts` times. */
|
|
33
|
+
retries: boolean;
|
|
34
|
+
/** Waits `backoff` between retries (vs retrying immediately). */
|
|
35
|
+
backoff: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** Job counts per state, for `basalt queue:stats`. */
|
|
38
|
+
export interface QueueStats {
|
|
39
|
+
waiting: number;
|
|
40
|
+
active: number;
|
|
41
|
+
completed: number;
|
|
42
|
+
failed: number;
|
|
43
|
+
delayed: number;
|
|
44
|
+
}
|
|
45
|
+
/** Queue driver contract. BullMQ in production; sync in tests/dev. */
|
|
46
|
+
export interface QueueDriver {
|
|
47
|
+
/** Short identifier used in diagnostics (e.g. 'bullmq', 'sync'). */
|
|
48
|
+
readonly name?: string;
|
|
49
|
+
/** What this backend honors — see {@link DriverCapabilities}. */
|
|
50
|
+
readonly capabilities?: DriverCapabilities;
|
|
51
|
+
/** Called once by the QueueManager — how to execute a received job. */
|
|
52
|
+
setExecutor(executor: JobExecutor): void;
|
|
53
|
+
add(queue: string, jobName: string, data: unknown, options: AddJobOptions): Promise<void>;
|
|
54
|
+
/** Starts a worker for the queue (no-op in the sync driver: add executes inline). */
|
|
55
|
+
startWorker(queue: string, options?: {
|
|
56
|
+
concurrency?: number;
|
|
57
|
+
}): void;
|
|
58
|
+
/**
|
|
59
|
+
* Optional: job counts per state, for `basalt queue:stats`. Backends that
|
|
60
|
+
* cannot introspect (e.g. the inline sync driver) omit it — the CLI then
|
|
61
|
+
* reports the operation as unsupported rather than guessing.
|
|
62
|
+
*/
|
|
63
|
+
stats?(queue: string): Promise<QueueStats>;
|
|
64
|
+
/**
|
|
65
|
+
* Optional: re-enqueue failed jobs (`basalt queue:retry`). Returns how many
|
|
66
|
+
* were retried. `limit` caps how many are processed (default driver's choice).
|
|
67
|
+
*/
|
|
68
|
+
retryFailed?(queue: string, options?: {
|
|
69
|
+
limit?: number;
|
|
70
|
+
}): Promise<number>;
|
|
71
|
+
close(): Promise<void>;
|
|
72
|
+
}
|
package/dist/driver.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ConnectionOptions } from 'bullmq';
|
|
2
|
+
import type { AddJobOptions, JobExecutor, QueueDriver, QueueStats } from '../driver.js';
|
|
3
|
+
export interface BullmqDriverOptions {
|
|
4
|
+
/** Redis URL (redis://... or rediss://...) or ioredis connection options. */
|
|
5
|
+
connection: string | ConnectionOptions;
|
|
6
|
+
/**
|
|
7
|
+
* Infra errors from BullMQ's Worker/Queue emitters (e.g. Redis down). BullMQ
|
|
8
|
+
* emits these as EventEmitter 'error' events — unhandled, they CRASH the
|
|
9
|
+
* process. Default: logged via console.error with full context — observable,
|
|
10
|
+
* never fatal, never silent. (Same pattern as realtime's onBridgeError.)
|
|
11
|
+
*/
|
|
12
|
+
onError?: (error: unknown, info: {
|
|
13
|
+
queue: string;
|
|
14
|
+
source: 'worker' | 'queue';
|
|
15
|
+
}) => void;
|
|
16
|
+
/**
|
|
17
|
+
* A job exhausted its retries (BullMQ 'failed'). Default: console.error —
|
|
18
|
+
* without this, exhausted jobs were only visible by polling queue stats.
|
|
19
|
+
*/
|
|
20
|
+
onJobFailed?: (info: {
|
|
21
|
+
queue: string;
|
|
22
|
+
job: string;
|
|
23
|
+
jobId?: string;
|
|
24
|
+
error: unknown;
|
|
25
|
+
}) => void;
|
|
26
|
+
}
|
|
27
|
+
export declare class BullmqQueueDriver implements QueueDriver {
|
|
28
|
+
readonly name = "bullmq";
|
|
29
|
+
readonly capabilities: {
|
|
30
|
+
delayed: boolean;
|
|
31
|
+
priority: boolean;
|
|
32
|
+
retries: boolean;
|
|
33
|
+
backoff: boolean;
|
|
34
|
+
};
|
|
35
|
+
private readonly connection;
|
|
36
|
+
private readonly queues;
|
|
37
|
+
private readonly workers;
|
|
38
|
+
private executor;
|
|
39
|
+
private readonly onError;
|
|
40
|
+
private readonly onJobFailed;
|
|
41
|
+
constructor(options: BullmqDriverOptions);
|
|
42
|
+
setExecutor(executor: JobExecutor): void;
|
|
43
|
+
add(queue: string, jobName: string, data: unknown, options: AddJobOptions): Promise<void>;
|
|
44
|
+
startWorker(queue: string, options?: {
|
|
45
|
+
concurrency?: number;
|
|
46
|
+
}): void;
|
|
47
|
+
stats(queue: string): Promise<QueueStats>;
|
|
48
|
+
retryFailed(queue: string, options?: {
|
|
49
|
+
limit?: number;
|
|
50
|
+
}): Promise<number>;
|
|
51
|
+
close(): Promise<void>;
|
|
52
|
+
private queue;
|
|
53
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Queue, Worker } from 'bullmq';
|
|
2
|
+
/** Map the driver-neutral retention to BullMQ's (age in seconds), falling back to a default. */
|
|
3
|
+
function toBullRetention(retention, fallback) {
|
|
4
|
+
if (retention === undefined)
|
|
5
|
+
return fallback;
|
|
6
|
+
if (typeof retention === 'boolean' || typeof retention === 'number')
|
|
7
|
+
return retention;
|
|
8
|
+
const out = {};
|
|
9
|
+
if (retention.ageMs !== undefined)
|
|
10
|
+
out.age = Math.max(1, Math.round(retention.ageMs / 1000));
|
|
11
|
+
if (retention.count !== undefined)
|
|
12
|
+
out.count = retention.count;
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
export class BullmqQueueDriver {
|
|
16
|
+
name = 'bullmq';
|
|
17
|
+
capabilities = { delayed: true, priority: true, retries: true, backoff: true };
|
|
18
|
+
connection;
|
|
19
|
+
queues = new Map();
|
|
20
|
+
workers = [];
|
|
21
|
+
executor;
|
|
22
|
+
onError;
|
|
23
|
+
onJobFailed;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.connection =
|
|
26
|
+
typeof options.connection === 'string'
|
|
27
|
+
? parseRedisUrl(options.connection)
|
|
28
|
+
: options.connection;
|
|
29
|
+
this.onError =
|
|
30
|
+
options.onError ??
|
|
31
|
+
((error, info) => console.error(`[basalt:queue] bullmq ${info.source} error (queue "${info.queue}"):`, error));
|
|
32
|
+
this.onJobFailed =
|
|
33
|
+
options.onJobFailed ??
|
|
34
|
+
((info) => console.error(`[basalt:queue] job "${info.job}"${info.jobId ? ` (id ${info.jobId})` : ''} on queue "${info.queue}" failed permanently:`, info.error));
|
|
35
|
+
}
|
|
36
|
+
setExecutor(executor) {
|
|
37
|
+
this.executor = executor;
|
|
38
|
+
}
|
|
39
|
+
async add(queue, jobName, data, options) {
|
|
40
|
+
await this.queue(queue).add(jobName, data, {
|
|
41
|
+
attempts: options.attempts,
|
|
42
|
+
...(options.backoff
|
|
43
|
+
? { backoff: { type: options.backoff.type, delay: options.backoff.delayMs } }
|
|
44
|
+
: {}),
|
|
45
|
+
...(options.delayMs !== undefined ? { delay: options.delayMs } : {}),
|
|
46
|
+
...(options.priority !== undefined ? { priority: options.priority } : {}),
|
|
47
|
+
removeOnComplete: toBullRetention(options.removeOnComplete, { count: 1000 }),
|
|
48
|
+
removeOnFail: toBullRetention(options.removeOnFail, false),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
startWorker(queue, options = {}) {
|
|
52
|
+
const worker = new Worker(queue, async (job) => this.executor?.(job.name, job.data), {
|
|
53
|
+
connection: this.connection,
|
|
54
|
+
concurrency: options.concurrency ?? 1,
|
|
55
|
+
});
|
|
56
|
+
// Without these listeners an emitted 'error' crashes the process (Node
|
|
57
|
+
// EventEmitter semantics) and exhausted jobs fail invisibly (Q-2).
|
|
58
|
+
worker.on('error', (error) => this.onError(error, { queue, source: 'worker' }));
|
|
59
|
+
worker.on('failed', (job, error) => this.onJobFailed({
|
|
60
|
+
queue,
|
|
61
|
+
job: job?.name ?? '(unknown)',
|
|
62
|
+
...(job?.id !== undefined ? { jobId: String(job.id) } : {}),
|
|
63
|
+
error,
|
|
64
|
+
}));
|
|
65
|
+
this.workers.push(worker);
|
|
66
|
+
}
|
|
67
|
+
async stats(queue) {
|
|
68
|
+
const c = await this.queue(queue).getJobCounts('waiting', 'active', 'completed', 'failed', 'delayed');
|
|
69
|
+
return {
|
|
70
|
+
waiting: c['waiting'] ?? 0,
|
|
71
|
+
active: c['active'] ?? 0,
|
|
72
|
+
completed: c['completed'] ?? 0,
|
|
73
|
+
failed: c['failed'] ?? 0,
|
|
74
|
+
delayed: c['delayed'] ?? 0,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async retryFailed(queue, options = {}) {
|
|
78
|
+
const limit = options.limit ?? 1000;
|
|
79
|
+
const failed = await this.queue(queue).getFailed(0, limit - 1);
|
|
80
|
+
let retried = 0;
|
|
81
|
+
for (const job of failed) {
|
|
82
|
+
await job.retry();
|
|
83
|
+
retried++;
|
|
84
|
+
}
|
|
85
|
+
return retried;
|
|
86
|
+
}
|
|
87
|
+
async close() {
|
|
88
|
+
await Promise.all(this.workers.map((worker) => worker.close()));
|
|
89
|
+
await Promise.all([...this.queues.values()].map((queue) => queue.close()));
|
|
90
|
+
}
|
|
91
|
+
queue(name) {
|
|
92
|
+
let queue = this.queues.get(name);
|
|
93
|
+
if (!queue) {
|
|
94
|
+
queue = new Queue(name, { connection: this.connection });
|
|
95
|
+
queue.on('error', (error) => this.onError(error, { queue: name, source: 'queue' }));
|
|
96
|
+
this.queues.set(name, queue);
|
|
97
|
+
}
|
|
98
|
+
return queue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function parseRedisUrl(url) {
|
|
102
|
+
const parsed = new URL(url);
|
|
103
|
+
return {
|
|
104
|
+
host: parsed.hostname,
|
|
105
|
+
port: parsed.port ? Number(parsed.port) : 6379,
|
|
106
|
+
...(parsed.username ? { username: parsed.username } : {}),
|
|
107
|
+
...(parsed.password ? { password: parsed.password } : {}),
|
|
108
|
+
...(parsed.pathname && parsed.pathname !== '/' ? { db: Number(parsed.pathname.slice(1)) } : {}),
|
|
109
|
+
...(parsed.protocol === 'rediss:' ? { tls: {} } : {}),
|
|
110
|
+
// required by BullMQ for workers
|
|
111
|
+
maxRetriesPerRequest: null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AddJobOptions, JobExecutor, QueueDriver } from '../driver.js';
|
|
2
|
+
/**
|
|
3
|
+
* Synchronous driver: executes the job inline on dispatch, honoring `attempts`
|
|
4
|
+
* (immediate retry). It is the driver for tests and Redis-less dev — the
|
|
5
|
+
* equivalent of Laravel's `sync` queue driver.
|
|
6
|
+
*/
|
|
7
|
+
export declare class SyncQueueDriver implements QueueDriver {
|
|
8
|
+
readonly name = "sync";
|
|
9
|
+
readonly capabilities: {
|
|
10
|
+
delayed: boolean;
|
|
11
|
+
priority: boolean;
|
|
12
|
+
retries: boolean;
|
|
13
|
+
backoff: boolean;
|
|
14
|
+
};
|
|
15
|
+
private executor;
|
|
16
|
+
/** execution history — useful in test assertions */
|
|
17
|
+
readonly executed: {
|
|
18
|
+
queue: string;
|
|
19
|
+
jobName: string;
|
|
20
|
+
attempts: number;
|
|
21
|
+
}[];
|
|
22
|
+
setExecutor(executor: JobExecutor): void;
|
|
23
|
+
add(queue: string, jobName: string, data: unknown, options: AddJobOptions): Promise<void>;
|
|
24
|
+
startWorker(): void;
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous driver: executes the job inline on dispatch, honoring `attempts`
|
|
3
|
+
* (immediate retry). It is the driver for tests and Redis-less dev — the
|
|
4
|
+
* equivalent of Laravel's `sync` queue driver.
|
|
5
|
+
*/
|
|
6
|
+
export class SyncQueueDriver {
|
|
7
|
+
name = 'sync';
|
|
8
|
+
// Runs inline on dispatch: retries are honored (immediately), but there is no
|
|
9
|
+
// deferred delivery and no ordering, so delayed/priority are not supported.
|
|
10
|
+
capabilities = { delayed: false, priority: false, retries: true, backoff: false };
|
|
11
|
+
executor;
|
|
12
|
+
/** execution history — useful in test assertions */
|
|
13
|
+
executed = [];
|
|
14
|
+
setExecutor(executor) {
|
|
15
|
+
this.executor = executor;
|
|
16
|
+
}
|
|
17
|
+
async add(queue, jobName, data, options) {
|
|
18
|
+
let lastError;
|
|
19
|
+
for (let attempt = 1; attempt <= options.attempts; attempt++) {
|
|
20
|
+
try {
|
|
21
|
+
await this.executor?.(jobName, data);
|
|
22
|
+
this.executed.push({ queue, jobName, attempts: attempt });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
lastError = error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
this.executed.push({ queue, jobName, attempts: options.attempts });
|
|
30
|
+
throw lastError;
|
|
31
|
+
}
|
|
32
|
+
startWorker() {
|
|
33
|
+
// no-op: add() already executes inline
|
|
34
|
+
}
|
|
35
|
+
async close() { }
|
|
36
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,289 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
backoff?: {
|
|
14
|
-
type: 'exponential' | 'fixed';
|
|
15
|
-
delayMs: number;
|
|
16
|
-
} | undefined;
|
|
17
|
-
delayMs?: number | undefined;
|
|
18
|
-
priority?: number | undefined;
|
|
19
|
-
/** Retention for completed jobs. Undefined → the driver's default. */
|
|
20
|
-
removeOnComplete?: RetentionOption | undefined;
|
|
21
|
-
/** Retention for failed jobs. Undefined → the driver's default. */
|
|
22
|
-
removeOnFail?: RetentionOption | undefined;
|
|
23
|
-
}
|
|
24
|
-
type JobExecutor = (jobName: string, data: unknown) => Promise<void>;
|
|
25
|
-
/**
|
|
26
|
-
* What a driver's backend honors. Backends differ: RabbitMQ needs a
|
|
27
|
-
* dead-letter setup for delayed jobs, Kafka has no message priority, etc. The
|
|
28
|
-
* QueueManager checks a dispatch's options against these and reacts per the
|
|
29
|
-
* `onUnsupported` policy instead of silently dropping them. A driver that omits
|
|
30
|
-
* `capabilities` is assumed fully capable (back-compat for existing drivers).
|
|
31
|
-
*/
|
|
32
|
-
interface DriverCapabilities {
|
|
33
|
-
/** Honors delayed delivery (`delay`). */
|
|
34
|
-
delayed: boolean;
|
|
35
|
-
/** Honors message priority. */
|
|
36
|
-
priority: boolean;
|
|
37
|
-
/** Re-runs a failed job up to `attempts` times. */
|
|
38
|
-
retries: boolean;
|
|
39
|
-
/** Waits `backoff` between retries (vs retrying immediately). */
|
|
40
|
-
backoff: boolean;
|
|
41
|
-
}
|
|
42
|
-
/** Job counts per state, for `basalt queue:stats`. */
|
|
43
|
-
interface QueueStats {
|
|
44
|
-
waiting: number;
|
|
45
|
-
active: number;
|
|
46
|
-
completed: number;
|
|
47
|
-
failed: number;
|
|
48
|
-
delayed: number;
|
|
49
|
-
}
|
|
50
|
-
/** Queue driver contract. BullMQ in production; sync in tests/dev. */
|
|
51
|
-
interface QueueDriver {
|
|
52
|
-
/** Short identifier used in diagnostics (e.g. 'bullmq', 'sync'). */
|
|
53
|
-
readonly name?: string;
|
|
54
|
-
/** What this backend honors — see {@link DriverCapabilities}. */
|
|
55
|
-
readonly capabilities?: DriverCapabilities;
|
|
56
|
-
/** Called once by the QueueManager — how to execute a received job. */
|
|
57
|
-
setExecutor(executor: JobExecutor): void;
|
|
58
|
-
add(queue: string, jobName: string, data: unknown, options: AddJobOptions): Promise<void>;
|
|
59
|
-
/** Starts a worker for the queue (no-op in the sync driver: add executes inline). */
|
|
60
|
-
startWorker(queue: string, options?: {
|
|
61
|
-
concurrency?: number;
|
|
62
|
-
}): void;
|
|
63
|
-
/**
|
|
64
|
-
* Optional: job counts per state, for `basalt queue:stats`. Backends that
|
|
65
|
-
* cannot introspect (e.g. the inline sync driver) omit it — the CLI then
|
|
66
|
-
* reports the operation as unsupported rather than guessing.
|
|
67
|
-
*/
|
|
68
|
-
stats?(queue: string): Promise<QueueStats>;
|
|
69
|
-
/**
|
|
70
|
-
* Optional: re-enqueue failed jobs (`basalt queue:retry`). Returns how many
|
|
71
|
-
* were retried. `limit` caps how many are processed (default driver's choice).
|
|
72
|
-
*/
|
|
73
|
-
retryFailed?(queue: string, options?: {
|
|
74
|
-
limit?: number;
|
|
75
|
-
}): Promise<number>;
|
|
76
|
-
close(): Promise<void>;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
interface BullmqDriverOptions {
|
|
80
|
-
/** Redis URL (redis://... or rediss://...) or ioredis connection options. */
|
|
81
|
-
connection: string | ConnectionOptions;
|
|
82
|
-
}
|
|
83
|
-
declare class BullmqQueueDriver implements QueueDriver {
|
|
84
|
-
readonly name = "bullmq";
|
|
85
|
-
readonly capabilities: {
|
|
86
|
-
delayed: boolean;
|
|
87
|
-
priority: boolean;
|
|
88
|
-
retries: boolean;
|
|
89
|
-
backoff: boolean;
|
|
90
|
-
};
|
|
91
|
-
private readonly connection;
|
|
92
|
-
private readonly queues;
|
|
93
|
-
private readonly workers;
|
|
94
|
-
private executor;
|
|
95
|
-
constructor(options: BullmqDriverOptions);
|
|
96
|
-
setExecutor(executor: JobExecutor): void;
|
|
97
|
-
add(queue: string, jobName: string, data: unknown, options: AddJobOptions): Promise<void>;
|
|
98
|
-
startWorker(queue: string, options?: {
|
|
99
|
-
concurrency?: number;
|
|
100
|
-
}): void;
|
|
101
|
-
stats(queue: string): Promise<QueueStats>;
|
|
102
|
-
retryFailed(queue: string, options?: {
|
|
103
|
-
limit?: number;
|
|
104
|
-
}): Promise<number>;
|
|
105
|
-
close(): Promise<void>;
|
|
106
|
-
private queue;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Structural schema compatible with Zod. */
|
|
110
|
-
interface JobSchema<T> {
|
|
111
|
-
safeParse(input: unknown): {
|
|
112
|
-
success: boolean;
|
|
113
|
-
data?: T;
|
|
114
|
-
error?: unknown;
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
declare class JobValidationError extends BasaltError {
|
|
118
|
-
readonly job: string;
|
|
119
|
-
readonly issues: unknown;
|
|
120
|
-
constructor(job: string, issues: unknown);
|
|
121
|
-
}
|
|
122
|
-
declare class JobNotRegisteredError extends BasaltError {
|
|
123
|
-
constructor(job: string);
|
|
124
|
-
}
|
|
125
|
-
interface DispatchOptions {
|
|
126
|
-
delay?: DurationInput;
|
|
127
|
-
priority?: number;
|
|
128
|
-
}
|
|
129
|
-
interface JobBackoff {
|
|
130
|
-
type: 'exponential' | 'fixed';
|
|
131
|
-
delay: DurationInput;
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Redis retention for finished jobs (BullMQ driver): `true` removes it as soon as
|
|
135
|
-
* it finishes, `false` keeps it forever, a number keeps that many most-recent, and
|
|
136
|
-
* `{ age, count }` keeps by age and/or count. Defaults: completed `{ count: 1000 }`,
|
|
137
|
-
* failed `false` (keep all). The sync driver ignores it (it stores nothing).
|
|
138
|
-
*/
|
|
139
|
-
type JobRetention = boolean | number | {
|
|
140
|
-
age?: DurationInput;
|
|
141
|
-
count?: number;
|
|
142
|
-
};
|
|
143
|
-
interface JobDefinition<T = unknown> {
|
|
144
|
-
readonly name: string;
|
|
145
|
-
readonly schema?: JobSchema<T> | undefined;
|
|
146
|
-
readonly queue: string;
|
|
147
|
-
readonly attempts: number;
|
|
148
|
-
readonly backoff?: JobBackoff | undefined;
|
|
149
|
-
/** Retention for completed jobs. Overrides the queuePlugin default. */
|
|
150
|
-
readonly removeOnComplete?: JobRetention | undefined;
|
|
151
|
-
/** Retention for failed jobs. Overrides the queuePlugin default. */
|
|
152
|
-
readonly removeOnFail?: JobRetention | undefined;
|
|
153
|
-
handle(payload: T): void | Promise<void>;
|
|
154
|
-
/** Enqueues the job — available after registration in a QueueManager. */
|
|
155
|
-
dispatch(payload: T, options?: DispatchOptions): Promise<void>;
|
|
156
|
-
/** @internal used by the QueueManager when registering */
|
|
157
|
-
__bind(dispatcher: JobDispatcher): void;
|
|
158
|
-
}
|
|
159
|
-
interface JobDispatcher {
|
|
160
|
-
dispatch<T>(job: JobDefinition<T>, payload: T, options?: DispatchOptions): Promise<void>;
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Defines a declarative job:
|
|
164
|
-
*
|
|
165
|
-
* export const SendWelcomeEmail = defineJob({
|
|
166
|
-
* name: 'email.welcome',
|
|
167
|
-
* schema: z.object({ userId: z.string() }),
|
|
168
|
-
* attempts: 3,
|
|
169
|
-
* backoff: { type: 'exponential', delay: '30s' },
|
|
170
|
-
* async handle({ userId }) { ... },
|
|
171
|
-
* })
|
|
172
|
-
*/
|
|
173
|
-
declare function defineJob<T = unknown>(config: {
|
|
174
|
-
name: string;
|
|
175
|
-
schema?: JobSchema<T>;
|
|
176
|
-
queue?: string;
|
|
177
|
-
attempts?: number;
|
|
178
|
-
backoff?: JobBackoff;
|
|
179
|
-
removeOnComplete?: JobRetention;
|
|
180
|
-
removeOnFail?: JobRetention;
|
|
181
|
-
handle(payload: T): void | Promise<void>;
|
|
182
|
-
}): JobDefinition<T>;
|
|
183
|
-
|
|
184
|
-
declare class UnknownJobError extends BasaltError {
|
|
185
|
-
constructor(job: string);
|
|
186
|
-
}
|
|
187
|
-
/** A job used an option the active driver doesn't support (with policy 'throw'). */
|
|
188
|
-
declare class UnsupportedJobOptionError extends BasaltError {
|
|
189
|
-
readonly status = 500;
|
|
190
|
-
constructor(driver: string, job: string, features: string[]);
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* What to do when a dispatch uses an option the driver can't honor:
|
|
194
|
-
* - `throw`: raise {@link UnsupportedJobOptionError} (strict; recommended in prod)
|
|
195
|
-
* - `warn`: log once per job+feature and proceed (default — never silent)
|
|
196
|
-
* - `ignore`: proceed silently (legacy behavior)
|
|
197
|
-
*/
|
|
198
|
-
type UnsupportedPolicy = 'throw' | 'warn' | 'ignore';
|
|
199
|
-
interface QueueManagerOptions {
|
|
200
|
-
/** Reaction when a job uses an option the driver can't honor. Default 'warn'. */
|
|
201
|
-
onUnsupported?: UnsupportedPolicy;
|
|
202
|
-
/** Sink for 'warn' diagnostics. Default console.warn. */
|
|
203
|
-
warn?: (message: string) => void;
|
|
204
|
-
/** Default retention for completed jobs (a job can override). Driver default: keep 1000. */
|
|
205
|
-
removeOnComplete?: JobRetention;
|
|
206
|
-
/** Default retention for failed jobs (a job can override). Driver default: keep all. */
|
|
207
|
-
removeOnFail?: JobRetention;
|
|
208
|
-
}
|
|
209
|
-
declare class QueueManager implements JobDispatcher {
|
|
210
|
-
private readonly driver;
|
|
211
|
-
private readonly jobs;
|
|
212
|
-
private readonly onUnsupported;
|
|
213
|
-
private readonly warn;
|
|
214
|
-
private readonly warned;
|
|
215
|
-
private readonly defaultRemoveOnComplete;
|
|
216
|
-
private readonly defaultRemoveOnFail;
|
|
217
|
-
constructor(driver: QueueDriver, options?: QueueManagerOptions);
|
|
218
|
-
/**
|
|
219
|
-
* Checks the dispatch's options against the driver's declared capabilities.
|
|
220
|
-
* A driver that omits `capabilities` is assumed fully capable (back-compat).
|
|
221
|
-
*/
|
|
222
|
-
private assertSupported;
|
|
223
|
-
register(job: JobDefinition<never> | JobDefinition<unknown>): this;
|
|
224
|
-
dispatch<T>(job: JobDefinition<T>, payload: T, options?: DispatchOptions): Promise<void>;
|
|
225
|
-
/** Starts a worker for the queue. With the sync driver it is a no-op. */
|
|
226
|
-
work(queue?: string, options?: {
|
|
227
|
-
concurrency?: number;
|
|
228
|
-
}): void;
|
|
229
|
-
/** Job counts per state, or `undefined` if the driver can't introspect. */
|
|
230
|
-
stats(queue?: string): Promise<QueueStats | undefined>;
|
|
231
|
-
/**
|
|
232
|
-
* Re-enqueues failed jobs; returns the count, or `undefined` if the driver
|
|
233
|
-
* doesn't support retrying (e.g. the inline sync driver).
|
|
234
|
-
*/
|
|
235
|
-
retryFailed(queue?: string, options?: {
|
|
236
|
-
limit?: number;
|
|
237
|
-
}): Promise<number | undefined>;
|
|
238
|
-
close(): Promise<void>;
|
|
239
|
-
/** Executes a job received from the driver: validates, restores the context, runs the handler. */
|
|
240
|
-
private execute;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
interface QueuedListenerOptions {
|
|
244
|
-
queue?: string;
|
|
245
|
-
attempts?: number;
|
|
246
|
-
backoff?: JobBackoff;
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* The events→queue bridge: the listener becomes a job — emit only enqueues,
|
|
250
|
-
* and the handler runs in the worker with the driver's retry/backoff and the
|
|
251
|
-
* context (tenant/requestId) restored.
|
|
252
|
-
*
|
|
253
|
-
* queuedOn(bus, queue, OrderCreated, async ({ orderId }) => { ... })
|
|
254
|
-
*
|
|
255
|
-
* Returns the listener's unsubscribe function.
|
|
256
|
-
*/
|
|
257
|
-
declare function queuedOn<T>(bus: EventBus, manager: QueueManager, event: BasaltEvent<T>, handler: (payload: T) => void | Promise<void>, options?: QueuedListenerOptions): () => void;
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Synchronous driver: executes the job inline on dispatch, honoring `attempts`
|
|
261
|
-
* (immediate retry). It is the driver for tests and Redis-less dev — the
|
|
262
|
-
* equivalent of Laravel's `sync` queue driver.
|
|
263
|
-
*/
|
|
264
|
-
declare class SyncQueueDriver implements QueueDriver {
|
|
265
|
-
readonly name = "sync";
|
|
266
|
-
readonly capabilities: {
|
|
267
|
-
delayed: boolean;
|
|
268
|
-
priority: boolean;
|
|
269
|
-
retries: boolean;
|
|
270
|
-
backoff: boolean;
|
|
271
|
-
};
|
|
272
|
-
private executor;
|
|
273
|
-
/** execution history — useful in test assertions */
|
|
274
|
-
readonly executed: {
|
|
275
|
-
queue: string;
|
|
276
|
-
jobName: string;
|
|
277
|
-
attempts: number;
|
|
278
|
-
}[];
|
|
279
|
-
setExecutor(executor: JobExecutor): void;
|
|
280
|
-
add(queue: string, jobName: string, data: unknown, options: AddJobOptions): Promise<void>;
|
|
281
|
-
startWorker(): void;
|
|
282
|
-
close(): Promise<void>;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
declare const QUEUE: _basaltkit_core.Token<QueueManager>;
|
|
286
|
-
interface QueuePluginOptions {
|
|
1
|
+
import { type BullmqDriverOptions } from './drivers/bullmq.js';
|
|
2
|
+
import type { QueueDriver } from './driver.js';
|
|
3
|
+
import type { JobDefinition, JobRetention } from './job.js';
|
|
4
|
+
import { QueueManager, type UnsupportedPolicy } from './manager.js';
|
|
5
|
+
export { defineJob, JobValidationError, JobNotRegisteredError, type JobDefinition, type JobSchema, type JobBackoff, type JobRetention, type DispatchOptions, } from './job.js';
|
|
6
|
+
export { QueueManager, UnknownJobError, UnsupportedJobOptionError, type UnsupportedPolicy, type QueueManagerOptions, } from './manager.js';
|
|
7
|
+
export { queuedOn, type QueuedListenerOptions } from './bridge.js';
|
|
8
|
+
export { SyncQueueDriver } from './drivers/sync.js';
|
|
9
|
+
export { BullmqQueueDriver, type BullmqDriverOptions } from './drivers/bullmq.js';
|
|
10
|
+
export type { QueueDriver, QueueStats, AddJobOptions, JobExecutor, DriverCapabilities } from './driver.js';
|
|
11
|
+
export declare const QUEUE: import("@basaltkit/core").Token<QueueManager>;
|
|
12
|
+
export interface QueuePluginOptions {
|
|
287
13
|
/** Jobs known to this process (producer and/or worker). */
|
|
288
14
|
jobs?: JobDefinition<unknown>[];
|
|
289
15
|
/** Redis connection → BullMQ driver. No connection → sync driver (dev/test). */
|
|
@@ -313,6 +39,4 @@ interface QueuePluginOptions {
|
|
|
313
39
|
*/
|
|
314
40
|
removeOnFail?: JobRetention;
|
|
315
41
|
}
|
|
316
|
-
declare function queuePlugin(options?: QueuePluginOptions):
|
|
317
|
-
|
|
318
|
-
export { type AddJobOptions, type BullmqDriverOptions, BullmqQueueDriver, type DispatchOptions, type DriverCapabilities, type JobBackoff, type JobDefinition, type JobExecutor, JobNotRegisteredError, type JobRetention, type JobSchema, JobValidationError, QUEUE, type QueueDriver, QueueManager, type QueueManagerOptions, type QueuePluginOptions, type QueueStats, type QueuedListenerOptions, SyncQueueDriver, UnknownJobError, UnsupportedJobOptionError, type UnsupportedPolicy, defineJob, queuePlugin, queuedOn };
|
|
42
|
+
export declare function queuePlugin(options?: QueuePluginOptions): import("@basaltkit/core").BasaltPlugin<unknown>;
|