atomic-queues 1.0.16 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ A plug-and-play NestJS library for atomic process handling per entity with BullM
4
4
 
5
5
  ## Overview
6
6
 
7
- `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.
7
+ `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 decorator-based API**.
8
8
 
9
9
  ### Problem It Solves
10
10
 
@@ -18,18 +18,359 @@ This library solves all of these with a single, cohesive module.
18
18
 
19
19
  ---
20
20
 
21
- ## Example Scenario: Order Processing System
21
+ ## Features
22
+
23
+ - **Decorator-based API**: Use `@WorkerProcessor` and `@JobHandler` for declarative job routing
24
+ - **Auto-discovery**: Processors and scalers are automatically discovered and registered
25
+ - **Dynamic Per-Entity Queues**: Automatically create and manage queues for each entity
26
+ - **Worker Lifecycle Management**: Heartbeat-based worker tracking with TTL expiration
27
+ - **Distributed Resource Locking**: Atomic lock acquisition
28
+ - **Graceful Shutdown**: Coordinated shutdown via Redis pub/sub across cluster nodes
29
+ - **Cron-based Scaling**: Automatic worker spawning and termination based on demand
30
+
31
+ ---
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ npm install atomic-queues bullmq ioredis
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Quick Start (Decorator-based API) ✨
42
+
43
+ The recommended way to use `atomic-queues` is with the decorator-based API for clean, declarative code.
44
+
45
+ ### 1. Import the Module
46
+
47
+ ```typescript
48
+ import { Module } from '@nestjs/common';
49
+ import { AtomicQueuesModule } from 'atomic-queues';
50
+
51
+ @Module({
52
+ imports: [
53
+ AtomicQueuesModule.forRootAsync({
54
+ imports: [ConfigModule],
55
+ useFactory: (configService: ConfigService) => ({
56
+ redis: {
57
+ url: configService.get('REDIS_URL'),
58
+ },
59
+ keyPrefix: 'myapp',
60
+ enableCronManager: true,
61
+ workerDefaults: {
62
+ concurrency: 1,
63
+ heartbeatTTL: 3,
64
+ },
65
+ }),
66
+ inject: [ConfigService],
67
+ }),
68
+ ],
69
+ })
70
+ export class AppModule {}
71
+ ```
72
+
73
+ ### 2. Create a Worker Processor
74
+
75
+ Use `@WorkerProcessor` to define a processor class and `@JobHandler` to route jobs to methods:
76
+
77
+ ```typescript
78
+ import { Injectable } from '@nestjs/common';
79
+ import { CommandBus } from '@nestjs/cqrs';
80
+ import { Job } from 'bullmq';
81
+ import { WorkerProcessor, JobHandler } from 'atomic-queues';
82
+
83
+ @WorkerProcessor({
84
+ entityType: 'order',
85
+ queueName: (orderId) => `order-${orderId}-queue`,
86
+ workerName: (orderId) => `order-${orderId}-worker`,
87
+ workerConfig: {
88
+ concurrency: 1,
89
+ heartbeatTTL: 3,
90
+ },
91
+ })
92
+ @Injectable()
93
+ export class OrderWorkerProcessor {
94
+ constructor(private readonly commandBus: CommandBus) {}
95
+
96
+ @JobHandler('validate')
97
+ async handleValidate(job: Job, orderId: string) {
98
+ const { items } = job.data;
99
+ return this.commandBus.execute(new ValidateOrderCommand(orderId, items));
100
+ }
101
+
102
+ @JobHandler('process-payment')
103
+ async handlePayment(job: Job, orderId: string) {
104
+ const { amount } = job.data;
105
+ return this.commandBus.execute(new ProcessPaymentCommand(orderId, amount));
106
+ }
107
+
108
+ @JobHandler('ship')
109
+ async handleShip(job: Job, orderId: string) {
110
+ return this.commandBus.execute(new ShipOrderCommand(orderId));
111
+ }
112
+
113
+ // Wildcard handler for any unmatched job names
114
+ @JobHandler('*')
115
+ async handleOther(job: Job, orderId: string) {
116
+ console.log(`Unknown job type: ${job.name} for order ${orderId}`);
117
+ }
118
+ }
119
+ ```
120
+
121
+ ### 3. Create an Entity Scaler
122
+
123
+ Use `@EntityScaler` to define scaling logic with decorated methods:
124
+
125
+ ```typescript
126
+ import { Injectable } from '@nestjs/common';
127
+ import { EntityScaler, GetActiveEntities, GetDesiredWorkerCount } from 'atomic-queues';
128
+
129
+ @EntityScaler({
130
+ entityType: 'order',
131
+ maxWorkersPerEntity: 1,
132
+ })
133
+ @Injectable()
134
+ export class OrderEntityScaler {
135
+ constructor(private readonly orderRepository: OrderRepository) {}
136
+
137
+ @GetActiveEntities()
138
+ async getActiveOrders(): Promise<string[]> {
139
+ // Return order IDs that have pending work
140
+ return this.orderRepository.findPendingOrderIds();
141
+ }
142
+
143
+ @GetDesiredWorkerCount()
144
+ async getWorkerCount(orderId: string): Promise<number> {
145
+ // Each order gets exactly 1 worker
146
+ return 1;
147
+ }
148
+ }
149
+ ```
150
+
151
+ ### 4. Register in Your Module
152
+
153
+ ```typescript
154
+ @Module({
155
+ imports: [AtomicQueuesModule.forRootAsync({ ... })],
156
+ providers: [
157
+ OrderWorkerProcessor, // Auto-discovered by @WorkerProcessor
158
+ OrderEntityScaler, // Auto-discovered by @EntityScaler
159
+ ],
160
+ })
161
+ export class OrderModule {}
162
+ ```
163
+
164
+ ### 5. Queue Jobs
165
+
166
+ ```typescript
167
+ import { Injectable } from '@nestjs/common';
168
+ import { QueueManagerService } from 'atomic-queues';
169
+
170
+ @Injectable()
171
+ export class OrderService {
172
+ constructor(private readonly queueManager: QueueManagerService) {}
173
+
174
+ async createOrder(orderId: string, items: any[], amount: number) {
175
+ const queue = this.queueManager.getOrCreateQueue(`order-${orderId}-queue`);
176
+
177
+ // Jobs are processed in order (FIFO) by the worker
178
+ await queue.add('validate', { items });
179
+ await queue.add('process-payment', { amount });
180
+ await queue.add('ship', {});
181
+
182
+ return orderId;
183
+ }
184
+ }
185
+ ```
186
+
187
+ That's it! The library will:
188
+ 1. **Auto-discover** your `OrderWorkerProcessor` and `OrderEntityScaler`
189
+ 2. **Create workers** for active jobs via `CronManagerService`
190
+ 3. **Route jobs** to the correct `@JobHandler` method
191
+ 4. **Clean up** workers when jobs are complete
192
+
193
+ ---
194
+
195
+ ## Decorators Reference
196
+
197
+ ### @WorkerProcessor(options)
198
+
199
+ Class decorator that marks a service as a worker processor for an entity type.
200
+
201
+ ```typescript
202
+ @WorkerProcessor({
203
+ entityType: string; // Required: Entity type (e.g., 'order', 'user')
204
+ queueName?: string | ((entityId: string) => string); // Queue name or function
205
+ workerName?: string | ((entityId: string) => string); // Worker name or function
206
+ workerConfig?: {
207
+ concurrency?: number; // Default: 1
208
+ stalledInterval?: number; // Default: 1000ms
209
+ lockDuration?: number; // Default: 30000ms
210
+ heartbeatTTL?: number; // Default: 3 seconds
211
+ heartbeatInterval?: number; // Default: 1000ms
212
+ };
213
+ })
214
+ ```
215
+
216
+ ### @JobHandler(jobName)
217
+
218
+ Method decorator that routes jobs with a specific name to this handler.
219
+
220
+ ```typescript
221
+ @JobHandler('validate') // Handles jobs named 'validate'
222
+ async handleValidate(job: Job, entityId: string) { ... }
223
+
224
+ @JobHandler('*') // Wildcard: handles any unmatched job
225
+ async handleOther(job: Job, entityId: string) { ... }
226
+ ```
227
+
228
+ ### @EntityScaler(options)
229
+
230
+ Class decorator for entity scaling configuration.
231
+
232
+ ```typescript
233
+ @EntityScaler({
234
+ entityType: string; // Required: Entity type to scale
235
+ maxWorkersPerEntity?: number; // Default: 1
236
+ })
237
+ ```
238
+
239
+ ### @GetActiveEntities()
240
+
241
+ Method decorator marking the method that returns active entity IDs.
242
+
243
+ ```typescript
244
+ @GetActiveEntities()
245
+ async getActiveOrders(): Promise<string[]> {
246
+ return ['order-1', 'order-2'];
247
+ }
248
+ ```
249
+
250
+ ### @GetDesiredWorkerCount()
251
+
252
+ Method decorator for desired worker count calculation.
253
+
254
+ ```typescript
255
+ @GetDesiredWorkerCount()
256
+ async getWorkerCount(entityId: string): Promise<number> {
257
+ return 1;
258
+ }
259
+ ```
260
+
261
+ ### @OnSpawnWorker() / @OnTerminateWorker()
22
262
 
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.
263
+ Optional method decorators for custom spawn/terminate logic.
24
264
 
265
+ ```typescript
266
+ @OnSpawnWorker()
267
+ async customSpawn(entityId: string): Promise<void> {
268
+ console.log(`Spawning worker for ${entityId}`);
269
+ }
270
+
271
+ @OnTerminateWorker()
272
+ async customTerminate(entityId: string, workerId: string): Promise<void> {
273
+ console.log(`Terminating worker ${workerId} for ${entityId}`);
274
+ }
25
275
  ```
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]
276
+
277
+ ---
278
+
279
+ ## Migration Guide
280
+
281
+ ### Migrating from Manual Registration to Decorators
282
+
283
+ **Before (Manual Registration):**
284
+
285
+ ```typescript
286
+ // order-job.processor.ts (one file per job type)
287
+ @Injectable()
288
+ @JobProcessor('validate-order')
289
+ export class ValidateOrderProcessor {
290
+ async process(job: Job) {
291
+ // validation logic
292
+ }
293
+ }
294
+
295
+ // order-worker.service.ts (manual worker creation)
296
+ @Injectable()
297
+ export class OrderWorkerService {
298
+ constructor(
299
+ private workerManager: WorkerManagerService,
300
+ private jobRegistry: JobProcessorRegistry,
301
+ ) {}
302
+
303
+ async createOrderWorker(orderId: string) {
304
+ await this.workerManager.createWorker({
305
+ workerName: `order-${orderId}-worker`,
306
+ queueName: `order-${orderId}-queue`,
307
+ processor: async (job) => {
308
+ const processor = this.jobRegistry.getProcessor(job.name);
309
+ await processor.process(job);
310
+ },
311
+ });
312
+ }
313
+ }
314
+
315
+ // app.module.ts (manual entity type registration)
316
+ cronManager.registerEntityType({
317
+ entityType: 'order',
318
+ getActiveEntityIds: async () => [...],
319
+ getDesiredWorkerCount: async (id) => 1,
320
+ onSpawnWorker: async (id) => orderWorkerService.createOrderWorker(id),
321
+ });
322
+ ```
323
+
324
+ **After (Decorator-based):**
325
+
326
+ ```typescript
327
+ // table-worker.processor.ts (single file with all handlers)
328
+ @WorkerProcessor({
329
+ entityType: 'order',
330
+ queueName: (id) => `order-${id}-queue`,
331
+ workerName: (id) => `order-${id}-worker`,
332
+ })
333
+ @Injectable()
334
+ export class OrderWorkerProcessor {
335
+ @JobHandler('validate-order')
336
+ async handleValidate(job: Job, orderId: string) {
337
+ // validation logic
338
+ }
339
+
340
+ @JobHandler('process-payment')
341
+ async handlePayment(job: Job, orderId: string) {
342
+ // payment logic
343
+ }
344
+ }
345
+
346
+ // table-entity.scaler.ts (scaling config in one place)
347
+ @EntityScaler({ entityType: 'order', maxWorkersPerEntity: 1 })
348
+ @Injectable()
349
+ export class OrderEntityScaler {
350
+ @GetActiveEntities()
351
+ async getActiveOrders(): Promise<string[]> { return [...]; }
352
+
353
+ @GetDesiredWorkerCount()
354
+ async getWorkerCount(id: string): Promise<number> { return 1; }
355
+ }
356
+
357
+ // app.module.ts (just provide the classes, auto-discovery handles the rest)
358
+ @Module({
359
+ providers: [OrderWorkerProcessor, OrderEntityScaler],
360
+ })
361
+ export class OrderModule {}
29
362
  ```
30
363
 
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.
364
+ ### Key Benefits of Migration
365
+
366
+ | Aspect | Manual API | Decorator API |
367
+ |--------|-----------|---------------|
368
+ | **Job routing** | Manual switch/case or registry lookup | Automatic via `@JobHandler` |
369
+ | **Worker creation** | Explicit service method | Auto-generated by library |
370
+ | **Scaling config** | Imperative `registerEntityType()` call | Declarative `@EntityScaler` class |
371
+ | **Entity ID access** | Manual parsing from job data | Injected as method parameter |
372
+ | **Code organization** | Multiple files and services | Single processor class per entity type |
373
+ | **Registration** | Manual in `onModuleInit` | Auto-discovered at startup |
33
374
 
34
375
  ---
35
376
 
@@ -299,29 +640,11 @@ Customer B places Order 3 → [validate] → [pay] → [reserve] → [ship]
299
640
 
300
641
  ---
301
642
 
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 atomic-queues bullmq ioredis
318
- ```
319
-
320
- ---
643
+ ## Manual API (Legacy)
321
644
 
322
- ## Quick Start
645
+ The manual API is still available for advanced use cases or gradual migration. **For most use cases, prefer the decorator-based API above.**
323
646
 
324
- ### 1. Import the Module
647
+ ### 1. Module Configuration
325
648
 
326
649
  ```typescript
327
650
  import { Module } from '@nestjs/common';
@@ -343,35 +666,7 @@ import { AtomicQueuesModule } from 'atomic-queues';
343
666
  export class AppModule {}
344
667
  ```
345
668
 
346
- ### 2. Async Configuration
347
-
348
- ```typescript
349
- import { Module } from '@nestjs/common';
350
- import { ConfigModule, ConfigService } from '@nestjs/config';
351
- import { AtomicQueuesModule } from '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
669
+ ### 2. Register Job Processors Manually
375
670
 
376
671
  ```typescript
377
672
  import { Injectable } from '@nestjs/common';
@@ -388,20 +683,9 @@ export class ValidateOrderProcessor {
388
683
  await this.commandBus.execute(new ValidateOrderCommand(orderId, items));
389
684
  }
390
685
  }
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
686
  ```
403
687
 
404
- ### 4. Queue Jobs
688
+ ### 3. Queue Jobs Manually
405
689
 
406
690
  ```typescript
407
691
  import { Injectable } from '@nestjs/common';
@@ -431,7 +715,7 @@ export class OrderService {
431
715
  }
432
716
  ```
433
717
 
434
- ### 5. Create Workers
718
+ ### 4. Create Workers Manually
435
719
 
436
720
  ```typescript
437
721
  import { Injectable } from '@nestjs/common';
@@ -556,6 +840,24 @@ const available = await lockService.getAvailableResource(
556
840
 
557
841
  Automatic worker scaling based on demand.
558
842
 
843
+ **Recommended: Use `@EntityScaler` decorator (see Quick Start section above)**
844
+
845
+ The decorator-based approach is preferred as it's cleaner and auto-discovered:
846
+
847
+ ```typescript
848
+ @EntityScaler({ entityType: 'order', maxWorkersPerEntity: 1 })
849
+ @Injectable()
850
+ export class OrderEntityScaler {
851
+ @GetActiveEntities()
852
+ async getActiveOrders(): Promise<string[]> { ... }
853
+
854
+ @GetDesiredWorkerCount()
855
+ async getWorkerCount(orderId: string): Promise<number> { return 1; }
856
+ }
857
+ ```
858
+
859
+ **Legacy API (Manual Registration):**
860
+
559
861
  ```typescript
560
862
  // Register entity type for automatic scaling
561
863
  cronManager.registerEntityType({