atomic-queues 1.0.15 → 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
@@ -1,10 +1,10 @@
1
- # @nestjs/atomic-queues
1
+ # atomic-queues
2
2
 
3
3
  A plug-and-play NestJS library for atomic process handling per entity with BullMQ, Redis distributed locking, and dynamic worker management.
4
4
 
5
5
  ## Overview
6
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.
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()
262
+
263
+ Optional method decorators for custom spawn/terminate logic.
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
+ }
275
+ ```
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
+ }
22
294
 
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.
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
+ ) {}
24
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
+ });
25
322
  ```
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]
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
 
@@ -39,7 +380,7 @@ Customer B places Order 3 → [validate] → [pay] → [reserve] → [ship]
39
380
 
40
381
  ```
41
382
  ┌─────────────────────────────────────────────────────────────────────────────────────────────┐
42
- @nestjs/atomic-queues ARCHITECTURE
383
+ atomic-queues ARCHITECTURE
43
384
  └─────────────────────────────────────────────────────────────────────────────────────────────┘
44
385
 
45
386
  ┌─────────────────────┐
@@ -299,33 +640,15 @@ 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
643
+ ## Manual API (Legacy)
311
644
 
312
- ---
645
+ The manual API is still available for advanced use cases or gradual migration. **For most use cases, prefer the decorator-based API above.**
313
646
 
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
647
+ ### 1. Module Configuration
325
648
 
326
649
  ```typescript
327
650
  import { Module } from '@nestjs/common';
328
- import { AtomicQueuesModule } from '@nestjs/atomic-queues';
651
+ import { AtomicQueuesModule } from 'atomic-queues';
329
652
 
330
653
  @Module({
331
654
  imports: [
@@ -343,39 +666,11 @@ import { AtomicQueuesModule } from '@nestjs/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 '@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
669
+ ### 2. Register Job Processors Manually
375
670
 
376
671
  ```typescript
377
672
  import { Injectable } from '@nestjs/common';
378
- import { JobProcessor, JobProcessorRegistry } from '@nestjs/atomic-queues';
673
+ import { JobProcessor, JobProcessorRegistry } from 'atomic-queues';
379
674
  import { CommandBus } from '@nestjs/cqrs';
380
675
 
381
676
  @Injectable()
@@ -388,24 +683,13 @@ 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';
408
- import { QueueManagerService, IndexManagerService } from '@nestjs/atomic-queues';
692
+ import { QueueManagerService, IndexManagerService } from 'atomic-queues';
409
693
 
410
694
  @Injectable()
411
695
  export class OrderService {
@@ -431,11 +715,11 @@ 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';
438
- import { WorkerManagerService, JobProcessorRegistry } from '@nestjs/atomic-queues';
722
+ import { WorkerManagerService, JobProcessorRegistry } from 'atomic-queues';
439
723
 
440
724
  @Injectable()
441
725
  export class OrderWorkerService {
@@ -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({