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.
Files changed (58) hide show
  1. package/README.md +754 -538
  2. package/dist/index.d.ts +546 -29
  3. package/dist/index.js +1279 -2410
  4. package/dist/scripts/claim-job.lua +117 -0
  5. package/dist/scripts/clear-queue.lua +42 -0
  6. package/dist/scripts/list-jobs.lua +50 -0
  7. package/dist/scripts/remove-job.lua +41 -0
  8. package/dist/scripts/save-job.lua +52 -0
  9. package/dist/scripts/update-job.lua +183 -0
  10. package/package.json +91 -110
  11. package/CHANGELOG.md +0 -160
  12. package/LICENSE +0 -21
  13. package/assets/queue-jobs-worker-demo.mp4 +0 -0
  14. package/assets/queue-jobs-worker-github.png +0 -0
  15. package/dist/core/backoff.d.ts +0 -24
  16. package/dist/core/backoff.d.ts.map +0 -1
  17. package/dist/core/client.d.ts +0 -95
  18. package/dist/core/client.d.ts.map +0 -1
  19. package/dist/core/id.d.ts +0 -9
  20. package/dist/core/id.d.ts.map +0 -1
  21. package/dist/core/index.d.ts +0 -7
  22. package/dist/core/index.d.ts.map +0 -1
  23. package/dist/core/job.d.ts +0 -75
  24. package/dist/core/job.d.ts.map +0 -1
  25. package/dist/core/queue.d.ts +0 -70
  26. package/dist/core/queue.d.ts.map +0 -1
  27. package/dist/core/worker.d.ts +0 -65
  28. package/dist/core/worker.d.ts.map +0 -1
  29. package/dist/events/emitter.d.ts +0 -24
  30. package/dist/events/emitter.d.ts.map +0 -1
  31. package/dist/index.cjs +0 -2529
  32. package/dist/index.cjs.map +0 -1
  33. package/dist/index.d.ts.map +0 -1
  34. package/dist/index.js.map +0 -1
  35. package/dist/storage/in-memory.adapter.d.ts +0 -33
  36. package/dist/storage/in-memory.adapter.d.ts.map +0 -1
  37. package/dist/storage/index.d.ts +0 -5
  38. package/dist/storage/index.d.ts.map +0 -1
  39. package/dist/storage/mysql.adapter.d.ts +0 -38
  40. package/dist/storage/mysql.adapter.d.ts.map +0 -1
  41. package/dist/storage/postgres.adapter.d.ts +0 -38
  42. package/dist/storage/postgres.adapter.d.ts.map +0 -1
  43. package/dist/storage/redis.adapter.d.ts +0 -45
  44. package/dist/storage/redis.adapter.d.ts.map +0 -1
  45. package/dist/types/client.types.d.ts +0 -41
  46. package/dist/types/client.types.d.ts.map +0 -1
  47. package/dist/types/events.types.d.ts +0 -22
  48. package/dist/types/events.types.d.ts.map +0 -1
  49. package/dist/types/index.d.ts +0 -10
  50. package/dist/types/index.d.ts.map +0 -1
  51. package/dist/types/job.types.d.ts +0 -97
  52. package/dist/types/job.types.d.ts.map +0 -1
  53. package/dist/types/queue.types.d.ts +0 -43
  54. package/dist/types/queue.types.d.ts.map +0 -1
  55. package/dist/types/storage.types.d.ts +0 -138
  56. package/dist/types/storage.types.d.ts.map +0 -1
  57. package/dist/types/worker.types.d.ts +0 -25
  58. package/dist/types/worker.types.d.ts.map +0 -1
package/README.md CHANGED
@@ -1,538 +1,754 @@
1
- ![queue-jobs-worker](./assets/queue-jobs-worker-github.png)
2
-
3
- # queue-jobs-worker
4
-
5
- A durable, TypeScript-first job queue for Node.js built for asynchronous work, retries, scheduling, and recovery. It based on multiple storage adapter with postgresql, mysql, redis also in-memory support for dev/testing.
6
-
7
- <p align="center">
8
- <a href="https://www.npmjs.com/package/queue-jobs-worker">
9
- <img src="https://img.shields.io/npm/v/queue-jobs-worker.svg" alt="npm version">
10
- </a>&nbsp;
11
- <a href="./LICENSE">
12
- <img src="https://img.shields.io/npm/l/queue-jobs-worker.svg" alt="license">
13
- </a>&nbsp;
14
- <a href="https://nodejs.org">
15
- <img src="https://img.shields.io/node/v/queue-jobs-worker.svg" alt="node">
16
- </a>
17
- </p>
18
-
19
- ---
20
-
21
- ## Overview
22
-
23
- `queue-jobs-worker` helps you move background work out of the request lifecycle and into a reliable, persistent queue. Define the processor logic once and let the library handle enqueueing, persistence, retries, schedules, concurrency, rate limiting, and recovery.
24
-
25
- It supports all major local and production-friendly backends:
26
-
27
- - In-memory queue for development and tests
28
- - Redis via `node-redis` v4+
29
- - PostgreSQL via `pg`
30
- - MySQL via `mysql2`
31
-
32
- For a detailed feature breakdown, see [FEATURES.md](./FEATURES.md).
33
-
34
- ---
35
-
36
- ## Installation
37
-
38
- ```bash
39
- npm install queue-jobs-worker
40
- ```
41
-
42
- Install the driver you plan to use:
43
-
44
- ```bash
45
- # Redis
46
- npm install redis
47
-
48
- # PostgreSQL
49
- npm install pg
50
-
51
- # MySQL
52
- npm install mysql2
53
- ```
54
-
55
- ---
56
-
57
- ## Quick Start
58
-
59
- ```js
60
- const { QueueClient } = require("queue-jobs-worker");
61
-
62
- const client = new QueueClient();
63
- const emails = client.createQueue("emails");
64
-
65
- emails.process("send-email", async (job) => {
66
- await sendEmail(job.data.to, job.data.subject);
67
- // Return to mark the job complete; throw to trigger retry or DLQ handling
68
- });
69
-
70
- emails.createWorker({ concurrency: 5 });
71
-
72
- await emails.enqueue("send-email", {
73
- to: "user@example.com",
74
- subject: "Welcome!",
75
- });
76
-
77
- process.on("SIGTERM", async () => {
78
- await client.close();
79
- process.exit(0);
80
- });
81
- ```
82
-
83
- If you are using TypeScript, you can optionally make the queue payload type-safe with a generic like `client.createQueue<{ to: string; subject: string }>("emails")`.
84
-
85
- ---
86
-
87
- ## Supported Backends
88
-
89
- Pass `dialect` and `connectionString` to `QueueClient`. Call `await client.init()` to establish database connections and create required schema tables.
90
-
91
- ```js
92
- // 1. In-Memory (Default for local development & tests, no persistence)
93
- const client = new QueueClient({ dialect: "memory" });
94
-
95
- // 2. Redis (Supports standard redis://, TLS rediss://, and authentication)
96
- const client = new QueueClient({
97
- dialect: "redis",
98
- connectionString: process.env.REDIS_URL || "redis://localhost:6379",
99
- });
100
-
101
- // 3. PostgreSQL (Auto-creates required queue tables on init)
102
- const client = new QueueClient({
103
- dialect: "postgres",
104
- connectionString: process.env.POSTGRES_URL || "postgresql://user:password@localhost:5432/mydb",
105
- });
106
-
107
- // 4. MySQL (Auto-creates required queue tables on init)
108
- const client = new QueueClient({
109
- dialect: "mysql",
110
- connectionString: process.env.MYSQL_URL || "mysql://user:password@localhost:3306/mydb",
111
- });
112
-
113
- // Initialize backend connection (Required for Redis, PostgreSQL, MySQL)
114
- await client.init();
115
- ```
116
-
117
- | Dialect | Connection Format | `client.init()` Behavior |
118
- |---|---|---|
119
- | `memory` | N/A | No-op (transient memory store) |
120
- | `redis` | `redis://...`, `rediss://...` (TLS), Auth URL | Connects & verifies with `PING` |
121
- | `postgres` | `postgresql://user:pass@host:5432/dbname` | `SELECT 1` check & creates schema |
122
- | `mysql` | `mysql://user:pass@host:3306/dbname` | Connection check & creates schema |
123
-
124
- ---
125
-
126
- ## Core Concepts
127
-
128
- | Concept | Description |
129
- |---|---|
130
- | `QueueClient` | Entry point that owns configuration, storage, and queues |
131
- | `Queue` | A separate job stream with its own settings |
132
- | `Job` | A unit of work passed to your processor |
133
- | `Worker` | Claims and executes jobs |
134
- | `Processor` | Your async function, e.g. `async (job) => { ... }` |
135
- | `StorageAdapter` | A backend abstraction for durable storage |
136
- | `DLQ` | Dead Letter Queue for permanently failed jobs |
137
-
138
- ---
139
-
140
- ## Configuration
141
-
142
- Settings are layered so more specific config overrides broader defaults:
143
-
144
- ```
145
- Client defaults → Queue options → Worker options → Job options
146
- ```
147
-
148
- ```js
149
- const { QueueClient } = require("queue-jobs-worker");
150
-
151
- const client = new QueueClient({
152
- dialect: "redis",
153
- connectionString: process.env.REDIS_URL,
154
-
155
- defaults: {
156
- attempts: 3,
157
- retryDelay: 1000,
158
- backoff: "exponential",
159
- timeout: 30_000,
160
- concurrency: 10,
161
- pollInterval: 1_000,
162
- stalledInterval: 30_000,
163
- lockDuration: 60_000,
164
- rateLimit: {
165
- max: 100,
166
- duration: 60_000,
167
- },
168
- },
169
- });
170
-
171
- await client.init();
172
- ```
173
-
174
- ---
175
-
176
- ## Enqueueing Jobs
177
-
178
- ```js
179
- const queue = client.createQueue("notifications");
180
-
181
- await queue.enqueue("send-push", { userId: "u_123" });
182
-
183
- await queue.enqueue("send-push", { userId: "u_123" }, {
184
- attempts: 5,
185
- retryDelay: 2000,
186
- backoff: "linear",
187
- timeout: 10_000,
188
- priority: 10,
189
- });
190
- ```
191
-
192
- If you want TypeScript type safety for `job.data`, pass a generic when creating the queue, such as `client.createQueue<{ userId: string }>("notifications")`.
193
-
194
- ---
195
-
196
- ## Processing Jobs
197
-
198
- Register a processor before creating or starting a worker. Processors receive the `job` instance as well as an `AbortSignal` for cooperative cancellation when a job attempt times out:
199
-
200
- ```js
201
- queue.process("send-push", async (job, signal) => {
202
- const { userId } = job.data;
203
-
204
- // Pass signal to APIs that support cancellation (e.g. fetch, DB queries):
205
- await pushService.send(userId, "You have a new message", { signal });
206
-
207
- // Or check signal.aborted before performing expensive steps:
208
- if (signal.aborted) return;
209
-
210
- // Return to mark the job complete.
211
- // Throw any error to trigger retry logic or DLQ handling.
212
- });
213
- ```
214
-
215
- > **Note on Timeout Cancellation**: In Node.js, asynchronous operations cannot be forcibly terminated from the outside. Processors should cooperate with cancellation by checking `signal.aborted` or forwarding `signal` to abortable APIs to ensure timed-out executions do not continue running in the background.
216
-
217
- See [FEATURES.md](./FEATURES.md) for the full `job` model and helper methods.
218
-
219
- ---
220
-
221
- ## Workers
222
-
223
- ```js
224
- const worker = queue.createWorker({
225
- concurrency: 10,
226
- shutdownTimeout: 30_000,
227
- });
228
-
229
- console.log(worker.status);
230
- console.log(worker.id);
231
-
232
- await worker.stop();
233
- ```
234
-
235
- Multiple workers can share the same queue and coordinate through the storage layer:
236
-
237
- ```js
238
- const w1 = queue.createWorker({ concurrency: 5 });
239
- const w2 = queue.createWorker({ concurrency: 5 });
240
- // total capacity: 10 concurrent jobs
241
- ```
242
-
243
- ---
244
-
245
- ## Events
246
-
247
- The client emits lifecycle events that are useful for monitoring and alerting:
248
-
249
- ```js
250
- client.on("job:completed", (job) => console.log("Done:", job.id));
251
- client.on("job:failed", (job, err) => console.error("Failed:", job.id, err.message));
252
- client.on("job:dead", (job, err) => console.error("DLQ:", job.id, err.message));
253
- client.on("worker:error", (workerId, err) => console.error("Worker error:", err));
254
-
255
- client.off("job:completed", myListener);
256
- client.once("job:dead", (job, err) => alertTeam(job, err));
257
- ```
258
-
259
- ---
260
-
261
- ## Querying Jobs
262
-
263
- ```js
264
- const job = await queue.getJob("job-id-here");
265
- if (job) {
266
- console.log(job.status, job.attemptsMade);
267
- }
268
-
269
- const waiting = await queue.getJobs("waiting", 50, 0);
270
- const active = await queue.getJobs("active");
271
- const completed = await queue.getJobs("completed", 100, 0);
272
- const dead = await queue.getJobs("dead");
273
-
274
- const counts = await queue.getJobCounts();
275
- // {
276
- // waiting: 12,
277
- // active: 3,
278
- // completed: 204,
279
- // delayed: 5,
280
- // dead: 1
281
- // }
282
- ```
283
-
284
- ---
285
-
286
- ## Retry & Backoff
287
-
288
- Retries can be configured at the client, queue, or job level:
289
-
290
- ```js
291
- const queue = client.createQueue("tasks", {
292
- attempts: 5,
293
- retryDelay: 2000,
294
- backoff: "exponential",
295
- });
296
-
297
- await queue.enqueue("task", payload, {
298
- attempts: 3,
299
- retryDelay: 500,
300
- backoff: "fixed",
301
- });
302
- ```
303
-
304
- Available strategies are `fixed`, `linear`, and `exponential`. Each failure is tracked in `job.attemptHistory` so you can inspect what happened without losing context.
305
-
306
- ---
307
-
308
- ## Scheduling
309
-
310
- ```js
311
- await queue.enqueue("reminder", payload, { schedule: { delay: 30_000 } });
312
- await queue.enqueue("report", payload, { schedule: { runAt: "2026-09-01T09:00:00Z" } });
313
- await queue.enqueue("cleanup", payload, { schedule: { cron: "0 3 * * *" } });
314
- ```
315
-
316
- Delayed jobs stay dormant until their scheduled time is reached.
317
-
318
- ---
319
-
320
- ## Priority
321
-
322
- Jobs with a higher priority value are processed sooner. The default is `0`.
323
-
324
- ```js
325
- await queue.enqueue("urgent-task", payload, { priority: 100 });
326
- await queue.enqueue("normal-task", payload, { priority: 0 });
327
- await queue.enqueue("low-task", payload, { priority: -10 });
328
- // order: urgent → normal → low
329
- ```
330
-
331
- ---
332
-
333
- ## Rate Limiting
334
-
335
- ```js
336
- const queue = client.createQueue("webhooks", {
337
- rateLimit: { max: 50, duration: 60_000 },
338
- });
339
- ```
340
-
341
- When a queue reaches its limit, workers pause claiming new jobs until the time window resets. Jobs are not discarded.
342
-
343
- ---
344
-
345
- ## Dead Letter Queue
346
-
347
- When a job reaches the end of its retry budget, it is moved to the dead-letter queue with status `"dead"`.
348
-
349
- ```js
350
- client.on("job:dead", async (job, error) => {
351
- await alertOncall({ jobId: job.id, type: job.type, error: error.message });
352
- });
353
-
354
- const deadJobs = await queue.getJobs("dead");
355
- ```
356
-
357
- All failure history remains attached to the job record.
358
-
359
- ---
360
-
361
- ## Graceful Shutdown
362
-
363
- Call `client.close()` before your process exits:
364
-
365
- ```js
366
- process.on("SIGTERM", async () => {
367
- await client.close();
368
- process.exit(0);
369
- });
370
-
371
- process.on("SIGINT", async () => {
372
- await client.close();
373
- process.exit(0);
374
- });
375
- ```
376
-
377
- This stops workers cleanly, releases locks, and allows stalled-job recovery to continue safely after restarts.
378
-
379
- ---
380
-
381
- ## Custom Storage Adapter
382
-
383
- You can provide a custom backend by implementing the `StorageAdapter` interface. The same idea applies in JavaScript or TypeScript; the main difference is whether you add explicit interface typing in TypeScript.
384
-
385
- ```js
386
- const { QueueClient } = require("queue-jobs-worker");
387
-
388
- class MongoStorageAdapter {
389
- async initialize() { /* connect, create indexes */ }
390
- async close() { /* disconnect */ }
391
- async enqueue(input) { /* ... */ }
392
- async claim(input) { /* atomic claim */ }
393
- async complete(jobId) { /* ... */ }
394
- async requeue(input) { /* ... */ }
395
- async moveToDlq(input) { /* ... */ }
396
- async releaseLock(jobId) { /* ... */ }
397
- async recoverStalledJobs(queue, now) { /* ... */ }
398
- async getJob(jobId) { /* ... */ }
399
- async getJobs(filter) { /* ... */ }
400
- async getJobCounts(queue) { /* ... */ }
401
- async checkAndIncrementRateLimit(queue, max, windowMs, now) { /* ... */ }
402
- }
403
-
404
- const client = QueueClient.withAdapter(new MongoStorageAdapter(), {
405
- defaults: { attempts: 5 },
406
- });
407
-
408
- await client.init();
409
- ```
410
-
411
- ---
412
-
413
- ## API Reference
414
-
415
- ### `new QueueClient(options?)`
416
-
417
- | Option | Type | Default | Description |
418
- |---|---|---|---|
419
- | `dialect` | `"memory" \| "redis" \| "postgres" \| "mysql"` | `"memory"` | Storage backend |
420
- | `connectionString` | `string` | — | Required for Redis/PostgreSQL/MySQL |
421
- | `defaults.attempts` | `number` | `3` | Max retries per job |
422
- | `defaults.retryDelay` | `number` | `1000` | Base retry delay in ms |
423
- | `defaults.backoff` | `"fixed" \| "linear" \| "exponential"` | `"exponential"` | Retry strategy |
424
- | `defaults.timeout` | `number` | `30000` | Per-attempt timeout in ms |
425
- | `defaults.concurrency` | `number` | `10` | Default worker concurrency |
426
- | `defaults.pollInterval` | `number` | `1000` | Poll interval in ms |
427
- | `defaults.stalledInterval` | `number` | `30000` | Stalled-job check interval in ms |
428
- | `defaults.lockDuration` | `number` | `60000` | Lock TTL in ms |
429
- | `defaults.rateLimit` | `{ max, duration }` | — | Optional rate limiting |
430
-
431
- ### `client.init()`
432
-
433
- Initializes the configured backend. This is required for Redis, PostgreSQL, and MySQL before queue operations. It is safe to call more than once.
434
-
435
- ### `client.createQueue<TPayload>(name, options?)`
436
-
437
- Creates and returns a queue. Queue-specific options override client defaults.
438
-
439
- ### `client.getQueue<TPayload>(name)` / `client.requireQueue<TPayload>(name)`
440
-
441
- Fetches an existing queue by name. `requireQueue()` throws if none exists.
442
-
443
- ### `client.on(event, listener)` / `client.once(...)` / `client.off(...)`
444
-
445
- Registers and removes event listeners for queue and worker lifecycle events.
446
-
447
- ### `client.close()`
448
-
449
- Stops workers and closes storage connections gracefully.
450
-
451
- ### `QueueClient.withAdapter(adapter, options?)`
452
-
453
- Creates a client using a custom storage backend.
454
-
455
- ### `queue.enqueue(type, payload, options?)`
456
-
457
- | Option | Type | Description |
458
- |---|---|---|
459
- | `attempts` | `number` | Maximum attempts for this job |
460
- | `retryDelay` | `number` | Base retry delay in ms |
461
- | `backoff` | `string` | Retry backoff strategy |
462
- | `timeout` | `number` | Per-attempt timeout in ms |
463
- | `priority` | `number` | Higher values are processed first |
464
- | `schedule.delay` | `number` | Delay before the job becomes eligible |
465
- | `schedule.runAt` | `string \| number` | Absolute run time |
466
- | `schedule.cron` | `string` | Cron expression for recurring jobs |
467
-
468
- ### `queue.process(type, processor)`
469
-
470
- Registers an async processor for a job type. The processor signature is `async (job, signal) => ...`, where `signal` is an `AbortSignal` aborted when the per-attempt timeout is reached.
471
-
472
- ### `queue.createWorker(options?)`
473
-
474
- | Option | Type | Default | Description |
475
- |---|---|---|---|
476
- | `concurrency` | `number` | queue config | Maximum simultaneous job executions |
477
- | `shutdownTimeout` | `number` | `30000` | Graceful shutdown wait time in ms |
478
-
479
- ### `queue.getJob(id)` / `queue.getJobs(status?, limit?, offset?)`
480
- ### `queue.getJobCounts()`
481
-
482
- ---
483
-
484
- ## Storage Support Matrix
485
-
486
- | Feature | Memory | Redis | PostgreSQL | MySQL |
487
- |---|:---:|:---:|:---:|:---:|
488
- | Persistence | — | ✓ | ✓ | ✓ |
489
- | Atomic claim | ✓ | ✓ (Lua) | ✓ (SKIP LOCKED) | ✓ (SKIP LOCKED) |
490
- | Priority ordering | ✓ | ✓ | ✓ | ✓ |
491
- | Delayed jobs | ✓ | ✓ | ✓ | ✓ |
492
- | Retry + backoff | ✓ | ✓ | ✓ | ✓ |
493
- | DLQ | ✓ | ✓ | ✓ | ✓ |
494
- | Stalled recovery | ✓ | ✓ | ✓ | ✓ |
495
- | Rate limiting | ✓ | ✓ | ✓ | ✓ |
496
- | Connection check on init | — | ✓ PING | ✓ SELECT 1 | ✓ SELECT 1 |
497
- | Auto-create schema | — | — | ✓ | ✓ |
498
-
499
- ---
500
-
501
- ## Security
502
-
503
- - Job payloads are not logged by default.
504
- - Error messages do not expose payload data.
505
- - Connection strings should come from environment variables rather than source code.
506
- - See [SECURITY.md](./SECURITY.md) for the full policy.
507
-
508
- ```js
509
- // Good
510
- const client = new QueueClient({
511
- dialect: "postgres",
512
- connectionString: process.env.DATABASE_URL,
513
- });
514
-
515
- // Bad — never hard-code credentials
516
- const client = new QueueClient({
517
- dialect: "postgres",
518
- connectionString: "postgresql://admin:secret@prod-db:5432/app",
519
- });
520
- ```
521
-
522
- ---
523
-
524
- ## Contributing
525
-
526
- Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution guidelines.
527
-
528
- ---
529
-
530
- ## License
531
-
532
- MIT — [LICENSE](./LICENSE)
533
-
534
- ## Donation
535
-
536
- If this project has been useful to you, consider supporting it with a coffee.
537
-
538
- **BTC:** `12dxgVQ3sRFhc4g7M6oydsN2tTMMthJJqS`
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