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

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 +179 -337
  2. package/dist/index.d.ts +215 -179
  3. package/dist/index.js +475 -422
  4. package/package.json +6 -10
package/dist/index.d.ts CHANGED
@@ -1,264 +1,300 @@
1
1
  import { Iso } from 'iso-fns2';
2
2
  import { Log } from '@maestro-js/log';
3
3
 
4
- interface QueueMessage<MessageBody extends Record<string, any> = Record<string, any>> {
5
- id: string;
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;
6
28
  queue: string;
29
+ scheduledDate: Iso.Instant;
30
+ enqueuedDate: Iso.Instant;
31
+ startedDate: Iso.Instant | null;
7
32
  body: MessageBody;
8
- attempts: number;
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;
9
40
  }
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;
10
47
  interface QueueDriver {
11
- size(): Promise<number>;
12
- push(message: {
13
- queue: string;
14
- body: Record<string, any>;
15
- scheduledDate: Iso.Instant;
16
- }): Promise<string>;
17
- bulk(messages: {
18
- queue: string;
19
- body: Record<string, any>;
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;
20
72
  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;
73
+ body: MessageBody;
38
74
  }): Promise<void>;
75
+ isQueueEmpty(options: {
76
+ queueName: string;
77
+ }): Promise<boolean>;
39
78
  }
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;
63
79
  type QueueLogMessage = {
64
80
  event: 'messageEnqueued';
65
81
  queue: string;
82
+ driver: QueueDriver;
83
+ scheduledDate: Iso.Instant | null;
66
84
  body: Record<string, any>;
85
+ messageId: number;
67
86
  } | {
68
87
  event: 'messageDequeued';
69
88
  queue: string;
89
+ driver: QueueDriver;
70
90
  body: Record<string, any>;
71
- messageId: string;
91
+ messageId: number;
72
92
  } | {
73
93
  event: 'messageProcessed';
74
94
  queue: string;
95
+ driver: QueueDriver;
75
96
  body: Record<string, any>;
76
- messageId: string;
77
- result: any;
97
+ result: unknown;
98
+ messageId: number;
78
99
  } | {
79
100
  event: 'messageFailed';
80
101
  queue: string;
102
+ driver: QueueDriver;
81
103
  body: Record<string, any>;
82
- messageId: string;
83
104
  error: unknown;
105
+ messageId: number;
84
106
  } | {
85
- event: 'messageReleased';
107
+ event: 'queueStarted';
86
108
  queue: string;
87
- body: Record<string, any>;
88
- messageId: string;
89
- error: unknown;
90
- attempt: number;
109
+ driver: QueueDriver;
110
+ } | {
111
+ event: 'queueStopped';
112
+ queue: string;
113
+ driver: QueueDriver;
91
114
  };
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;
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;
98
131
  }
99
- type KeysWithFallback = keyof Queue.Provider.Keys extends never ? {
100
- default: unknown;
101
- } : Queue.Provider.Keys;
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
+ };
102
141
  declare function provider(key: Queue.Provider.Key): {
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>;
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;
109
147
  };
110
148
  /**
111
- * Driver-backed job queue with middleware, retries, and configurable backoff.
149
+ * Job queue for deferring work beyond the current request.
112
150
  *
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.
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.
117
156
  *
118
157
  * @example
119
158
  * ```ts
159
+ * // Create and register a queue service
120
160
  * Queue.Provider.register('default', Queue.Provider.create({
121
- * driver: myDriver,
161
+ * driver: Queue.drivers.db({ db: Db.provider('default'), databaseTable: 'jobs' }),
122
162
  * logger: Log.provider('default')
123
163
  * }))
124
164
  *
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]
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
+ * }
130
172
  * })
131
173
  *
132
- * await emailQueue.dispatch({ to: 'alice@example.com', subject: 'Welcome' })
133
- * await Queue.work({ stopWhenEmpty: true })
174
+ * // Enqueue a message and start processing
175
+ * await orderQueue.addMessage({ orderId: 'ord_123' }, { scheduledDate: null })
176
+ * orderQueue.startProcessing()
134
177
  * ```
135
178
  */
136
179
  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;
147
180
  /** Per-queue configuration passed to {@link QueueService.create} */
148
- interface Config<MessageBody extends Record<string, any> = Record<string, any>, Result = any> {
181
+ interface QueueOptions<T extends Record<string, any> = Record<string, any>, Result = any> {
149
182
  name: string;
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;
183
+ workFn(body: T): Promise<Result>;
184
+ logger?: Log.LogFunctions<[QueueLogMessage]>;
162
185
  concurrency?: number;
163
186
  }
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;
164
195
  /**
165
- * A named queue instance returned by {@link QueueService.create}.
196
+ * A named queue instance with methods for adding messages and controlling processing.
166
197
  *
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}.
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.
170
202
  *
171
203
  * @example
172
204
  * ```ts
173
- * const emailQueue = Queue.create({
205
+ * const queue = Queue.create({
174
206
  * name: 'send-emails',
175
- * handle: async (body) => sendEmail(body.to, body.subject),
176
- * tries: 3,
177
- * backoffSeconds: [30, 60, 120]
207
+ * workFn: async (body) => sendEmail(body.to, body.subject),
208
+ * concurrency: 3
178
209
  * })
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 }])
210
+ * await queue.addMessage({ to: 'alice@example.com', subject: 'Welcome' }, { scheduledDate: null })
211
+ * queue.startProcessing()
183
212
  * ```
184
213
  */
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: {
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;
194
238
  body: MessageBody;
195
- scheduledDate?: Iso.Instant;
196
- }[]): Promise<void>;
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
+ }
197
251
  }
198
252
  /**
199
253
  * Container that manages multiple named queues sharing the same driver and logger.
200
254
  *
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.
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.
206
258
  *
207
259
  * @example
208
260
  * ```ts
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 })
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
218
264
  * ```
219
265
  */
220
266
  interface QueueService {
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
- }
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;
240
277
  }
241
278
  }
242
279
  /**
243
- * Driver-backed job queue with middleware, retries, and configurable backoff.
280
+ * Job queue for deferring work beyond the current request.
244
281
  * Call `Queue.Provider.create()` with a driver, register it, then use `Queue.create()` to define queues.
245
282
  */
246
283
  declare const Queue: {
284
+ drivers: {
285
+ db: typeof DbQueueDriver;
286
+ };
247
287
  Provider: {
248
288
  create: typeof createService;
249
289
  register: (name: Queue.Provider.Key, item: Queue.QueueService) => void;
290
+ list: () => "default"[];
250
291
  };
251
292
  provider: typeof provider;
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
- };
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;
262
298
  };
263
299
 
264
300
  export { Queue };