atomic-queues 1.0.13

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 (110) hide show
  1. package/README.md +686 -0
  2. package/dist/decorators/decorators.d.ts +67 -0
  3. package/dist/decorators/decorators.d.ts.map +1 -0
  4. package/dist/decorators/decorators.js +91 -0
  5. package/dist/decorators/decorators.js.map +1 -0
  6. package/dist/decorators/index.d.ts +2 -0
  7. package/dist/decorators/index.d.ts.map +1 -0
  8. package/dist/decorators/index.js +18 -0
  9. package/dist/decorators/index.js.map +1 -0
  10. package/dist/domain/index.d.ts +5 -0
  11. package/dist/domain/index.d.ts.map +1 -0
  12. package/dist/domain/index.js +21 -0
  13. package/dist/domain/index.js.map +1 -0
  14. package/dist/domain/interfaces.d.ts +614 -0
  15. package/dist/domain/interfaces.d.ts.map +1 -0
  16. package/dist/domain/interfaces.js +19 -0
  17. package/dist/domain/interfaces.js.map +1 -0
  18. package/dist/index.d.ts +40 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +61 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/module/atomic-queues.module.d.ts +97 -0
  23. package/dist/module/atomic-queues.module.d.ts.map +1 -0
  24. package/dist/module/atomic-queues.module.js +197 -0
  25. package/dist/module/atomic-queues.module.js.map +1 -0
  26. package/dist/module/index.d.ts +2 -0
  27. package/dist/module/index.d.ts.map +1 -0
  28. package/dist/module/index.js +18 -0
  29. package/dist/module/index.js.map +1 -0
  30. package/dist/services/constants.d.ts +10 -0
  31. package/dist/services/constants.d.ts.map +1 -0
  32. package/dist/services/constants.js +13 -0
  33. package/dist/services/constants.js.map +1 -0
  34. package/dist/services/cron-manager/cron-manager.service.d.ts +188 -0
  35. package/dist/services/cron-manager/cron-manager.service.d.ts.map +1 -0
  36. package/dist/services/cron-manager/cron-manager.service.js +534 -0
  37. package/dist/services/cron-manager/cron-manager.service.js.map +1 -0
  38. package/dist/services/cron-manager/index.d.ts +2 -0
  39. package/dist/services/cron-manager/index.d.ts.map +1 -0
  40. package/dist/services/cron-manager/index.js +18 -0
  41. package/dist/services/cron-manager/index.js.map +1 -0
  42. package/dist/services/index-manager/index-manager.service.d.ts +146 -0
  43. package/dist/services/index-manager/index-manager.service.d.ts.map +1 -0
  44. package/dist/services/index-manager/index-manager.service.js +337 -0
  45. package/dist/services/index-manager/index-manager.service.js.map +1 -0
  46. package/dist/services/index-manager/index.d.ts +2 -0
  47. package/dist/services/index-manager/index.d.ts.map +1 -0
  48. package/dist/services/index-manager/index.js +18 -0
  49. package/dist/services/index-manager/index.js.map +1 -0
  50. package/dist/services/index.d.ts +10 -0
  51. package/dist/services/index.d.ts.map +1 -0
  52. package/dist/services/index.js +26 -0
  53. package/dist/services/index.js.map +1 -0
  54. package/dist/services/job-processor/index.d.ts +2 -0
  55. package/dist/services/job-processor/index.d.ts.map +1 -0
  56. package/dist/services/job-processor/index.js +18 -0
  57. package/dist/services/job-processor/index.js.map +1 -0
  58. package/dist/services/job-processor/job-processor.service.d.ts +156 -0
  59. package/dist/services/job-processor/job-processor.service.d.ts.map +1 -0
  60. package/dist/services/job-processor/job-processor.service.js +331 -0
  61. package/dist/services/job-processor/job-processor.service.js.map +1 -0
  62. package/dist/services/queue-manager/index.d.ts +2 -0
  63. package/dist/services/queue-manager/index.d.ts.map +1 -0
  64. package/dist/services/queue-manager/index.js +18 -0
  65. package/dist/services/queue-manager/index.js.map +1 -0
  66. package/dist/services/queue-manager/queue-manager.service.d.ts +128 -0
  67. package/dist/services/queue-manager/queue-manager.service.d.ts.map +1 -0
  68. package/dist/services/queue-manager/queue-manager.service.js +308 -0
  69. package/dist/services/queue-manager/queue-manager.service.js.map +1 -0
  70. package/dist/services/resource-lock/index.d.ts +2 -0
  71. package/dist/services/resource-lock/index.d.ts.map +1 -0
  72. package/dist/services/resource-lock/index.js +18 -0
  73. package/dist/services/resource-lock/index.js.map +1 -0
  74. package/dist/services/resource-lock/resource-lock.service.d.ts +124 -0
  75. package/dist/services/resource-lock/resource-lock.service.d.ts.map +1 -0
  76. package/dist/services/resource-lock/resource-lock.service.js +379 -0
  77. package/dist/services/resource-lock/resource-lock.service.js.map +1 -0
  78. package/dist/services/service-queue/index.d.ts +2 -0
  79. package/dist/services/service-queue/index.d.ts.map +1 -0
  80. package/dist/services/service-queue/index.js +18 -0
  81. package/dist/services/service-queue/index.js.map +1 -0
  82. package/dist/services/service-queue/service-queue.service.d.ts +232 -0
  83. package/dist/services/service-queue/service-queue.service.d.ts.map +1 -0
  84. package/dist/services/service-queue/service-queue.service.js +647 -0
  85. package/dist/services/service-queue/service-queue.service.js.map +1 -0
  86. package/dist/services/shutdown-state/index.d.ts +2 -0
  87. package/dist/services/shutdown-state/index.d.ts.map +1 -0
  88. package/dist/services/shutdown-state/index.js +18 -0
  89. package/dist/services/shutdown-state/index.js.map +1 -0
  90. package/dist/services/shutdown-state/shutdown-state.service.d.ts +69 -0
  91. package/dist/services/shutdown-state/shutdown-state.service.d.ts.map +1 -0
  92. package/dist/services/shutdown-state/shutdown-state.service.js +127 -0
  93. package/dist/services/shutdown-state/shutdown-state.service.js.map +1 -0
  94. package/dist/services/worker-manager/index.d.ts +2 -0
  95. package/dist/services/worker-manager/index.d.ts.map +1 -0
  96. package/dist/services/worker-manager/index.js +18 -0
  97. package/dist/services/worker-manager/index.js.map +1 -0
  98. package/dist/services/worker-manager/worker-manager.service.d.ts +163 -0
  99. package/dist/services/worker-manager/worker-manager.service.d.ts.map +1 -0
  100. package/dist/services/worker-manager/worker-manager.service.js +460 -0
  101. package/dist/services/worker-manager/worker-manager.service.js.map +1 -0
  102. package/dist/utils/helpers.d.ts +124 -0
  103. package/dist/utils/helpers.d.ts.map +1 -0
  104. package/dist/utils/helpers.js +229 -0
  105. package/dist/utils/helpers.js.map +1 -0
  106. package/dist/utils/index.d.ts +2 -0
  107. package/dist/utils/index.d.ts.map +1 -0
  108. package/dist/utils/index.js +18 -0
  109. package/dist/utils/index.js.map +1 -0
  110. package/package.json +80 -0
package/README.md ADDED
@@ -0,0 +1,686 @@
1
+ # @nestjs/atomic-queues
2
+
3
+ A plug-and-play NestJS library for atomic process handling per entity with BullMQ, Redis distributed locking, and dynamic worker management.
4
+
5
+ ## Overview
6
+
7
+ `@nestjs/atomic-queues` provides a unified architecture for handling atomic, sequential processing of jobs on a per-entity basis. It abstracts the complexity of managing dynamic queues, workers, and distributed locking into a simple, declarative API.
8
+
9
+ ### Problem It Solves
10
+
11
+ In distributed systems, you often need to:
12
+ - Process jobs **sequentially** for a specific entity (user, order, session)
13
+ - **Dynamically spawn workers** based on load
14
+ - **Prevent race conditions** when multiple services handle the same entity
15
+ - **Scale horizontally** while maintaining per-entity ordering guarantees
16
+
17
+ This library solves all of these with a single, cohesive module.
18
+
19
+ ---
20
+
21
+ ## Example Scenario: Order Processing System
22
+
23
+ Imagine an e-commerce platform where each customer can place multiple orders. Each order goes through several stages: validation, payment, inventory reservation, and shipping. These stages **must happen in sequence** for each order, but different orders can be processed in parallel.
24
+
25
+ ```
26
+ Customer A places Order 1 → [validate] → [pay] → [reserve] → [ship]
27
+ Customer A places Order 2 → [validate] → [pay] → [reserve] → [ship]
28
+ Customer B places Order 3 → [validate] → [pay] → [reserve] → [ship]
29
+ ```
30
+
31
+ **Without atomic queues**: Race conditions, duplicate payments, inventory overselling.
32
+ **With atomic queues**: Each order gets its own queue and worker, ensuring sequential processing.
33
+
34
+ ---
35
+
36
+ ## Architecture
37
+
38
+ ### High-Level Flow
39
+
40
+ ```
41
+ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐
42
+ │ @nestjs/atomic-queues ARCHITECTURE │
43
+ └─────────────────────────────────────────────────────────────────────────────────────────────┘
44
+
45
+ ┌─────────────────────┐
46
+ │ External Triggers │
47
+ │ (WebSocket, HTTP, │
48
+ │ Cron, Pub/Sub) │
49
+ └──────────┬──────────┘
50
+
51
+
52
+ ┌──────────────────────────────────────────────────────────────────────────────────────────────┐
53
+ │ APPLICATION LAYER │
54
+ │ ┌────────────────────────────────────────────────────────────────────────────────────────┐ │
55
+ │ │ QueueManagerService │ │
56
+ │ │ │ │
57
+ │ │ queueManager.addJob(entityQueue, jobName, { entityId, action, payload }) │ │
58
+ │ │ │ │
59
+ │ └────────────────────────────────────────────────────────────────────────────────────────┘ │
60
+ └──────────────────────────────────────────────────────────────────────────────────────────────┘
61
+
62
+
63
+ ┌──────────────────────────────────────────────────────────────────────────────────────────────┐
64
+ │ REDIS (BullMQ) │
65
+ │ │
66
+ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
67
+ │ │ entity-A-q │ │ entity-B-q │ │ entity-C-q │ │ entity-N-q │ │
68
+ │ │ │ │ │ │ │ │ │ │
69
+ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │
70
+ │ │ │ Job 1 │ │ │ │ Job 1 │ │ │ │ Job 1 │ │ │ │ Job 1 │ │ │
71
+ │ │ │ Job 2 │ │ │ │ Job 2 │ │ │ └─────────┘ │ │ │ Job 2 │ │ │
72
+ │ │ │ Job 3 │ │ │ └─────────┘ │ │ │ │ │ Job 3 │ │ │
73
+ │ │ │ ... │ │ │ │ │ │ │ │ ... │ │ │
74
+ │ │ └─────────┘ │ │ │ │ │ │ └─────────┘ │ │
75
+ │ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
76
+ │ │ │ │ │ │
77
+ └───────────┼────────────────────┼────────────────────┼────────────────────┼───────────────────┘
78
+ │ │ │ │
79
+ ▼ ▼ ▼ ▼
80
+ ┌──────────────────────────────────────────────────────────────────────────────────────────────┐
81
+ │ WORKER LAYER (Per-Entity) │
82
+ │ │
83
+ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
84
+ │ │ Worker A │ │ Worker B │ │ Worker C │ │ Worker N │ │
85
+ │ │ concurrency=1 │ │ concurrency=1 │ │ concurrency=1 │ │ concurrency=1 │ │
86
+ │ │ │ │ │ │ │ │ │ │
87
+ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │
88
+ │ │ │Heartbeat│ │ │ │Heartbeat│ │ │ │Heartbeat│ │ │ │Heartbeat│ │ │
89
+ │ │ │ TTL=3s │ │ │ │ TTL=3s │ │ │ │ TTL=3s │ │ │ │ TTL=3s │ │ │
90
+ │ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ │
91
+ │ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
92
+ │ │ │ │ │ │
93
+ │ │ WorkerManagerService (Lifecycle, Heartbeats, Shutdown Signals) │
94
+ │ └────────────────────┴────────────────────┴────────────────────┘ │
95
+ │ │ │
96
+ └──────────────────────────────────────────┼───────────────────────────────────────────────────┘
97
+
98
+
99
+ ┌──────────────────────────────────────────────────────────────────────────────────────────────┐
100
+ │ JOB PROCESSOR SERVICE │
101
+ │ │
102
+ │ ┌───────────────────────────────────────────────────────────────────────────────────────┐ │
103
+ │ │ JobProcessorRegistry │ │
104
+ │ │ │ │
105
+ │ │ @JobProcessor('validate') @JobProcessor('pay') @JobProcessor('ship') │ │
106
+ │ │ class ValidateProcessor {} class PayProcessor {} class ShipProcessor {} │ │
107
+ │ │ │ │
108
+ │ └───────────────────────────────────────────────────────────────────────────────────────┘ │
109
+ │ │ │
110
+ │ ▼ │
111
+ │ ┌───────────────────────────────────────────────────────────────────────────────────────┐ │
112
+ │ │ CQRS CommandBus / QueryBus │ │
113
+ │ │ │ │
114
+ │ │ commandBus.execute(new ValidateOrderCommand(...)) │ │
115
+ │ │ commandBus.execute(new ProcessPaymentCommand(...)) │ │
116
+ │ │ │ │
117
+ │ └───────────────────────────────────────────────────────────────────────────────────────┘ │
118
+ │ │
119
+ └──────────────────────────────────────────────────────────────────────────────────────────────┘
120
+
121
+
122
+ ┌──────────────────────────────────────────────────────────────────────────────────────────────┐
123
+ │ SUPPORTING SERVICES │
124
+ │ │
125
+ │ ┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────────┐ │
126
+ │ │ CronManagerService │ │ IndexManagerService │ │ ResourceLockService │ │
127
+ │ │ │ │ │ │ │ │
128
+ │ │ • Poll for entities │ │ • Track jobs per │ │ • Lua-based atomic │ │
129
+ │ │ needing workers │ │ entity │ │ locks │ │
130
+ │ │ • Spawn workers on │ │ • Track worker states │ │ • Lock pooling │ │
131
+ │ │ demand │ │ • Track queue states │ │ • TTL-based expiry │ │
132
+ │ │ • Terminate idle │ │ • Cleanup on entity │ │ • Owner tracking │ │
133
+ │ │ workers │ │ completion │ │ │ │
134
+ │ │ │ │ │ │ │ │
135
+ │ └─────────────────────────┘ └─────────────────────────┘ └─────────────────────────┘ │
136
+ │ │
137
+ └──────────────────────────────────────────────────────────────────────────────────────────────┘
138
+ ```
139
+
140
+ ### Detailed Component Interaction
141
+
142
+ ```
143
+ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
144
+ │ COMPLETE JOB LIFECYCLE │
145
+ └─────────────────────────────────────────────────────────────────────────────────────────────────┘
146
+
147
+ 1. JOB CREATION 2. WORKER SPAWNING 3. JOB PROCESSING
148
+ ───────────────── ────────────────── ─────────────────
149
+
150
+ ┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐
151
+ │ Service │ │ CronManager │ │ Worker │
152
+ │ (HTTP/WS) │ │ Service │ │ (BullMQ) │
153
+ └──────┬──────┘ └────────┬────────┘ └────────┬────────┘
154
+ │ │ │
155
+ │ 1. Receive request │ 1. Every N seconds │ 1. Poll queue
156
+ │ (create order, etc) │ check entities │ for jobs
157
+ ▼ │ with pending jobs │
158
+ ┌─────────────┐ ▼ ▼
159
+ │ Queue │ ┌─────────────────┐ ┌─────────────────┐
160
+ │ Manager │ │ Index │ │ Job │
161
+ │ Service │ │ Manager │ │ Processor │
162
+ └──────┬──────┘ └────────┬────────┘ │ Registry │
163
+ │ │ └────────┬────────┘
164
+ │ 2. Get/create queue │ 2. Return entities │
165
+ │ for entity │ with job counts │ 2. Lookup processor
166
+ ▼ │ │ by job name
167
+ ┌─────────────┐ ▼ ▼
168
+ │ Redis │ ┌─────────────────┐ ┌─────────────────┐
169
+ │ Queue │◄────────────────── │ Worker │ │ @JobProcessor │
170
+ │ (entity-X) │ │ Manager │ │ Handler Class │
171
+ └──────┬──────┘ └────────┬────────┘ └────────┬────────┘
172
+ │ │ │
173
+ │ 3. Add job to queue │ 3. Spawn worker │ 3. Execute
174
+ │ (FIFO ordered) │ for entity │ command/query
175
+ ▼ │ ▼
176
+ ┌─────────────┐ ▼ ┌─────────────────┐
177
+ │ Index │ ┌─────────────────┐ │ CommandBus │
178
+ │ Manager │ │ New Worker │ │ / QueryBus │
179
+ └─────────────┘ │ (concurrency=1)│ └────────┬────────┘
180
+ │ └─────────────────┘ │
181
+ │ 4. Track job in index │ 4. Domain
182
+ │ for entity │ logic
183
+ ▼ ▼
184
+ ┌─────────────────────────────────────────────────────────────────────────────────────┐
185
+ │ REDIS │
186
+ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
187
+ │ │ Queues │ │ Workers │ │ Indices │ │ Locks │ │
188
+ │ │ (BullMQ) │ │ (Heartbeat) │ │ (Jobs/Qs) │ │ (Lua Atom) │ │
189
+ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
190
+ └─────────────────────────────────────────────────────────────────────────────────────┘
191
+
192
+
193
+ 4. JOB COMPLETION 5. WORKER TERMINATION 6. GRACEFUL SHUTDOWN
194
+ ───────────────── ───────────────────── ────────────────────
195
+
196
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
197
+ │ Worker │ │ CronManager │ │ SIGTERM/INT │
198
+ │ completes │ │ Service │ │ Signal │
199
+ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘
200
+ │ │ │
201
+ │ 1. Job finished │ 1. Check worker │ 1. Caught by
202
+ │ │ idle time │ process handler
203
+ ▼ │ ▼
204
+ ┌─────────────────┐ ▼ ┌─────────────────┐
205
+ │ Index │ ┌─────────────────┐ │ Worker │
206
+ │ Manager │ │ No pending │ │ Manager │
207
+ └────────┬────────┘ │ jobs for │ └────────┬────────┘
208
+ │ │ entity? │ │
209
+ │ 2. Remove job from └────────┬────────┘ │ 2. Signal all
210
+ │ entity index │ │ workers to close
211
+ ▼ │ YES ▼
212
+ ┌─────────────────┐ ▼ ┌─────────────────┐
213
+ │ Check pending │ ┌─────────────────┐ │ Redis │
214
+ │ jobs for │ │ Worker │ │ Pub/Sub │
215
+ │ entity │ │ Manager │ │ (shutdown │
216
+ └────────┬────────┘ └────────┬────────┘ │ channel) │
217
+ │ │ └────────┬────────┘
218
+ │ 3. If no pending │ 2. Signal worker │
219
+ │ jobs, cleanup │ to close │ 3. Workers receive
220
+ ▼ ▼ │ shutdown signal
221
+ ┌─────────────────┐ ┌─────────────────┐ ▼
222
+ │ Entity indices │ │ Worker │ ┌─────────────────┐
223
+ │ cleaned up │ │ gracefully │ │ Workers │
224
+ │ │ │ closes │ │ finish │
225
+ └─────────────────┘ └─────────────────┘ │ current job │
226
+ │ then exit │
227
+ └─────────────────┘
228
+ ```
229
+
230
+ ### Multi-Node Cluster Architecture
231
+
232
+ ```
233
+ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
234
+ │ MULTI-NODE CLUSTER DEPLOYMENT │
235
+ └─────────────────────────────────────────────────────────────────────────────────────────────────┘
236
+
237
+ ┌─────────────────┐
238
+ │ Load Balancer │
239
+ └────────┬────────┘
240
+
241
+ ┌──────────────────────────────┼──────────────────────────────┐
242
+ │ │ │
243
+ ▼ ▼ ▼
244
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
245
+ │ Node 1 │ │ Node 2 │ │ Node 3 │
246
+ │ (PM2 Cluster) │ │ (PM2 Cluster) │ │ (K8s Pod) │
247
+ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤
248
+ │ │ │ │ │ │
249
+ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
250
+ │ │ Worker A │ │ │ │ Worker C │ │ │ │ Worker E │ │
251
+ │ │(Entity 1) │ │ │ │(Entity 3) │ │ │ │(Entity 5) │ │
252
+ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │
253
+ │ │ │ │ │ │
254
+ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
255
+ │ │ Worker B │ │ │ │ Worker D │ │ │ │ Worker F │ │
256
+ │ │(Entity 2) │ │ │ │(Entity 4) │ │ │ │(Entity 6) │ │
257
+ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │
258
+ │ │ │ │ │ │
259
+ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘
260
+ │ │ │
261
+ └──────────────────────────────┼──────────────────────────────┘
262
+
263
+
264
+ ┌─────────────────────────────────────────────────────────────────────────────────┐
265
+ │ REDIS CLUSTER │
266
+ │ │
267
+ │ ┌─────────────────────────────────────────────────────────────────────────┐ │
268
+ │ │ BullMQ Queues │ │
269
+ │ │ entity-1-queue │ entity-2-queue │ entity-3-queue │ ... │ entity-N-q │ │
270
+ │ └─────────────────────────────────────────────────────────────────────────┘ │
271
+ │ │
272
+ │ ┌─────────────────────────────────────────────────────────────────────────┐ │
273
+ │ │ Worker Heartbeats (TTL) │ │
274
+ │ │ aq:workers:entity-1-worker │ aq:workers:entity-2-worker │ ... │ │
275
+ │ └─────────────────────────────────────────────────────────────────────────┘ │
276
+ │ │
277
+ │ ┌─────────────────────────────────────────────────────────────────────────┐ │
278
+ │ │ Job/Entity Indices │ │
279
+ │ │ aq:idx:entity:jobs │ aq:idx:entity:queues │ aq:idx:entity:workers │ │
280
+ │ └─────────────────────────────────────────────────────────────────────────┘ │
281
+ │ │
282
+ │ ┌─────────────────────────────────────────────────────────────────────────┐ │
283
+ │ │ Pub/Sub Shutdown Channels │ │
284
+ │ │ aq:worker:entity-1-worker:shutdown │ aq:worker:entity-2-worker:shut │ │
285
+ │ └─────────────────────────────────────────────────────────────────────────┘ │
286
+ │ │
287
+ └─────────────────────────────────────────────────────────────────────────────────┘
288
+
289
+
290
+ KEY GUARANTEES:
291
+ ───────────────
292
+ ✓ Only ONE worker processes jobs for each entity (concurrency=1)
293
+ ✓ Jobs for same entity are processed in FIFO order
294
+ ✓ Worker heartbeats detected across all nodes
295
+ ✓ Graceful shutdown via Redis pub/sub (not local signals)
296
+ ✓ Any node can spawn workers for any entity
297
+ ✓ Dead workers detected via TTL expiration
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Features
303
+
304
+ - **Dynamic Per-Entity Queues**: Automatically create and manage queues for each entity (user, order, session, etc.)
305
+ - **Worker Lifecycle Management**: Heartbeat-based worker tracking with TTL expiration
306
+ - **Distributed Resource Locking**: Atomic lock acquisition using Lua scripts
307
+ - **Graceful Shutdown**: Coordinated shutdown via Redis pub/sub across cluster nodes
308
+ - **Cron-based Scaling**: Automatic worker spawning and termination based on demand
309
+ - **Job Processor Registry**: Decorator-based job handler registration
310
+ - **Index Tracking**: Track jobs, workers, and queue states across entities
311
+
312
+ ---
313
+
314
+ ## Installation
315
+
316
+ ```bash
317
+ npm install @nestjs/atomic-queues bullmq ioredis
318
+ ```
319
+
320
+ ---
321
+
322
+ ## Quick Start
323
+
324
+ ### 1. Import the Module
325
+
326
+ ```typescript
327
+ import { Module } from '@nestjs/common';
328
+ import { AtomicQueuesModule } from '@nestjs/atomic-queues';
329
+
330
+ @Module({
331
+ imports: [
332
+ AtomicQueuesModule.forRoot({
333
+ redis: {
334
+ host: 'localhost',
335
+ port: 6379,
336
+ },
337
+ enableCronManager: true,
338
+ cronInterval: 5000,
339
+ keyPrefix: 'myapp',
340
+ }),
341
+ ],
342
+ })
343
+ export class AppModule {}
344
+ ```
345
+
346
+ ### 2. Async Configuration
347
+
348
+ ```typescript
349
+ import { Module } from '@nestjs/common';
350
+ import { ConfigModule, ConfigService } from '@nestjs/config';
351
+ import { AtomicQueuesModule } from '@nestjs/atomic-queues';
352
+
353
+ @Module({
354
+ imports: [
355
+ AtomicQueuesModule.forRootAsync({
356
+ imports: [ConfigModule],
357
+ useFactory: (configService: ConfigService) => ({
358
+ redis: {
359
+ url: configService.get('REDIS_URL'),
360
+ },
361
+ enableCronManager: true,
362
+ workerDefaults: {
363
+ concurrency: 1,
364
+ heartbeatTTL: 3,
365
+ },
366
+ }),
367
+ inject: [ConfigService],
368
+ }),
369
+ ],
370
+ })
371
+ export class AppModule {}
372
+ ```
373
+
374
+ ### 3. Register Job Processors
375
+
376
+ ```typescript
377
+ import { Injectable } from '@nestjs/common';
378
+ import { JobProcessor, JobProcessorRegistry } from '@nestjs/atomic-queues';
379
+ import { CommandBus } from '@nestjs/cqrs';
380
+
381
+ @Injectable()
382
+ @JobProcessor('validate-order')
383
+ export class ValidateOrderProcessor {
384
+ constructor(private readonly commandBus: CommandBus) {}
385
+
386
+ async process(job: Job) {
387
+ const { orderId, items } = job.data;
388
+ await this.commandBus.execute(new ValidateOrderCommand(orderId, items));
389
+ }
390
+ }
391
+
392
+ @Injectable()
393
+ @JobProcessor('process-payment')
394
+ export class ProcessPaymentProcessor {
395
+ constructor(private readonly commandBus: CommandBus) {}
396
+
397
+ async process(job: Job) {
398
+ const { orderId, amount } = job.data;
399
+ await this.commandBus.execute(new ProcessPaymentCommand(orderId, amount));
400
+ }
401
+ }
402
+ ```
403
+
404
+ ### 4. Queue Jobs
405
+
406
+ ```typescript
407
+ import { Injectable } from '@nestjs/common';
408
+ import { QueueManagerService, IndexManagerService } from '@nestjs/atomic-queues';
409
+
410
+ @Injectable()
411
+ export class OrderService {
412
+ constructor(
413
+ private readonly queueManager: QueueManagerService,
414
+ private readonly indexManager: IndexManagerService,
415
+ ) {}
416
+
417
+ async createOrder(orderId: string, items: any[], amount: number) {
418
+ const queue = this.queueManager.getOrCreateEntityQueue('order', orderId);
419
+
420
+ // Queue validation job
421
+ const job = await this.queueManager.addJob(queue.name, 'validate-order', { orderId, items });
422
+
423
+ // Queue payment job (will run after validation completes due to FIFO)
424
+ await this.queueManager.addJob(queue.name, 'process-payment', { orderId, amount });
425
+
426
+ // Track job for scaling decisions
427
+ await this.indexManager.indexJob('order', orderId, job.id!);
428
+
429
+ return orderId;
430
+ }
431
+ }
432
+ ```
433
+
434
+ ### 5. Create Workers
435
+
436
+ ```typescript
437
+ import { Injectable } from '@nestjs/common';
438
+ import { WorkerManagerService, JobProcessorRegistry } from '@nestjs/atomic-queues';
439
+
440
+ @Injectable()
441
+ export class OrderWorkerService {
442
+ constructor(
443
+ private readonly workerManager: WorkerManagerService,
444
+ private readonly jobRegistry: JobProcessorRegistry,
445
+ ) {}
446
+
447
+ async createOrderWorker(orderId: string) {
448
+ const queueName = `order-${orderId}-queue`;
449
+
450
+ await this.workerManager.createWorker({
451
+ workerName: `${orderId}-worker`,
452
+ queueName,
453
+ processor: async (job) => {
454
+ const processor = this.jobRegistry.getProcessor(job.name);
455
+ if (!processor) {
456
+ throw new Error(`No processor for job: ${job.name}`);
457
+ }
458
+ await processor.process(job);
459
+ },
460
+ events: {
461
+ onReady: async (worker, name) => {
462
+ console.log(`Worker ${name} is ready`);
463
+ },
464
+ onCompleted: async (job, name) => {
465
+ console.log(`Job ${job.id} completed by ${name}`);
466
+ },
467
+ onFailed: async (job, error, name) => {
468
+ console.error(`Job ${job?.id} failed in ${name}:`, error.message);
469
+ },
470
+ },
471
+ });
472
+ }
473
+ }
474
+ ```
475
+
476
+ ---
477
+
478
+ ## Core Services
479
+
480
+ ### QueueManagerService
481
+
482
+ Manages dynamic queue creation and destruction per entity.
483
+
484
+ ```typescript
485
+ // Get or create a queue for an entity
486
+ const queue = queueManager.getOrCreateEntityQueue('order', '123');
487
+
488
+ // Add a job to a queue
489
+ await queueManager.addJob(queue.name, 'process', { data: 'hello' });
490
+
491
+ // Get job counts
492
+ const counts = await queueManager.getJobCounts(queue.name);
493
+
494
+ // Close a queue
495
+ await queueManager.closeQueue(queue.name);
496
+ ```
497
+
498
+ ### WorkerManagerService
499
+
500
+ Manages worker lifecycle with heartbeat-based liveness tracking.
501
+
502
+ ```typescript
503
+ // Create a worker
504
+ await workerManager.createWorker({
505
+ workerName: 'my-worker',
506
+ queueName: 'my-queue',
507
+ processor: async (job) => { /* process job */ },
508
+ config: {
509
+ concurrency: 1,
510
+ heartbeatTTL: 3,
511
+ },
512
+ });
513
+
514
+ // Check if worker exists
515
+ const exists = await workerManager.workerExists('my-worker');
516
+
517
+ // Signal worker to close via Redis pub/sub
518
+ await workerManager.signalWorkerClose('my-worker');
519
+
520
+ // Get all workers for an entity
521
+ const workers = await workerManager.getEntityWorkers('order', '123');
522
+ ```
523
+
524
+ ### ResourceLockService
525
+
526
+ Provides distributed resource locking using Redis Lua scripts.
527
+
528
+ ```typescript
529
+ // Acquire a lock
530
+ const result = await lockService.acquireLock(
531
+ 'resource', // resourceType
532
+ 'resource-123', // resourceId
533
+ 'owner-456', // ownerId
534
+ 'service', // ownerType
535
+ 60, // TTL in seconds
536
+ );
537
+
538
+ if (result.acquired) {
539
+ try {
540
+ // Do work with the locked resource
541
+ } finally {
542
+ await lockService.releaseLock('resource', 'resource-123');
543
+ }
544
+ }
545
+
546
+ // Get first available resource from a pool
547
+ const available = await lockService.getAvailableResource(
548
+ 'resource',
549
+ ['res-1', 'res-2', 'res-3'],
550
+ 'owner-456',
551
+ 'service',
552
+ );
553
+ ```
554
+
555
+ ### CronManagerService
556
+
557
+ Automatic worker scaling based on demand.
558
+
559
+ ```typescript
560
+ // Register entity type for automatic scaling
561
+ cronManager.registerEntityType({
562
+ entityType: 'order',
563
+ getDesiredWorkerCount: async (orderId) => {
564
+ // Return how many workers this entity needs
565
+ return 1;
566
+ },
567
+ getActiveEntityIds: async () => {
568
+ return Object.keys(await indexManager.getEntitiesWithJobs('order'));
569
+ },
570
+ maxWorkersPerEntity: 5,
571
+ onSpawnWorker: async (orderId) => {
572
+ await orderWorkerService.createOrderWorker(orderId);
573
+ },
574
+ });
575
+
576
+ // Start the cron manager
577
+ cronManager.start(5000);
578
+ ```
579
+
580
+ ### IndexManagerService
581
+
582
+ Track jobs, workers, and queue states.
583
+
584
+ ```typescript
585
+ // Index a job
586
+ await indexManager.indexJob('order', '123', 'job-456');
587
+
588
+ // Get all entities with pending jobs
589
+ const entitiesWithJobs = await indexManager.getEntitiesWithJobs('order');
590
+ // Returns: { '123': 5, '456': 2 } (entityId: jobCount)
591
+
592
+ // Track queue existence
593
+ await indexManager.indexEntityQueue('order', '123');
594
+
595
+ // Clean up all indices for an entity
596
+ await indexManager.cleanupEntityIndices('order', '123');
597
+ ```
598
+
599
+ ---
600
+
601
+ ## Configuration Options
602
+
603
+ ```typescript
604
+ interface IAtomicQueuesModuleConfig {
605
+ // Redis connection
606
+ redis: {
607
+ host?: string;
608
+ port?: number;
609
+ password?: string;
610
+ db?: number;
611
+ url?: string;
612
+ maxRetriesPerRequest?: number | null;
613
+ };
614
+
615
+ // Worker defaults
616
+ workerDefaults?: {
617
+ concurrency?: number; // Default: 1
618
+ stalledInterval?: number; // Default: 1000ms
619
+ lockDuration?: number; // Default: 30000ms
620
+ maxStalledCount?: number; // Default: MAX_SAFE_INTEGER
621
+ heartbeatTTL?: number; // Default: 3 seconds
622
+ heartbeatInterval?: number; // Default: 1000ms
623
+ };
624
+
625
+ // Queue defaults
626
+ queueDefaults?: {
627
+ defaultJobOptions?: {
628
+ removeOnComplete?: boolean;
629
+ removeOnFail?: boolean;
630
+ attempts?: number;
631
+ backoff?: { type: 'fixed' | 'exponential'; delay: number };
632
+ priority?: number;
633
+ };
634
+ };
635
+
636
+ // Cron manager
637
+ enableCronManager?: boolean; // Default: false
638
+ cronInterval?: number; // Default: 5000ms
639
+
640
+ // Key prefix for Redis keys
641
+ keyPrefix?: string; // Default: 'aq'
642
+ }
643
+ ```
644
+
645
+ ---
646
+
647
+ ## Graceful Shutdown
648
+
649
+ The library handles graceful shutdown automatically via Redis pub/sub:
650
+
651
+ 1. On `SIGTERM`/`SIGINT`, the node publishes shutdown signals to Redis
652
+ 2. All workers (even on other nodes) subscribed to shutdown channels receive the signal
653
+ 3. Workers finish their current job (with configurable timeout)
654
+ 4. Heartbeat TTLs expire, marking workers as dead
655
+ 5. Resources are cleaned up
656
+
657
+ ```typescript
658
+ // Manual shutdown
659
+ await workerManager.signalNodeWorkersClose();
660
+ await workerManager.waitForWorkersToClose(30000);
661
+ ```
662
+
663
+ ---
664
+
665
+ ## Use Cases
666
+
667
+ ### 1. Per-Order Processing (E-commerce)
668
+ Each order has its own queue ensuring stages (validate → pay → ship) happen sequentially.
669
+
670
+ ### 2. Per-User Message Queues (Chat/Messaging)
671
+ Each user has their own queue for message delivery, ensuring order.
672
+
673
+ ### 3. Per-Tenant Job Processing (SaaS)
674
+ Each tenant's jobs are isolated and processed sequentially.
675
+
676
+ ### 4. Per-Document Processing (Document Management)
677
+ Each document goes through OCR → validation → storage in sequence.
678
+
679
+ ### 5. Per-Device Commands (IoT)
680
+ Each device receives commands in order, preventing race conditions.
681
+
682
+ ---
683
+
684
+ ## License
685
+
686
+ MIT