@maestro-js/queue 1.0.0-alpha.2 → 1.0.0-alpha.20

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 (4) hide show
  1. package/README.md +337 -179
  2. package/dist/index.d.ts +179 -215
  3. package/dist/index.js +422 -475
  4. package/package.json +10 -6
package/dist/index.d.ts CHANGED
@@ -1,300 +1,264 @@
1
1
  import { Iso } from 'iso-fns2';
2
2
  import { Log } from '@maestro-js/log';
3
3
 
4
- interface DehydratedTrace {
5
- traceId: string;
6
- spanId: string;
7
- traceFlags: number;
8
- traceState: string | undefined;
9
- }
10
- interface QueueTracing {
11
- hydrate: <T>(trace: DehydratedTrace | null, callback: () => T) => T;
12
- dehydrate: () => DehydratedTrace | null;
13
- startActiveSpan: <R>(name: string | {
14
- name: string;
15
- attributes?: Record<string, unknown>;
16
- }, fn: () => R) => R;
17
- getActiveSpan: () => {
18
- setAttribute(key: string, value: unknown): void;
19
- setStatus(status: {
20
- code: number;
21
- message: string;
22
- }): void;
23
- } | undefined;
24
- }
25
- interface QueueMessage<MessageBody extends Record<string, any> = Record<string, any>, Result = any> {
26
- id: number;
27
- batchId: number | null;
4
+ interface QueueMessage<MessageBody extends Record<string, any> = Record<string, any>> {
5
+ id: string;
28
6
  queue: string;
29
- scheduledDate: Iso.Instant;
30
- enqueuedDate: Iso.Instant;
31
- startedDate: Iso.Instant | null;
32
7
  body: MessageBody;
33
- trace: DehydratedTrace | null;
34
- status: 'waiting' | 'processing' | 'success' | 'error' | 'stuck';
35
- result: Result;
36
- recovered: number;
37
- runningTimeSeconds: number | null;
38
- uniqueKey: number | null;
39
- priority: number | null;
8
+ attempts: number;
40
9
  }
41
- type QueueWorkFunction<MessageBody extends Record<string, any> = Record<string, any>> = (args: {
42
- body: MessageBody;
43
- id: number;
44
- trace: DehydratedTrace | null;
45
- scheduledDate: Iso.Instant;
46
- }) => any;
47
10
  interface QueueDriver {
48
- start<MessageBody extends Record<string, any> = Record<string, any>>(options: {
49
- queueName: string;
50
- workFn: QueueWorkFunction<MessageBody>;
51
- concurrency?: number;
52
- }): void;
53
- stop(options: {
54
- queueName: string;
55
- }): void;
56
- listMessages<MessageBody extends Record<string, any> = Record<string, any>, Result = any>(options: {
57
- pageSize: number;
58
- pageNumber: number;
59
- queueName: string;
60
- }): Promise<QueueMessage<MessageBody, Result>[]>;
61
- addMessage<MessageBody extends Record<string, any> = Record<string, any>>(options: {
62
- body: MessageBody;
63
- scheduledDate: Iso.Instant | null;
64
- queueName: string;
65
- traceData: DehydratedTrace | null;
66
- }): Promise<{
67
- id: number;
68
- }>;
69
- updateMessage<MessageBody extends Record<string, any> = Record<string, any>>(options: {
70
- queueName: string;
71
- messageId: number;
11
+ size(): Promise<number>;
12
+ push(message: {
13
+ queue: string;
14
+ body: Record<string, any>;
72
15
  scheduledDate: Iso.Instant;
73
- body: MessageBody;
16
+ }): Promise<string>;
17
+ bulk(messages: {
18
+ queue: string;
19
+ body: Record<string, any>;
20
+ scheduledDate: Iso.Instant;
21
+ }[]): Promise<void>;
22
+ pop(): Promise<QueueMessage | null>;
23
+ fail(options: {
24
+ id: string;
25
+ attempts: number;
26
+ error: Error;
27
+ }): Promise<void>;
28
+ success(options: {
29
+ id: string;
30
+ attempts: number;
31
+ result: any;
32
+ }): Promise<void>;
33
+ release(options: {
34
+ id: string;
35
+ attempts: number;
36
+ error: Error;
37
+ delaySeconds: number;
74
38
  }): Promise<void>;
75
- isQueueEmpty(options: {
76
- queueName: string;
77
- }): Promise<boolean>;
78
39
  }
40
+
41
+ interface DbQueueDriverConfig {
42
+ db: {
43
+ update(query: string, params?: any[]): Promise<{
44
+ changedRows: number;
45
+ }>;
46
+ select(query: string, params?: any[]): Promise<Array<Record<string, any>>>;
47
+ insert(query: string, params?: any[]): Promise<{
48
+ insertId: number;
49
+ }>;
50
+ };
51
+ databaseTable: string;
52
+ queue?: string | string[];
53
+ batchingMaxWaitSeconds?: number;
54
+ }
55
+ declare function DbQueueDriver({ db, databaseTable, queue: queueFilter, batchingMaxWaitSeconds }: DbQueueDriverConfig): QueueDriver;
56
+
57
+ interface LocalQueueDriverConfig {
58
+ queue?: string | string[];
59
+ }
60
+ declare function localQueueDriver({ queue: queueFilter }?: LocalQueueDriverConfig): QueueDriver;
61
+
62
+ declare function createService({ driver, logger: serviceLogger }: Queue.Provider.QueueServiceConfig): Queue.QueueService;
79
63
  type QueueLogMessage = {
80
64
  event: 'messageEnqueued';
81
65
  queue: string;
82
- driver: QueueDriver;
83
- scheduledDate: Iso.Instant | null;
84
66
  body: Record<string, any>;
85
- messageId: number;
86
67
  } | {
87
68
  event: 'messageDequeued';
88
69
  queue: string;
89
- driver: QueueDriver;
90
70
  body: Record<string, any>;
91
- messageId: number;
71
+ messageId: string;
92
72
  } | {
93
73
  event: 'messageProcessed';
94
74
  queue: string;
95
- driver: QueueDriver;
96
75
  body: Record<string, any>;
97
- result: unknown;
98
- messageId: number;
76
+ messageId: string;
77
+ result: any;
99
78
  } | {
100
79
  event: 'messageFailed';
101
80
  queue: string;
102
- driver: QueueDriver;
103
81
  body: Record<string, any>;
82
+ messageId: string;
104
83
  error: unknown;
105
- messageId: number;
106
84
  } | {
107
- event: 'queueStarted';
85
+ event: 'messageReleased';
108
86
  queue: string;
109
- driver: QueueDriver;
110
- } | {
111
- event: 'queueStopped';
112
- queue: string;
113
- driver: QueueDriver;
87
+ body: Record<string, any>;
88
+ messageId: string;
89
+ error: unknown;
90
+ attempt: number;
114
91
  };
115
-
116
- interface DbQueueDriverConfig {
117
- db: {
118
- update(query: string, params?: any[]): Promise<{
119
- changedRows: number;
120
- }>;
121
- select(query: string, params?: any[]): Promise<Array<Record<string, any>>>;
122
- insert(query: string, params?: any[]): Promise<{
123
- insertId: number;
124
- }>;
125
- };
126
- databaseTable: string;
127
- extraFields?: string[];
128
- fastestPollingRateMs?: number;
129
- slowestPollingRateMs?: number;
130
- pollingBackoffRate?: number;
92
+ type QueueMiddleware<MessageBody extends Record<string, any> = Record<string, any>, Result = any> = (message: MessageBody, next: {
93
+ next: () => Promise<Result>;
94
+ } & QueueActions) => Promise<Result>;
95
+ interface QueueActions {
96
+ fail(error: Error): never;
97
+ release(error: Error, delaySeconds?: number): never;
131
98
  }
132
- declare function DbQueueDriver({ db, databaseTable, extraFields, fastestPollingRateMs, slowestPollingRateMs, pollingBackoffRate }: DbQueueDriverConfig): QueueDriver;
133
-
134
- declare function createService(config: Queue.Provider.QueueServiceConfig): {
135
- list: () => string[];
136
- get: <MessageBody extends Record<string, any>, Result = any>(name: string) => Queue.Queue<MessageBody, Result> | undefined;
137
- create: <MessageBody extends Record<string, any>, Result = any>(options: Queue.QueueOptions<MessageBody, Result>) => Queue.Queue<MessageBody, Result>;
138
- startProcessing: () => void;
139
- stopProcessing: () => void;
140
- };
99
+ type KeysWithFallback = keyof Queue.Provider.Keys extends never ? {
100
+ default: unknown;
101
+ } : Queue.Provider.Keys;
141
102
  declare function provider(key: Queue.Provider.Key): {
142
- list: () => string[];
143
- get: <MessageBody extends Record<string, any>, Result = any>(name: string) => Queue.Queue<MessageBody, Result> | undefined;
144
- create: <MessageBody extends Record<string, any>, Result = any>(options: Queue.QueueOptions<MessageBody, Result>) => Queue.Queue<MessageBody, Result>;
145
- startProcessing: () => void;
146
- stopProcessing: () => void;
103
+ work: (options: Queue.WorkOptions) => Promise<void> & {
104
+ abort(reason?: string | undefined): void;
105
+ };
106
+ stopAllWork: () => Promise<void>;
107
+ processMessage: <MessageBody extends Record<string, any> = Record<string, any>>(queueMessage: Queue.Message<MessageBody>) => Promise<unknown>;
108
+ create: <MessageBody extends Record<string, any> = {}, Result = any>(config: Queue.Config<MessageBody, Result>) => Queue.QueueHandle<MessageBody, Result>;
147
109
  };
148
110
  /**
149
- * Job queue for deferring work beyond the current request.
111
+ * Driver-backed job queue with middleware, retries, and configurable backoff.
150
112
  *
151
- * Messages are stored by a pluggable driver and processed asynchronously by a
152
- * worker either in the same process or a dedicated consumer. Each message
153
- * tracks its full lifecycle (waiting processing success/error/stuck) and
154
- * supports scheduled future execution, concurrent processing, and distributed
155
- * trace propagation across the producer/consumer boundary.
113
+ * Register a service with `Queue.Provider.register('default', Queue.Provider.create({ driver }))`,
114
+ * then define individual queues with `Queue.create({ name, handle })`. Messages can be dispatched
115
+ * immediately, with a delay, or synchronously. Handlers receive `fail` and `release` callbacks
116
+ * to permanently fail a message or release it for retry with backoff.
156
117
  *
157
118
  * @example
158
119
  * ```ts
159
- * // Create and register a queue service
160
120
  * Queue.Provider.register('default', Queue.Provider.create({
161
- * driver: Queue.drivers.db({ db: Db.provider('default'), databaseTable: 'jobs' }),
121
+ * driver: myDriver,
162
122
  * logger: Log.provider('default')
163
123
  * }))
164
124
  *
165
- * // Create a queue with a work function
166
- * const orderQueue = Queue.create({
167
- * name: 'fulfill-orders',
168
- * workFn: async (body) => {
169
- * await fulfillOrder(body.orderId)
170
- * return { fulfilled: true }
171
- * }
125
+ * const emailQueue = Queue.create({
126
+ * name: 'send-emails',
127
+ * handle: async (body) => sendEmail(body.to, body.subject),
128
+ * tries: 3,
129
+ * backoffSeconds: [30, 60, 120]
172
130
  * })
173
131
  *
174
- * // Enqueue a message and start processing
175
- * await orderQueue.addMessage({ orderId: 'ord_123' }, { scheduledDate: null })
176
- * orderQueue.startProcessing()
132
+ * await emailQueue.dispatch({ to: 'alice@example.com', subject: 'Welcome' })
133
+ * await Queue.work({ stopWhenEmpty: true })
177
134
  * ```
178
135
  */
179
136
  declare namespace Queue {
137
+ /** Union of all queue lifecycle log event shapes */
138
+ type LogMessage = QueueLogMessage;
139
+ /** Middleware function for wrapping queue message handling */
140
+ type Middleware<MessageBody extends Record<string, any> = Record<string, any>, Result = any> = QueueMiddleware<MessageBody, Result>;
141
+ /** A single message popped from the queue */
142
+ type Message<MessageBody extends Record<string, any> = Record<string, any>> = QueueMessage<MessageBody>;
143
+ /** Pluggable storage backend — see driver implementations for built-in options */
144
+ type Driver = QueueDriver;
145
+ /** Callbacks passed to handlers and middleware for explicit message control */
146
+ type Actions = QueueActions;
180
147
  /** Per-queue configuration passed to {@link QueueService.create} */
181
- interface QueueOptions<T extends Record<string, any> = Record<string, any>, Result = any> {
148
+ interface Config<MessageBody extends Record<string, any> = Record<string, any>, Result = any> {
182
149
  name: string;
183
- workFn(body: T): Promise<Result>;
184
- logger?: Log.LogFunctions<[QueueLogMessage]>;
150
+ handle(params: MessageBody, actions: Actions): Promise<Result>;
151
+ middleware?: Middleware<MessageBody, Result>[];
152
+ tries?: number;
153
+ backoffSeconds?: number | number[];
154
+ logger?: Log.LogFunctions<[LogMessage]>;
155
+ }
156
+ /** Options for controlling the work polling loop */
157
+ interface WorkOptions {
158
+ maxJobs?: number;
159
+ stopWhenEmpty?: boolean;
160
+ maxTimeSeconds?: number;
161
+ sleepSeconds?: number;
185
162
  concurrency?: number;
186
163
  }
187
- /** Pluggable storage backend that handles message persistence, polling, and locking */
188
- type Driver = QueueDriver;
189
- /** A single message stored in the queue with its full lifecycle metadata */
190
- type QueueMessage<MessageBody extends Record<string, any> = Record<string, any>, Result = any> = QueueMessage<MessageBody, Result>;
191
- /** Union of all queue lifecycle log event shapes */
192
- type LogMessage = QueueLogMessage;
193
- /** Distributed tracing adapter for propagating trace context across producer/consumer boundaries */
194
- type Tracing = QueueTracing;
195
164
  /**
196
- * A named queue instance with methods for adding messages and controlling processing.
165
+ * A named queue instance returned by {@link QueueService.create}.
197
166
  *
198
- * Each queue has its own work function, concurrency setting, and independent
199
- * start/stop lifecycle. Messages are typed via the `MessageBody` and `Result`
200
- * generics the body is what the producer enqueues, the result is what the
201
- * work function returns on success.
167
+ * Each handle is bound to a single queue name and provides methods to enqueue
168
+ * messages immediately, with a delay, synchronously (bypassing the driver), or
169
+ * in bulk. Dispatched messages are picked up by {@link QueueService.work}.
202
170
  *
203
171
  * @example
204
172
  * ```ts
205
- * const queue = Queue.create({
173
+ * const emailQueue = Queue.create({
206
174
  * name: 'send-emails',
207
- * workFn: async (body) => sendEmail(body.to, body.subject),
208
- * concurrency: 3
175
+ * handle: async (body) => sendEmail(body.to, body.subject),
176
+ * tries: 3,
177
+ * backoffSeconds: [30, 60, 120]
209
178
  * })
210
- * await queue.addMessage({ to: 'alice@example.com', subject: 'Welcome' }, { scheduledDate: null })
211
- * queue.startProcessing()
179
+ *
180
+ * await emailQueue.dispatch({ to: 'alice@example.com', subject: 'Welcome' })
181
+ * await emailQueue.dispatchWithDelay(body, scheduledDate)
182
+ * await emailQueue.bulk([{ body: msg1 }, { body: msg2 }])
212
183
  * ```
213
184
  */
214
- type Queue<MessageBody extends Record<string, any> = Record<string, any>, Result = any> = {
215
- /** Starts polling for and processing messages on this queue */
216
- startProcessing(): void;
217
- /** Starts processing and resolves when no waiting or in-progress messages remain */
218
- processUntilEmpty(options?: {
219
- pollingMs?: number;
220
- }): Promise<void>;
221
- /** Stops polling for new messages on this queue */
222
- stopProcessing(): void;
223
- name: string;
224
- /** Returns a paginated list of messages in this queue */
225
- listMessages(options: {
226
- pageSize: number;
227
- pageNumber: number;
228
- }): Promise<QueueMessage<MessageBody, Result>[]>;
229
- /** Adds a message to this queue for immediate or scheduled processing */
230
- addMessage(body: MessageBody, options: {
231
- scheduledDate: Iso.Instant | null;
232
- tracingContext?: DehydratedTrace | null;
233
- }): Promise<void>;
234
- /** Updates the body and scheduled date of a waiting message */
235
- updateMessage(options: {
236
- messageId: number;
237
- scheduledDate: Iso.Instant;
185
+ interface QueueHandle<MessageBody extends Record<string, any> = Record<string, any>, Result = any> {
186
+ /** Dispatches a message onto the queue for immediate processing */
187
+ dispatch(...params: {} extends MessageBody ? [] | [MessageBody] : [MessageBody]): Promise<void>;
188
+ /** Dispatches a message onto the queue with a scheduled processing date */
189
+ dispatchWithDelay(...params: {} extends MessageBody ? [scheduledDate: Iso.Instant] | [message: MessageBody, scheduledDate: Iso.Instant] : [message: MessageBody, scheduledDate: Iso.Instant]): Promise<void>;
190
+ /** Executes the handler synchronously without going through the driver — bypasses the driver entirely, useful for testing */
191
+ dispatchSync(...params: {} extends MessageBody ? [] | [MessageBody] : [MessageBody]): Promise<Result>;
192
+ /** Dispatches multiple messages in a single bulk operation */
193
+ bulk(messages: {
238
194
  body: MessageBody;
239
- }): Promise<void>;
240
- };
241
- namespace Provider {
242
- interface QueueServiceConfig {
243
- driver: Driver;
244
- logger: Log.Logger<[QueueLogMessage]>;
245
- tracing?: QueueTracing;
246
- }
247
- type Key = keyof Keys;
248
- interface Keys {
249
- default: unknown;
250
- }
195
+ scheduledDate?: Iso.Instant;
196
+ }[]): Promise<void>;
251
197
  }
252
198
  /**
253
199
  * Container that manages multiple named queues sharing the same driver and logger.
254
200
  *
255
- * Create individual queues with {@link create}, then start or stop processing
256
- * across all queues at once or individually. Each queue has its own work function,
257
- * concurrency, and message lifecycle.
201
+ * Returned by `Queue.Provider.create()` or resolved via `Queue.provider()`.
202
+ * Use {@link QueueService.create create} to define queues, then call
203
+ * {@link QueueService.work work} to start polling for and processing messages.
204
+ * Handlers receive `fail` and `release` callbacks to permanently fail a message
205
+ * or release it for retry with backoff.
258
206
  *
259
207
  * @example
260
208
  * ```ts
261
- * const orders = Queue.create({ name: 'orders', workFn: processOrder })
262
- * const emails = Queue.create({ name: 'emails', workFn: sendEmail, concurrency: 5 })
263
- * Queue.startProcessing() // starts both queues
209
+ * const service = Queue.Provider.create({
210
+ * driver: Queue.drivers.db({ db, databaseTable: 'queue' }),
211
+ * logger: Log.provider('default')
212
+ * })
213
+ * Queue.Provider.register('default', service)
214
+ *
215
+ * const q = Queue.create({ name: 'emails', handle: async (body) => send(body) })
216
+ * await q.dispatch({ to: 'alice@example.com' })
217
+ * await Queue.work({ stopWhenEmpty: true })
264
218
  * ```
265
219
  */
266
220
  interface QueueService {
267
- /** Returns the names of all queues registered with this service */
268
- list(): string[];
269
- /** Returns the queue with the given name, or `undefined` if not found */
270
- get<MessageBody extends Record<string, any>, Result = any>(name: string): Queue.Queue<MessageBody, Result> | undefined;
271
- /** Creates a new named queue with the given work function and options */
272
- create<MessageBody extends Record<string, any>, Result = any>(options: Queue.QueueOptions<MessageBody, Result>): Queue<MessageBody, Result>;
273
- /** Stops processing messages on all queues */
274
- stopProcessing(): void;
275
- /** Starts processing messages on all queues */
276
- startProcessing(): void;
221
+ /** Starts polling for and processing messages returns a promise with an `abort()` method to stop the loop */
222
+ work(options: WorkOptions): Promise<void> & {
223
+ abort(reason?: string | undefined): void;
224
+ };
225
+ /** Aborts all active work loops and waits for in-flight messages to finish */
226
+ stopAllWork(): Promise<void>;
227
+ /** Processes a single message directly looks up the queue handler by `queueMessage.queue` */
228
+ processMessage<MessageBody extends Record<string, any> = Record<string, any>>(queueMessage: Message<MessageBody>): Promise<unknown>;
229
+ /** Creates a new named queue with the given handler, retry, and middleware options */
230
+ create<MessageBody extends Record<string, any> = {}, Result = any>(config: Config<MessageBody, Result>): QueueHandle<MessageBody, Result>;
231
+ }
232
+ namespace Provider {
233
+ interface QueueServiceConfig {
234
+ driver: Driver;
235
+ logger?: Log.Logger<[LogMessage]>;
236
+ }
237
+ type Key = keyof KeysWithFallback;
238
+ interface Keys {
239
+ }
277
240
  }
278
241
  }
279
242
  /**
280
- * Job queue for deferring work beyond the current request.
243
+ * Driver-backed job queue with middleware, retries, and configurable backoff.
281
244
  * Call `Queue.Provider.create()` with a driver, register it, then use `Queue.create()` to define queues.
282
245
  */
283
246
  declare const Queue: {
284
- drivers: {
285
- db: typeof DbQueueDriver;
286
- };
287
247
  Provider: {
288
248
  create: typeof createService;
289
249
  register: (name: Queue.Provider.Key, item: Queue.QueueService) => void;
290
- list: () => "default"[];
291
250
  };
292
251
  provider: typeof provider;
293
- list: () => string[];
294
- get: <MessageBody extends Record<string, any>, Result = any>(name: string) => Queue.Queue<MessageBody, Result> | undefined;
295
- create: <MessageBody extends Record<string, any>, Result = any>(options: Queue.QueueOptions<MessageBody, Result>) => Queue.Queue<MessageBody, Result>;
296
- startProcessing: () => void;
297
- stopProcessing: () => void;
252
+ work: (options: Queue.WorkOptions) => Promise<void> & {
253
+ abort(reason?: string | undefined): void;
254
+ };
255
+ stopAllWork: () => Promise<void>;
256
+ processMessage: <MessageBody extends Record<string, any> = Record<string, any>>(queueMessage: Queue.Message<MessageBody>) => Promise<unknown>;
257
+ create: <MessageBody extends Record<string, any> = {}, Result = any>(config: Queue.Config<MessageBody, Result>) => Queue.QueueHandle<MessageBody, Result>;
258
+ drivers: {
259
+ db: typeof DbQueueDriver;
260
+ local: typeof localQueueDriver;
261
+ };
298
262
  };
299
263
 
300
264
  export { Queue };