@mantiq/queue 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/package.json +72 -0
- package/src/Job.ts +122 -0
- package/src/JobBatch.ts +188 -0
- package/src/JobChain.ts +119 -0
- package/src/JobRegistry.ts +37 -0
- package/src/PendingDispatch.ts +80 -0
- package/src/QueueManager.ts +76 -0
- package/src/QueueServiceProvider.ts +88 -0
- package/src/Worker.ts +282 -0
- package/src/commands/MakeJobCommand.ts +29 -0
- package/src/commands/QueueFailedCommand.ts +50 -0
- package/src/commands/QueueFlushCommand.ts +19 -0
- package/src/commands/QueueRetryCommand.ts +52 -0
- package/src/commands/QueueWorkCommand.ts +49 -0
- package/src/commands/ScheduleRunCommand.ts +58 -0
- package/src/contracts/JobContract.ts +64 -0
- package/src/contracts/QueueDriver.ts +70 -0
- package/src/drivers/KafkaDriver.ts +334 -0
- package/src/drivers/RedisDriver.ts +292 -0
- package/src/drivers/SQLiteDriver.ts +280 -0
- package/src/drivers/SqsDriver.ts +280 -0
- package/src/drivers/SyncDriver.ts +142 -0
- package/src/errors/QueueError.ts +22 -0
- package/src/events/QueueEvents.ts +36 -0
- package/src/helpers/queue.ts +76 -0
- package/src/index.ts +85 -0
- package/src/schedule/Schedule.ts +252 -0
- package/src/testing/QueueFake.ts +209 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import type { QueueDriver } from '../contracts/QueueDriver.ts'
|
|
2
|
+
import type {
|
|
3
|
+
QueuedJob,
|
|
4
|
+
FailedJob,
|
|
5
|
+
SerializedPayload,
|
|
6
|
+
BatchRecord,
|
|
7
|
+
} from '../contracts/JobContract.ts'
|
|
8
|
+
|
|
9
|
+
export interface SqsQueueConfig {
|
|
10
|
+
driver: 'sqs'
|
|
11
|
+
/** SQS queue URL — required */
|
|
12
|
+
queueUrl: string
|
|
13
|
+
/** AWS region. Default: 'us-east-1' */
|
|
14
|
+
region?: string | undefined
|
|
15
|
+
/** Override endpoint for local testing (e.g. LocalStack) */
|
|
16
|
+
endpoint?: string | undefined
|
|
17
|
+
/** AWS credentials — if omitted, uses default credential chain */
|
|
18
|
+
credentials?: {
|
|
19
|
+
accessKeyId: string
|
|
20
|
+
secretAccessKey: string
|
|
21
|
+
} | undefined
|
|
22
|
+
/** Prefix for queue names. Default: '' */
|
|
23
|
+
prefix?: string | undefined
|
|
24
|
+
/** Visibility timeout in seconds for popped messages. Default: 60 */
|
|
25
|
+
visibilityTimeout?: number | undefined
|
|
26
|
+
/** Long-poll wait time in seconds (0-20). Default: 5 */
|
|
27
|
+
waitTimeSeconds?: number | undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Amazon SQS queue driver using @aws-sdk/client-sqs.
|
|
32
|
+
*
|
|
33
|
+
* Maps MantiqJS queue operations to SQS API calls:
|
|
34
|
+
* - push → SendMessage (with optional DelaySeconds)
|
|
35
|
+
* - pop → ReceiveMessage (with VisibilityTimeout)
|
|
36
|
+
* - delete → DeleteMessage (using ReceiptHandle)
|
|
37
|
+
* - release → ChangeMessageVisibility
|
|
38
|
+
*
|
|
39
|
+
* Failed jobs and batches are tracked in-memory since SQS
|
|
40
|
+
* doesn't have native storage for these. For production use,
|
|
41
|
+
* pair with a database-backed failed job store.
|
|
42
|
+
*
|
|
43
|
+
* Requires `@aws-sdk/client-sqs`:
|
|
44
|
+
* bun add @aws-sdk/client-sqs
|
|
45
|
+
*/
|
|
46
|
+
export class SqsDriver implements QueueDriver {
|
|
47
|
+
private sqs: any
|
|
48
|
+
private readonly queueUrl: string
|
|
49
|
+
private readonly prefix: string
|
|
50
|
+
private readonly visibilityTimeout: number
|
|
51
|
+
private readonly waitTimeSeconds: number
|
|
52
|
+
|
|
53
|
+
/** Map of job ID → SQS ReceiptHandle (needed for delete/release) */
|
|
54
|
+
private receiptHandles = new Map<string, string>()
|
|
55
|
+
|
|
56
|
+
/** In-memory failed job tracking */
|
|
57
|
+
private failedJobs: FailedJob[] = []
|
|
58
|
+
private nextFailedId = 1
|
|
59
|
+
|
|
60
|
+
/** In-memory batch tracking */
|
|
61
|
+
private batches = new Map<string, BatchRecord>()
|
|
62
|
+
|
|
63
|
+
constructor(config: SqsQueueConfig) {
|
|
64
|
+
this.queueUrl = config.queueUrl
|
|
65
|
+
this.prefix = config.prefix ?? ''
|
|
66
|
+
this.visibilityTimeout = config.visibilityTimeout ?? 60
|
|
67
|
+
this.waitTimeSeconds = config.waitTimeSeconds ?? 5
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const { SQSClient } = require('@aws-sdk/client-sqs')
|
|
71
|
+
const clientConfig: any = { region: config.region ?? 'us-east-1' }
|
|
72
|
+
if (config.endpoint) clientConfig.endpoint = config.endpoint
|
|
73
|
+
if (config.credentials) clientConfig.credentials = config.credentials
|
|
74
|
+
this.sqs = new SQSClient(clientConfig)
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error(
|
|
77
|
+
'@aws-sdk/client-sqs is required for the SQS queue driver. Install it with: bun add @aws-sdk/client-sqs',
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Core job operations ──────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
async push(payload: SerializedPayload, queue: string, delay = 0): Promise<string | number> {
|
|
85
|
+
const { SendMessageCommand } = require('@aws-sdk/client-sqs')
|
|
86
|
+
|
|
87
|
+
const params: any = {
|
|
88
|
+
QueueUrl: this.resolveQueueUrl(queue),
|
|
89
|
+
MessageBody: JSON.stringify(payload),
|
|
90
|
+
MessageAttributes: {
|
|
91
|
+
MantiqQueue: { DataType: 'String', StringValue: queue },
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// SQS supports delay up to 900 seconds (15 minutes)
|
|
96
|
+
if (delay > 0) {
|
|
97
|
+
params.DelaySeconds = Math.min(delay, 900)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const result = await this.sqs.send(new SendMessageCommand(params))
|
|
101
|
+
return result.MessageId as string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async pop(queue: string): Promise<QueuedJob | null> {
|
|
105
|
+
const { ReceiveMessageCommand } = require('@aws-sdk/client-sqs')
|
|
106
|
+
|
|
107
|
+
const result = await this.sqs.send(new ReceiveMessageCommand({
|
|
108
|
+
QueueUrl: this.resolveQueueUrl(queue),
|
|
109
|
+
MaxNumberOfMessages: 1,
|
|
110
|
+
VisibilityTimeout: this.visibilityTimeout,
|
|
111
|
+
WaitTimeSeconds: this.waitTimeSeconds,
|
|
112
|
+
MessageAttributeNames: ['All'],
|
|
113
|
+
}))
|
|
114
|
+
|
|
115
|
+
const messages = result.Messages
|
|
116
|
+
if (!messages || messages.length === 0) return null
|
|
117
|
+
|
|
118
|
+
const msg = messages[0]
|
|
119
|
+
const payload: SerializedPayload = JSON.parse(msg.Body)
|
|
120
|
+
|
|
121
|
+
// Parse attempt count from message attribute or default to 0
|
|
122
|
+
const approxReceiveCount = parseInt(msg.Attributes?.ApproximateReceiveCount ?? '1', 10)
|
|
123
|
+
|
|
124
|
+
const job: QueuedJob = {
|
|
125
|
+
id: msg.MessageId,
|
|
126
|
+
queue,
|
|
127
|
+
payload,
|
|
128
|
+
attempts: approxReceiveCount,
|
|
129
|
+
reservedAt: Math.floor(Date.now() / 1000),
|
|
130
|
+
availableAt: 0,
|
|
131
|
+
createdAt: 0,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Store receipt handle for later delete/release
|
|
135
|
+
this.receiptHandles.set(msg.MessageId, msg.ReceiptHandle)
|
|
136
|
+
|
|
137
|
+
return job
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async delete(job: QueuedJob): Promise<void> {
|
|
141
|
+
const { DeleteMessageCommand } = require('@aws-sdk/client-sqs')
|
|
142
|
+
|
|
143
|
+
const receiptHandle = this.receiptHandles.get(String(job.id))
|
|
144
|
+
if (!receiptHandle) return
|
|
145
|
+
|
|
146
|
+
await this.sqs.send(new DeleteMessageCommand({
|
|
147
|
+
QueueUrl: this.resolveQueueUrl(job.queue),
|
|
148
|
+
ReceiptHandle: receiptHandle,
|
|
149
|
+
}))
|
|
150
|
+
|
|
151
|
+
this.receiptHandles.delete(String(job.id))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async release(job: QueuedJob, delay: number): Promise<void> {
|
|
155
|
+
const { ChangeMessageVisibilityCommand } = require('@aws-sdk/client-sqs')
|
|
156
|
+
|
|
157
|
+
const receiptHandle = this.receiptHandles.get(String(job.id))
|
|
158
|
+
if (!receiptHandle) return
|
|
159
|
+
|
|
160
|
+
await this.sqs.send(new ChangeMessageVisibilityCommand({
|
|
161
|
+
QueueUrl: this.resolveQueueUrl(job.queue),
|
|
162
|
+
ReceiptHandle: receiptHandle,
|
|
163
|
+
VisibilityTimeout: delay,
|
|
164
|
+
}))
|
|
165
|
+
|
|
166
|
+
this.receiptHandles.delete(String(job.id))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async size(queue: string): Promise<number> {
|
|
170
|
+
const { GetQueueAttributesCommand } = require('@aws-sdk/client-sqs')
|
|
171
|
+
|
|
172
|
+
const result = await this.sqs.send(new GetQueueAttributesCommand({
|
|
173
|
+
QueueUrl: this.resolveQueueUrl(queue),
|
|
174
|
+
AttributeNames: [
|
|
175
|
+
'ApproximateNumberOfMessages',
|
|
176
|
+
'ApproximateNumberOfMessagesDelayed',
|
|
177
|
+
],
|
|
178
|
+
}))
|
|
179
|
+
|
|
180
|
+
const visible = parseInt(result.Attributes?.ApproximateNumberOfMessages ?? '0', 10)
|
|
181
|
+
const delayed = parseInt(result.Attributes?.ApproximateNumberOfMessagesDelayed ?? '0', 10)
|
|
182
|
+
return visible + delayed
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async clear(queue: string): Promise<void> {
|
|
186
|
+
const { PurgeQueueCommand } = require('@aws-sdk/client-sqs')
|
|
187
|
+
await this.sqs.send(new PurgeQueueCommand({
|
|
188
|
+
QueueUrl: this.resolveQueueUrl(queue),
|
|
189
|
+
}))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Failed jobs (in-memory) ──────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
async fail(job: QueuedJob, error: Error): Promise<void> {
|
|
195
|
+
await this.delete(job)
|
|
196
|
+
this.failedJobs.push({
|
|
197
|
+
id: this.nextFailedId++,
|
|
198
|
+
queue: job.queue,
|
|
199
|
+
payload: job.payload,
|
|
200
|
+
exception: `${error.name}: ${error.message}\n${error.stack ?? ''}`,
|
|
201
|
+
failedAt: Math.floor(Date.now() / 1000),
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async getFailedJobs(): Promise<FailedJob[]> {
|
|
206
|
+
return [...this.failedJobs]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async findFailedJob(id: string | number): Promise<FailedJob | null> {
|
|
210
|
+
return this.failedJobs.find((j) => j.id === id) ?? null
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async forgetFailedJob(id: string | number): Promise<boolean> {
|
|
214
|
+
const idx = this.failedJobs.findIndex((j) => j.id === id)
|
|
215
|
+
if (idx === -1) return false
|
|
216
|
+
this.failedJobs.splice(idx, 1)
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async flushFailedJobs(): Promise<void> {
|
|
221
|
+
this.failedJobs = []
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Batch support (in-memory) ────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
async createBatch(batch: BatchRecord): Promise<string> {
|
|
227
|
+
this.batches.set(batch.id, { ...batch })
|
|
228
|
+
return batch.id
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async findBatch(id: string): Promise<BatchRecord | null> {
|
|
232
|
+
const b = this.batches.get(id)
|
|
233
|
+
return b ? { ...b, failedJobIds: [...b.failedJobIds], options: { ...b.options } } : null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async updateBatchProgress(id: string, processed: number, failed: number): Promise<BatchRecord | null> {
|
|
237
|
+
const b = this.batches.get(id)
|
|
238
|
+
if (!b) return null
|
|
239
|
+
b.processedJobs += processed
|
|
240
|
+
b.failedJobs += failed
|
|
241
|
+
return { ...b, failedJobIds: [...b.failedJobIds], options: { ...b.options } }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async markBatchFinished(id: string): Promise<void> {
|
|
245
|
+
const b = this.batches.get(id)
|
|
246
|
+
if (b) b.finishedAt = Math.floor(Date.now() / 1000)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async cancelBatch(id: string): Promise<void> {
|
|
250
|
+
const b = this.batches.get(id)
|
|
251
|
+
if (b) b.cancelledAt = Math.floor(Date.now() / 1000)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async pruneBatches(olderThanSeconds: number): Promise<void> {
|
|
255
|
+
const cutoff = Math.floor(Date.now() / 1000) - olderThanSeconds
|
|
256
|
+
for (const [id, b] of this.batches) {
|
|
257
|
+
if (b.createdAt < cutoff) this.batches.delete(id)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Resolve a logical queue name to an SQS Queue URL.
|
|
265
|
+
* If the queue name is 'default', uses the configured queueUrl.
|
|
266
|
+
* Otherwise, replaces the last path segment of the URL.
|
|
267
|
+
*/
|
|
268
|
+
private resolveQueueUrl(queue: string): string {
|
|
269
|
+
if (queue === 'default' || !this.queueUrl) return this.queueUrl
|
|
270
|
+
|
|
271
|
+
// Replace the queue name portion of the URL
|
|
272
|
+
const base = this.queueUrl.replace(/\/[^/]+$/, '')
|
|
273
|
+
return `${base}/${this.prefix}${queue}`
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Get the underlying SQSClient */
|
|
277
|
+
getClient(): any {
|
|
278
|
+
return this.sqs
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { QueueDriver } from '../contracts/QueueDriver.ts'
|
|
2
|
+
import type {
|
|
3
|
+
QueuedJob,
|
|
4
|
+
FailedJob,
|
|
5
|
+
SerializedPayload,
|
|
6
|
+
BatchRecord,
|
|
7
|
+
} from '../contracts/JobContract.ts'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* In-memory synchronous queue driver.
|
|
11
|
+
* Jobs are stored in memory — useful for development, testing,
|
|
12
|
+
* and situations where immediate execution is acceptable.
|
|
13
|
+
*/
|
|
14
|
+
export class SyncDriver implements QueueDriver {
|
|
15
|
+
private queues = new Map<string, QueuedJob[]>()
|
|
16
|
+
private failedJobs: FailedJob[] = []
|
|
17
|
+
private batches = new Map<string, BatchRecord>()
|
|
18
|
+
private nextId = 1
|
|
19
|
+
|
|
20
|
+
// ── Core job operations ──────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
async push(payload: SerializedPayload, queue: string, delay = 0): Promise<string | number> {
|
|
23
|
+
const id = this.nextId++
|
|
24
|
+
const now = Math.floor(Date.now() / 1000)
|
|
25
|
+
const job: QueuedJob = {
|
|
26
|
+
id,
|
|
27
|
+
queue,
|
|
28
|
+
payload,
|
|
29
|
+
attempts: 0,
|
|
30
|
+
reservedAt: null,
|
|
31
|
+
availableAt: now + delay,
|
|
32
|
+
createdAt: now,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!this.queues.has(queue)) this.queues.set(queue, [])
|
|
36
|
+
this.queues.get(queue)!.push(job)
|
|
37
|
+
return id
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async pop(queue: string): Promise<QueuedJob | null> {
|
|
41
|
+
const jobs = this.queues.get(queue)
|
|
42
|
+
if (!jobs || jobs.length === 0) return null
|
|
43
|
+
|
|
44
|
+
const now = Math.floor(Date.now() / 1000)
|
|
45
|
+
const idx = jobs.findIndex((j) => j.reservedAt === null && j.availableAt <= now)
|
|
46
|
+
if (idx === -1) return null
|
|
47
|
+
|
|
48
|
+
const job = jobs[idx]!
|
|
49
|
+
job.reservedAt = now
|
|
50
|
+
job.attempts++
|
|
51
|
+
return job
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async delete(job: QueuedJob): Promise<void> {
|
|
55
|
+
const jobs = this.queues.get(job.queue)
|
|
56
|
+
if (!jobs) return
|
|
57
|
+
const idx = jobs.findIndex((j) => j.id === job.id)
|
|
58
|
+
if (idx !== -1) jobs.splice(idx, 1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async release(job: QueuedJob, delay: number): Promise<void> {
|
|
62
|
+
job.reservedAt = null
|
|
63
|
+
job.availableAt = Math.floor(Date.now() / 1000) + delay
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async size(queue: string): Promise<number> {
|
|
67
|
+
return this.queues.get(queue)?.length ?? 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async clear(queue: string): Promise<void> {
|
|
71
|
+
this.queues.delete(queue)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Failed jobs ──────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
async fail(job: QueuedJob, error: Error): Promise<void> {
|
|
77
|
+
await this.delete(job)
|
|
78
|
+
this.failedJobs.push({
|
|
79
|
+
id: job.id,
|
|
80
|
+
queue: job.queue,
|
|
81
|
+
payload: job.payload,
|
|
82
|
+
exception: `${error.name}: ${error.message}\n${error.stack ?? ''}`,
|
|
83
|
+
failedAt: Math.floor(Date.now() / 1000),
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getFailedJobs(): Promise<FailedJob[]> {
|
|
88
|
+
return [...this.failedJobs]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async findFailedJob(id: string | number): Promise<FailedJob | null> {
|
|
92
|
+
return this.failedJobs.find((j) => j.id === id) ?? null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async forgetFailedJob(id: string | number): Promise<boolean> {
|
|
96
|
+
const idx = this.failedJobs.findIndex((j) => j.id === id)
|
|
97
|
+
if (idx === -1) return false
|
|
98
|
+
this.failedJobs.splice(idx, 1)
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async flushFailedJobs(): Promise<void> {
|
|
103
|
+
this.failedJobs = []
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Batch support ────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
async createBatch(batch: BatchRecord): Promise<string> {
|
|
109
|
+
this.batches.set(batch.id, { ...batch })
|
|
110
|
+
return batch.id
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async findBatch(id: string): Promise<BatchRecord | null> {
|
|
114
|
+
const b = this.batches.get(id)
|
|
115
|
+
return b ? { ...b, failedJobIds: [...b.failedJobIds], options: { ...b.options } } : null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async updateBatchProgress(id: string, processed: number, failed: number): Promise<BatchRecord | null> {
|
|
119
|
+
const b = this.batches.get(id)
|
|
120
|
+
if (!b) return null
|
|
121
|
+
b.processedJobs += processed
|
|
122
|
+
b.failedJobs += failed
|
|
123
|
+
return { ...b, failedJobIds: [...b.failedJobIds], options: { ...b.options } }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async markBatchFinished(id: string): Promise<void> {
|
|
127
|
+
const b = this.batches.get(id)
|
|
128
|
+
if (b) b.finishedAt = Math.floor(Date.now() / 1000)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async cancelBatch(id: string): Promise<void> {
|
|
132
|
+
const b = this.batches.get(id)
|
|
133
|
+
if (b) b.cancelledAt = Math.floor(Date.now() / 1000)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async pruneBatches(olderThanSeconds: number): Promise<void> {
|
|
137
|
+
const cutoff = Math.floor(Date.now() / 1000) - olderThanSeconds
|
|
138
|
+
for (const [id, b] of this.batches) {
|
|
139
|
+
if (b.createdAt < cutoff) this.batches.delete(id)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MantiqError } from '@mantiq/core'
|
|
2
|
+
|
|
3
|
+
/** Base error for all queue-related failures */
|
|
4
|
+
export class QueueError extends MantiqError {
|
|
5
|
+
constructor(message: string, context?: Record<string, any>) {
|
|
6
|
+
super(message, context)
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Thrown when a job exceeds its timeout */
|
|
11
|
+
export class JobTimeoutError extends QueueError {
|
|
12
|
+
constructor(jobName: string, timeout: number) {
|
|
13
|
+
super(`Job "${jobName}" exceeded timeout of ${timeout}s`, { jobName, timeout })
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Thrown when a job exhausts all retry attempts */
|
|
18
|
+
export class MaxAttemptsExceededError extends QueueError {
|
|
19
|
+
constructor(jobName: string, maxTries: number) {
|
|
20
|
+
super(`Job "${jobName}" has been attempted ${maxTries} times`, { jobName, maxTries })
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Event } from '@mantiq/core'
|
|
2
|
+
import type { SerializedPayload } from '../contracts/JobContract.ts'
|
|
3
|
+
|
|
4
|
+
/** Fired just before a job's handle() method is called */
|
|
5
|
+
export class JobProcessing extends Event {
|
|
6
|
+
constructor(public readonly payload: SerializedPayload) {
|
|
7
|
+
super()
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Fired after a job completes successfully */
|
|
12
|
+
export class JobProcessed extends Event {
|
|
13
|
+
constructor(public readonly payload: SerializedPayload) {
|
|
14
|
+
super()
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Fired when a job permanently fails (exhausted all retries) */
|
|
19
|
+
export class JobFailed extends Event {
|
|
20
|
+
constructor(
|
|
21
|
+
public readonly payload: SerializedPayload,
|
|
22
|
+
public readonly error: Error,
|
|
23
|
+
) {
|
|
24
|
+
super()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Fired when a job throws an exception (may still be retried) */
|
|
29
|
+
export class JobExceptionOccurred extends Event {
|
|
30
|
+
constructor(
|
|
31
|
+
public readonly payload: SerializedPayload,
|
|
32
|
+
public readonly error: Error,
|
|
33
|
+
) {
|
|
34
|
+
super()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Job } from '../Job.ts'
|
|
2
|
+
import type { QueueManager } from '../QueueManager.ts'
|
|
3
|
+
import { PendingDispatch } from '../PendingDispatch.ts'
|
|
4
|
+
import { Chain } from '../JobChain.ts'
|
|
5
|
+
import { PendingBatch } from '../JobBatch.ts'
|
|
6
|
+
|
|
7
|
+
export const QUEUE_MANAGER = Symbol('QueueManager')
|
|
8
|
+
|
|
9
|
+
let _manager: QueueManager | null = null
|
|
10
|
+
|
|
11
|
+
export function setQueueManager(manager: QueueManager): void {
|
|
12
|
+
_manager = manager
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getQueueManager(): QueueManager {
|
|
16
|
+
if (!_manager) throw new Error('QueueManager not initialized. Call setQueueManager() first.')
|
|
17
|
+
return _manager
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Dispatch a job to the queue.
|
|
22
|
+
* Returns a thenable PendingDispatch for fluent configuration.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* await dispatch(new ProcessPayment(order))
|
|
27
|
+
* await dispatch(new ProcessPayment(order)).delay(60).onQueue('payments')
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function dispatch(job: Job): PendingDispatch {
|
|
31
|
+
return new PendingDispatch(job)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get a queue driver instance by connection name.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const size = await queue().size('default')
|
|
40
|
+
* const size = await queue('redis').size('default')
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function queue(connection?: string) {
|
|
44
|
+
return getQueueManager().driver(connection)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Bus provides static methods for dispatching chains and batches.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* // Chain
|
|
53
|
+
* await Bus.chain([
|
|
54
|
+
* new ProcessPodcast(podcast),
|
|
55
|
+
* new OptimizeAudio(podcast),
|
|
56
|
+
* new PublishPodcast(podcast),
|
|
57
|
+
* ]).catch(new NotifyFailure(podcast)).dispatch()
|
|
58
|
+
*
|
|
59
|
+
* // Batch
|
|
60
|
+
* const batch = await Bus.batch([
|
|
61
|
+
* new ImportChunk(file, 0, 1000),
|
|
62
|
+
* new ImportChunk(file, 1000, 2000),
|
|
63
|
+
* ]).then(new NotifyComplete(file)).dispatch()
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export const Bus = {
|
|
67
|
+
/** Create a job chain (sequential execution) */
|
|
68
|
+
chain(jobs: Job[]): Chain {
|
|
69
|
+
return Chain.of(jobs)
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/** Create a job batch (parallel execution with progress) */
|
|
73
|
+
batch(jobs: Job[]): PendingBatch {
|
|
74
|
+
return PendingBatch.of(jobs)
|
|
75
|
+
},
|
|
76
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// @mantiq/queue — public API exports
|
|
2
|
+
|
|
3
|
+
// Core
|
|
4
|
+
export { Job } from './Job.ts'
|
|
5
|
+
export { QueueManager } from './QueueManager.ts'
|
|
6
|
+
export type { QueueConfig, QueueConnectionConfig } from './QueueManager.ts'
|
|
7
|
+
export { Worker } from './Worker.ts'
|
|
8
|
+
export type { WorkerOptions } from './Worker.ts'
|
|
9
|
+
|
|
10
|
+
// Dispatch
|
|
11
|
+
export { PendingDispatch } from './PendingDispatch.ts'
|
|
12
|
+
export { Chain } from './JobChain.ts'
|
|
13
|
+
export { Batch, PendingBatch } from './JobBatch.ts'
|
|
14
|
+
|
|
15
|
+
// Registry
|
|
16
|
+
export {
|
|
17
|
+
registerJob,
|
|
18
|
+
registerJobs,
|
|
19
|
+
resolveJob,
|
|
20
|
+
getRegisteredJobs,
|
|
21
|
+
clearJobRegistry,
|
|
22
|
+
} from './JobRegistry.ts'
|
|
23
|
+
|
|
24
|
+
// Contracts
|
|
25
|
+
export type { QueueDriver } from './contracts/QueueDriver.ts'
|
|
26
|
+
export type {
|
|
27
|
+
SerializedPayload,
|
|
28
|
+
QueuedJob,
|
|
29
|
+
FailedJob,
|
|
30
|
+
BatchRecord,
|
|
31
|
+
BatchOptions,
|
|
32
|
+
Constructor,
|
|
33
|
+
} from './contracts/JobContract.ts'
|
|
34
|
+
|
|
35
|
+
// Drivers
|
|
36
|
+
export { SyncDriver } from './drivers/SyncDriver.ts'
|
|
37
|
+
export { SQLiteDriver } from './drivers/SQLiteDriver.ts'
|
|
38
|
+
export { RedisDriver } from './drivers/RedisDriver.ts'
|
|
39
|
+
export type { RedisQueueConfig } from './drivers/RedisDriver.ts'
|
|
40
|
+
export { SqsDriver } from './drivers/SqsDriver.ts'
|
|
41
|
+
export type { SqsQueueConfig } from './drivers/SqsDriver.ts'
|
|
42
|
+
export { KafkaDriver } from './drivers/KafkaDriver.ts'
|
|
43
|
+
export type { KafkaQueueConfig } from './drivers/KafkaDriver.ts'
|
|
44
|
+
|
|
45
|
+
// Events
|
|
46
|
+
export {
|
|
47
|
+
JobProcessing,
|
|
48
|
+
JobProcessed,
|
|
49
|
+
JobFailed,
|
|
50
|
+
JobExceptionOccurred,
|
|
51
|
+
} from './events/QueueEvents.ts'
|
|
52
|
+
|
|
53
|
+
// Errors
|
|
54
|
+
export {
|
|
55
|
+
QueueError,
|
|
56
|
+
JobTimeoutError,
|
|
57
|
+
MaxAttemptsExceededError,
|
|
58
|
+
} from './errors/QueueError.ts'
|
|
59
|
+
|
|
60
|
+
// Helpers
|
|
61
|
+
export {
|
|
62
|
+
dispatch,
|
|
63
|
+
queue,
|
|
64
|
+
Bus,
|
|
65
|
+
QUEUE_MANAGER,
|
|
66
|
+
setQueueManager,
|
|
67
|
+
getQueueManager,
|
|
68
|
+
} from './helpers/queue.ts'
|
|
69
|
+
|
|
70
|
+
// Service Provider
|
|
71
|
+
export { QueueServiceProvider, createQueueManager } from './QueueServiceProvider.ts'
|
|
72
|
+
|
|
73
|
+
// Schedule
|
|
74
|
+
export { Schedule, ScheduleEntry } from './schedule/Schedule.ts'
|
|
75
|
+
|
|
76
|
+
// Testing
|
|
77
|
+
export { QueueFake } from './testing/QueueFake.ts'
|
|
78
|
+
|
|
79
|
+
// Commands
|
|
80
|
+
export { QueueWorkCommand } from './commands/QueueWorkCommand.ts'
|
|
81
|
+
export { QueueRetryCommand } from './commands/QueueRetryCommand.ts'
|
|
82
|
+
export { QueueFailedCommand } from './commands/QueueFailedCommand.ts'
|
|
83
|
+
export { QueueFlushCommand } from './commands/QueueFlushCommand.ts'
|
|
84
|
+
export { MakeJobCommand } from './commands/MakeJobCommand.ts'
|
|
85
|
+
export { ScheduleRunCommand } from './commands/ScheduleRunCommand.ts'
|