@platformatic/job-queue 0.0.1 → 0.1.0-alpha.3
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 +511 -0
- package/dist/consumer.d.ts +43 -0
- package/dist/consumer.d.ts.map +1 -0
- package/dist/consumer.js +214 -0
- package/dist/consumer.js.map +1 -0
- package/dist/errors.d.ts +60 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +141 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/producer.d.ts +39 -0
- package/dist/producer.d.ts.map +1 -0
- package/dist/producer.js +163 -0
- package/dist/producer.js.map +1 -0
- package/dist/queue.d.ts +42 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +153 -0
- package/dist/queue.js.map +1 -0
- package/dist/reaper.d.ts +52 -0
- package/dist/reaper.d.ts.map +1 -0
- package/dist/reaper.js +376 -0
- package/dist/reaper.js.map +1 -0
- package/dist/serde/index.d.ts +19 -0
- package/dist/serde/index.d.ts.map +1 -0
- package/dist/serde/index.js +18 -0
- package/dist/serde/index.js.map +1 -0
- package/dist/storage/file.d.ts +44 -0
- package/dist/storage/file.d.ts.map +1 -0
- package/dist/storage/file.js +601 -0
- package/dist/storage/file.js.map +1 -0
- package/dist/storage/memory.d.ts +39 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +292 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/redis.d.ts +47 -0
- package/dist/storage/redis.d.ts.map +1 -0
- package/dist/storage/redis.js +317 -0
- package/dist/storage/redis.js.map +1 -0
- package/dist/storage/types.d.ts +179 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +2 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/types.d.ts +126 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/id.d.ts +10 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +18 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/state.d.ts +13 -0
- package/dist/utils/state.d.ts.map +1 -0
- package/dist/utils/state.js +22 -0
- package/dist/utils/state.js.map +1 -0
- package/package.json +45 -9
package/README.md
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
# @platformatic/job-queue
|
|
2
|
+
|
|
3
|
+
A reliable job queue for Node.js with deduplication, request/response support, and pluggable storage backends.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Deduplication** - Prevents duplicate job processing with configurable result caching
|
|
8
|
+
- **Request/Response** - Enqueue jobs and wait for results with `enqueueAndWait()`
|
|
9
|
+
- **Multiple Storage Backends** - Redis/Valkey, filesystem, or in-memory
|
|
10
|
+
- **Automatic Retries** - Configurable retry attempts with exponential backoff
|
|
11
|
+
- **Stalled Job Recovery** - Reaper automatically recovers jobs from crashed workers
|
|
12
|
+
- **Graceful Shutdown** - Complete in-flight jobs before stopping
|
|
13
|
+
- **TypeScript Native** - Full type safety with generic payload and result types
|
|
14
|
+
- **Node.js 22.19+** - Uses native TypeScript type stripping
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @platformatic/job-queue
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { Queue, MemoryStorage } from '@platformatic/job-queue'
|
|
26
|
+
|
|
27
|
+
// Create a queue with in-memory storage
|
|
28
|
+
const storage = new MemoryStorage()
|
|
29
|
+
const queue = new Queue<{ email: string }, { sent: boolean }>({
|
|
30
|
+
storage,
|
|
31
|
+
concurrency: 5
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Register a job handler
|
|
35
|
+
queue.execute(async (job) => {
|
|
36
|
+
console.log(`Processing job ${job.id}:`, job.payload)
|
|
37
|
+
// Send email...
|
|
38
|
+
return { sent: true }
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Start the queue
|
|
42
|
+
await queue.start()
|
|
43
|
+
|
|
44
|
+
// Enqueue jobs
|
|
45
|
+
await queue.enqueue('email-1', { email: 'user@example.com' })
|
|
46
|
+
|
|
47
|
+
// Or wait for the result
|
|
48
|
+
const result = await queue.enqueueAndWait('email-2', { email: 'another@example.com' }, {
|
|
49
|
+
timeout: 30000
|
|
50
|
+
})
|
|
51
|
+
console.log('Result:', result) // { sent: true }
|
|
52
|
+
|
|
53
|
+
// Graceful shutdown
|
|
54
|
+
await queue.stop()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API Reference
|
|
58
|
+
|
|
59
|
+
### Queue
|
|
60
|
+
|
|
61
|
+
The main class that combines producer and consumer functionality.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { Queue } from '@platformatic/job-queue'
|
|
65
|
+
|
|
66
|
+
const queue = new Queue<TPayload, TResult>(config)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### Configuration
|
|
70
|
+
|
|
71
|
+
| Option | Type | Default | Description |
|
|
72
|
+
|--------|------|---------|-------------|
|
|
73
|
+
| `storage` | `Storage` | *required* | Storage backend instance |
|
|
74
|
+
| `workerId` | `string` | `uuid()` | Unique identifier for this worker |
|
|
75
|
+
| `concurrency` | `number` | `1` | Number of jobs to process in parallel |
|
|
76
|
+
| `maxRetries` | `number` | `3` | Maximum retry attempts for failed jobs |
|
|
77
|
+
| `blockTimeout` | `number` | `5` | Seconds to wait when polling for jobs |
|
|
78
|
+
| `visibilityTimeout` | `number` | `30000` | Milliseconds before a processing job is considered stalled |
|
|
79
|
+
| `resultTTL` | `number` | `3600000` | Milliseconds to cache job results (1 hour) |
|
|
80
|
+
| `payloadSerde` | `Serde<TPayload>` | `JsonSerde` | Custom serializer for job payloads |
|
|
81
|
+
| `resultSerde` | `Serde<TResult>` | `JsonSerde` | Custom serializer for job results |
|
|
82
|
+
|
|
83
|
+
#### Methods
|
|
84
|
+
|
|
85
|
+
##### `queue.start(): Promise<void>`
|
|
86
|
+
|
|
87
|
+
Connect to storage and start processing jobs.
|
|
88
|
+
|
|
89
|
+
##### `queue.stop(): Promise<void>`
|
|
90
|
+
|
|
91
|
+
Gracefully stop processing. Waits for in-flight jobs to complete.
|
|
92
|
+
|
|
93
|
+
##### `queue.execute(handler): void`
|
|
94
|
+
|
|
95
|
+
Register a job handler. Call before or after `start()`.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
queue.execute(async (job) => {
|
|
99
|
+
// job.id - unique job identifier
|
|
100
|
+
// job.payload - the job data
|
|
101
|
+
// job.attempts - current attempt number (starts at 1)
|
|
102
|
+
// job.signal - AbortSignal for cancellation
|
|
103
|
+
return result
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
##### `queue.enqueue(id, payload, options?): Promise<EnqueueResult>`
|
|
108
|
+
|
|
109
|
+
Enqueue a job (fire-and-forget).
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
const result = await queue.enqueue('job-123', { data: 'value' })
|
|
113
|
+
|
|
114
|
+
// result.status can be:
|
|
115
|
+
// - 'queued': Job was added to the queue
|
|
116
|
+
// - 'duplicate': Job with this ID already exists
|
|
117
|
+
// - 'completed': Job already completed (returns cached result)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
##### `queue.enqueueAndWait(id, payload, options?): Promise<TResult>`
|
|
121
|
+
|
|
122
|
+
Enqueue a job and wait for the result.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const result = await queue.enqueueAndWait('job-123', payload, {
|
|
126
|
+
timeout: 30000, // Timeout in milliseconds
|
|
127
|
+
maxAttempts: 5 // Override default max retries
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Throws `TimeoutError` if the job doesn't complete within the timeout.
|
|
132
|
+
|
|
133
|
+
##### `queue.cancel(id): Promise<CancelResult>`
|
|
134
|
+
|
|
135
|
+
Cancel a queued job.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const result = await queue.cancel('job-123')
|
|
139
|
+
|
|
140
|
+
// result.status can be:
|
|
141
|
+
// - 'cancelled': Job was successfully cancelled
|
|
142
|
+
// - 'not_found': Job doesn't exist
|
|
143
|
+
// - 'processing': Job is currently being processed (cannot cancel)
|
|
144
|
+
// - 'completed': Job already completed
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
##### `queue.getResult(id): Promise<TResult | null>`
|
|
148
|
+
|
|
149
|
+
Get the cached result of a completed job.
|
|
150
|
+
|
|
151
|
+
##### `queue.getStatus(id): Promise<MessageStatus | null>`
|
|
152
|
+
|
|
153
|
+
Get the current status of a job.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const status = await queue.getStatus('job-123')
|
|
157
|
+
// {
|
|
158
|
+
// id: 'job-123',
|
|
159
|
+
// state: 'completed', // 'queued' | 'processing' | 'failing' | 'completed' | 'failed'
|
|
160
|
+
// createdAt: 1234567890,
|
|
161
|
+
// attempts: 1,
|
|
162
|
+
// result?: { ... },
|
|
163
|
+
// error?: { message: 'Error message', code?: 'ERROR_CODE', stack?: '...' }
|
|
164
|
+
// }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### Events
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
// Lifecycle events
|
|
171
|
+
queue.on('started', () => {
|
|
172
|
+
console.log('Queue started')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
queue.on('stopped', () => {
|
|
176
|
+
console.log('Queue stopped')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// Job events
|
|
180
|
+
queue.on('enqueued', (id) => {
|
|
181
|
+
console.log(`Job ${id} was enqueued`)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
queue.on('completed', (id, result) => {
|
|
185
|
+
console.log(`Job ${id} completed:`, result)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
queue.on('failed', (id, error) => {
|
|
189
|
+
console.log(`Job ${id} failed:`, error.message)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
queue.on('failing', (id, error, attempt) => {
|
|
193
|
+
console.log(`Job ${id} failed attempt ${attempt}, will retry:`, error.message)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
queue.on('requeued', (id) => {
|
|
197
|
+
console.log(`Job ${id} was returned to queue (e.g., during graceful shutdown)`)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
queue.on('cancelled', (id) => {
|
|
201
|
+
console.log(`Job ${id} was cancelled`)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Error events
|
|
205
|
+
queue.on('error', (error) => {
|
|
206
|
+
console.error('Queue error:', error)
|
|
207
|
+
})
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Storage Backends
|
|
211
|
+
|
|
212
|
+
#### MemoryStorage
|
|
213
|
+
|
|
214
|
+
In-memory storage for development and testing.
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import { MemoryStorage } from '@platformatic/job-queue'
|
|
218
|
+
|
|
219
|
+
const storage = new MemoryStorage()
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### RedisStorage
|
|
223
|
+
|
|
224
|
+
Production-ready storage using Redis or Valkey.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { RedisStorage } from '@platformatic/job-queue'
|
|
228
|
+
|
|
229
|
+
const storage = new RedisStorage({
|
|
230
|
+
url: 'redis://localhost:6379',
|
|
231
|
+
keyPrefix: 'myapp:' // Optional prefix for all keys
|
|
232
|
+
})
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Features:
|
|
236
|
+
- Atomic operations via Lua scripts
|
|
237
|
+
- Blocking dequeue with `BLMOVE`
|
|
238
|
+
- Pub/sub for real-time notifications
|
|
239
|
+
- Compatible with Redis 7+ and Valkey 8+
|
|
240
|
+
|
|
241
|
+
#### FileStorage
|
|
242
|
+
|
|
243
|
+
Filesystem-based storage for single-node deployments.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { FileStorage } from '@platformatic/job-queue'
|
|
247
|
+
|
|
248
|
+
const storage = new FileStorage({
|
|
249
|
+
basePath: '/var/lib/myapp/queue'
|
|
250
|
+
})
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Features:
|
|
254
|
+
- Atomic writes with `fast-write-atomic`
|
|
255
|
+
- FIFO ordering via sequence numbers
|
|
256
|
+
- `fs.watch` for real-time notifications
|
|
257
|
+
- Survives process restarts
|
|
258
|
+
|
|
259
|
+
### Reaper
|
|
260
|
+
|
|
261
|
+
The Reaper monitors for stalled jobs and requeues them. Use when running multiple workers.
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { Reaper } from '@platformatic/job-queue'
|
|
265
|
+
|
|
266
|
+
const reaper = new Reaper({
|
|
267
|
+
storage,
|
|
268
|
+
visibilityTimeout: 30000 // Same as your queue's visibilityTimeout
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
await reaper.start()
|
|
272
|
+
|
|
273
|
+
reaper.on('stalled', (id) => {
|
|
274
|
+
console.log(`Job ${id} was stalled and requeued`)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
// On shutdown
|
|
278
|
+
await reaper.stop()
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The Reaper uses event-based monitoring: it subscribes to job state changes and sets per-job timers. An initial scan at startup catches any jobs that were processing before the Reaper started.
|
|
282
|
+
|
|
283
|
+
#### Leader Election
|
|
284
|
+
|
|
285
|
+
For high availability, you can run multiple Reaper instances with leader election enabled. Only one instance will be active at a time, and if it fails, another will automatically take over.
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
const reaper = new Reaper({
|
|
289
|
+
storage,
|
|
290
|
+
visibilityTimeout: 30000,
|
|
291
|
+
leaderElection: {
|
|
292
|
+
enabled: true, // Enable leader election (default: false)
|
|
293
|
+
lockTTL: 30000, // Lock expiry in ms (default: 30s)
|
|
294
|
+
renewalInterval: 10000, // Renew lock every 10s (default: 1/3 of TTL)
|
|
295
|
+
acquireRetryInterval: 5000 // Followers retry every 5s (default: 5s)
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
reaper.on('leadershipAcquired', () => {
|
|
300
|
+
console.log('This reaper is now the leader')
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
reaper.on('leadershipLost', () => {
|
|
304
|
+
console.log('This reaper lost leadership')
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
await reaper.start()
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Leader election uses Redis's `SET NX PX` pattern for atomic lock acquisition:
|
|
311
|
+
- The leader acquires a lock with a TTL and renews it periodically
|
|
312
|
+
- If the leader stops gracefully, it releases the lock immediately
|
|
313
|
+
- If the leader crashes, the lock expires and a follower takes over
|
|
314
|
+
- Only RedisStorage supports leader election; other storage backends will emit an error
|
|
315
|
+
|
|
316
|
+
### Custom Serialization
|
|
317
|
+
|
|
318
|
+
Implement the `Serde` interface for custom serialization:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
import { Serde } from '@platformatic/job-queue'
|
|
322
|
+
import * as msgpack from 'msgpackr'
|
|
323
|
+
|
|
324
|
+
class MsgPackSerde<T> implements Serde<T> {
|
|
325
|
+
serialize(value: T): Buffer {
|
|
326
|
+
return msgpack.pack(value)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
deserialize(buffer: Buffer): T {
|
|
330
|
+
return msgpack.unpack(buffer) as T
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const queue = new Queue({
|
|
335
|
+
storage,
|
|
336
|
+
payloadSerde: new MsgPackSerde(),
|
|
337
|
+
resultSerde: new MsgPackSerde()
|
|
338
|
+
})
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Patterns
|
|
342
|
+
|
|
343
|
+
### Producer/Consumer Separation
|
|
344
|
+
|
|
345
|
+
Run producers and consumers as separate processes:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// producer.ts
|
|
349
|
+
import { Queue, RedisStorage } from '@platformatic/job-queue'
|
|
350
|
+
|
|
351
|
+
const storage = new RedisStorage({ url: process.env.REDIS_URL })
|
|
352
|
+
const producer = new Queue({ storage })
|
|
353
|
+
|
|
354
|
+
await producer.start()
|
|
355
|
+
await producer.enqueue('task-1', { ... })
|
|
356
|
+
await producer.stop()
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
// worker.ts
|
|
361
|
+
import { Queue, RedisStorage, Reaper } from '@platformatic/job-queue'
|
|
362
|
+
|
|
363
|
+
const storage = new RedisStorage({ url: process.env.REDIS_URL })
|
|
364
|
+
const queue = new Queue({
|
|
365
|
+
storage,
|
|
366
|
+
workerId: `worker-${process.pid}`,
|
|
367
|
+
concurrency: 10
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
const reaper = new Reaper({
|
|
371
|
+
storage,
|
|
372
|
+
visibilityTimeout: 30000
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
queue.execute(async (job) => {
|
|
376
|
+
// Process job
|
|
377
|
+
return result
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
await queue.start()
|
|
381
|
+
await reaper.start()
|
|
382
|
+
|
|
383
|
+
process.on('SIGTERM', async () => {
|
|
384
|
+
await queue.stop()
|
|
385
|
+
await reaper.stop()
|
|
386
|
+
})
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Request/Response with Timeout
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
try {
|
|
393
|
+
const result = await queue.enqueueAndWait('request-1', payload, {
|
|
394
|
+
timeout: 10000
|
|
395
|
+
})
|
|
396
|
+
console.log('Got result:', result)
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (error instanceof TimeoutError) {
|
|
399
|
+
console.log('Request timed out')
|
|
400
|
+
} else if (error instanceof JobFailedError) {
|
|
401
|
+
console.log('Job failed:', error.originalError)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Graceful Shutdown
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
const queue = new Queue({
|
|
410
|
+
storage,
|
|
411
|
+
visibilityTimeout: 30000 // Jobs have 30s to complete
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
queue.execute(async (job) => {
|
|
415
|
+
// Check for cancellation
|
|
416
|
+
if (job.signal.aborted) {
|
|
417
|
+
throw new Error('Job cancelled')
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Long-running work...
|
|
421
|
+
await doWork()
|
|
422
|
+
|
|
423
|
+
return result
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
await queue.start()
|
|
427
|
+
|
|
428
|
+
// Handle shutdown signals
|
|
429
|
+
process.on('SIGTERM', async () => {
|
|
430
|
+
console.log('Shutting down...')
|
|
431
|
+
await queue.stop() // Waits for in-flight jobs
|
|
432
|
+
process.exit(0)
|
|
433
|
+
})
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
## Error Handling
|
|
437
|
+
|
|
438
|
+
The library exports typed errors for specific failure conditions:
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
import {
|
|
442
|
+
TimeoutError, // enqueueAndWait timeout
|
|
443
|
+
MaxRetriesError, // Job failed after all retries
|
|
444
|
+
JobNotFoundError, // Job doesn't exist
|
|
445
|
+
JobCancelledError, // Job was cancelled
|
|
446
|
+
JobFailedError, // Job failed with error
|
|
447
|
+
StorageError // Storage backend error
|
|
448
|
+
} from '@platformatic/job-queue'
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Benchmarks
|
|
452
|
+
|
|
453
|
+
Run request/response benchmarks over Redis:
|
|
454
|
+
|
|
455
|
+
```bash
|
|
456
|
+
# With Redis
|
|
457
|
+
npm run bench:redis
|
|
458
|
+
|
|
459
|
+
# With Valkey
|
|
460
|
+
npm run bench:valkey
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Sample output:
|
|
464
|
+
|
|
465
|
+
```
|
|
466
|
+
Single Request Baseline (100 requests, concurrency=1)
|
|
467
|
+
──────────────────────────────────────────────────
|
|
468
|
+
Requests: 100
|
|
469
|
+
Throughput: 312.50 req/s
|
|
470
|
+
Latency:
|
|
471
|
+
min: 2.15 ms
|
|
472
|
+
p50: 3.02 ms
|
|
473
|
+
p95: 4.21 ms
|
|
474
|
+
p99: 5.43 ms
|
|
475
|
+
max: 6.12 ms
|
|
476
|
+
avg: 3.20 ms
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
## Testing
|
|
480
|
+
|
|
481
|
+
Run tests against different backends:
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
# Memory storage only
|
|
485
|
+
npm run test:memory
|
|
486
|
+
|
|
487
|
+
# With Redis (requires Redis on localhost:6379)
|
|
488
|
+
npm run test:redis
|
|
489
|
+
|
|
490
|
+
# With Valkey (requires Valkey on localhost:6380)
|
|
491
|
+
npm run test:valkey
|
|
492
|
+
|
|
493
|
+
# All backends
|
|
494
|
+
npm test
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Start test infrastructure with Docker:
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
npm run docker:up # Start Redis and Valkey
|
|
501
|
+
npm run docker:down # Stop containers
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
## Requirements
|
|
505
|
+
|
|
506
|
+
- Node.js 22.19.0 or later (for native TypeScript support)
|
|
507
|
+
- Redis 7+ or Valkey 8+ (for RedisStorage)
|
|
508
|
+
|
|
509
|
+
## License
|
|
510
|
+
|
|
511
|
+
Apache 2.0
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { Storage } from './storage/types.ts';
|
|
3
|
+
import type { Serde } from './serde/index.ts';
|
|
4
|
+
import type { JobHandler } from './types.ts';
|
|
5
|
+
interface ConsumerConfig<TPayload, TResult> {
|
|
6
|
+
storage: Storage;
|
|
7
|
+
workerId: string;
|
|
8
|
+
payloadSerde?: Serde<TPayload>;
|
|
9
|
+
resultSerde?: Serde<TResult>;
|
|
10
|
+
concurrency?: number;
|
|
11
|
+
blockTimeout?: number;
|
|
12
|
+
maxRetries?: number;
|
|
13
|
+
resultTTL?: number;
|
|
14
|
+
visibilityTimeout?: number;
|
|
15
|
+
}
|
|
16
|
+
interface ConsumerEvents<TResult> {
|
|
17
|
+
error: [error: Error];
|
|
18
|
+
completed: [id: string, result: TResult];
|
|
19
|
+
failed: [id: string, error: Error];
|
|
20
|
+
failing: [id: string, error: Error, attempt: number];
|
|
21
|
+
requeued: [id: string];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Consumer handles processing jobs from the queue
|
|
25
|
+
*/
|
|
26
|
+
export declare class Consumer<TPayload, TResult> extends EventEmitter<ConsumerEvents<TResult>> {
|
|
27
|
+
#private;
|
|
28
|
+
constructor(config: ConsumerConfig<TPayload, TResult>);
|
|
29
|
+
/**
|
|
30
|
+
* Register a job handler
|
|
31
|
+
*/
|
|
32
|
+
execute(handler: JobHandler<TPayload, TResult>): void;
|
|
33
|
+
/**
|
|
34
|
+
* Start consuming jobs
|
|
35
|
+
*/
|
|
36
|
+
start(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Stop consuming jobs gracefully
|
|
39
|
+
*/
|
|
40
|
+
stop(): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
43
|
+
//# sourceMappingURL=consumer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consumer.d.ts","sourceRoot":"","sources":["../src/consumer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAC7C,OAAO,KAAK,EAAqB,UAAU,EAAE,MAAM,YAAY,CAAA;AAI/D,UAAU,cAAc,CAAC,QAAQ,EAAE,OAAO;IACxC,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;IAC9B,WAAW,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,UAAU,cAAc,CAAC,OAAO;IAC9B,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;IACrB,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;IAClC,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;IACpD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;CACvB;AAED;;GAEG;AACH,qBAAa,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAE,SAAQ,YAAY,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;;gBAiBvE,MAAM,EAAE,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC;IAatD;;OAEG;IACH,OAAO,CAAE,OAAO,EAAE,UAAU,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,IAAI;IAItD;;OAEG;IACG,KAAK,IAAK,OAAO,CAAC,IAAI,CAAC;IAwB7B;;OAEG;IACG,IAAI,IAAK,OAAO,CAAC,IAAI,CAAC;CA6K7B"}
|