@maestro-js/queue 1.0.0-alpha.19 → 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/README.md CHANGED
@@ -1,437 +1,279 @@
1
1
  # @maestro-js/queue
2
2
 
3
- Database-backed job queue with middleware, configurable retries, and backoff strategies.
3
+ Database-backed job queue that stores messages in MySQL via a pluggable driver and processes them asynchronously. Follows the Provider pattern from `@maestro-js/service-registry` with a producer/consumer split supporting scheduled execution, concurrency, and distributed trace propagation.
4
4
 
5
5
  ## Quick Setup
6
6
 
7
7
  ```ts
8
8
  import { Queue } from '@maestro-js/queue'
9
9
  import { Db } from '@maestro-js/db'
10
+ import { Log } from '@maestro-js/log'
11
+
12
+ // Register the queue service with the db driver
13
+ Queue.Provider.register(
14
+ 'default',
15
+ Queue.Provider.create({
16
+ driver: Queue.drivers.db({
17
+ db: Db.provider('default'),
18
+ databaseTable: 'jobs'
19
+ }),
20
+ logger: Log
21
+ })
22
+ )
10
23
 
11
- // Create and register a queue service with the db driver
12
- const service = Queue.Provider.create({
13
- driver: Queue.drivers.db({ db: Db.provider('default'), databaseTable: 'queue' }),
14
- logger: Log.provider('default')
24
+ // Create a named queue with a work function
25
+ const orderQueue = Queue.create({
26
+ name: 'fulfill-orders',
27
+ workFn: async (body) => {
28
+ await fulfillOrder(body.orderId)
29
+ return { fulfilled: true }
30
+ },
31
+ concurrency: 3
15
32
  })
16
- Queue.Provider.register('default', service)
17
33
 
18
- // Define a queue with a handler
19
- const emailQueue = Queue.create({
20
- name: 'send-emails',
21
- handle: async (body) => sendEmail(body.to, body.subject),
22
- tries: 3,
23
- backoffSeconds: [30, 60, 120]
24
- })
34
+ // Enqueue a message
35
+ await orderQueue.addMessage({ orderId: 'ord_123' }, { scheduledDate: null })
25
36
 
26
- // Dispatch a message and start processing
27
- await emailQueue.dispatch({ to: 'alice@example.com', subject: 'Welcome' })
28
- await Queue.work({ stopWhenEmpty: true })
37
+ // Start processing
38
+ orderQueue.startProcessing()
29
39
  ```
30
40
 
31
- ## Provider Pattern
32
-
33
- Queue follows the standard Maestro Provider pattern built on `@maestro-js/service-registry`:
41
+ ## Database Schema
42
+
43
+ The db driver expects a MySQL table (name configurable via `databaseTable`). The column-to-field mapping used internally:
44
+
45
+ | Column | Type | Description |
46
+ | --------------- | ------------- | -------------------------------------------------------- |
47
+ | `id` | int (PK, AI) | Message ID |
48
+ | `batch_id` | int / NULL | Lock batch for concurrent dequeue |
49
+ | `job_type` | varchar | Queue name string |
50
+ | `body` | text (JSON) | Serialized message body |
51
+ | `trace` | text (JSON) | Serialized `DehydratedTrace` for distributed tracing |
52
+ | `status` | enum | `waiting`, `processing`, `success`, `error`, `stuck` |
53
+ | `scheduledDate` | datetime | Earliest time the message can be processed |
54
+ | `enqueuedDate` | datetime | When the message was inserted |
55
+ | `started_ts` | datetime | When processing began |
56
+ | `result` | text | JSON result on success, error stack on failure |
57
+ | `recovered` | tinyint | 1 if the message was recovered from stuck state |
58
+ | `running_time` | int / NULL | Seconds elapsed during processing |
59
+ | `unique_key` | int / NULL | MD5-derived dedup key (nullable) |
60
+ | `priority` | bigint / NULL | Lower value = higher priority (defaults to `Date.now()`) |
61
+
62
+ The driver requires a `FORCE INDEX (locking_update)` index on `(batch_id, status, job_type, scheduledDate)` for efficient locking queries.
34
63
 
35
- 1. **`Queue.Provider.create(config)`** -- factory that instantiates a queue service with a driver
36
- 2. **`Queue.Provider.register(name, service)`** -- register the instance in a named registry
37
- 3. **`Queue.provider(key)`** -- resolve a named instance (deferred via Proxy)
38
- 4. **Facade** -- `Queue.create()`, `Queue.work()`, etc. spread from `provider('default')`
39
- 5. **`Queue.drivers`** -- factory functions for pluggable backends (`db`, `local`)
64
+ ## API Reference
40
65
 
41
- Extend provider keys via declaration merging:
66
+ ### Provider Setup
42
67
 
43
68
  ```ts
44
- declare module '@maestro-js/queue' {
45
- namespace Queue.Provider {
46
- interface Keys {
47
- default: unknown
48
- highPriority: unknown
49
- }
50
- }
51
- }
69
+ Queue.Provider.create(config: {
70
+ driver: Queue.Driver
71
+ logger: Log.Logger<[Queue.LogMessage]>
72
+ tracing?: Queue.Tracing // optional distributed tracing adapter
73
+ }): Queue.QueueService
52
74
  ```
53
75
 
54
- Source: `packages/queue/src/index.ts`
55
-
56
- ## Drivers
57
-
58
- ### db (MySQL)
59
-
60
- Production driver backed by MySQL. Uses optimistic locking with `UPDATE ... INNER JOIN` for
61
- concurrent pop operations.
62
-
63
76
  ```ts
64
- Queue.drivers.db({
65
- db: Db.provider('default'), // must implement select/insert/update
66
- databaseTable: 'queue', // table name (alphanumeric + underscores only)
67
- queue: 'send-emails', // optional: filter to specific queue name(s)
68
- batchingMaxWaitSeconds: 0.5 // optional: batch pop requests over this window
69
- })
70
- ```
71
-
72
- Config interface:
73
-
74
- | Field | Type | Default | Description |
75
- |--------------------------|----------------------|---------|------------------------------------------------|
76
- | `db` | `{ select, insert, update }` | -- | Database connection with query methods |
77
- | `databaseTable` | `string` | -- | MySQL table name |
78
- | `queue` | `string \| string[]` | -- | Optional queue name filter |
79
- | `batchingMaxWaitSeconds` | `number` | `0` | Batch pop requests over this window (seconds) |
80
-
81
- Required database schema:
82
-
83
- ```sql
84
- CREATE TABLE queue (
85
- id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
86
- attempt INT UNSIGNED NOT NULL DEFAULT 0,
87
- queue VARCHAR(255) NOT NULL,
88
- status ENUM('waiting', 'processing', 'succeeded', 'failed', 'failedPermanently') NOT NULL DEFAULT 'waiting',
89
- scheduledDate DATETIME NOT NULL,
90
- enqueuedDate DATETIME NOT NULL DEFAULT NOW(),
91
- startedDate DATETIME,
92
- completedDate DATETIME,
93
- body JSON,
94
- result TEXT,
95
- lockId BIGINT UNSIGNED DEFAULT NULL,
96
- PRIMARY KEY (id, attempt),
97
- INDEX locking_pop (lockId, status, scheduledDate),
98
- INDEX queue (queue)
99
- );
77
+ Queue.Provider.register(name: Queue.Provider.Key, service: Queue.QueueService): void
100
78
  ```
101
79
 
102
- Source: `packages/queue/src/db-queue-driver.ts`
103
-
104
- ### local (In-Memory)
105
-
106
- In-memory driver for testing. Same interface as db but stores messages in a plain array:
80
+ ### Drivers
107
81
 
108
82
  ```ts
109
- Queue.drivers.local({
110
- queue: 'send-emails' // optional: filter to specific queue name(s)
111
- })
83
+ Queue.drivers.db(config: {
84
+ db: { update, select, insert } // Db provider interface
85
+ databaseTable: string
86
+ extraFields?: string[] // additional columns to insert
87
+ fastestPollingRateMs?: number // default 100
88
+ slowestPollingRateMs?: number // default 10_000
89
+ pollingBackoffRate?: number // default 1.1
90
+ }): Queue.Driver
112
91
  ```
113
92
 
114
- Source: `packages/queue/src/local-queue-driver.ts`
115
-
116
- ## Core Concepts
117
-
118
- ### Retry and Backoff
93
+ ### Queue Instance Methods
119
94
 
120
- Configure retries per queue with `tries` and `backoffSeconds`:
95
+ Create a queue:
121
96
 
122
97
  ```ts
123
- const q = Queue.create({
124
- name: 'flaky-api',
125
- handle: async (body) => callExternalApi(body),
126
- tries: 4, // total attempts (1 initial + 3 retries)
127
- backoffSeconds: [10, 30, 120] // delay before retry 1, 2, 3
98
+ const queue = Queue.create<MessageBody, Result>({
99
+ name: string,
100
+ workFn: (body: MessageBody) => Promise<Result>,
101
+ logger?: Log.LogFunctions<[Queue.LogMessage]>, // additional per-queue logger
102
+ concurrency?: number // default 1
128
103
  })
129
104
  ```
130
105
 
131
- - `tries` defaults to `1` (no retries)
132
- - `backoffSeconds` can be a single `number` (constant delay) or an `number[]` (per-attempt delay)
133
- - If the array is shorter than the number of retries, the last value is reused
134
- - If omitted entirely, defaults to `60` seconds between retries
135
- - `attempts` on the message is 0-indexed -- the first attempt is `0`, the first retry is `1`
136
-
137
- ### Middleware
138
-
139
- Wrap message handling with middleware functions for cross-cutting concerns:
106
+ Dispatch a message:
140
107
 
141
108
  ```ts
142
- const loggingMiddleware: Queue.Middleware = async (message, next) => {
143
- console.log('Processing:', message)
144
- const result = await next()
145
- console.log('Done:', result)
146
- return result
147
- }
148
-
149
- const q = Queue.create({
150
- name: 'with-logging',
151
- handle: async (body) => processBody(body),
152
- middleware: [loggingMiddleware]
109
+ await queue.addMessage(body, {
110
+ scheduledDate: Iso.Instant | null, // null = process immediately
111
+ tracingContext?: DehydratedTrace | null
153
112
  })
154
113
  ```
155
114
 
156
- Middleware is composed right-to-left (`reduceRight`), so the first middleware in the array is the
157
- outermost wrapper.
158
-
159
- ### Fail and Release
160
-
161
- Handlers receive `fail` and `release` callbacks as a second argument for explicit message control:
115
+ Update a waiting message:
162
116
 
163
117
  ```ts
164
- const q = Queue.create({
165
- name: 'validate-first',
166
- tries: 3,
167
- handle: async (body, { fail, release }) => {
168
- if (!body.email) {
169
- fail(new Error('Missing email -- will never succeed')) // permanently fail, skip retries
170
- }
171
- const result = await sendEmail(body.email)
172
- if (result.rateLimited) {
173
- release(new Error('Rate limited'), 300) // retry in 5 minutes
174
- }
175
- }
118
+ await queue.updateMessage({
119
+ messageId: number,
120
+ scheduledDate: Iso.Instant,
121
+ body: MessageBody
176
122
  })
177
123
  ```
178
124
 
179
- - **`fail(error)`** -- permanently fails the message, regardless of remaining retries
180
- - **`release(error, delaySeconds?)`** -- releases the message for retry. Pass `delaySeconds` to override the configured backoff, or omit it to use the default.
181
-
182
- Both return `never` -- they halt handler execution at the call site.
183
-
184
- Any other thrown error also triggers a retry (or permanent failure on the last attempt), using the
185
- configured `backoffSeconds`. Middleware also receives these callbacks as a third argument.
186
-
187
- ### Work Loop
188
-
189
- `Queue.work()` starts a polling loop that pops messages from the driver and dispatches them to the
190
- appropriate handler:
125
+ List messages (paginated):
191
126
 
192
127
  ```ts
193
- const workHandle = Queue.work({
194
- maxJobs: 100, // stop after processing 100 messages
195
- stopWhenEmpty: false, // keep polling even when queue is empty
196
- maxTimeSeconds: 3600, // stop after 1 hour
197
- sleepSeconds: 1, // pause between polls when idle (default 1)
198
- concurrency: 4 // number of parallel polling workers
199
- })
200
-
201
- // Later: gracefully stop
202
- workHandle.abort('shutting down')
203
-
204
- // Or stop all active work loops
205
- await Queue.stopAllWork()
128
+ const messages = await queue.listMessages({ pageSize: 50, pageNumber: 0 })
206
129
  ```
207
130
 
208
- The work loop swallows handler errors -- failed messages are logged and released/failed via the
209
- driver, but the loop continues.
210
-
211
- ## API Reference
212
-
213
- ### QueueService Methods
131
+ Processing control:
214
132
 
215
133
  ```ts
216
- Queue.Provider.create(config: { driver: Queue.Driver, logger?: Log.Logger }): Queue.QueueService
134
+ queue.startProcessing() // begin polling and processing
135
+ queue.stopProcessing() // stop polling
136
+ await queue.processUntilEmpty({ pollingMs?: number }) // process then resolve when empty
217
137
  ```
218
- Creates a new queue service with the given driver and optional logger.
219
138
 
220
- ```ts
221
- Queue.create<MessageBody, Result>(config: Queue.Config): Queue.QueueHandle
222
- ```
223
- Creates a named queue bound to the default service. Throws if a queue with the same name already
224
- exists.
139
+ ### Service-Level Methods
225
140
 
226
141
  ```ts
227
- Queue.work(options: Queue.WorkOptions): Promise<void> & { abort(reason?: string): void }
142
+ Queue.list() // names of all registered queues
143
+ Queue.get<Body, Result>(name) // retrieve a queue by name
144
+ Queue.startProcessing() // start all queues
145
+ Queue.stopProcessing() // stop all queues
228
146
  ```
229
- Starts a polling work loop. Returns a promise that resolves when the loop stops, with an `abort()`
230
- method to stop it.
231
147
 
232
- ```ts
233
- Queue.stopAllWork(): Promise<void>
234
- ```
235
- Aborts all active work loops and waits for in-flight messages to finish.
148
+ ### Message Statuses
236
149
 
237
- ```ts
238
- Queue.processMessage(queueMessage: Queue.Message): Promise<unknown>
239
- ```
240
- Processes a single message directly by looking up the queue handler by name. Used by
241
- `@maestro-js/recurring-jobs` to process messages outside the normal work loop.
150
+ | Status | Meaning |
151
+ | ------------ | -------------------------------------------------------------- |
152
+ | `waiting` | Queued and eligible for processing when scheduled time arrives |
153
+ | `processing` | Locked by a worker and currently executing |
154
+ | `success` | Work function completed without error |
155
+ | `error` | Work function threw or timed out |
156
+ | `stuck` | Processing exceeded timeout; recovered or marked stuck |
242
157
 
243
- ### QueueHandle Methods
158
+ ### Log Events
244
159
 
245
- ```ts
246
- handle.dispatch(body: MessageBody): Promise<void>
247
- ```
248
- Enqueues a message for immediate processing.
160
+ The queue emits structured log messages with these `event` values:
249
161
 
250
- ```ts
251
- handle.dispatchWithDelay(body: MessageBody, scheduledDate: Iso.Instant): Promise<void>
252
- ```
253
- Enqueues a message scheduled for a future date.
162
+ - `messageEnqueued` -- message added to the queue
163
+ - `messageDequeued` -- message picked up by a worker
164
+ - `messageProcessed` -- work function completed successfully
165
+ - `messageFailed` -- work function threw an error
166
+ - `queueStarted` -- queue processing started
167
+ - `queueStopped` -- queue processing stopped
254
168
 
255
- ```ts
256
- handle.dispatchSync(body: MessageBody): Promise<Result>
257
- ```
258
- Runs the handler synchronously without going through the driver. Useful for testing. Middleware
259
- still executes.
169
+ ## Common Patterns
170
+
171
+ ### Schedule a Future Job
260
172
 
261
173
  ```ts
262
- handle.bulk(messages: { body: MessageBody; scheduledDate?: Iso.Instant }[]): Promise<void>
263
- ```
264
- Enqueues multiple messages in a single operation. Uses the driver's bulk method for efficiency.
174
+ import { instantFns } from 'iso-fns2'
265
175
 
266
- ### Queue.Config
176
+ // Schedule 1 hour from now
177
+ const oneHourLater = instantFns.add(instantFns.now(), { hours: 1 })
178
+ await queue.addMessage({ task: 'reminder' }, { scheduledDate: oneHourLater })
179
+ ```
267
180
 
268
- | Field | Type | Default | Description |
269
- |------------------|----------------------------------------|---------|------------------------------------------------------|
270
- | `name` | `string` | -- | Unique queue name |
271
- | `handle` | `(message: MessageBody, actions: Queue.Actions) => Promise<Result>` | -- | Handler function for each message |
272
- | `middleware` | `Queue.Middleware[]` | `[]` | Middleware wrapping the handler |
273
- | `tries` | `number` | `1` | Total attempts (1 = no retries) |
274
- | `backoffSeconds` | `number \| number[]` | `60` | Seconds to wait between retries |
275
- | `logger` | `Log.LogFunctions` | -- | Per-queue logger (in addition to service-level logger)|
181
+ ### Deduplication with Unique Keys
276
182
 
277
- ### Queue.WorkOptions
183
+ The db driver supports unique keys via the `addMessage` options on the driver level. When a duplicate `uniqueKey` is inserted, the row is not duplicated -- instead the priority is updated if the new priority is higher (lower number).
278
184
 
279
- | Field | Type | Default | Description |
280
- |------------------|-----------|---------|----------------------------------------------|
281
- | `maxJobs` | `number` | -- | Stop after processing this many messages |
282
- | `stopWhenEmpty` | `boolean` | -- | Stop when no messages are available |
283
- | `maxTimeSeconds` | `number` | -- | Stop after this many seconds |
284
- | `sleepSeconds` | `number` | `1` | Seconds to sleep between polls when idle |
285
- | `concurrency` | `number` | `1` | Number of parallel polling workers |
185
+ ### Process Until Empty (Worker Scripts)
286
186
 
287
- ## Common Patterns
187
+ ```ts
188
+ // Useful for one-off batch processing or tests
189
+ await queue.processUntilEmpty({ pollingMs: 1000 })
190
+ // Resolves when no waiting/processing messages remain
191
+ ```
288
192
 
289
- ### Email queue with retries
193
+ ### Concurrency Control
290
194
 
291
195
  ```ts
196
+ // High-concurrency queue for lightweight jobs
292
197
  const emailQueue = Queue.create({
293
198
  name: 'send-emails',
294
- handle: async (body, { fail }) => {
295
- const result = await sendEmail(body.to, body.subject, body.html)
296
- if (result.rejected) {
297
- fail(new Error(`Rejected: ${body.to}`))
298
- }
299
- },
300
- tries: 3,
301
- backoffSeconds: [60, 300, 900]
199
+ workFn: async (body) => sendEmail(body),
200
+ concurrency: 10
302
201
  })
303
-
304
- await emailQueue.dispatch({ to: 'bob@example.com', subject: 'Hello', html: '<p>Hi!</p>' })
305
202
  ```
306
203
 
307
- ### Delayed scheduling
204
+ ### Stuck Job Recovery
308
205
 
309
- ```ts
310
- import { instantFns } from 'iso-fns2'
206
+ The db driver automatically recovers stuck jobs every 60 seconds. Jobs in `processing` status that exceed the timeout (default 45 minutes) are reset to `waiting` and reprocessed. Set `shouldRecoverStuckJobs: false` in the driver `start` options to mark them as `stuck` instead of recovering.
311
207
 
312
- // Schedule a reminder for 24 hours from now
313
- await reminderQueue.dispatchWithDelay(
314
- { userId: 42, message: 'Check in' },
315
- instantFns.add(instantFns.now(), { hours: 24 })
316
- )
317
- ```
318
-
319
- ### Bulk dispatch
320
-
321
- ```ts
322
- const messages = users.map((user) => ({
323
- body: { userId: user.id, type: 'weekly-digest' }
324
- }))
325
- await digestQueue.bulk(messages)
326
- ```
208
+ ### Distributed Tracing
327
209
 
328
- ### Middleware (logging, tracing)
210
+ Pass a `tracing` adapter when creating the service to propagate trace context across producer/consumer boundaries:
329
211
 
330
212
  ```ts
331
- const tracingMiddleware: Queue.Middleware = async (message, next) => {
332
- const span = tracer.startSpan('queue.process')
333
- try {
334
- return await next()
335
- } finally {
336
- span.end()
337
- }
338
- }
339
-
340
- const q = Queue.create({
341
- name: 'traced-queue',
342
- handle: async (body) => process(body),
343
- middleware: [tracingMiddleware]
344
- })
213
+ Queue.Provider.register(
214
+ 'default',
215
+ Queue.Provider.create({
216
+ driver: Queue.drivers.db({ db: Db.provider('default'), databaseTable: 'jobs' }),
217
+ logger: Log,
218
+ tracing: {
219
+ hydrate: (trace, callback) => {
220
+ /* restore trace context */ return callback()
221
+ },
222
+ dehydrate: () => {
223
+ /* return current trace context */ return null
224
+ },
225
+ startActiveSpan: (name, fn) => fn(),
226
+ getActiveSpan: () => undefined
227
+ }
228
+ })
229
+ )
345
230
  ```
346
231
 
347
- ### Sync dispatch for testing
348
-
349
- ```ts
350
- // Bypass the driver entirely -- runs the handler (and middleware) inline
351
- const result = await emailQueue.dispatchSync({ to: 'test@example.com', subject: 'Test' })
352
- ```
232
+ The trace data is serialized as `DehydratedTrace` (traceId, spanId, traceFlags, traceState) and stored in the `trace` column.
353
233
 
354
234
  ## Testing
355
235
 
356
- Use the local driver in tests. The local driver stores messages in-memory:
236
+ This package has no tests yet (`pnpm --filter @maestro-js/queue test` prints "No tests yet"). To test queue behavior in consuming packages, mock the driver interface:
357
237
 
358
238
  ```ts
359
- import { Queue } from '@maestro-js/queue'
360
- import { test } from 'beartest-js'
361
- import { expect } from 'expect'
362
-
363
- const service = Queue.Provider.create({ driver: Queue.drivers.local() })
364
- Queue.Provider.register('default', service)
365
-
366
- const testQueue = Queue.create({
367
- name: 'test-queue',
368
- handle: async (body) => ({ processed: body.value })
369
- })
370
-
371
- test('dispatches and processes a message', async () => {
372
- await testQueue.dispatch({ value: 42 })
373
- await Queue.work({ stopWhenEmpty: true })
374
- })
239
+ const mockDriver: Queue.Driver = {
240
+ start: () => {},
241
+ stop: () => {},
242
+ listMessages: async () => [],
243
+ addMessage: async () => ({ id: 1 }),
244
+ updateMessage: async () => {},
245
+ isQueueEmpty: async () => true
246
+ }
375
247
 
376
- test('dispatchSync runs handler inline', async () => {
377
- const result = await testQueue.dispatchSync({ value: 99 })
378
- expect(result).toEqual({ processed: 99 })
379
- })
248
+ Queue.Provider.register(
249
+ 'default',
250
+ Queue.Provider.create({
251
+ driver: mockDriver,
252
+ logger: Log
253
+ })
254
+ )
380
255
  ```
381
256
 
382
- Run tests:
257
+ ## Cross-Package Integration
383
258
 
384
- ```bash
385
- pnpm --filter @maestro-js/queue test
386
- cd packages/queue && npx beartest ./tests/**/*
387
- ```
259
+ ### Mail (async email sending)
388
260
 
389
- ## Warnings and Gotchas
390
-
391
- - **Queue name uniqueness** -- calling `Queue.create()` with a name that's already registered
392
- throws an error. Each queue name must be unique within a service instance.
393
- - **`attempts` is 0-indexed** -- the first execution has `attempts: 0`, the first retry has
394
- `attempts: 1`. The message permanently fails when `attempts + 1 >= tries`.
395
- - **`backoffSeconds` array indexing** -- `backoffSeconds[attempts]` is used for the delay, where
396
- `attempts` is 0-indexed. If the array is shorter than the retry count, the last element is reused.
397
- - **`dispatchSync` bypasses the driver** -- messages sent with `dispatchSync` are never persisted.
398
- The handler runs inline, and `work()` will never pick them up. Middleware still executes.
399
- - **`work()` swallows handler errors** -- the work loop catches all errors from handlers. Failed
400
- messages are released/failed via the driver and logged, but the loop continues polling.
401
- - **Database schema must be created manually** -- the db driver does not create or migrate the
402
- queue table. You must create it before using the driver. See the schema in the Drivers section.
403
- - **`work()` returns a hybrid promise** -- the returned value is both a `Promise<void>` and has an
404
- `abort()` method. Call `abort()` to stop the loop, then `await` the promise.
261
+ The `@maestro-js/mail` package uses Queue to dispatch email-sending jobs asynchronously. Mail enqueues messages; a queue consumer processes them via the configured mail driver.
405
262
 
406
- ## Cross-Package Integration
263
+ ### Recurring Jobs (cron scheduling)
407
264
 
408
- - **Depends on**: `@maestro-js/service-registry` (Provider pattern foundation),
409
- `@maestro-js/log` (structured logging)
410
- - **Used by**: `@maestro-js/mail` (async email sending via queue), `@maestro-js/recurring-jobs`
411
- (cron-scheduled jobs backed by queue)
412
- - **Dev dependency**: `@maestro-js/db` (MySQL driver needs a db connection)
265
+ The `@maestro-js/recurring-jobs` package uses Queue to execute scheduled/cron-based jobs. It dispatches queue messages on a schedule and relies on the queue's processing infrastructure.
413
266
 
414
- ## Driver Interface
267
+ ### Db (database driver)
415
268
 
416
- Implement `Queue.Driver` to create a custom backend:
269
+ The db driver requires a `Db` provider that exposes `select`, `update`, and `insert` methods. Pass `Db.provider('default')` as the `db` config option.
417
270
 
418
- ```ts
419
- interface QueueDriver {
420
- size(): Promise<number>
421
- push(message: { queue: string; body: Record<string, any>; scheduledDate: Iso.Instant }): Promise<string>
422
- bulk(messages: { queue: string; body: Record<string, any>; scheduledDate: Iso.Instant }[]): Promise<void>
423
- pop(): Promise<QueueMessage | null>
424
- fail(options: { id: string; attempts: number; error: Error }): Promise<void>
425
- success(options: { id: string; attempts: number; result: any }): Promise<void>
426
- release(options: { id: string; attempts: number; error: Error; delaySeconds: number }): Promise<void>
427
- }
428
- ```
271
+ ### Tracing
429
272
 
430
- Source: `packages/queue/src/queue-driver.ts`
273
+ Integrate with `@maestro-js/tracing` by implementing the `Queue.Tracing` interface to propagate OpenTelemetry-compatible trace context across the producer/consumer boundary.
431
274
 
432
- ## Key Source Files
275
+ ## Source Files
433
276
 
434
- - `packages/queue/src/index.ts` -- main export, Provider pattern, `createService()`, queue handles
435
- - `packages/queue/src/queue-driver.ts` -- `QueueDriver` and `QueueMessage` interfaces
436
- - `packages/queue/src/db-queue-driver.ts` -- MySQL driver with optimistic locking
437
- - `packages/queue/src/local-queue-driver.ts` -- in-memory driver for testing
277
+ - `packages/queue/src/index.ts` -- Provider pattern, facade, queue creation and lifecycle
278
+ - `packages/queue/src/queue-types.ts` -- TypeScript interfaces for QueueMessage, QueueDriver, QueueTracing, log events
279
+ - `packages/queue/src/db-queue-driver.ts` -- MySQL-backed driver with polling, batched locking, stuck job recovery