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