@maestro-js/agent-skills 1.0.0-alpha.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/dist/choosing-an-npm-package/SKILL.md +72 -0
- package/dist/maestro-cli/SKILL.md +257 -0
- package/dist/maestro-packages/SKILL.md +42 -0
- package/dist/maestro-packages/broadcast.md +306 -0
- package/dist/maestro-packages/cache-control.md +337 -0
- package/dist/maestro-packages/cache.md +397 -0
- package/dist/maestro-packages/config.md +203 -0
- package/dist/maestro-packages/context.md +365 -0
- package/dist/maestro-packages/crypt.md +253 -0
- package/dist/maestro-packages/db.md +391 -0
- package/dist/maestro-packages/events.md +295 -0
- package/dist/maestro-packages/exceptions.md +231 -0
- package/dist/maestro-packages/form.md +391 -0
- package/dist/maestro-packages/hash.md +248 -0
- package/dist/maestro-packages/helpers.md +249 -0
- package/dist/maestro-packages/log.md +321 -0
- package/dist/maestro-packages/mail.md +365 -0
- package/dist/maestro-packages/metrics.md +236 -0
- package/dist/maestro-packages/queue.md +284 -0
- package/dist/maestro-packages/rate-limiting.md +206 -0
- package/dist/maestro-packages/react-router-file-routes.md +337 -0
- package/dist/maestro-packages/recurring-jobs.md +353 -0
- package/dist/maestro-packages/sql.md +316 -0
- package/dist/maestro-packages/tracing.md +240 -0
- package/package.json +33 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: metrics
|
|
3
|
+
description: "Use when working with @maestro-js/metrics. Unified metrics collection piped through the Log transport system. Covers four metric types (counters, distributions, sets, gauges), provider setup, logger integration, Sentry transport, and custom transport authoring. Trigger on metrics emission, observability instrumentation, Sentry metrics integration, or application telemetry tasks."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Metrics
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The Metrics package provides unified application metrics collection that routes through the Log transport system. Emit counters, distributions, sets, and gauges as structured log messages, then attach transports to forward metrics to external systems like Sentry.
|
|
11
|
+
|
|
12
|
+
## Quick Setup
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { Metrics } from '@maestro-js/metrics'
|
|
16
|
+
import { Log } from '@maestro-js/log'
|
|
17
|
+
import * as Sentry from '@sentry/node'
|
|
18
|
+
|
|
19
|
+
// 1. Create a typed logger for metrics messages
|
|
20
|
+
const metricsLogger = Log.create<[Metrics.LogMessage]>()
|
|
21
|
+
|
|
22
|
+
// 2. Register the metrics service
|
|
23
|
+
Metrics.Provider.register('default', Metrics.Provider.create({ logger: metricsLogger }))
|
|
24
|
+
|
|
25
|
+
// 3. Attach the Sentry transport (or any custom transport)
|
|
26
|
+
metricsLogger.addTransport(Metrics.transports.sentryMetrics(Sentry.metrics))
|
|
27
|
+
|
|
28
|
+
// 4. Emit metrics
|
|
29
|
+
Metrics.increment('order_placed', 1, { tags: { region: 'us-east' } })
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## API Reference
|
|
33
|
+
|
|
34
|
+
### Provider
|
|
35
|
+
|
|
36
|
+
#### `Metrics.Provider.create(config)`
|
|
37
|
+
|
|
38
|
+
Create a metrics service instance.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
Metrics.Provider.create({
|
|
42
|
+
logger: Log.Logger<[Metrics.LogMessage]>
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The `logger` parameter is a typed `Log.Logger` that accepts `Metrics.LogMessage` entries. All metric emissions flow through this logger, so output destination is controlled entirely by the transports attached to it.
|
|
47
|
+
|
|
48
|
+
#### `Metrics.Provider.register(name, service)`
|
|
49
|
+
|
|
50
|
+
Register a metrics service instance in the named registry.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
Metrics.Provider.register('default', Metrics.Provider.create({ logger: metricsLogger }))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Metric Methods
|
|
57
|
+
|
|
58
|
+
All four methods are available directly on the `Metrics` facade (resolving the `'default'` provider) or on a named provider instance.
|
|
59
|
+
|
|
60
|
+
#### `Metrics.increment(name, value?, data?)`
|
|
61
|
+
|
|
62
|
+
Emit a counter metric, incrementing by `value` (defaults to 1 when omitted).
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
Metrics.increment('user_signup') // increment by 1
|
|
66
|
+
Metrics.increment('items_purchased', 3, { tags: { store: 'web' } }) // increment by 3
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### `Metrics.distribution(name, value, data?)`
|
|
70
|
+
|
|
71
|
+
Emit a distribution metric for statistical aggregation (p50, p95, p99).
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
Metrics.distribution('api_response_time', 142, { unit: 'millisecond' })
|
|
75
|
+
Metrics.distribution('payload_size', 2048, { unit: 'byte', tags: { endpoint: '/upload' } })
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### `Metrics.set(name, value, data?)`
|
|
79
|
+
|
|
80
|
+
Emit a set metric tracking unique string or integer values.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
Metrics.set('unique_customers', 'cust_123')
|
|
84
|
+
Metrics.set('unique_customers', 'cust_456', { tags: { channel: 'mobile' } })
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### `Metrics.gauge(name, value, data?)`
|
|
88
|
+
|
|
89
|
+
Emit a gauge metric capturing a point-in-time value that can increase or decrease.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
Metrics.gauge('active_connections', 47)
|
|
93
|
+
Metrics.gauge('queue_depth', 312, { tags: { queue: 'email' } })
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### MetricsAdditionalData
|
|
97
|
+
|
|
98
|
+
Optional metadata passed as the last argument to any metric method.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
interface MetricsAdditionalData {
|
|
102
|
+
unit?: MeasurementUnit // 'millisecond', 'byte', 'percent', 'none', etc.
|
|
103
|
+
tags?: Record<string, Primitive> // arbitrary key-value pairs for grouping
|
|
104
|
+
timestamp?: number // custom Unix timestamp
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Supported unit categories:**
|
|
109
|
+
|
|
110
|
+
- **Duration**: `'nanosecond'`, `'microsecond'`, `'millisecond'`, `'second'`, `'minute'`, `'hour'`, `'day'`, `'week'`
|
|
111
|
+
- **Information**: `'bit'`, `'byte'`, `'kilobyte'`, `'megabyte'`, `'gigabyte'`, `'terabyte'`, etc.
|
|
112
|
+
- **Fraction**: `'ratio'`, `'percent'`
|
|
113
|
+
- **None**: `''`, `'none'`
|
|
114
|
+
|
|
115
|
+
### MetricsLogMessage
|
|
116
|
+
|
|
117
|
+
Discriminated union of the four metric operations, keyed by `op`. Each variant carries `name`, `value`, and `data` fields. Used to type the logger and transports.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
type MetricsLogMessage =
|
|
121
|
+
| { op: 'increment'; name: string; value: number | undefined; data: MetricsAdditionalData | undefined }
|
|
122
|
+
| { op: 'distribution'; name: string; value: number; data: MetricsAdditionalData | undefined }
|
|
123
|
+
| { op: 'set'; name: string; value: number | string; data: MetricsAdditionalData | undefined }
|
|
124
|
+
| { op: 'gauge'; name: string; value: number; data: MetricsAdditionalData | undefined }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Transports
|
|
128
|
+
|
|
129
|
+
### Built-in: Sentry Metrics
|
|
130
|
+
|
|
131
|
+
Route all metrics to Sentry via `Metrics.transports.sentryMetrics()`.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import * as Sentry from '@sentry/node'
|
|
135
|
+
|
|
136
|
+
const metricsLogger = Log.create<[Metrics.LogMessage]>()
|
|
137
|
+
metricsLogger.addTransport(Metrics.transports.sentryMetrics(Sentry.metrics))
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The transport accepts any object matching the `Metrics.MetricFunctions` interface (the four metric methods), so it works with any Sentry-compatible metrics API.
|
|
141
|
+
|
|
142
|
+
### Custom Transports
|
|
143
|
+
|
|
144
|
+
Write a custom transport by implementing `Log.Transport<[Metrics.LogMessage]>`. The `write` method receives a log message whose `data[0]` is the `MetricsLogMessage`.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
function datadogTransport(client: DatadogClient): Log.Transport<[Metrics.LogMessage]> {
|
|
148
|
+
return {
|
|
149
|
+
write(message) {
|
|
150
|
+
const metric = message.data[0]
|
|
151
|
+
if (metric.op === 'increment') {
|
|
152
|
+
client.increment(metric.name, metric.value ?? 1, metric.data?.tags)
|
|
153
|
+
} else if (metric.op === 'distribution') {
|
|
154
|
+
client.distribution(metric.name, metric.value, metric.data?.tags)
|
|
155
|
+
} else if (metric.op === 'gauge') {
|
|
156
|
+
client.gauge(metric.name, metric.value, metric.data?.tags)
|
|
157
|
+
}
|
|
158
|
+
// handle 'set' as needed
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
metricsLogger.addTransport(datadogTransport(myDatadogClient))
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Common Patterns
|
|
167
|
+
|
|
168
|
+
### Tagging metrics by request context
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
Metrics.increment('api_request', 1, {
|
|
172
|
+
tags: { method: 'POST', path: '/orders', status: 201 }
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
Metrics.distribution('db_query_time', 23, {
|
|
176
|
+
unit: 'millisecond',
|
|
177
|
+
tags: { table: 'orders', operation: 'insert' }
|
|
178
|
+
})
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Tracking unique visitors
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
Metrics.set('unique_visitors', sessionId, {
|
|
185
|
+
tags: { page: '/checkout' }
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Monitoring resource usage
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
Metrics.gauge('memory_usage_mb', process.memoryUsage().heapUsed / 1024 / 1024, {
|
|
193
|
+
unit: 'megabyte'
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
Metrics.gauge('active_websocket_connections', connectionPool.size)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Multiple metric providers
|
|
200
|
+
|
|
201
|
+
Register named providers for different metric pipelines.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
const appLogger = Log.create<[Metrics.LogMessage]>()
|
|
205
|
+
const perfLogger = Log.create<[Metrics.LogMessage]>()
|
|
206
|
+
|
|
207
|
+
appLogger.addTransport(Metrics.transports.sentryMetrics(Sentry.metrics))
|
|
208
|
+
perfLogger.addTransport(datadogTransport(myDatadogClient))
|
|
209
|
+
|
|
210
|
+
Metrics.Provider.register('default', Metrics.Provider.create({ logger: appLogger }))
|
|
211
|
+
Metrics.Provider.register('perf', Metrics.Provider.create({ logger: perfLogger }))
|
|
212
|
+
|
|
213
|
+
// Facade calls use 'default'
|
|
214
|
+
Metrics.increment('order_placed')
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Cross-Package Integration
|
|
218
|
+
|
|
219
|
+
### Depends on
|
|
220
|
+
|
|
221
|
+
- **@maestro-js/service-registry** -- Provider pattern, named registry, deferred Proxy resolution.
|
|
222
|
+
- **@maestro-js/log** -- Typed logger and transport system. Metrics emissions are structured log messages routed through Log transports.
|
|
223
|
+
|
|
224
|
+
### Works with
|
|
225
|
+
|
|
226
|
+
- **@maestro-js/log** -- Metrics piggybacks on the Log transport architecture. Create a dedicated `Log.Logger<[Metrics.LogMessage]>` for metrics, then attach transports to route emissions to Sentry, Datadog, or any custom destination. The same logger can have multiple transports to fan out metrics to several backends simultaneously.
|
|
227
|
+
|
|
228
|
+
## Package Info
|
|
229
|
+
|
|
230
|
+
- **Package name**: `@maestro-js/metrics`
|
|
231
|
+
- **Source**: `packages/metrics/src/index.ts`
|
|
232
|
+
- **Types**: `packages/metrics/src/metrics-types.ts`
|
|
233
|
+
- **Sentry transport**: `packages/metrics/src/sentry-metrics-transport.ts`
|
|
234
|
+
- **Tests**: None yet
|
|
235
|
+
- **Build**: `pnpm --filter @maestro-js/metrics build`
|
|
236
|
+
- **Typecheck**: `pnpm --filter @maestro-js/metrics typecheck`
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: queue
|
|
3
|
+
description: "Database-backed job queue for deferring work beyond the current request with scheduled execution, concurrency control, and distributed tracing. Use when working with @maestro-js/queue, including dispatching jobs, processing queues, configuring the db driver, writing queue consumers, handling retries/stuck jobs, integrating with mail or recurring-jobs, and understanding the queue database schema."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @maestro-js/queue
|
|
7
|
+
|
|
8
|
+
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.
|
|
9
|
+
|
|
10
|
+
## Quick Setup
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { Queue } from '@maestro-js/queue'
|
|
14
|
+
import { Db } from '@maestro-js/db'
|
|
15
|
+
import { Log } from '@maestro-js/log'
|
|
16
|
+
|
|
17
|
+
// Register the queue service with the db driver
|
|
18
|
+
Queue.Provider.register(
|
|
19
|
+
'default',
|
|
20
|
+
Queue.Provider.create({
|
|
21
|
+
driver: Queue.drivers.db({
|
|
22
|
+
db: Db.provider('default'),
|
|
23
|
+
databaseTable: 'jobs'
|
|
24
|
+
}),
|
|
25
|
+
logger: Log
|
|
26
|
+
})
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
// Create a named queue with a work function
|
|
30
|
+
const orderQueue = Queue.create({
|
|
31
|
+
name: 'fulfill-orders',
|
|
32
|
+
workFn: async (body) => {
|
|
33
|
+
await fulfillOrder(body.orderId)
|
|
34
|
+
return { fulfilled: true }
|
|
35
|
+
},
|
|
36
|
+
concurrency: 3
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Enqueue a message
|
|
40
|
+
await orderQueue.addMessage({ orderId: 'ord_123' }, { scheduledDate: null })
|
|
41
|
+
|
|
42
|
+
// Start processing
|
|
43
|
+
orderQueue.startProcessing()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Database Schema
|
|
47
|
+
|
|
48
|
+
The db driver expects a MySQL table (name configurable via `databaseTable`). The column-to-field mapping used internally:
|
|
49
|
+
|
|
50
|
+
| Column | Type | Description |
|
|
51
|
+
| --------------- | ------------- | -------------------------------------------------------- |
|
|
52
|
+
| `id` | int (PK, AI) | Message ID |
|
|
53
|
+
| `batch_id` | int / NULL | Lock batch for concurrent dequeue |
|
|
54
|
+
| `job_type` | varchar | Queue name string |
|
|
55
|
+
| `body` | text (JSON) | Serialized message body |
|
|
56
|
+
| `trace` | text (JSON) | Serialized `DehydratedTrace` for distributed tracing |
|
|
57
|
+
| `status` | enum | `waiting`, `processing`, `success`, `error`, `stuck` |
|
|
58
|
+
| `scheduledDate` | datetime | Earliest time the message can be processed |
|
|
59
|
+
| `enqueuedDate` | datetime | When the message was inserted |
|
|
60
|
+
| `started_ts` | datetime | When processing began |
|
|
61
|
+
| `result` | text | JSON result on success, error stack on failure |
|
|
62
|
+
| `recovered` | tinyint | 1 if the message was recovered from stuck state |
|
|
63
|
+
| `running_time` | int / NULL | Seconds elapsed during processing |
|
|
64
|
+
| `unique_key` | int / NULL | MD5-derived dedup key (nullable) |
|
|
65
|
+
| `priority` | bigint / NULL | Lower value = higher priority (defaults to `Date.now()`) |
|
|
66
|
+
|
|
67
|
+
The driver requires a `FORCE INDEX (locking_update)` index on `(batch_id, status, job_type, scheduledDate)` for efficient locking queries.
|
|
68
|
+
|
|
69
|
+
## API Reference
|
|
70
|
+
|
|
71
|
+
### Provider Setup
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
Queue.Provider.create(config: {
|
|
75
|
+
driver: Queue.Driver
|
|
76
|
+
logger: Log.Logger<[Queue.LogMessage]>
|
|
77
|
+
tracing?: Queue.Tracing // optional distributed tracing adapter
|
|
78
|
+
}): Queue.QueueService
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
Queue.Provider.register(name: Queue.Provider.Key, service: Queue.QueueService): void
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Drivers
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
Queue.drivers.db(config: {
|
|
89
|
+
db: { update, select, insert } // Db provider interface
|
|
90
|
+
databaseTable: string
|
|
91
|
+
extraFields?: string[] // additional columns to insert
|
|
92
|
+
fastestPollingRateMs?: number // default 100
|
|
93
|
+
slowestPollingRateMs?: number // default 10_000
|
|
94
|
+
pollingBackoffRate?: number // default 1.1
|
|
95
|
+
}): Queue.Driver
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Queue Instance Methods
|
|
99
|
+
|
|
100
|
+
Create a queue:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const queue = Queue.create<MessageBody, Result>({
|
|
104
|
+
name: string,
|
|
105
|
+
workFn: (body: MessageBody) => Promise<Result>,
|
|
106
|
+
logger?: Log.LogFunctions<[Queue.LogMessage]>, // additional per-queue logger
|
|
107
|
+
concurrency?: number // default 1
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Dispatch a message:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
await queue.addMessage(body, {
|
|
115
|
+
scheduledDate: Iso.Instant | null, // null = process immediately
|
|
116
|
+
tracingContext?: DehydratedTrace | null
|
|
117
|
+
})
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Update a waiting message:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
await queue.updateMessage({
|
|
124
|
+
messageId: number,
|
|
125
|
+
scheduledDate: Iso.Instant,
|
|
126
|
+
body: MessageBody
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
List messages (paginated):
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const messages = await queue.listMessages({ pageSize: 50, pageNumber: 0 })
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Processing control:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
queue.startProcessing() // begin polling and processing
|
|
140
|
+
queue.stopProcessing() // stop polling
|
|
141
|
+
await queue.processUntilEmpty({ pollingMs?: number }) // process then resolve when empty
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Service-Level Methods
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
Queue.list() // names of all registered queues
|
|
148
|
+
Queue.get<Body, Result>(name) // retrieve a queue by name
|
|
149
|
+
Queue.startProcessing() // start all queues
|
|
150
|
+
Queue.stopProcessing() // stop all queues
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Message Statuses
|
|
154
|
+
|
|
155
|
+
| Status | Meaning |
|
|
156
|
+
| ------------ | -------------------------------------------------------------- |
|
|
157
|
+
| `waiting` | Queued and eligible for processing when scheduled time arrives |
|
|
158
|
+
| `processing` | Locked by a worker and currently executing |
|
|
159
|
+
| `success` | Work function completed without error |
|
|
160
|
+
| `error` | Work function threw or timed out |
|
|
161
|
+
| `stuck` | Processing exceeded timeout; recovered or marked stuck |
|
|
162
|
+
|
|
163
|
+
### Log Events
|
|
164
|
+
|
|
165
|
+
The queue emits structured log messages with these `event` values:
|
|
166
|
+
|
|
167
|
+
- `messageEnqueued` -- message added to the queue
|
|
168
|
+
- `messageDequeued` -- message picked up by a worker
|
|
169
|
+
- `messageProcessed` -- work function completed successfully
|
|
170
|
+
- `messageFailed` -- work function threw an error
|
|
171
|
+
- `queueStarted` -- queue processing started
|
|
172
|
+
- `queueStopped` -- queue processing stopped
|
|
173
|
+
|
|
174
|
+
## Common Patterns
|
|
175
|
+
|
|
176
|
+
### Schedule a Future Job
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { instantFns } from 'iso-fns2'
|
|
180
|
+
|
|
181
|
+
// Schedule 1 hour from now
|
|
182
|
+
const oneHourLater = instantFns.add(instantFns.now(), { hours: 1 })
|
|
183
|
+
await queue.addMessage({ task: 'reminder' }, { scheduledDate: oneHourLater })
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Deduplication with Unique Keys
|
|
187
|
+
|
|
188
|
+
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).
|
|
189
|
+
|
|
190
|
+
### Process Until Empty (Worker Scripts)
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
// Useful for one-off batch processing or tests
|
|
194
|
+
await queue.processUntilEmpty({ pollingMs: 1000 })
|
|
195
|
+
// Resolves when no waiting/processing messages remain
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Concurrency Control
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
// High-concurrency queue for lightweight jobs
|
|
202
|
+
const emailQueue = Queue.create({
|
|
203
|
+
name: 'send-emails',
|
|
204
|
+
workFn: async (body) => sendEmail(body),
|
|
205
|
+
concurrency: 10
|
|
206
|
+
})
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Stuck Job Recovery
|
|
210
|
+
|
|
211
|
+
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.
|
|
212
|
+
|
|
213
|
+
### Distributed Tracing
|
|
214
|
+
|
|
215
|
+
Pass a `tracing` adapter when creating the service to propagate trace context across producer/consumer boundaries:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
Queue.Provider.register(
|
|
219
|
+
'default',
|
|
220
|
+
Queue.Provider.create({
|
|
221
|
+
driver: Queue.drivers.db({ db: Db.provider('default'), databaseTable: 'jobs' }),
|
|
222
|
+
logger: Log,
|
|
223
|
+
tracing: {
|
|
224
|
+
hydrate: (trace, callback) => {
|
|
225
|
+
/* restore trace context */ return callback()
|
|
226
|
+
},
|
|
227
|
+
dehydrate: () => {
|
|
228
|
+
/* return current trace context */ return null
|
|
229
|
+
},
|
|
230
|
+
startActiveSpan: (name, fn) => fn(),
|
|
231
|
+
getActiveSpan: () => undefined
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The trace data is serialized as `DehydratedTrace` (traceId, spanId, traceFlags, traceState) and stored in the `trace` column.
|
|
238
|
+
|
|
239
|
+
## Testing
|
|
240
|
+
|
|
241
|
+
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:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const mockDriver: Queue.Driver = {
|
|
245
|
+
start: () => {},
|
|
246
|
+
stop: () => {},
|
|
247
|
+
listMessages: async () => [],
|
|
248
|
+
addMessage: async () => ({ id: 1 }),
|
|
249
|
+
updateMessage: async () => {},
|
|
250
|
+
isQueueEmpty: async () => true
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
Queue.Provider.register(
|
|
254
|
+
'default',
|
|
255
|
+
Queue.Provider.create({
|
|
256
|
+
driver: mockDriver,
|
|
257
|
+
logger: Log
|
|
258
|
+
})
|
|
259
|
+
)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Cross-Package Integration
|
|
263
|
+
|
|
264
|
+
### Mail (async email sending)
|
|
265
|
+
|
|
266
|
+
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.
|
|
267
|
+
|
|
268
|
+
### Recurring Jobs (cron scheduling)
|
|
269
|
+
|
|
270
|
+
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.
|
|
271
|
+
|
|
272
|
+
### Db (database driver)
|
|
273
|
+
|
|
274
|
+
The db driver requires a `Db` provider that exposes `select`, `update`, and `insert` methods. Pass `Db.provider('default')` as the `db` config option.
|
|
275
|
+
|
|
276
|
+
### Tracing
|
|
277
|
+
|
|
278
|
+
Integrate with `@maestro-js/tracing` by implementing the `Queue.Tracing` interface to propagate OpenTelemetry-compatible trace context across the producer/consumer boundary.
|
|
279
|
+
|
|
280
|
+
## Source Files
|
|
281
|
+
|
|
282
|
+
- `packages/queue/src/index.ts` -- Provider pattern, facade, queue creation and lifecycle
|
|
283
|
+
- `packages/queue/src/queue-types.ts` -- TypeScript interfaces for QueueMessage, QueueDriver, QueueTracing, log events
|
|
284
|
+
- `packages/queue/src/db-queue-driver.ts` -- MySQL-backed driver with polling, batched locking, stuck job recovery
|