queue-jobs-worker 1.0.4 → 2.0.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 +754 -538
- package/dist/index.d.ts +546 -29
- package/dist/index.js +1279 -2410
- package/dist/scripts/claim-job.lua +117 -0
- package/dist/scripts/clear-queue.lua +42 -0
- package/dist/scripts/list-jobs.lua +50 -0
- package/dist/scripts/remove-job.lua +41 -0
- package/dist/scripts/save-job.lua +52 -0
- package/dist/scripts/update-job.lua +183 -0
- package/package.json +91 -110
- package/CHANGELOG.md +0 -160
- package/LICENSE +0 -21
- package/assets/queue-jobs-worker-demo.mp4 +0 -0
- package/assets/queue-jobs-worker-github.png +0 -0
- package/dist/core/backoff.d.ts +0 -24
- package/dist/core/backoff.d.ts.map +0 -1
- package/dist/core/client.d.ts +0 -95
- package/dist/core/client.d.ts.map +0 -1
- package/dist/core/id.d.ts +0 -9
- package/dist/core/id.d.ts.map +0 -1
- package/dist/core/index.d.ts +0 -7
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/job.d.ts +0 -75
- package/dist/core/job.d.ts.map +0 -1
- package/dist/core/queue.d.ts +0 -70
- package/dist/core/queue.d.ts.map +0 -1
- package/dist/core/worker.d.ts +0 -65
- package/dist/core/worker.d.ts.map +0 -1
- package/dist/events/emitter.d.ts +0 -24
- package/dist/events/emitter.d.ts.map +0 -1
- package/dist/index.cjs +0 -2529
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/storage/in-memory.adapter.d.ts +0 -33
- package/dist/storage/in-memory.adapter.d.ts.map +0 -1
- package/dist/storage/index.d.ts +0 -5
- package/dist/storage/index.d.ts.map +0 -1
- package/dist/storage/mysql.adapter.d.ts +0 -38
- package/dist/storage/mysql.adapter.d.ts.map +0 -1
- package/dist/storage/postgres.adapter.d.ts +0 -38
- package/dist/storage/postgres.adapter.d.ts.map +0 -1
- package/dist/storage/redis.adapter.d.ts +0 -45
- package/dist/storage/redis.adapter.d.ts.map +0 -1
- package/dist/types/client.types.d.ts +0 -41
- package/dist/types/client.types.d.ts.map +0 -1
- package/dist/types/events.types.d.ts +0 -22
- package/dist/types/events.types.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -10
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/job.types.d.ts +0 -97
- package/dist/types/job.types.d.ts.map +0 -1
- package/dist/types/queue.types.d.ts +0 -43
- package/dist/types/queue.types.d.ts.map +0 -1
- package/dist/types/storage.types.d.ts +0 -138
- package/dist/types/storage.types.d.ts.map +0 -1
- package/dist/types/worker.types.d.ts +0 -25
- package/dist/types/worker.types.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,538 +1,754 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
queue.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
await queue.
|
|
298
|
-
attempts:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
await
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
|
429
|
-
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1
|
+
# queue-jobs-worker
|
|
2
|
+
|
|
3
|
+
Reliable background job queue and worker system for Node.js.
|
|
4
|
+
|
|
5
|
+
Supports **in-memory**, **Redis**, **PostgreSQL**, and **MySQL**. Pick the storage that fits your project — the API stays identical regardless of which backend you choose.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm install queue-jobs-worker
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Core concept
|
|
14
|
+
|
|
15
|
+
The library is built around a clean separation of responsibilities:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
QueueClient → opens and holds the storage connection
|
|
19
|
+
Queue → adds, inspects, and removes jobs (producer)
|
|
20
|
+
Worker → picks up and processes jobs (consumer)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`Queue` and `Worker` are completely independent. Any part of your application can hold a `Queue` reference and add jobs. Only the service that processes jobs needs a `Worker`.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { QueueClient, Queue, Worker } from "queue-jobs-worker";
|
|
27
|
+
|
|
28
|
+
// 1. Configure storage — once at startup
|
|
29
|
+
const client = new QueueClient({ dialect: "memory" });
|
|
30
|
+
await client.init();
|
|
31
|
+
|
|
32
|
+
// 2. Producer — add jobs from anywhere
|
|
33
|
+
const queue = new Queue("emails", client);
|
|
34
|
+
await queue.add("welcome", { to: "alice@example.com" });
|
|
35
|
+
|
|
36
|
+
// 3. Consumer — process jobs in one place
|
|
37
|
+
const worker = new Worker(queue, async (job) => {
|
|
38
|
+
await sendEmail(job.data.to);
|
|
39
|
+
});
|
|
40
|
+
worker.start();
|
|
41
|
+
|
|
42
|
+
// 4. Shutdown
|
|
43
|
+
await worker.close();
|
|
44
|
+
await client.close();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Contents
|
|
50
|
+
|
|
51
|
+
- [Storage backends](#storage-backends)
|
|
52
|
+
- [QueueClient](#queueclient)
|
|
53
|
+
- [Queue](#queue)
|
|
54
|
+
- [Worker](#worker)
|
|
55
|
+
- [Job options](#job-options)
|
|
56
|
+
- [Delayed jobs](#delayed-jobs)
|
|
57
|
+
- [Scheduled (cron) jobs](#scheduled-cron-jobs)
|
|
58
|
+
- [Retries & backoff](#retries--backoff)
|
|
59
|
+
- [Concurrency](#concurrency)
|
|
60
|
+
- [Priority](#priority)
|
|
61
|
+
- [Events](#events)
|
|
62
|
+
- [Express integration](#express-integration)
|
|
63
|
+
- [NestJS integration](#nestjs-integration)
|
|
64
|
+
- [Custom storage backend](#custom-storage-backend)
|
|
65
|
+
- [TypeScript](#typescript)
|
|
66
|
+
- [API reference](#api-reference)
|
|
67
|
+
- [Job lifecycle](#job-lifecycle)
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Storage backends
|
|
72
|
+
|
|
73
|
+
### In-memory (no extra dependencies)
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const client = new QueueClient({ dialect: "memory" });
|
|
77
|
+
await client.init();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Data lives in the Node.js process. Suitable for local development, testing, and single-process apps.
|
|
81
|
+
|
|
82
|
+
### Redis
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
npm install redis
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const client = new QueueClient({
|
|
90
|
+
dialect: "redis",
|
|
91
|
+
connectionString: "redis://localhost:6379",
|
|
92
|
+
});
|
|
93
|
+
await client.init();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The Redis backend uses Lua scripts loaded on connect (`SCRIPT LOAD` / `EVALSHA`) for atomic job claiming and status updates — no double-pickup under concurrency.
|
|
97
|
+
|
|
98
|
+
### PostgreSQL
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
npm install pg
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const client = new QueueClient({
|
|
106
|
+
dialect: "postgres",
|
|
107
|
+
connectionString: "postgresql://user:pass@localhost:5432/mydb",
|
|
108
|
+
});
|
|
109
|
+
await client.init();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The required table (`qjw_jobs`) is created automatically on the first `init()` call.
|
|
113
|
+
|
|
114
|
+
### MySQL / MariaDB
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
npm install mysql2
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const client = new QueueClient({
|
|
122
|
+
dialect: "mysql",
|
|
123
|
+
connectionString: "mysql://user:pass@localhost:3306/mydb",
|
|
124
|
+
});
|
|
125
|
+
await client.init();
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The required table (`qjw_jobs`) is created automatically on the first `init()` call.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## QueueClient
|
|
133
|
+
|
|
134
|
+
`QueueClient` holds the storage connection and the global job execution defaults that every `Queue` and `Worker` inherits.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const client = new QueueClient({
|
|
138
|
+
dialect: "memory", // "memory" | "redis" | "postgres" | "mysql"
|
|
139
|
+
connectionString: "…", // required for redis / postgres / mysql
|
|
140
|
+
debug: true, // log internal operations to console
|
|
141
|
+
options: {
|
|
142
|
+
attempts: 3, // max retry attempts per job (0 = unlimited)
|
|
143
|
+
retryDelay: 1000, // base delay in ms between retries
|
|
144
|
+
backoff: "exponential", // "fixed" | "linear" | "exponential"
|
|
145
|
+
timeout: 30_000, // ms a job may run before it is killed
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await client.init(); // call once at app startup
|
|
150
|
+
await client.close(); // call on shutdown — waits for storage to disconnect
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Default client
|
|
154
|
+
|
|
155
|
+
The first `init()` call registers this client as the **process-wide default**. Any `Queue` or `Worker` created without an explicit client argument picks it up automatically:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
await new QueueClient({ dialect: "memory" }).init();
|
|
159
|
+
|
|
160
|
+
// No client argument needed — uses the default.
|
|
161
|
+
const queue = new Queue("emails");
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Queue
|
|
167
|
+
|
|
168
|
+
`Queue` is the **producer**. It manages a named collection of jobs in storage. It has no polling loop and no execution logic — that is the Worker's job.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const queue = new Queue("emails", client, {
|
|
172
|
+
defaultJobOpts: {
|
|
173
|
+
attempts: 5,
|
|
174
|
+
removeOnComplete: true,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Add a job
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
const job = await queue.add("welcome", { to: "alice@example.com" });
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Get a job by ID
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const job = await queue.get(jobId); // Job | undefined
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Remove a job
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
await queue.remove(jobId);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### List jobs
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
const all = await queue.list();
|
|
201
|
+
const waiting = await queue.list("waiting");
|
|
202
|
+
const active = await queue.list("active");
|
|
203
|
+
const completed = await queue.list("completed");
|
|
204
|
+
const failed = await queue.list("failed");
|
|
205
|
+
const retrying = await queue.list("retrying");
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Results are sorted by priority (ascending) then `createdAt` (ascending).
|
|
209
|
+
|
|
210
|
+
### Clear all jobs
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
await queue.clear();
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Count jobs
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const n = await queue.count();
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Worker
|
|
225
|
+
|
|
226
|
+
`Worker` is the **consumer**. It takes a `Queue` and a handler function, polls for eligible jobs, and manages the full job lifecycle.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const worker = new Worker(
|
|
230
|
+
queue, // Queue to consume from
|
|
231
|
+
async (job) => {
|
|
232
|
+
// handler — throw to fail, return to complete
|
|
233
|
+
await processJob(job.data);
|
|
234
|
+
return "done"; // stored in job.result on success
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
concurrency: 3, // max parallel jobs (default: 1)
|
|
238
|
+
pollInterval: 500, // ms between polls when idle (default: 500)
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Start and stop
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
worker.start(); // begin polling — non-blocking
|
|
247
|
+
await worker.close(); // graceful shutdown — waits for in-flight jobs
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`close()` guarantees that any job currently being processed will finish before the worker stops. Safe to call before `client.close()`.
|
|
251
|
+
|
|
252
|
+
Calling `start()` after `close()` throws — create a new `Worker` instance instead.
|
|
253
|
+
|
|
254
|
+
### Shutting down a queue and all its workers at once
|
|
255
|
+
|
|
256
|
+
`queue.close()` is the recommended shutdown pattern. It automatically stops every `Worker` that was created from this queue, waits for in-flight jobs to drain, then cancels all cron schedules:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
// Instead of closing each worker individually:
|
|
260
|
+
const queue = new Queue("emails", client);
|
|
261
|
+
const w1 = new Worker(queue, handler, { concurrency: 2 });
|
|
262
|
+
const w2 = new Worker(queue, handler, { concurrency: 2 });
|
|
263
|
+
w1.start();
|
|
264
|
+
w2.start();
|
|
265
|
+
|
|
266
|
+
// One call closes everything tied to this queue:
|
|
267
|
+
await queue.close();
|
|
268
|
+
await client.close();
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`queue.close()` is idempotent — calling it more than once is safe.
|
|
272
|
+
|
|
273
|
+
### Worker does not manage jobs
|
|
274
|
+
|
|
275
|
+
All job management methods (`add`, `get`, `remove`, `list`, `count`) live on `Queue`. The Worker's only responsibility is execution.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
// Add from the queue (producer side)
|
|
279
|
+
const job = await queue.add("task", { payload: "…" });
|
|
280
|
+
|
|
281
|
+
// Inspect from the queue (anywhere in your app)
|
|
282
|
+
const found = await queue.get(job.id);
|
|
283
|
+
const jobs = await queue.list("completed");
|
|
284
|
+
const n = await queue.count();
|
|
285
|
+
|
|
286
|
+
// Remove from the queue
|
|
287
|
+
await queue.remove(job.id);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Job options
|
|
293
|
+
|
|
294
|
+
Pass options as the third argument to `queue.add()`:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
await queue.add("task", data, {
|
|
298
|
+
attempts: 5, // max retry attempts for this job
|
|
299
|
+
delay: 5_000, // run 5 s from now
|
|
300
|
+
priority: 1, // lower = runs first (default: 0)
|
|
301
|
+
jobId: "my-id", // custom ID — auto UUID if omitted
|
|
302
|
+
cron: "0 9 * * *", // run every day at 09:00 (croner syntax)
|
|
303
|
+
removeOnComplete: true, // delete from storage after success
|
|
304
|
+
removeOnFail: true, // delete from storage after permanent failure
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Delayed jobs
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
// Run in 30 seconds
|
|
314
|
+
await queue.add("reminder", { userId: 42 }, { delay: 30_000 });
|
|
315
|
+
|
|
316
|
+
// Run in 1 hour
|
|
317
|
+
await queue.add("follow-up", { orderId: "x" }, { delay: 60 * 60 * 1_000 });
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
A delayed job has `status: "waiting"` immediately. The Worker checks `runAt <= Date.now()` before picking it up — no separate scheduler required.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Scheduled (cron) jobs
|
|
325
|
+
|
|
326
|
+
Pass any [croner](https://github.com/hexagon/croner)-compatible cron expression:
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
// Every day at 9 AM
|
|
330
|
+
await queue.add("daily-report", {}, { cron: "0 9 * * *" });
|
|
331
|
+
|
|
332
|
+
// Every hour
|
|
333
|
+
await queue.add("hourly-sync", {}, { cron: "0 * * * *" });
|
|
334
|
+
|
|
335
|
+
// Every 5 minutes
|
|
336
|
+
await queue.add("health-check", {}, { cron: "*/5 * * * *" });
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
The job starts as `"delayed"`. After each cron tick it is reset to `"waiting"` and the Worker picks it up like any other job. Remove the job to cancel the schedule:
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
await queue.remove(cronJobId);
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Retries & backoff
|
|
348
|
+
|
|
349
|
+
Configure globally on the client or override per job:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
// Client-level defaults
|
|
353
|
+
const client = new QueueClient({
|
|
354
|
+
dialect: "memory",
|
|
355
|
+
options: {
|
|
356
|
+
attempts: 5,
|
|
357
|
+
retryDelay: 1_000,
|
|
358
|
+
backoff: "exponential",
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Per-job override
|
|
363
|
+
await queue.add("risky", data, { attempts: 10 });
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Backoff strategies
|
|
367
|
+
|
|
368
|
+
| Strategy | Formula | Example (base = 1 s) |
|
|
369
|
+
| ------------- | ---------------------- | --------------------- |
|
|
370
|
+
| `fixed` | `base` | 1 s, 1 s, 1 s, … |
|
|
371
|
+
| `linear` | `base × attempt` | 1 s, 2 s, 3 s, … |
|
|
372
|
+
| `exponential` | `base × 2^(attempt−1)` | 1 s, 2 s, 4 s, 8 s, … |
|
|
373
|
+
|
|
374
|
+
Exponential strategy is capped at **30 minutes**.
|
|
375
|
+
|
|
376
|
+
Set `attempts: 0` for unlimited retries.
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## Concurrency
|
|
381
|
+
|
|
382
|
+
Concurrency is a **Worker** option — not a Queue option. The Queue itself is storage-only.
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
const worker = new Worker(queue, handler, { concurrency: 5 });
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
This means you can also scale by running multiple Workers against the same Queue:
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
const w1 = new Worker(queue, handler, { concurrency: 3 });
|
|
392
|
+
const w2 = new Worker(queue, handler, { concurrency: 3 });
|
|
393
|
+
w1.start();
|
|
394
|
+
w2.start();
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
The Redis backend uses Lua scripts to ensure atomic job claiming — two workers will never pick up the same job.
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Priority
|
|
402
|
+
|
|
403
|
+
Lower number = higher priority (default: `0`).
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
await queue.add("urgent", data, { priority: 1 });
|
|
407
|
+
await queue.add("normal", data, { priority: 5 });
|
|
408
|
+
await queue.add("bulk", data, { priority: 10 });
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Jobs with equal priority are processed in FIFO order.
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Events
|
|
416
|
+
|
|
417
|
+
`Worker` extends `EventEmitter` with typed events:
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
worker.on("active", (job) => console.log("processing", job.id));
|
|
421
|
+
worker.on("completed", (job, result) => console.log("done", job.id, result));
|
|
422
|
+
worker.on("error", (job, err) => console.warn("attempt failed", err.message));
|
|
423
|
+
worker.on("failed", (job, err) => console.error("permanent fail", job.id));
|
|
424
|
+
worker.on("started", () => console.log("worker polling"));
|
|
425
|
+
worker.on("stopped", () => console.log("worker stopped"));
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
| Event | Arguments | When |
|
|
429
|
+
| ----------- | ------------- | ------------------------------------ |
|
|
430
|
+
| `active` | `job` | Job picked up, processing started |
|
|
431
|
+
| `completed` | `job, result` | Job finished successfully |
|
|
432
|
+
| `error` | `job, error` | One attempt failed (may still retry) |
|
|
433
|
+
| `failed` | `job, error` | All attempts exhausted |
|
|
434
|
+
| `started` | — | `worker.start()` called |
|
|
435
|
+
| `stopped` | — | `worker.close()` resolved |
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## Express integration
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
import express from "express";
|
|
443
|
+
import { QueueClient, Queue, Worker } from "queue-jobs-worker";
|
|
444
|
+
|
|
445
|
+
const app = express();
|
|
446
|
+
|
|
447
|
+
// ── Startup ───────────────────────────────────────────────────────────────────
|
|
448
|
+
const client = new QueueClient({
|
|
449
|
+
dialect: "redis",
|
|
450
|
+
connectionString: process.env.REDIS_URL,
|
|
451
|
+
});
|
|
452
|
+
await client.init();
|
|
453
|
+
|
|
454
|
+
// Producer — anyone can import emailQueue and call add()
|
|
455
|
+
const emailQueue = new Queue<{ to: string; subject: string }>("emails", client);
|
|
456
|
+
|
|
457
|
+
// Consumer — only this module cares about the worker
|
|
458
|
+
const emailWorker = new Worker(
|
|
459
|
+
emailQueue,
|
|
460
|
+
async (job) => {
|
|
461
|
+
await mailer.send(job.data);
|
|
462
|
+
return "sent";
|
|
463
|
+
},
|
|
464
|
+
{ concurrency: 5 },
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
emailWorker.on("failed", (job, err) => {
|
|
468
|
+
console.error(`Job ${job.id} permanently failed:`, err.message);
|
|
469
|
+
});
|
|
470
|
+
emailWorker.start();
|
|
471
|
+
|
|
472
|
+
// ── Route ─────────────────────────────────────────────────────────────────────
|
|
473
|
+
app.post("/register", async (req, res) => {
|
|
474
|
+
await emailQueue.add("welcome", { to: req.body.email, subject: "Welcome!" });
|
|
475
|
+
res.status(202).json({ message: "accepted" });
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// ── Shutdown ──────────────────────────────────────────────────────────────────
|
|
479
|
+
process.on("SIGTERM", async () => {
|
|
480
|
+
await emailWorker.close(); // drain in-flight jobs
|
|
481
|
+
await client.close(); // then close storage
|
|
482
|
+
process.exit(0);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
app.listen(3000);
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## NestJS integration
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
// queue.module.ts
|
|
494
|
+
import { Module } from "@nestjs/common";
|
|
495
|
+
import { QueueService } from "./queue.service.js";
|
|
496
|
+
|
|
497
|
+
@Module({ providers: [QueueService], exports: [QueueService] })
|
|
498
|
+
export class QueueModule {}
|
|
499
|
+
|
|
500
|
+
// queue.service.ts
|
|
501
|
+
import { Injectable, OnModuleInit, OnModuleDestroy } from "@nestjs/common";
|
|
502
|
+
import { QueueClient, Queue, Worker } from "queue-jobs-worker";
|
|
503
|
+
|
|
504
|
+
@Injectable()
|
|
505
|
+
export class QueueService implements OnModuleInit, OnModuleDestroy {
|
|
506
|
+
private client!: QueueClient;
|
|
507
|
+
|
|
508
|
+
// Export the Queue so other modules can add jobs without touching the Worker.
|
|
509
|
+
public emailQueue!: Queue<{ to: string }>;
|
|
510
|
+
private emailWorker!: Worker<{ to: string }, string>;
|
|
511
|
+
|
|
512
|
+
async onModuleInit() {
|
|
513
|
+
this.client = new QueueClient({
|
|
514
|
+
dialect: "postgres",
|
|
515
|
+
connectionString: process.env.DATABASE_URL,
|
|
516
|
+
});
|
|
517
|
+
await this.client.init();
|
|
518
|
+
|
|
519
|
+
this.emailQueue = new Queue("emails", this.client);
|
|
520
|
+
this.emailWorker = new Worker(
|
|
521
|
+
this.emailQueue,
|
|
522
|
+
async (job) => {
|
|
523
|
+
await mailer.send(job.data.to);
|
|
524
|
+
return "sent";
|
|
525
|
+
},
|
|
526
|
+
{ concurrency: 3 },
|
|
527
|
+
);
|
|
528
|
+
this.emailWorker.start();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async onModuleDestroy() {
|
|
532
|
+
await this.emailWorker.close();
|
|
533
|
+
await this.client.close();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// users.controller.ts
|
|
538
|
+
@Controller("users")
|
|
539
|
+
export class UsersController {
|
|
540
|
+
constructor(private readonly queue: QueueService) {}
|
|
541
|
+
|
|
542
|
+
@Post("register")
|
|
543
|
+
async register(@Body() dto: RegisterDto) {
|
|
544
|
+
// Only the Queue is needed here — no Worker import required.
|
|
545
|
+
await this.queue.emailQueue.add("welcome", { to: dto.email });
|
|
546
|
+
return { message: "accepted" };
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## Custom storage backend
|
|
554
|
+
|
|
555
|
+
Implement `IStorage` to plug in any database or service:
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
import type { IStorage, Job, JobStatus } from "queue-jobs-worker";
|
|
559
|
+
|
|
560
|
+
export class MongoStorage implements IStorage {
|
|
561
|
+
async connect() {
|
|
562
|
+
/* open client */
|
|
563
|
+
}
|
|
564
|
+
async disconnect() {
|
|
565
|
+
/* close client */
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async saveJob(queueName, job) {
|
|
569
|
+
/* upsert */
|
|
570
|
+
}
|
|
571
|
+
async getJob(queueName, jobId) {
|
|
572
|
+
/* findOne → Job | undefined */
|
|
573
|
+
}
|
|
574
|
+
async updateJob(queueName, jobId, patch) {
|
|
575
|
+
/* findOneAndUpdate */
|
|
576
|
+
}
|
|
577
|
+
async removeJob(queueName, jobId) {
|
|
578
|
+
/* deleteOne */
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async listJobs(queueName, status?) {
|
|
582
|
+
/* find, sorted by priority+createdAt */
|
|
583
|
+
}
|
|
584
|
+
async getNextJob(queueName) {
|
|
585
|
+
/* atomic claim: status IN (waiting,retrying) AND runAt <= now */
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async clearQueue(queueName) {
|
|
589
|
+
/* deleteMany */
|
|
590
|
+
}
|
|
591
|
+
async countJobs(queueName) {
|
|
592
|
+
/* countDocuments */
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
The `getNextJob` implementation must be **atomic** in multi-process environments — use a transaction, `findOneAndUpdate`, or a server-side script to prevent two workers claiming the same job.
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
## TypeScript
|
|
602
|
+
|
|
603
|
+
The library is written in TypeScript and ships full `.d.ts` declarations.
|
|
604
|
+
|
|
605
|
+
Type your job data and result for end-to-end safety:
|
|
606
|
+
|
|
607
|
+
```ts
|
|
608
|
+
interface EmailData {
|
|
609
|
+
to: string;
|
|
610
|
+
subject: string;
|
|
611
|
+
body: string;
|
|
612
|
+
}
|
|
613
|
+
interface EmailResult {
|
|
614
|
+
messageId: string;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const queue = new Queue<EmailData, EmailResult>("emails", client);
|
|
618
|
+
const worker = new Worker<EmailData, EmailResult>(queue, async (job) => {
|
|
619
|
+
const id = await mailer.send(job.data); // job.data → EmailData
|
|
620
|
+
return { messageId: id }; // return type checked as EmailResult
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
worker.on("completed", (job, result) => {
|
|
624
|
+
console.log(result.messageId); // result → EmailResult ✓
|
|
625
|
+
});
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
## API reference
|
|
631
|
+
|
|
632
|
+
### `QueueClient`
|
|
633
|
+
|
|
634
|
+
| Method | Returns | Description |
|
|
635
|
+
| ---------------------------------- | -------------------------- | ----------------------------------- |
|
|
636
|
+
| `new QueueClient(opts)` | — | Create a client |
|
|
637
|
+
| `init()` | `Promise<this>` | Open storage, register as default |
|
|
638
|
+
| `close()` | `Promise<void>` | Close storage (idempotent) |
|
|
639
|
+
| `isInitialized()` | `boolean` | True between `init()` and `close()` |
|
|
640
|
+
| `isClosed()` | `boolean` | True after `close()` |
|
|
641
|
+
| `getConfig()` | `QueueClientConfigOptions` | Merged global defaults |
|
|
642
|
+
| `getDialect()` | `StorageDialect` | Configured dialect |
|
|
643
|
+
| `getStorage()` | `IStorage` | Underlying storage instance |
|
|
644
|
+
| `QueueClient.getDefaultClient()` | `QueueClient \| undefined` | Process-wide default |
|
|
645
|
+
| `QueueClient.setDefaultClient(c)` | `void` | Override the default |
|
|
646
|
+
| `QueueClient.clearDefaultClient()` | `void` | Clear the default |
|
|
647
|
+
|
|
648
|
+
### `Queue`
|
|
649
|
+
|
|
650
|
+
| Method | Returns | Description |
|
|
651
|
+
| --------------------------------- | --------------------------- | --------------------------------- |
|
|
652
|
+
| `new Queue(name, client?, opts?)` | — | Create a queue |
|
|
653
|
+
| `add(name, data, opts?)` | `Promise<Job>` | Persist a new job |
|
|
654
|
+
| `get(jobId)` | `Promise<Job \| undefined>` | Fetch a job by ID |
|
|
655
|
+
| `remove(jobId)` | `Promise<void>` | Delete a job |
|
|
656
|
+
| `list(status?)` | `Promise<Job[]>` | List jobs, optional status filter |
|
|
657
|
+
| `clear()` | `Promise<void>` | Remove all jobs |
|
|
658
|
+
| `count()` | `Promise<number>` | Total job count |
|
|
659
|
+
| `close()` | `Promise<void>` | Close all workers + cron (idempotent) |
|
|
660
|
+
|
|
661
|
+
### `Worker`
|
|
662
|
+
|
|
663
|
+
| Method | Returns | Description |
|
|
664
|
+
| ----------------------------------- | --------------- | ------------------------------ |
|
|
665
|
+
| `new Worker(queue, handler, opts?)` | — | Create a worker |
|
|
666
|
+
| `start()` | `this` | Start polling |
|
|
667
|
+
| `close()` | `Promise<void>` | Graceful shutdown |
|
|
668
|
+
| `isRunning()` | `boolean` | True while polling |
|
|
669
|
+
| `isClosed()` | `boolean` | True after close() |
|
|
670
|
+
| `on(event, fn)` | `this` | Subscribe to a lifecycle event |
|
|
671
|
+
|
|
672
|
+
### `Job`
|
|
673
|
+
|
|
674
|
+
| Field | Type | Description |
|
|
675
|
+
| -------------- | ----------- | ------------------------------- |
|
|
676
|
+
| `id` | `string` | UUID v4 |
|
|
677
|
+
| `name` | `string` | Job type name |
|
|
678
|
+
| `data` | `TData` | Payload |
|
|
679
|
+
| `status` | `JobStatus` | Current lifecycle state |
|
|
680
|
+
| `attempts` | `number` | Max attempts allowed |
|
|
681
|
+
| `attemptsMade` | `number` | Attempts made so far |
|
|
682
|
+
| `delay` | `number` | Initial delay in ms |
|
|
683
|
+
| `runAt` | `number` | Timestamp when eligible to run |
|
|
684
|
+
| `priority` | `number` | Scheduling priority |
|
|
685
|
+
| `cron` | `string?` | Cron expression |
|
|
686
|
+
| `result` | `TResult?` | Handler return value on success |
|
|
687
|
+
| `error` | `string?` | Last error message |
|
|
688
|
+
| `stacktrace` | `string?` | Last error stack trace |
|
|
689
|
+
| `createdAt` | `number` | Unix ms — created |
|
|
690
|
+
| `updatedAt` | `number` | Unix ms — last status change |
|
|
691
|
+
| `processedAt` | `number?` | Unix ms — processing started |
|
|
692
|
+
| `finishedAt` | `number?` | Unix ms — completed or failed |
|
|
693
|
+
|
|
694
|
+
### `JobStatus`
|
|
695
|
+
|
|
696
|
+
```
|
|
697
|
+
"waiting" — eligible to be picked up (runAt <= now)
|
|
698
|
+
"delayed" — cron job waiting for its first tick
|
|
699
|
+
"active" — currently being processed by a Worker
|
|
700
|
+
"completed" — handler returned successfully
|
|
701
|
+
"failed" — all attempts exhausted
|
|
702
|
+
"retrying" — last attempt failed; waiting for retry delay
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
## Job lifecycle
|
|
708
|
+
|
|
709
|
+
```
|
|
710
|
+
queue.add()
|
|
711
|
+
│
|
|
712
|
+
▼
|
|
713
|
+
"waiting" ──────────────────────────────────────────────────────────┐
|
|
714
|
+
│ Worker picks up (runAt <= now) │
|
|
715
|
+
▼ │
|
|
716
|
+
"active" │
|
|
717
|
+
│ │
|
|
718
|
+
├─ handler returns ──► "completed" │
|
|
719
|
+
│ │
|
|
720
|
+
└─ handler throws │
|
|
721
|
+
│ │
|
|
722
|
+
├─ attempts remaining ──► "retrying" ──(delay)──► ───────┘
|
|
723
|
+
│
|
|
724
|
+
└─ no attempts left ──► "failed"
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
queue.add({ cron: "…" })
|
|
728
|
+
│
|
|
729
|
+
▼
|
|
730
|
+
"delayed"
|
|
731
|
+
│ croner tick fires
|
|
732
|
+
▼
|
|
733
|
+
"waiting" ──► (same flow above) ──► "completed"
|
|
734
|
+
│
|
|
735
|
+
next tick resets ───┘
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
---
|
|
739
|
+
|
|
740
|
+
## Peer dependencies
|
|
741
|
+
|
|
742
|
+
| Package | Version | Dialect |
|
|
743
|
+
| -------- | --------- | ------------ |
|
|
744
|
+
| `redis` | `>=4.0.0` | `"redis"` |
|
|
745
|
+
| `pg` | `>=8.0.0` | `"postgres"` |
|
|
746
|
+
| `mysql2` | `>=3.0.0` | `"mysql"` |
|
|
747
|
+
|
|
748
|
+
Install only the package you need. `croner` is a direct dependency — it is always installed automatically.
|
|
749
|
+
|
|
750
|
+
---
|
|
751
|
+
|
|
752
|
+
## License
|
|
753
|
+
|
|
754
|
+
MIT © Rafid Ahmed
|