@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 CHANGED
@@ -20,558 +20,430 @@ npm install @boringnode/queue
20
20
 
21
21
  ## Features
22
22
 
23
- - **Multiple Queue Adapters**: Support for Redis, Knex (PostgreSQL, MySQL, SQLite), and Sync
24
- - **Type-Safe Jobs**: Define jobs as TypeScript classes with typed payloads
25
- - **Delayed Jobs**: Schedule jobs to run after a specific delay
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
- - **Retry with Backoff**: Automatic retries with exponential, linear, or fixed backoff strategies
31
- - **Job Timeout**: Automatically fail or retry jobs that exceed a time limit
32
- - **Scheduled Jobs**: Cron-based or interval-based job scheduling with pause/resume support
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 { JobContext, JobOptions } from '@boringnode/queue/types'
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(`[Attempt ${this.context.attempt}] Sending email to: ${this.payload.to}`)
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
- const redisConnection = new Redis({
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
- sync: sync(),
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
- import SendEmailJob from './jobs/send_email_job.ts'
99
-
100
- // Dispatch immediately
82
+ // Simple dispatch
101
83
  await SendEmailJob.dispatch({ to: 'user@example.com' })
102
84
 
103
- // Dispatch with delay
104
- await SendEmailJob.dispatch({ to: 'user@example.com' }).in('5m')
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', 'reports'])
98
+ await worker.start(['default', 'email'])
116
99
  ```
117
100
 
118
- ## Configuration
101
+ ## Bulk Dispatch
119
102
 
120
- ### Queue Manager Options
103
+ Efficiently dispatch thousands of jobs in a single batch operation:
121
104
 
122
105
  ```typescript
123
- interface QueueManagerConfig {
124
- // Default adapter to use
125
- default: string
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
- // Available queue adapters
128
- adapters: {
129
- [key: string]: QueueAdapter
130
- }
118
+ This uses Redis MULTI/EXEC or SQL batch insert for optimal performance.
131
119
 
132
- // Worker configuration
133
- worker: {
134
- concurrency: number
135
- idleDelay: Duration
136
- }
120
+ ## Job Grouping
137
121
 
138
- // Job discovery locations
139
- locations: string[]
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
- ### Job Options
134
+ The `groupId` is stored with job data and accessible via `job.data.groupId`.
135
+
136
+ ## Job History & Retention
144
137
 
145
- Configure individual jobs with the `options` property:
138
+ Keep completed and failed jobs for debugging:
146
139
 
147
140
  ```typescript
148
- static options: JobOptions = {
149
- queue: 'email', // Queue name (default: 'default')
150
- adapter: 'redis', // Override default adapter
151
- priority: 1, // Lower number = higher priority (default: 5)
152
- maxRetries: 3, // Maximum retry attempts
153
- timeout: '30s', // Job timeout duration
154
- failOnTimeout: true, // Fail permanently on timeout (default: false, will retry)
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
- ## Adapters
152
+ <details>
153
+ <summary><strong>Retention options</strong></summary>
159
154
 
160
- ### Redis Adapter
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
- For production use with distributed systems:
163
+ Query job history:
163
164
 
164
165
  ```typescript
165
- import { redis } from '@boringnode/queue/drivers/redis_adapter'
166
- import { Redis } from 'ioredis'
167
-
168
- const redisConnection = new Redis({
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
- ### Sync Adapter
172
+ </details>
173
+
174
+ ## Adapters
178
175
 
179
- For testing and development:
176
+ ### Redis (recommended for production)
180
177
 
181
178
  ```typescript
182
- import { sync } from '@boringnode/queue/drivers/sync_adapter'
179
+ import { redis } from '@boringnode/queue/drivers/redis_adapter'
183
180
 
184
- const adapter = sync()
185
- ```
181
+ // With options
182
+ const adapter = redis({ host: 'localhost', port: 6379 })
186
183
 
187
- ### Knex Adapter
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
- For SQL databases (PostgreSQL, MySQL, SQLite) using Knex:
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
- // Or with an existing Knex instance (you manage connection lifecycle)
207
- import Knex from 'knex'
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
- The adapter automatically creates the `queue_jobs` table on first use. You can customize the table name:
214
-
215
- ```typescript
210
+ // Custom table name
216
211
  const adapter = knex(config, 'custom_jobs_table')
217
212
  ```
218
213
 
219
- ## Worker Configuration
214
+ The adapter automatically creates tables on first use.
220
215
 
221
- Workers process jobs from one or more queues:
216
+ </details>
217
+
218
+ ### Sync (for testing)
222
219
 
223
220
  ```typescript
224
- const worker = new Worker(config)
221
+ import { sync } from '@boringnode/queue/drivers/sync_adapter'
225
222
 
226
- // Process specific queues
227
- await worker.start(['default', 'email', 'reports'])
223
+ const adapter = sync() // Jobs execute immediately
224
+ ```
225
+
226
+ ## Job Options
228
227
 
229
- // Worker will:
230
- // - Process jobs with configured concurrency
231
- // - Poll queues at the configured interval
232
- // - Execute jobs in the order they were queued
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
- // Various time formats
241
- await SendEmailJob.dispatch(payload).in('30s') // 30 seconds
242
- await SendEmailJob.dispatch(payload).in('5m') // 5 minutes
243
- await SendEmailJob.dispatch(payload).in('2h') // 2 hours
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
- ## Priority
248
-
249
- Jobs with lower priority numbers are processed first:
251
+ ## Retry & Backoff
250
252
 
251
253
  ```typescript
252
- export default class UrgentJob extends Job<Payload> {
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
- priority: 1, // Processed before default priority (5)
257
- }
258
-
259
- async execute(): Promise<void> {
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
- ## Retry and Backoff
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
- export default class ReliableJob extends Job<Payload> {
273
- static readonly jobName = 'ReliableJob'
277
+ // Exponential: 1s, 2s, 4s, 8s...
278
+ exponentialBackoff({ baseDelay: '1s', maxDelay: '1m', multiplier: 2 })
274
279
 
275
- static options: JobOptions = {
276
- maxRetries: 5,
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
- async execute(): Promise<void> {
289
- // ...
290
- }
291
- }
283
+ // Fixed: 5s, 5s, 5s...
284
+ fixedBackoff({ baseDelay: '5s', jitter: true })
292
285
  ```
293
286
 
294
- Available backoff strategies:
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 LimitedJob extends Job<Payload> {
306
- static readonly jobName = 'LimitedJob'
307
-
292
+ export default class LongRunningJob extends Job<Payload> {
308
293
  static options: JobOptions = {
309
- timeout: '30s', // Maximum execution time
310
- failOnTimeout: false, // Retry on timeout (default)
294
+ timeout: '30s',
295
+ failOnTimeout: false, // Will retry (default)
311
296
  }
312
297
 
313
298
  async execute(): Promise<void> {
314
- // Long running operation...
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
- You can also set a global timeout in the worker configuration:
310
+ ## Job Context
311
+
312
+ Access execution metadata via `this.context`:
320
313
 
321
314
  ```typescript
322
- const config = {
323
- worker: {
324
- timeout: '1m', // Default timeout for all jobs
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
- ## Job Context
325
+ ## Scheduled Jobs
330
326
 
331
- Every job has access to execution context via `this.context`. This provides metadata about the current job execution:
327
+ Run jobs on a recurring basis:
332
328
 
333
329
  ```typescript
334
- import { Job } from '@boringnode/queue'
335
- import type { JobContext } from '@boringnode/queue'
330
+ // Every 10 seconds
331
+ await MetricsJob.schedule({ endpoint: '/health' })
332
+ .every('10s')
336
333
 
337
- export default class MyJob extends Job<Payload> {
338
- constructor(payload: Payload, context: JobContext) {
339
- super(payload, context)
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
- async execute(): Promise<void> {
343
- console.log(`Job ID: ${this.context.jobId}`)
344
- console.log(`Attempt: ${this.context.attempt}`) // 1, 2, 3...
345
- console.log(`Queue: ${this.context.queue}`)
346
- console.log(`Priority: ${this.context.priority}`)
347
- console.log(`Acquired at: ${this.context.acquiredAt}`)
348
-
349
- if (this.context.attempt > 1) {
350
- console.log('This is a retry!')
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
- ### Context Properties
359
+ **Schedule options:**
357
360
 
358
- | Property | Type | Description |
359
- |----------------|--------|-------------------------------------------------|
360
- | `jobId` | string | Unique identifier for this job |
361
- | `name` | string | Job class name |
362
- | `attempt` | number | Current attempt number (1-based) |
363
- | `queue` | string | Queue name this job is being processed from |
364
- | `priority` | number | Job priority (lower = higher priority) |
365
- | `acquiredAt` | Date | When this job was acquired by the worker |
366
- | `stalledCount` | number | Times this job was recovered from stalled state |
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
- Use the `jobFactory` option to integrate with IoC containers for dependency injection. This allows your jobs to receive injected services in their constructor.
375
+ Integrate with IoC containers:
371
376
 
372
377
  ```typescript
373
- import { QueueManager } from '@boringnode/queue'
374
-
375
378
  await QueueManager.init({
376
- default: 'redis',
377
- adapters: { redis: redis(connection) },
378
- jobFactory: async (JobClass, payload, context) => {
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
- Example with injected dependencies:
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
- payload: SendEmailPayload,
401
- context: JobContext,
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(payload, context)
395
+ super()
406
396
  }
407
397
 
408
398
  async execute(): Promise<void> {
409
- this.logger.info(`[Attempt ${this.context.attempt}] Sending email to ${this.payload.to}`)
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
- Without a `jobFactory`, jobs are instantiated with `new JobClass(payload, context)`.
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
- ### Managing Schedules
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
- locations: ['./app/jobs/**/*.ts', './modules/**/jobs/**/*.ts'],
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
- const config = {
533
- default: 'redis',
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 using realistic jobs (5ms simulated work per job):
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