@boringnode/queue 0.2.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,23 +20,22 @@ 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
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(`[Attempt ${this.context.attempt}] Sending email to: ${this.payload.to}`)
53
+ console.log(`Sending email to: ${this.payload.to}`)
55
54
  }
56
55
  }
57
56
  ```
58
57
 
59
- > **Note**: The job name defaults to the class name (`SendEmailJob`). You can override it with `name: 'CustomName'` in options if needed.
60
- >
61
- > **Warning**: If you minify your code in production, class names may be mangled. In that case, always specify `name` explicitly in your job options.
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
- const config = {
70
+ await QueueManager.init({
79
71
  default: 'redis',
80
-
81
72
  adapters: {
82
- sync: sync(),
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
- import SendEmailJob from './jobs/send_email_job.ts'
101
-
102
- // Dispatch immediately
82
+ // Simple dispatch
103
83
  await SendEmailJob.dispatch({ to: 'user@example.com' })
104
84
 
105
- // Dispatch with delay
106
- 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')
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', 'reports'])
98
+ await worker.start(['default', 'email'])
118
99
  ```
119
100
 
120
- ## Configuration
101
+ ## Bulk Dispatch
121
102
 
122
- ### Queue Manager Options
103
+ Efficiently dispatch thousands of jobs in a single batch operation:
123
104
 
124
105
  ```typescript
125
- interface QueueManagerConfig {
126
- // Default adapter to use
127
- 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
+ ```
128
117
 
129
- // Available queue adapters
130
- adapters: {
131
- [key: string]: QueueAdapter
132
- }
118
+ This uses Redis MULTI/EXEC or SQL batch insert for optimal performance.
133
119
 
134
- // Worker configuration
135
- worker: {
136
- concurrency: number
137
- idleDelay: Duration
138
- }
120
+ ## Job Grouping
139
121
 
140
- // Job discovery locations
141
- locations: string[]
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
- ### Job Options
134
+ The `groupId` is stored with job data and accessible via `job.data.groupId`.
135
+
136
+ ## Job History & Retention
146
137
 
147
- Configure individual jobs with the `options` property:
138
+ Keep completed and failed jobs for debugging:
148
139
 
149
140
  ```typescript
150
- static options: JobOptions = {
151
- queue: 'email', // Queue name (default: 'default')
152
- adapter: 'redis', // Override default adapter
153
- priority: 1, // Lower number = higher priority (default: 5)
154
- maxRetries: 3, // Maximum retry attempts
155
- timeout: '30s', // Job timeout duration
156
- 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
+ }
157
149
  }
158
150
  ```
159
151
 
160
- ## Adapters
152
+ <details>
153
+ <summary><strong>Retention options</strong></summary>
161
154
 
162
- ### 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 |
163
162
 
164
- For production use with distributed systems:
163
+ Query job history:
165
164
 
166
165
  ```typescript
167
- import { redis } from '@boringnode/queue/drivers/redis_adapter'
168
- import { Redis } from 'ioredis'
169
-
170
- const redisConnection = new Redis({
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
- ### Sync Adapter
172
+ </details>
180
173
 
181
- For testing and development:
174
+ ## Adapters
175
+
176
+ ### Redis (recommended for production)
182
177
 
183
178
  ```typescript
184
- import { sync } from '@boringnode/queue/drivers/sync_adapter'
179
+ import { redis } from '@boringnode/queue/drivers/redis_adapter'
185
180
 
186
- const adapter = sync()
187
- ```
181
+ // With options
182
+ const adapter = redis({ host: 'localhost', port: 6379 })
188
183
 
189
- ### 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
+ ```
190
189
 
191
- For SQL databases (PostgreSQL, MySQL, SQLite) using Knex:
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
- // Or with an existing Knex instance (you manage connection lifecycle)
209
- import Knex from 'knex'
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
- ```typescript
210
+ // Custom table name
218
211
  const adapter = knex(config, 'custom_jobs_table')
219
212
  ```
220
213
 
221
- ## Worker Configuration
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
- ## Delayed Jobs
216
+ </details>
238
217
 
239
- Schedule jobs to run in the future:
218
+ ### Sync (for testing)
240
219
 
241
220
  ```typescript
242
- // Various time formats
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
- ## Priority
223
+ const adapter = sync() // Jobs execute immediately
224
+ ```
250
225
 
251
- Jobs with lower priority numbers are processed first:
226
+ ## Job Options
252
227
 
253
228
  ```typescript
254
- export default class UrgentJob extends Job<Payload> {
229
+ export default class MyJob extends Job<Payload> {
255
230
  static options: JobOptions = {
256
- priority: 1, // Processed before default priority (5)
257
- }
258
-
259
- async execute(): Promise<void> {
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
- ## Retry and Backoff
242
+ ## Delayed Jobs
266
243
 
267
- Configure automatic retries with backoff strategies:
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, linearBackoff, fixedBackoff } from '@boringnode/queue'
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
- exponentialBackoff({
278
- baseDelay: '1s',
279
- maxDelay: '1m',
280
- multiplier: 2,
281
- jitter: true,
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
- Available backoff strategies:
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
- export default class LimitedJob extends Job<Payload> {
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
- async execute(): Promise<void> {
310
- // Long running operation...
311
- }
312
- }
313
- ```
277
+ // Exponential: 1s, 2s, 4s, 8s...
278
+ exponentialBackoff({ baseDelay: '1s', maxDelay: '1m', multiplier: 2 })
314
279
 
315
- You can also set a global timeout in the worker configuration:
280
+ // Linear: 1s, 2s, 3s, 4s...
281
+ linearBackoff({ baseDelay: '1s', maxDelay: '30s', multiplier: 1 })
316
282
 
317
- ```typescript
318
- const config = {
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
- ### Handling Timeout Gracefully
287
+ </details>
326
288
 
327
- Jobs have access to an abort signal via `this.signal` to handle timeouts gracefully:
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 if the job has been aborted
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
- Every job has access to execution context via `this.context`. This provides metadata about the current job execution:
312
+ Access execution metadata via `this.context`:
356
313
 
357
314
  ```typescript
358
- import { Job } from '@boringnode/queue'
359
-
360
- export default class MyJob extends Job<Payload> {
361
- async execute(): Promise<void> {
362
- console.log(`Job ID: ${this.context.jobId}`)
363
- console.log(`Attempt: ${this.context.attempt}`) // 1, 2, 3...
364
- console.log(`Queue: ${this.context.queue}`)
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
- ### Context Properties
325
+ ## Scheduled Jobs
376
326
 
377
- | Property | Type | Description |
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
- ## Dependency Injection
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
- Use the `jobFactory` option to integrate with IoC containers for dependency injection. The constructor is reserved for injecting dependencies - payload and context are provided separately by the worker.
341
+ <details>
342
+ <summary><strong>Schedule management</strong></summary>
390
343
 
391
344
  ```typescript
392
- import { QueueManager } from '@boringnode/queue'
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
- default: 'redis',
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
- Example with injected dependencies:
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, // Injected by IoC container
417
- private logger: Logger // Injected by IoC container
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(`[Attempt ${this.context.attempt}] Sending email to ${this.payload.to}`)
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
- Without a `jobFactory`, jobs are instantiated with `new JobClass()`.
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
- ```typescript
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
- 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
+ },
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
- const config = {
548
- default: 'redis',
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 using realistic jobs (5ms simulated work per job):
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