@boringnode/queue 0.2.0 → 0.3.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 +218 -361
- package/build/{chunk-RIXMQXYJ.js → chunk-LI2ZMCNO.js} +14 -1
- package/build/chunk-LI2ZMCNO.js.map +1 -0
- package/build/{chunk-NPQKBCCY.js → chunk-PBGPIFI5.js} +15 -1
- package/build/chunk-PBGPIFI5.js.map +1 -0
- package/build/{index-C0Xg6F4E.d.ts → index-BzPIqdx3.d.ts} +260 -3
- package/build/index.d.ts +6 -2
- package/build/index.js +218 -11
- package/build/index.js.map +1 -1
- package/build/src/contracts/adapter.d.ts +1 -1
- package/build/src/drivers/knex_adapter.d.ts +6 -3
- package/build/src/drivers/knex_adapter.js +90 -8
- package/build/src/drivers/knex_adapter.js.map +1 -1
- package/build/src/drivers/redis_adapter.d.ts +6 -3
- package/build/src/drivers/redis_adapter.js +335 -94
- package/build/src/drivers/redis_adapter.js.map +1 -1
- package/build/src/drivers/sync_adapter.d.ts +6 -3
- package/build/src/drivers/sync_adapter.js +14 -3
- package/build/src/drivers/sync_adapter.js.map +1 -1
- package/build/src/types/index.d.ts +1 -1
- package/build/src/types/main.d.ts +1 -1
- package/package.json +3 -2
- package/build/chunk-NPQKBCCY.js.map +0 -1
- package/build/chunk-RIXMQXYJ.js.map +0 -1
package/README.md
CHANGED
|
@@ -20,23 +20,22 @@ npm install @boringnode/queue
|
|
|
20
20
|
|
|
21
21
|
## Features
|
|
22
22
|
|
|
23
|
-
- **Multiple Queue Adapters**:
|
|
24
|
-
- **Type-Safe Jobs**:
|
|
25
|
-
- **Delayed Jobs**: Schedule jobs to run after a
|
|
26
|
-
- **Multiple Queues**: Organize jobs into different queues for better organization
|
|
27
|
-
- **Worker Management**: Process jobs with configurable concurrency
|
|
28
|
-
- **Auto-Discovery**: Automatically discover and register jobs from specified locations
|
|
23
|
+
- **Multiple Queue Adapters**: Redis, Knex (PostgreSQL, MySQL, SQLite), and Sync
|
|
24
|
+
- **Type-Safe Jobs**: TypeScript classes with typed payloads
|
|
25
|
+
- **Delayed Jobs**: Schedule jobs to run after a delay
|
|
29
26
|
- **Priority Queues**: Process high-priority jobs first
|
|
30
|
-
- **
|
|
31
|
-
- **Job
|
|
32
|
-
- **
|
|
27
|
+
- **Bulk Dispatch**: Efficiently dispatch thousands of jobs at once
|
|
28
|
+
- **Job Grouping**: Organize related jobs for monitoring
|
|
29
|
+
- **Retry with Backoff**: Exponential, linear, or fixed backoff strategies
|
|
30
|
+
- **Job Timeout**: Fail or retry jobs that exceed a time limit
|
|
31
|
+
- **Job History**: Retain completed/failed jobs for debugging
|
|
32
|
+
- **Scheduled Jobs**: Cron or interval-based recurring jobs
|
|
33
|
+
- **Auto-Discovery**: Automatically register jobs from specified locations
|
|
33
34
|
|
|
34
35
|
## Quick Start
|
|
35
36
|
|
|
36
37
|
### 1. Define a Job
|
|
37
38
|
|
|
38
|
-
Create a job by extending the `Job` class:
|
|
39
|
-
|
|
40
39
|
```typescript
|
|
41
40
|
import { Job } from '@boringnode/queue'
|
|
42
41
|
import type { JobOptions } from '@boringnode/queue/types'
|
|
@@ -51,542 +50,400 @@ export default class SendEmailJob extends Job<SendEmailPayload> {
|
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
async execute(): Promise<void> {
|
|
54
|
-
console.log(`
|
|
53
|
+
console.log(`Sending email to: ${this.payload.to}`)
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
56
|
```
|
|
58
57
|
|
|
59
|
-
>
|
|
60
|
-
>
|
|
61
|
-
|
|
58
|
+
> [!NOTE]
|
|
59
|
+
> The job name defaults to the class name (`SendEmailJob`). You can override it with `name: 'CustomName'` in options.
|
|
60
|
+
|
|
61
|
+
> [!WARNING]
|
|
62
|
+
> If you minify your code in production, class names may be mangled. Always specify `name` explicitly in your job options.
|
|
62
63
|
|
|
63
64
|
### 2. Configure the Queue Manager
|
|
64
65
|
|
|
65
66
|
```typescript
|
|
66
67
|
import { QueueManager } from '@boringnode/queue'
|
|
67
68
|
import { redis } from '@boringnode/queue/drivers/redis_adapter'
|
|
68
|
-
import { sync } from '@boringnode/queue/drivers/sync_adapter'
|
|
69
|
-
import { Redis } from 'ioredis'
|
|
70
|
-
|
|
71
|
-
const redisConnection = new Redis({
|
|
72
|
-
host: 'localhost',
|
|
73
|
-
port: 6379,
|
|
74
|
-
keyPrefix: 'boringnode::queue::',
|
|
75
|
-
db: 0,
|
|
76
|
-
})
|
|
77
69
|
|
|
78
|
-
|
|
70
|
+
await QueueManager.init({
|
|
79
71
|
default: 'redis',
|
|
80
|
-
|
|
81
72
|
adapters: {
|
|
82
|
-
|
|
83
|
-
redis: redis(redisConnection),
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
worker: {
|
|
87
|
-
concurrency: 5,
|
|
88
|
-
idleDelay: '2s',
|
|
73
|
+
redis: redis({ host: 'localhost', port: 6379 }),
|
|
89
74
|
},
|
|
90
|
-
|
|
91
75
|
locations: ['./app/jobs/**/*.ts'],
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
await QueueManager.init(config)
|
|
76
|
+
})
|
|
95
77
|
```
|
|
96
78
|
|
|
97
79
|
### 3. Dispatch Jobs
|
|
98
80
|
|
|
99
81
|
```typescript
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// Dispatch immediately
|
|
82
|
+
// Simple dispatch
|
|
103
83
|
await SendEmailJob.dispatch({ to: 'user@example.com' })
|
|
104
84
|
|
|
105
|
-
//
|
|
106
|
-
await SendEmailJob.dispatch({ to: 'user@example.com' })
|
|
85
|
+
// With options
|
|
86
|
+
await SendEmailJob.dispatch({ to: 'user@example.com' })
|
|
87
|
+
.toQueue('high-priority')
|
|
88
|
+
.priority(1)
|
|
89
|
+
.in('5m')
|
|
107
90
|
```
|
|
108
91
|
|
|
109
92
|
### 4. Start a Worker
|
|
110
93
|
|
|
111
|
-
Create a worker to process jobs:
|
|
112
|
-
|
|
113
94
|
```typescript
|
|
114
95
|
import { Worker } from '@boringnode/queue'
|
|
115
96
|
|
|
116
97
|
const worker = new Worker(config)
|
|
117
|
-
await worker.start(['default', 'email'
|
|
98
|
+
await worker.start(['default', 'email'])
|
|
118
99
|
```
|
|
119
100
|
|
|
120
|
-
##
|
|
101
|
+
## Bulk Dispatch
|
|
121
102
|
|
|
122
|
-
|
|
103
|
+
Efficiently dispatch thousands of jobs in a single batch operation:
|
|
123
104
|
|
|
124
105
|
```typescript
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
106
|
+
const { jobIds } = await SendEmailJob.dispatchMany([
|
|
107
|
+
{ to: 'user1@example.com' },
|
|
108
|
+
{ to: 'user2@example.com' },
|
|
109
|
+
{ to: 'user3@example.com' },
|
|
110
|
+
])
|
|
111
|
+
.group('newsletter-jan-2025')
|
|
112
|
+
.toQueue('emails')
|
|
113
|
+
.priority(3)
|
|
114
|
+
|
|
115
|
+
console.log(`Dispatched ${jobIds.length} jobs`)
|
|
116
|
+
```
|
|
128
117
|
|
|
129
|
-
|
|
130
|
-
adapters: {
|
|
131
|
-
[key: string]: QueueAdapter
|
|
132
|
-
}
|
|
118
|
+
This uses Redis MULTI/EXEC or SQL batch insert for optimal performance.
|
|
133
119
|
|
|
134
|
-
|
|
135
|
-
worker: {
|
|
136
|
-
concurrency: number
|
|
137
|
-
idleDelay: Duration
|
|
138
|
-
}
|
|
120
|
+
## Job Grouping
|
|
139
121
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
122
|
+
Organize related jobs together for monitoring and filtering:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// Group newsletter jobs
|
|
126
|
+
await SendEmailJob.dispatch({ to: 'user@example.com' })
|
|
127
|
+
.group('newsletter-jan-2025')
|
|
128
|
+
|
|
129
|
+
// Group with bulk dispatch
|
|
130
|
+
await SendEmailJob.dispatchMany(recipients)
|
|
131
|
+
.group('newsletter-jan-2025')
|
|
143
132
|
```
|
|
144
133
|
|
|
145
|
-
|
|
134
|
+
The `groupId` is stored with job data and accessible via `job.data.groupId`.
|
|
135
|
+
|
|
136
|
+
## Job History & Retention
|
|
146
137
|
|
|
147
|
-
|
|
138
|
+
Keep completed and failed jobs for debugging:
|
|
148
139
|
|
|
149
140
|
```typescript
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
141
|
+
export default class ImportantJob extends Job<Payload> {
|
|
142
|
+
static options: JobOptions = {
|
|
143
|
+
// Keep last 1000 completed jobs
|
|
144
|
+
removeOnComplete: { count: 1000 },
|
|
145
|
+
|
|
146
|
+
// Keep failed jobs for 7 days
|
|
147
|
+
removeOnFail: { age: '7d' },
|
|
148
|
+
}
|
|
157
149
|
}
|
|
158
150
|
```
|
|
159
151
|
|
|
160
|
-
|
|
152
|
+
<details>
|
|
153
|
+
<summary><strong>Retention options</strong></summary>
|
|
161
154
|
|
|
162
|
-
|
|
155
|
+
| Value | Behavior |
|
|
156
|
+
|-----------------------------|--------------------|
|
|
157
|
+
| `true` (default) | Remove immediately |
|
|
158
|
+
| `false` | Keep forever |
|
|
159
|
+
| `{ count: n }` | Keep last n jobs |
|
|
160
|
+
| `{ age: '7d' }` | Keep for duration |
|
|
161
|
+
| `{ count: 100, age: '1d' }` | Both limits apply |
|
|
163
162
|
|
|
164
|
-
|
|
163
|
+
Query job history:
|
|
165
164
|
|
|
166
165
|
```typescript
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
host: 'localhost',
|
|
172
|
-
port: 6379,
|
|
173
|
-
keyPrefix: 'boringnode::queue::',
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
const adapter = redis(redisConnection)
|
|
166
|
+
const job = await adapter.getJob('job-id', 'queue-name')
|
|
167
|
+
console.log(job.status) // 'completed' | 'failed'
|
|
168
|
+
console.log(job.finishedAt) // timestamp
|
|
169
|
+
console.log(job.error) // error message (if failed)
|
|
177
170
|
```
|
|
178
171
|
|
|
179
|
-
|
|
172
|
+
</details>
|
|
180
173
|
|
|
181
|
-
|
|
174
|
+
## Adapters
|
|
175
|
+
|
|
176
|
+
### Redis (recommended for production)
|
|
182
177
|
|
|
183
178
|
```typescript
|
|
184
|
-
import {
|
|
179
|
+
import { redis } from '@boringnode/queue/drivers/redis_adapter'
|
|
185
180
|
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
// With options
|
|
182
|
+
const adapter = redis({ host: 'localhost', port: 6379 })
|
|
188
183
|
|
|
189
|
-
|
|
184
|
+
// With existing ioredis instance
|
|
185
|
+
import { Redis } from 'ioredis'
|
|
186
|
+
const connection = new Redis({ host: 'localhost' })
|
|
187
|
+
const adapter = redis(connection)
|
|
188
|
+
```
|
|
190
189
|
|
|
191
|
-
|
|
190
|
+
### Knex (PostgreSQL, MySQL, SQLite)
|
|
192
191
|
|
|
193
192
|
```typescript
|
|
194
193
|
import { knex } from '@boringnode/queue/drivers/knex_adapter'
|
|
195
194
|
|
|
196
|
-
// With configuration (adapter manages connection lifecycle)
|
|
197
195
|
const adapter = knex({
|
|
198
196
|
client: 'pg',
|
|
199
|
-
connection: {
|
|
200
|
-
host: 'localhost',
|
|
201
|
-
port: 5432,
|
|
202
|
-
user: 'postgres',
|
|
203
|
-
password: 'postgres',
|
|
204
|
-
database: 'myapp',
|
|
205
|
-
},
|
|
197
|
+
connection: { host: 'localhost', database: 'myapp' },
|
|
206
198
|
})
|
|
199
|
+
```
|
|
207
200
|
|
|
208
|
-
|
|
209
|
-
|
|
201
|
+
<details>
|
|
202
|
+
<summary><strong>More Knex examples</strong></summary>
|
|
210
203
|
|
|
204
|
+
```typescript
|
|
205
|
+
// With existing Knex instance
|
|
206
|
+
import Knex from 'knex'
|
|
211
207
|
const connection = Knex({ client: 'pg', connection: '...' })
|
|
212
208
|
const adapter = knex(connection)
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
The adapter automatically creates the `queue_jobs` table on first use. You can customize the table name:
|
|
216
209
|
|
|
217
|
-
|
|
210
|
+
// Custom table name
|
|
218
211
|
const adapter = knex(config, 'custom_jobs_table')
|
|
219
212
|
```
|
|
220
213
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
Workers process jobs from one or more queues:
|
|
224
|
-
|
|
225
|
-
```typescript
|
|
226
|
-
const worker = new Worker(config)
|
|
227
|
-
|
|
228
|
-
// Process specific queues
|
|
229
|
-
await worker.start(['default', 'email', 'reports'])
|
|
230
|
-
|
|
231
|
-
// Worker will:
|
|
232
|
-
// - Process jobs with configured concurrency
|
|
233
|
-
// - Poll queues at the configured interval
|
|
234
|
-
// - Execute jobs in the order they were queued
|
|
235
|
-
```
|
|
214
|
+
The adapter automatically creates tables on first use.
|
|
236
215
|
|
|
237
|
-
|
|
216
|
+
</details>
|
|
238
217
|
|
|
239
|
-
|
|
218
|
+
### Sync (for testing)
|
|
240
219
|
|
|
241
220
|
```typescript
|
|
242
|
-
|
|
243
|
-
await SendEmailJob.dispatch(payload).in('30s') // 30 seconds
|
|
244
|
-
await SendEmailJob.dispatch(payload).in('5m') // 5 minutes
|
|
245
|
-
await SendEmailJob.dispatch(payload).in('2h') // 2 hours
|
|
246
|
-
await SendEmailJob.dispatch(payload).in('1d') // 1 day
|
|
247
|
-
```
|
|
221
|
+
import { sync } from '@boringnode/queue/drivers/sync_adapter'
|
|
248
222
|
|
|
249
|
-
|
|
223
|
+
const adapter = sync() // Jobs execute immediately
|
|
224
|
+
```
|
|
250
225
|
|
|
251
|
-
|
|
226
|
+
## Job Options
|
|
252
227
|
|
|
253
228
|
```typescript
|
|
254
|
-
export default class
|
|
229
|
+
export default class MyJob extends Job<Payload> {
|
|
255
230
|
static options: JobOptions = {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
//
|
|
231
|
+
queue: 'email', // Queue name (default: 'default')
|
|
232
|
+
priority: 1, // Lower = higher priority (default: 5)
|
|
233
|
+
maxRetries: 3, // Retry attempts before failing
|
|
234
|
+
timeout: '30s', // Max execution time
|
|
235
|
+
failOnTimeout: true, // Fail permanently on timeout (default: retry)
|
|
236
|
+
removeOnComplete: { count: 100 }, // Keep last 100 completed
|
|
237
|
+
removeOnFail: { age: '7d' }, // Keep failed for 7 days
|
|
261
238
|
}
|
|
262
239
|
}
|
|
263
240
|
```
|
|
264
241
|
|
|
265
|
-
##
|
|
242
|
+
## Delayed Jobs
|
|
266
243
|
|
|
267
|
-
|
|
244
|
+
```typescript
|
|
245
|
+
await SendEmailJob.dispatch(payload).in('30s') // 30 seconds
|
|
246
|
+
await SendEmailJob.dispatch(payload).in('5m') // 5 minutes
|
|
247
|
+
await SendEmailJob.dispatch(payload).in('2h') // 2 hours
|
|
248
|
+
await SendEmailJob.dispatch(payload).in('1d') // 1 day
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Retry & Backoff
|
|
268
252
|
|
|
269
253
|
```typescript
|
|
270
|
-
import { exponentialBackoff
|
|
254
|
+
import { exponentialBackoff } from '@boringnode/queue'
|
|
271
255
|
|
|
272
256
|
export default class ReliableJob extends Job<Payload> {
|
|
273
257
|
static options: JobOptions = {
|
|
274
258
|
maxRetries: 5,
|
|
275
259
|
retry: {
|
|
276
|
-
backoff: () =>
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}),
|
|
260
|
+
backoff: () => exponentialBackoff({
|
|
261
|
+
baseDelay: '1s',
|
|
262
|
+
maxDelay: '1m',
|
|
263
|
+
multiplier: 2,
|
|
264
|
+
jitter: true,
|
|
265
|
+
}),
|
|
283
266
|
},
|
|
284
267
|
}
|
|
285
|
-
|
|
286
|
-
async execute(): Promise<void> {
|
|
287
|
-
// ...
|
|
288
|
-
}
|
|
289
268
|
}
|
|
290
269
|
```
|
|
291
270
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
- `exponentialBackoff({ baseDelay, maxDelay, multiplier, jitter })` - Exponential increase
|
|
295
|
-
- `linearBackoff({ baseDelay, maxDelay, multiplier })` - Linear increase
|
|
296
|
-
- `fixedBackoff({ baseDelay, jitter })` - Fixed delay between retries
|
|
297
|
-
|
|
298
|
-
## Job Timeout
|
|
299
|
-
|
|
300
|
-
Set a maximum execution time for jobs:
|
|
271
|
+
<details>
|
|
272
|
+
<summary><strong>Available strategies</strong></summary>
|
|
301
273
|
|
|
302
274
|
```typescript
|
|
303
|
-
|
|
304
|
-
static options: JobOptions = {
|
|
305
|
-
timeout: '30s', // Maximum execution time
|
|
306
|
-
failOnTimeout: false, // Retry on timeout (default)
|
|
307
|
-
}
|
|
275
|
+
import { exponentialBackoff, linearBackoff, fixedBackoff } from '@boringnode/queue'
|
|
308
276
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
```
|
|
277
|
+
// Exponential: 1s, 2s, 4s, 8s...
|
|
278
|
+
exponentialBackoff({ baseDelay: '1s', maxDelay: '1m', multiplier: 2 })
|
|
314
279
|
|
|
315
|
-
|
|
280
|
+
// Linear: 1s, 2s, 3s, 4s...
|
|
281
|
+
linearBackoff({ baseDelay: '1s', maxDelay: '30s', multiplier: 1 })
|
|
316
282
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
worker: {
|
|
320
|
-
timeout: '1m', // Default timeout for all jobs
|
|
321
|
-
},
|
|
322
|
-
}
|
|
283
|
+
// Fixed: 5s, 5s, 5s...
|
|
284
|
+
fixedBackoff({ baseDelay: '5s', jitter: true })
|
|
323
285
|
```
|
|
324
286
|
|
|
325
|
-
|
|
287
|
+
</details>
|
|
326
288
|
|
|
327
|
-
|
|
289
|
+
## Job Timeout
|
|
328
290
|
|
|
329
291
|
```typescript
|
|
330
292
|
export default class LongRunningJob extends Job<Payload> {
|
|
331
293
|
static options: JobOptions = {
|
|
332
294
|
timeout: '30s',
|
|
295
|
+
failOnTimeout: false, // Will retry (default)
|
|
333
296
|
}
|
|
334
297
|
|
|
335
298
|
async execute(): Promise<void> {
|
|
336
299
|
for (const item of this.payload.items) {
|
|
337
|
-
// Check
|
|
300
|
+
// Check abort signal for graceful timeout handling
|
|
338
301
|
if (this.signal?.aborted) {
|
|
339
302
|
throw new Error('Job timed out')
|
|
340
303
|
}
|
|
341
|
-
|
|
342
304
|
await this.processItem(item)
|
|
343
305
|
}
|
|
344
306
|
}
|
|
345
|
-
|
|
346
|
-
private async processItem(item: any): Promise<void> {
|
|
347
|
-
// Pass the signal to fetch or other async operations
|
|
348
|
-
await fetch(item.url, { signal: this.signal })
|
|
349
|
-
}
|
|
350
307
|
}
|
|
351
308
|
```
|
|
352
309
|
|
|
353
310
|
## Job Context
|
|
354
311
|
|
|
355
|
-
|
|
312
|
+
Access execution metadata via `this.context`:
|
|
356
313
|
|
|
357
314
|
```typescript
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
console.log(`Priority: ${this.context.priority}`)
|
|
366
|
-
console.log(`Acquired at: ${this.context.acquiredAt}`)
|
|
367
|
-
|
|
368
|
-
if (this.context.attempt > 1) {
|
|
369
|
-
console.log('This is a retry!')
|
|
370
|
-
}
|
|
371
|
-
}
|
|
315
|
+
async execute(): Promise<void> {
|
|
316
|
+
console.log(this.context.jobId) // Unique job ID
|
|
317
|
+
console.log(this.context.attempt) // 1, 2, 3...
|
|
318
|
+
console.log(this.context.queue) // Queue name
|
|
319
|
+
console.log(this.context.priority) // Priority value
|
|
320
|
+
console.log(this.context.acquiredAt) // When acquired
|
|
321
|
+
console.log(this.context.stalledCount) // Stall recoveries
|
|
372
322
|
}
|
|
373
323
|
```
|
|
374
324
|
|
|
375
|
-
|
|
325
|
+
## Scheduled Jobs
|
|
376
326
|
|
|
377
|
-
|
|
378
|
-
|----------------|--------|-------------------------------------------------|
|
|
379
|
-
| `jobId` | string | Unique identifier for this job |
|
|
380
|
-
| `name` | string | Job class name |
|
|
381
|
-
| `attempt` | number | Current attempt number (1-based) |
|
|
382
|
-
| `queue` | string | Queue name this job is being processed from |
|
|
383
|
-
| `priority` | number | Job priority (lower = higher priority) |
|
|
384
|
-
| `acquiredAt` | Date | When this job was acquired by the worker |
|
|
385
|
-
| `stalledCount` | number | Times this job was recovered from stalled state |
|
|
327
|
+
Run jobs on a recurring basis:
|
|
386
328
|
|
|
387
|
-
|
|
329
|
+
```typescript
|
|
330
|
+
// Every 10 seconds
|
|
331
|
+
await MetricsJob.schedule({ endpoint: '/health' })
|
|
332
|
+
.every('10s')
|
|
333
|
+
|
|
334
|
+
// Cron schedule
|
|
335
|
+
await CleanupJob.schedule({ days: 30 })
|
|
336
|
+
.id('daily-cleanup')
|
|
337
|
+
.cron('0 0 * * *') // Midnight daily
|
|
338
|
+
.timezone('Europe/Paris')
|
|
339
|
+
```
|
|
388
340
|
|
|
389
|
-
|
|
341
|
+
<details>
|
|
342
|
+
<summary><strong>Schedule management</strong></summary>
|
|
390
343
|
|
|
391
344
|
```typescript
|
|
392
|
-
import {
|
|
345
|
+
import { Schedule } from '@boringnode/queue'
|
|
346
|
+
|
|
347
|
+
// Find and manage
|
|
348
|
+
const schedule = await Schedule.find('daily-cleanup')
|
|
349
|
+
await schedule.pause()
|
|
350
|
+
await schedule.resume()
|
|
351
|
+
await schedule.trigger() // Run now
|
|
352
|
+
await schedule.delete()
|
|
353
|
+
|
|
354
|
+
// List schedules
|
|
355
|
+
const all = await Schedule.list()
|
|
356
|
+
const active = await Schedule.list({ status: 'active' })
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Schedule options:**
|
|
393
360
|
|
|
361
|
+
| Method | Description |
|
|
362
|
+
|---------------------|-----------------------------------|
|
|
363
|
+
| `.id(string)` | Unique identifier |
|
|
364
|
+
| `.every(duration)` | Fixed interval ('5s', '1m', '1h') |
|
|
365
|
+
| `.cron(expression)` | Cron schedule |
|
|
366
|
+
| `.timezone(tz)` | Timezone (default: 'UTC') |
|
|
367
|
+
| `.from(date)` | Start boundary |
|
|
368
|
+
| `.to(date)` | End boundary |
|
|
369
|
+
| `.limit(n)` | Maximum runs |
|
|
370
|
+
|
|
371
|
+
</details>
|
|
372
|
+
|
|
373
|
+
## Dependency Injection
|
|
374
|
+
|
|
375
|
+
Integrate with IoC containers:
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
394
378
|
await QueueManager.init({
|
|
395
|
-
|
|
396
|
-
adapters: { redis: redis(connection) },
|
|
379
|
+
// ...
|
|
397
380
|
jobFactory: async (JobClass) => {
|
|
398
|
-
// Use your IoC container to instantiate jobs
|
|
399
381
|
return app.container.make(JobClass)
|
|
400
382
|
},
|
|
401
383
|
})
|
|
402
384
|
```
|
|
403
385
|
|
|
404
|
-
|
|
386
|
+
<details>
|
|
387
|
+
<summary><strong>Example with injected services</strong></summary>
|
|
405
388
|
|
|
406
389
|
```typescript
|
|
407
|
-
import { Job } from '@boringnode/queue'
|
|
408
|
-
|
|
409
|
-
interface SendEmailPayload {
|
|
410
|
-
to: string
|
|
411
|
-
subject: string
|
|
412
|
-
}
|
|
413
|
-
|
|
414
390
|
export default class SendEmailJob extends Job<SendEmailPayload> {
|
|
415
391
|
constructor(
|
|
416
|
-
private mailer: MailerService,
|
|
417
|
-
private logger: Logger
|
|
392
|
+
private mailer: MailerService,
|
|
393
|
+
private logger: Logger
|
|
418
394
|
) {
|
|
419
395
|
super()
|
|
420
396
|
}
|
|
421
397
|
|
|
422
398
|
async execute(): Promise<void> {
|
|
423
|
-
this.logger.info(`
|
|
399
|
+
this.logger.info(`Sending email to ${this.payload.to}`)
|
|
424
400
|
await this.mailer.send(this.payload)
|
|
425
401
|
}
|
|
426
402
|
}
|
|
427
403
|
```
|
|
428
404
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
## Scheduled Jobs
|
|
432
|
-
|
|
433
|
-
Schedule jobs to run on a recurring basis using cron expressions or fixed intervals. Schedules are persisted and survive worker restarts.
|
|
434
|
-
|
|
435
|
-
### Creating a Schedule
|
|
405
|
+
</details>
|
|
436
406
|
|
|
437
|
-
|
|
438
|
-
import { Schedule } from '@boringnode/queue'
|
|
439
|
-
|
|
440
|
-
// Run every 10 seconds (uses job name as schedule ID by default)
|
|
441
|
-
const { scheduleId } = await MetricsJob.schedule({ endpoint: '/api/health' }).every('10s').run()
|
|
442
|
-
|
|
443
|
-
// Run on a cron schedule with custom ID
|
|
444
|
-
await CleanupJob.schedule({ days: 30 })
|
|
445
|
-
.id('daily-cleanup') // Custom ID (optional, defaults to job name)
|
|
446
|
-
.cron('0 * * * *') // Every hour at minute 0
|
|
447
|
-
.timezone('Europe/Paris') // Optional timezone (default: UTC)
|
|
448
|
-
.run()
|
|
449
|
-
|
|
450
|
-
// Schedule with constraints
|
|
451
|
-
await ReportJob.schedule({ type: 'weekly' })
|
|
452
|
-
.id('weekly-report')
|
|
453
|
-
.cron('0 9 * * MON') // Every Monday at 9am
|
|
454
|
-
.from(new Date('2024-01-01')) // Start date
|
|
455
|
-
.to(new Date('2024-12-31')) // End date
|
|
456
|
-
.limit(52) // Maximum 52 runs
|
|
457
|
-
.run()
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
### Managing Schedules
|
|
461
|
-
|
|
462
|
-
```typescript
|
|
463
|
-
import { Schedule } from '@boringnode/queue'
|
|
464
|
-
|
|
465
|
-
// Find a schedule by ID
|
|
466
|
-
const schedule = await Schedule.find('health-check')
|
|
467
|
-
|
|
468
|
-
if (schedule) {
|
|
469
|
-
console.log(`Status: ${schedule.status}`) // 'active' or 'paused'
|
|
470
|
-
console.log(`Run count: ${schedule.runCount}`)
|
|
471
|
-
console.log(`Next run: ${schedule.nextRunAt}`)
|
|
472
|
-
console.log(`Last run: ${schedule.lastRunAt}`)
|
|
473
|
-
|
|
474
|
-
// Pause the schedule
|
|
475
|
-
await schedule.pause()
|
|
476
|
-
|
|
477
|
-
// Resume the schedule
|
|
478
|
-
await schedule.resume()
|
|
479
|
-
|
|
480
|
-
// Trigger an immediate run (outside of the normal schedule)
|
|
481
|
-
await schedule.trigger()
|
|
482
|
-
|
|
483
|
-
// Delete the schedule
|
|
484
|
-
await schedule.delete()
|
|
485
|
-
}
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
### Listing Schedules
|
|
489
|
-
|
|
490
|
-
```typescript
|
|
491
|
-
import { Schedule } from '@boringnode/queue'
|
|
492
|
-
|
|
493
|
-
// List all schedules
|
|
494
|
-
const all = await Schedule.list()
|
|
495
|
-
|
|
496
|
-
// Filter by status
|
|
497
|
-
const active = await Schedule.list({ status: 'active' })
|
|
498
|
-
const paused = await Schedule.list({ status: 'paused' })
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
### Schedule Options
|
|
502
|
-
|
|
503
|
-
| Method | Description |
|
|
504
|
-
|----------------------|-------------------------------------------------|
|
|
505
|
-
| `.id(string)` | Unique identifier (defaults to job name) |
|
|
506
|
-
| `.every(duration)` | Run at fixed intervals ('5s', '1m', '1h', '1d') |
|
|
507
|
-
| `.cron(expression)` | Run on a cron schedule |
|
|
508
|
-
| `.timezone(tz)` | Timezone for cron expressions (default: 'UTC') |
|
|
509
|
-
| `.from(date)` | Don't run before this date |
|
|
510
|
-
| `.to(date)` | Don't run after this date |
|
|
511
|
-
| `.between(from, to)` | Shorthand for `.from().to()` |
|
|
512
|
-
| `.limit(n)` | Maximum number of runs |
|
|
513
|
-
|
|
514
|
-
### How Scheduling Works
|
|
515
|
-
|
|
516
|
-
- Schedules are **persisted** in the database (via the adapter)
|
|
517
|
-
- The **Worker** polls for due schedules and dispatches jobs automatically
|
|
518
|
-
- Each schedule run creates a **new job** with a unique ID
|
|
519
|
-
- Multiple workers can run concurrently - only one will claim each due schedule
|
|
520
|
-
- Failed jobs do **not** affect the schedule (the next run will still occur)
|
|
521
|
-
|
|
522
|
-
## Job Discovery
|
|
523
|
-
|
|
524
|
-
The queue manager automatically discovers and registers jobs from the specified locations:
|
|
407
|
+
## Worker Configuration
|
|
525
408
|
|
|
526
409
|
```typescript
|
|
527
410
|
const config = {
|
|
528
|
-
|
|
411
|
+
worker: {
|
|
412
|
+
concurrency: 5, // Parallel jobs
|
|
413
|
+
idleDelay: '2s', // Poll interval when idle
|
|
414
|
+
timeout: '1m', // Default job timeout
|
|
415
|
+
stalledThreshold: '30s', // When to consider job stalled
|
|
416
|
+
stalledInterval: '30s', // How often to check
|
|
417
|
+
maxStalledCount: 1, // Max recoveries before failing
|
|
418
|
+
gracefulShutdown: true, // Wait for jobs on SIGTERM
|
|
419
|
+
},
|
|
529
420
|
}
|
|
530
421
|
```
|
|
531
422
|
|
|
532
|
-
Jobs must:
|
|
533
|
-
|
|
534
|
-
- Extend the `Job` class
|
|
535
|
-
- Implement the `execute` method
|
|
536
|
-
- Be exported as default
|
|
537
|
-
|
|
538
|
-
The job name is automatically derived from the class name, or can be explicitly set via `static options = { name: 'CustomName' }`.
|
|
539
|
-
|
|
540
423
|
## Logging
|
|
541
424
|
|
|
542
|
-
You can pass a logger to the queue manager for debugging or monitoring. The logger must be compatible with the [pino](https://github.com/pinojs/pino) interface.
|
|
543
|
-
|
|
544
425
|
```typescript
|
|
545
426
|
import { pino } from 'pino'
|
|
546
427
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
adapters: {
|
|
550
|
-
/* ... */
|
|
551
|
-
},
|
|
428
|
+
await QueueManager.init({
|
|
429
|
+
// ...
|
|
552
430
|
logger: pino(),
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
await QueueManager.init(config)
|
|
431
|
+
})
|
|
556
432
|
```
|
|
557
433
|
|
|
558
|
-
By default, a simple console logger is used that only outputs warnings and errors.
|
|
559
|
-
|
|
560
434
|
## Benchmarks
|
|
561
435
|
|
|
562
|
-
Performance comparison with BullMQ
|
|
436
|
+
Performance comparison with BullMQ (5ms simulated work per job):
|
|
563
437
|
|
|
564
438
|
| Jobs | Concurrency | @boringnode/queue | BullMQ | Diff |
|
|
565
439
|
|------|-------------|-------------------|--------|-------------|
|
|
566
|
-
| 100 | 1 | 562ms | 596ms | 5.7% faster |
|
|
567
|
-
| 100 | 5 | 116ms | 117ms | ~same |
|
|
568
|
-
| 100 | 10 | 62ms | 62ms | ~same |
|
|
569
|
-
| 500 | 1 | 2728ms | 2798ms | 2.5% faster |
|
|
570
|
-
| 500 | 5 | 565ms | 565ms | ~same |
|
|
571
|
-
| 500 | 10 | 287ms | 288ms | ~same |
|
|
572
|
-
| 1000 | 1 | 5450ms | 5547ms | 1.7% faster |
|
|
573
440
|
| 1000 | 5 | 1096ms | 1116ms | 1.8% faster |
|
|
574
441
|
| 1000 | 10 | 565ms | 579ms | 2.4% faster |
|
|
575
|
-
| 100K | 5 | 110.5s | 112.3s | 1.5% faster |
|
|
576
442
|
| 100K | 10 | 56.2s | 57.5s | 2.1% faster |
|
|
577
443
|
| 100K | 20 | 29.1s | 29.6s | 1.7% faster |
|
|
578
444
|
|
|
579
|
-
Run benchmarks yourself:
|
|
580
|
-
|
|
581
445
|
```bash
|
|
582
|
-
# Realistic benchmark (5ms job duration)
|
|
583
446
|
npm run benchmark -- --realistic
|
|
584
|
-
|
|
585
|
-
# Pure dequeue overhead (no-op jobs)
|
|
586
|
-
npm run benchmark
|
|
587
|
-
|
|
588
|
-
# Custom job duration
|
|
589
|
-
npm run benchmark -- --duration=10
|
|
590
447
|
```
|
|
591
448
|
|
|
592
449
|
[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/boringnode/queue/checks.yml?branch=main&style=for-the-badge
|