pp-command-bus 1.5.0 → 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 (193) hide show
  1. package/README.md +400 -1219
  2. package/dist/command-bus/command-bus.spec.js +138 -359
  3. package/dist/command-bus/command-bus.spec.js.map +1 -1
  4. package/dist/command-bus/command.d.ts +3 -4
  5. package/dist/command-bus/command.js +3 -32
  6. package/dist/command-bus/command.js.map +1 -1
  7. package/dist/command-bus/config/command-bus-config.d.ts +75 -21
  8. package/dist/command-bus/config/command-bus-config.js +99 -58
  9. package/dist/command-bus/config/command-bus-config.js.map +1 -1
  10. package/dist/command-bus/config/command-bus-config.spec.js +174 -100
  11. package/dist/command-bus/config/command-bus-config.spec.js.map +1 -1
  12. package/dist/command-bus/index.d.ts +39 -52
  13. package/dist/command-bus/index.js +133 -126
  14. package/dist/command-bus/index.js.map +1 -1
  15. package/dist/command-bus/logging/command-logger.d.ts +2 -0
  16. package/dist/command-bus/logging/command-logger.js +7 -0
  17. package/dist/command-bus/logging/command-logger.js.map +1 -1
  18. package/dist/command-bus/logging/command-logger.spec.js +36 -0
  19. package/dist/command-bus/logging/command-logger.spec.js.map +1 -1
  20. package/dist/command-bus/serialization/index.d.ts +6 -0
  21. package/dist/command-bus/serialization/index.js +9 -0
  22. package/dist/command-bus/serialization/index.js.map +1 -0
  23. package/dist/command-bus/serialization/msgpack-serializer.d.ts +26 -0
  24. package/dist/command-bus/serialization/msgpack-serializer.js +70 -0
  25. package/dist/command-bus/serialization/msgpack-serializer.js.map +1 -0
  26. package/dist/command-bus/serialization/msgpack-serializer.spec.js +223 -0
  27. package/dist/command-bus/serialization/msgpack-serializer.spec.js.map +1 -0
  28. package/dist/command-bus/serialization/serializer.interface.d.ts +21 -0
  29. package/dist/command-bus/serialization/serializer.interface.js +3 -0
  30. package/dist/command-bus/serialization/serializer.interface.js.map +1 -0
  31. package/dist/command-bus/transport/consumer-loop.d.ts +45 -0
  32. package/dist/command-bus/transport/consumer-loop.js +90 -0
  33. package/dist/command-bus/transport/consumer-loop.js.map +1 -0
  34. package/dist/command-bus/transport/consumer-loop.spec.js +216 -0
  35. package/dist/command-bus/transport/consumer-loop.spec.js.map +1 -0
  36. package/dist/command-bus/transport/index.d.ts +21 -0
  37. package/dist/command-bus/transport/index.js +23 -0
  38. package/dist/command-bus/transport/index.js.map +1 -0
  39. package/dist/command-bus/transport/message-processor.d.ts +59 -0
  40. package/dist/command-bus/transport/message-processor.js +111 -0
  41. package/dist/command-bus/transport/message-processor.js.map +1 -0
  42. package/dist/command-bus/transport/message-processor.spec.js +185 -0
  43. package/dist/command-bus/transport/message-processor.spec.js.map +1 -0
  44. package/dist/command-bus/transport/pending-recovery.d.ts +54 -0
  45. package/dist/command-bus/transport/pending-recovery.js +139 -0
  46. package/dist/command-bus/transport/pending-recovery.js.map +1 -0
  47. package/dist/command-bus/transport/pending-recovery.spec.js +176 -0
  48. package/dist/command-bus/transport/pending-recovery.spec.js.map +1 -0
  49. package/dist/command-bus/transport/redis-codec.d.ts +24 -0
  50. package/dist/command-bus/transport/redis-codec.js +33 -0
  51. package/dist/command-bus/transport/redis-codec.js.map +1 -0
  52. package/dist/command-bus/transport/redis-codec.spec.js +53 -0
  53. package/dist/command-bus/transport/redis-codec.spec.js.map +1 -0
  54. package/dist/command-bus/transport/redis-streams-transport.d.ts +91 -0
  55. package/dist/command-bus/transport/redis-streams-transport.js +134 -0
  56. package/dist/command-bus/transport/redis-streams-transport.js.map +1 -0
  57. package/dist/command-bus/transport/redis-streams-transport.spec.js +420 -0
  58. package/dist/command-bus/transport/redis-streams-transport.spec.js.map +1 -0
  59. package/dist/command-bus/transport/rpc-handler.d.ts +39 -0
  60. package/dist/command-bus/transport/rpc-handler.js +87 -0
  61. package/dist/command-bus/transport/rpc-handler.js.map +1 -0
  62. package/dist/command-bus/transport/rpc-handler.spec.js +157 -0
  63. package/dist/command-bus/transport/rpc-handler.spec.js.map +1 -0
  64. package/dist/command-bus/transport/stream-consumer.d.ts +89 -0
  65. package/dist/command-bus/transport/stream-consumer.js +181 -0
  66. package/dist/command-bus/transport/stream-consumer.js.map +1 -0
  67. package/dist/command-bus/transport/stream-consumer.spec.js +284 -0
  68. package/dist/command-bus/transport/stream-consumer.spec.js.map +1 -0
  69. package/dist/command-bus/transport/stream-producer.d.ts +23 -0
  70. package/dist/command-bus/transport/stream-producer.js +70 -0
  71. package/dist/command-bus/transport/stream-producer.js.map +1 -0
  72. package/dist/command-bus/transport/stream-producer.spec.js +125 -0
  73. package/dist/command-bus/transport/stream-producer.spec.js.map +1 -0
  74. package/dist/command-bus/transport/transport.interface.d.ts +87 -0
  75. package/dist/command-bus/transport/transport.interface.js +3 -0
  76. package/dist/command-bus/transport/transport.interface.js.map +1 -0
  77. package/dist/command-bus/types/index.d.ts +0 -84
  78. package/dist/examples/rpc.demo.js +1 -1
  79. package/dist/examples/rpc.demo.js.map +1 -1
  80. package/dist/index.d.ts +8 -5
  81. package/dist/index.js +6 -4
  82. package/dist/index.js.map +1 -1
  83. package/dist/pp-command-bus-2.0.0.tgz +0 -0
  84. package/dist/shared/redis/connection-pool.d.ts +54 -0
  85. package/dist/shared/redis/connection-pool.js +117 -0
  86. package/dist/shared/redis/connection-pool.js.map +1 -0
  87. package/dist/shared/redis/connection-pool.spec.js +114 -0
  88. package/dist/shared/redis/connection-pool.spec.js.map +1 -0
  89. package/dist/shared/redis/index.d.ts +5 -3
  90. package/dist/shared/redis/index.js +6 -4
  91. package/dist/shared/redis/index.js.map +1 -1
  92. package/dist/shared/redis/rpc-connection-pool.d.ts +61 -0
  93. package/dist/shared/redis/rpc-connection-pool.js +154 -0
  94. package/dist/shared/redis/rpc-connection-pool.js.map +1 -0
  95. package/dist/shared/redis/rpc-connection-pool.spec.d.ts +1 -0
  96. package/dist/shared/redis/rpc-connection-pool.spec.js +173 -0
  97. package/dist/shared/redis/rpc-connection-pool.spec.js.map +1 -0
  98. package/dist/shared/types.d.ts +0 -4
  99. package/dist/shared/utils/error-utils.d.ts +8 -0
  100. package/dist/shared/utils/error-utils.js +14 -0
  101. package/dist/shared/utils/error-utils.js.map +1 -0
  102. package/package.json +12 -12
  103. package/dist/command-bus/config/auto-config-optimizer.d.ts +0 -35
  104. package/dist/command-bus/config/auto-config-optimizer.js +0 -52
  105. package/dist/command-bus/config/auto-config-optimizer.js.map +0 -1
  106. package/dist/command-bus/config/auto-config-optimizer.spec.js +0 -42
  107. package/dist/command-bus/config/auto-config-optimizer.spec.js.map +0 -1
  108. package/dist/command-bus/job/index.d.ts +0 -6
  109. package/dist/command-bus/job/index.js +0 -15
  110. package/dist/command-bus/job/index.js.map +0 -1
  111. package/dist/command-bus/job/job-options-builder.d.ts +0 -21
  112. package/dist/command-bus/job/job-options-builder.js +0 -58
  113. package/dist/command-bus/job/job-options-builder.js.map +0 -1
  114. package/dist/command-bus/job/job-options-builder.spec.js +0 -156
  115. package/dist/command-bus/job/job-options-builder.spec.js.map +0 -1
  116. package/dist/command-bus/job/job-processor.d.ts +0 -39
  117. package/dist/command-bus/job/job-processor.js +0 -203
  118. package/dist/command-bus/job/job-processor.js.map +0 -1
  119. package/dist/command-bus/job/job-processor.spec.js +0 -436
  120. package/dist/command-bus/job/job-processor.spec.js.map +0 -1
  121. package/dist/command-bus/queue/index.d.ts +0 -5
  122. package/dist/command-bus/queue/index.js +0 -13
  123. package/dist/command-bus/queue/index.js.map +0 -1
  124. package/dist/command-bus/queue/queue-manager.d.ts +0 -56
  125. package/dist/command-bus/queue/queue-manager.js +0 -163
  126. package/dist/command-bus/queue/queue-manager.js.map +0 -1
  127. package/dist/command-bus/queue/queue-manager.spec.js +0 -371
  128. package/dist/command-bus/queue/queue-manager.spec.js.map +0 -1
  129. package/dist/command-bus/rpc/index.d.ts +0 -11
  130. package/dist/command-bus/rpc/index.js +0 -19
  131. package/dist/command-bus/rpc/index.js.map +0 -1
  132. package/dist/command-bus/rpc/payload-compression.service.d.ts +0 -50
  133. package/dist/command-bus/rpc/payload-compression.service.js +0 -215
  134. package/dist/command-bus/rpc/payload-compression.service.js.map +0 -1
  135. package/dist/command-bus/rpc/payload-compression.service.spec.js +0 -376
  136. package/dist/command-bus/rpc/payload-compression.service.spec.js.map +0 -1
  137. package/dist/command-bus/rpc/rpc-coordinator.d.ts +0 -96
  138. package/dist/command-bus/rpc/rpc-coordinator.js +0 -500
  139. package/dist/command-bus/rpc/rpc-coordinator.js.map +0 -1
  140. package/dist/command-bus/rpc/rpc-coordinator.spec.js +0 -621
  141. package/dist/command-bus/rpc/rpc-coordinator.spec.js.map +0 -1
  142. package/dist/command-bus/rpc/rpc-job-cancellation.service.d.ts +0 -82
  143. package/dist/command-bus/rpc/rpc-job-cancellation.service.js +0 -180
  144. package/dist/command-bus/rpc/rpc-job-cancellation.service.js.map +0 -1
  145. package/dist/command-bus/rpc/rpc-job-cancellation.service.spec.js +0 -286
  146. package/dist/command-bus/rpc/rpc-job-cancellation.service.spec.js.map +0 -1
  147. package/dist/command-bus/worker/index.d.ts +0 -10
  148. package/dist/command-bus/worker/index.js +0 -19
  149. package/dist/command-bus/worker/index.js.map +0 -1
  150. package/dist/command-bus/worker/worker-benchmark.d.ts +0 -71
  151. package/dist/command-bus/worker/worker-benchmark.js +0 -202
  152. package/dist/command-bus/worker/worker-benchmark.js.map +0 -1
  153. package/dist/command-bus/worker/worker-benchmark.spec.js +0 -310
  154. package/dist/command-bus/worker/worker-benchmark.spec.js.map +0 -1
  155. package/dist/command-bus/worker/worker-metrics-collector.d.ts +0 -98
  156. package/dist/command-bus/worker/worker-metrics-collector.js +0 -242
  157. package/dist/command-bus/worker/worker-metrics-collector.js.map +0 -1
  158. package/dist/command-bus/worker/worker-orchestrator.d.ts +0 -70
  159. package/dist/command-bus/worker/worker-orchestrator.js +0 -339
  160. package/dist/command-bus/worker/worker-orchestrator.js.map +0 -1
  161. package/dist/command-bus/worker/worker-orchestrator.spec.js +0 -712
  162. package/dist/command-bus/worker/worker-orchestrator.spec.js.map +0 -1
  163. package/dist/examples/auto-config.demo.d.ts +0 -9
  164. package/dist/examples/auto-config.demo.js +0 -106
  165. package/dist/examples/auto-config.demo.js.map +0 -1
  166. package/dist/examples/rpc-compression.demo.d.ts +0 -5
  167. package/dist/examples/rpc-compression.demo.js +0 -358
  168. package/dist/examples/rpc-compression.demo.js.map +0 -1
  169. package/dist/examples/rpc-resilience.demo.d.ts +0 -15
  170. package/dist/examples/rpc-resilience.demo.js +0 -233
  171. package/dist/examples/rpc-resilience.demo.js.map +0 -1
  172. package/dist/pp-command-bus-1.5.0.tgz +0 -0
  173. package/dist/shared/config/base-config.d.ts +0 -54
  174. package/dist/shared/config/base-config.js +0 -114
  175. package/dist/shared/config/base-config.js.map +0 -1
  176. package/dist/shared/config/base-config.spec.js +0 -204
  177. package/dist/shared/config/base-config.spec.js.map +0 -1
  178. package/dist/shared/config/index.d.ts +0 -1
  179. package/dist/shared/config/index.js +0 -9
  180. package/dist/shared/config/index.js.map +0 -1
  181. package/dist/shared/redis/redis-connection-factory.d.ts +0 -66
  182. package/dist/shared/redis/redis-connection-factory.js +0 -113
  183. package/dist/shared/redis/redis-connection-factory.js.map +0 -1
  184. /package/dist/command-bus/{config/auto-config-optimizer.spec.d.ts → serialization/msgpack-serializer.spec.d.ts} +0 -0
  185. /package/dist/command-bus/{job/job-options-builder.spec.d.ts → transport/consumer-loop.spec.d.ts} +0 -0
  186. /package/dist/command-bus/{job/job-processor.spec.d.ts → transport/message-processor.spec.d.ts} +0 -0
  187. /package/dist/command-bus/{queue/queue-manager.spec.d.ts → transport/pending-recovery.spec.d.ts} +0 -0
  188. /package/dist/command-bus/{rpc/payload-compression.service.spec.d.ts → transport/redis-codec.spec.d.ts} +0 -0
  189. /package/dist/command-bus/{rpc/rpc-coordinator.spec.d.ts → transport/redis-streams-transport.spec.d.ts} +0 -0
  190. /package/dist/command-bus/{rpc/rpc-job-cancellation.service.spec.d.ts → transport/rpc-handler.spec.d.ts} +0 -0
  191. /package/dist/command-bus/{worker/worker-benchmark.spec.d.ts → transport/stream-consumer.spec.d.ts} +0 -0
  192. /package/dist/command-bus/{worker/worker-orchestrator.spec.d.ts → transport/stream-producer.spec.d.ts} +0 -0
  193. /package/dist/shared/{config/base-config.spec.d.ts → redis/connection-pool.spec.d.ts} +0 -0
package/README.md CHANGED
@@ -1,239 +1,103 @@
1
1
  # PP Command Bus
2
2
 
3
- Distributed Command Bus library supporting RPC and job queuing with BullMQ for Redis/DragonflyDB.
3
+ Rozproszona biblioteka Command Bus oparta na **Redis Streams** z serializacją **MessagePack** i RPC przez **LPUSH/BRPOP**. Zoptymalizowana pod **DragonflyDB**.
4
+
5
+ ## Spis treści
6
+
7
+ - [Opis](#opis)
8
+ - [Architektura](#architektura)
9
+ - [Instalacja](#instalacja)
10
+ - [Szybki start](#szybki-start)
11
+ - [API](#api)
12
+ - [Konfiguracja](#konfiguracja)
13
+ - [Flow danych](#flow-danych)
14
+ - [Komponenty](#komponenty)
15
+ - [Struktura projektu](#struktura-projektu)
16
+ - [Testowanie](#testowanie)
17
+ - [Changelog](#changelog)
4
18
 
5
19
  ## Opis
6
20
 
7
- **pp-command-bus** to zaawansowana biblioteka do obsługi rozproszonych komend zgodna ze wzorcem **CQRS (Command Query Responsibility Segregation)**. Zapewnia wysoką wydajność, automatyczną optymalizację i zaawansowane funkcje produkcyjne.
8
-
9
- ### Kluczowe Cechy
10
-
11
- - **Fire-and-forget commands** - wysyłanie komend bez oczekiwania na wynik
12
- - **RPC (Remote Procedure Call)** - synchroniczne wywołania przez Redis Pub/Sub z oczekiwaniem na odpowiedź
13
- - **Automatyczna kompresja** - gzip dla payloadów RPC >1KB (konfigurowalne)
14
- - **Auto-optymalizacja** - dynamiczne dostosowywanie concurrency na podstawie CPU/RAM
15
- - **Process isolation** - izolacja odpowiedzi RPC między procesami Node.js
16
- - ✅ **Job queuing** - kolejkowanie zadań z retry, backoff i delayed execution
17
- - ✅ **BullMQ integration** - wydajna kolejka zadań na Redis/DragonflyDB
18
- - ✅ **TypeScript** - pełne wsparcie typów i strict mode
19
- - **Memory leak protection** - zaawansowana diagnostyka i cleanup
20
- - ✅ **Command logging** - opcjonalne logowanie komend do plików JSONL
21
- - ✅ **RPC Job Cancellation** - automatyczne usuwanie niepodjętych jobów RPC przy timeout
22
- - ✅ **Modularna architektura** - komponenty zgodne z zasadami SOLID, DDD i SRP
23
-
24
- ## Architektura Systemu
25
-
26
- ### Diagram Komponentów
27
-
28
- ```
29
- ┌─────────────────────────────────────────────────────────────────────┐
30
- CommandBus │
31
- │ ┌────────────────┐ ┌──────────────┐ ┌─────────────────────────┐ │
32
- QueueManager │ │ RpcCoordinator│ │ WorkerOrchestrator │ │
33
- - Cache kolejek│ │ - Pub/Sub │ │ - Dynamiczne concurrency
34
- │ │ - BullMQ Queue │ │ - Process ID │ │ - Worker benchmark │ │
35
- └────────┬───────┘ └──────┬───────┘ └────────┬────────────────┘
36
- │ │ │ │
37
-
38
- ┌────────▼──────────────────▼────────────────────▼───────────────┐
39
- │ │ JobProcessor (Command Handler Execution) │ │
40
- │ - Kompresja/dekompresja payloadów │
41
- - Wykonywanie handlerów
42
- - Wysyłanie odpowiedzi RPC przez Pub/Sub │ │
43
- └──────────────────────────┬──────────────────────────────────────┘
44
- └─────────────────────────────┼────────────────────────────────────────┘
45
-
46
- ┌─────────▼──────────┐
47
- PayloadCompression
48
- Service
49
- │ - gzip compression │
50
- │ - threshold: 1KB
51
- └─────────────────────┘
52
- ```
21
+ **pp-command-bus** to biblioteka do obsługi rozproszonych komend zgodna ze wzorcem **CQRS**. Zapewnia:
22
+
23
+ - **Fire-and-forget dispatch** — komendy wysyłane przez `XADD` do Redis Streams
24
+ - **Batch dispatch** — wiele komend w jednym pipeline Redis
25
+ - **RPC (request/response)** synchroniczne wywołania z timeout przez `LPUSH/BRPOP`
26
+ - **Consumer Groups** dokładnie jedno przetworzenie komendy (exactly-once delivery)
27
+ - **Dead Letter Queue** wiadomości po przekroczeniu prób trafiają do `dlq:{stream}`
28
+ - **Auto-recovery** automatyczne przejmowanie stalled wiadomości przez `XPENDING/XCLAIM`
29
+ - **MessagePack** binarna serializacja z natywną obsługą `Date` (extension type)
30
+
31
+ ## Architektura
32
+
33
+ ### Diagram komponentów
34
+
35
+ ```
36
+ ┌─────────────────────────────────────────────────────────────┐
37
+ │ CommandBus │
38
+ │ dispatch() | dispatchBatch() | call() | handle() | close() │
39
+ └────────────────────────────┬────────────────────────────────┘
40
+
41
+ ┌────────┴────────┐
42
+ │ ITransport │
43
+ └────────┬────────┘
44
+
45
+ ┌────────────────────────────┴────────────────────────────────┐
46
+ RedisStreamsTransport
47
+ (fasada / kompozycja)
48
+ ├─────────────┬──────────────┬──────────────┬─────────────────┤
49
+ │ │ │
50
+ ┌───────────┴─┐ ┌─────────┴──────┐ ┌────┴─────┐ ┌─────────┴──┐
51
+ Stream Stream │ Rpc │ │ Pending │
52
+ Producer │ Consumer │ │ Handler │ │ Recovery │
53
+ (XADD) (koordynator) │ (BRPOP) │ │ (XCLAIM) │
54
+ └─────────────┘ └───────┬────────┘ └──────────┘ └────────────┘
55
+
56
+ ┌──────────┴──────────┐
57
+ │ │
58
+ │ ┌────────┴─────────┐ ┌────────┴────────┐ │
59
+ │ Message │ │ Consumer │ │
60
+ │ │ Processor │ │ Loop │ │
61
+ (parse/XACK/RPC) │ (XREADGROUP) │ │
62
+ └──────────────────┘ └─────────────────┘
63
+ ├─────────────────────────────────────────────────────────────┤
64
+ Warstwa połączeń
65
+ │ ┌─────────────────────┐ ┌──────────────────────┐ │
66
+ │ │ RedisConnectionPool │ │ RpcConnectionPool │ │
67
+ │ │ (round-robin, eager)│ │ (bounded, lazy, BRPOP)│ │
68
+ │ └─────────────────────┘ └──────────────────────┘ │
69
+ ├─────────────────────────────────────────────────────────────┤
70
+ │ Warstwa serializacji │
71
+ │ ┌─────────────────────┐ ┌──────────────────────┐ │
72
+ │ │ MsgpackSerializer │ │ RedisCodec │ │
73
+ │ │ (Date extension) │ │ (base64 encode) │ │
74
+ │ └─────────────────────┘ └──────────────────────┘ │
75
+ └─────────────────────────────────────────────────────────────┘
76
+ ```
77
+
78
+ ### Segregacja interfejsów (ISP)
79
+
80
+ ```typescript
81
+ interface IStreamProducer {
82
+ enqueue(streamName: string, data: Buffer): Promise<string>;
83
+ enqueueBatch(entries: Array<{ streamName: string; data: Buffer }>): Promise<string[]>;
84
+ }
53
85
 
54
- ### Główne Serwisy
55
-
56
- #### 1. **CommandBus** (główna klasa orkiestrująca)
57
- - **Odpowiedzialność**: Główny punkt wejścia dla użytkownika, orkiestracja wszystkich komponentów
58
- - **Metody publiczne**:
59
- - `dispatch(command)` - wysyłanie fire-and-forget
60
- - `call(command, timeout)` - synchroniczne RPC
61
- - `handle(commandClass, handler)` - rejestracja handlerów
62
- - `close()` - graceful shutdown
63
- - **Zarządzanie połączeniami**: 3 dedykowane połączenia Redis (Queue, Worker, RPC)
64
-
65
- #### 2. **RpcCoordinator** (zarządzanie RPC)
66
- - **Odpowiedzialność**: Zarządzanie cyklem życia wywołań RPC przez Redis Pub/Sub
67
- - **Kluczowe funkcje**:
68
- - **Shared Subscriber** - jeden subscriber dla wszystkich RPC calls (pattern matching)
69
- - **Process Isolation** - UUID procesu Node.js dla izolacji odpowiedzi między procesami
70
- - **Timeout Management** - automatyczne cleanup po timeout
71
- - **Job Cancellation** - usuwanie niepodjętych jobów przy timeout (optymalizacja zasobów)
72
- - **Multiplexing** - routing odpowiedzi do odpowiednich promises
73
- - **Kanały**: `rpc:response:{processId}:{correlationId}`
74
-
75
- #### 3. **WorkerOrchestrator** (orkiestracja workerów)
76
- - **Odpowiedzialność**: Zarządzanie workerami BullMQ i dynamiczna optymalizacja
77
- - **Kluczowe funkcje**:
78
- - **WorkerBenchmark** - automatyczny benchmark przy rejestracji handlera
79
- - **WorkerMetricsCollector** - event-driven metrics collection
80
- - **Dynamic Concurrency** - dostosowywanie concurrency +/-20% co 30s
81
- - **Event Handlers** - obsługa zdarzeń: active, completed, failed, stalled
82
- - **Limity concurrency**: min 10, max 2000
83
-
84
- #### 4. **JobProcessor** (wykonywanie handlerów)
85
- - **Odpowiedzialność**: Wykonywanie handlerów komend i obsługa odpowiedzi RPC
86
- - **Flow przetwarzania**:
87
- 1. Dekompresja payloadu (jeśli skompresowany)
88
- 2. Sprawdzenie flagi cancellation (pomijanie anulowanych RPC)
89
- 3. Rekonstrukcja obiektów Date
90
- 4. Opcjonalne logowanie komendy
91
- 5. Wykonanie handlera
92
- 6. Wysłanie odpowiedzi RPC przez Pub/Sub (jeśli RPC)
93
- - **Kompresja odpowiedzi**: automatyczna kompresja przez PayloadCompressionService
94
- - **RPC Cancellation**: pomijanie jobów oznaczonych jako anulowane (timeout)
95
-
96
- #### 5. **QueueManager** (zarządzanie kolejkami)
97
- - **Odpowiedzialność**: Cache kolejek BullMQ dla optymalizacji pamięci
98
- - **Funkcje**:
99
- - `getOrCreateQueue(commandName)` - lazy loading kolejek
100
- - `closeAllQueues()` - graceful shutdown wszystkich kolejek
101
- - **Naming**: `{CommandName}` jako nazwa kolejki
102
-
103
- #### 6. **PayloadCompressionService** (kompresja)
104
- - **Odpowiedzialność**: Automatyczna kompresja/dekompresja payloadów gzip
105
- - **Threshold**: 1024 bajty (1KB) domyślnie, konfigurowalne przez ENV
106
- - **Metody**:
107
- - `compressCommand(command)` - dodaje flagę `__compressed`
108
- - `decompressCommand(command)` - dekompresja i usunięcie flagi
109
- - `compress(data)` - generyczna kompresja do base64
110
- - `decompress(data, compressed)` - generyczna dekompresja
111
- - **Współdzielony serwis**: jedna instancja dla całego CommandBus
112
-
113
- #### 7. **RpcJobCancellationService** (anulowanie jobów RPC)
114
- - **Odpowiedzialność**: Zarządzanie anulowaniem niepodjętych jobów RPC przy timeout
115
- - **Kluczowe funkcje**:
116
- - **markAsCancelled(correlationId)** - oznacza job jako anulowany w Redis
117
- - **isCancelled(correlationId)** - sprawdza flagę cancellation
118
- - **clearCancellation(correlationId)** - usuwa flagę po przetworzeniu/usunięciu
119
- - **tryRemoveJob(jobId, queueName, callback)** - próba usunięcia joba z kolejki
120
- - **TTL kluczy Redis**: 24 godziny (automatyczne wygaśnięcie)
121
- - **Graceful degradation**: błędy Redis nie blokują przetwarzania (zwraca false)
122
- - **Klucze Redis**: `rpc:cancelled:{correlationId}`
123
-
124
- #### 8. **CommandLogger** (opcjonalne logowanie)
125
- - **Odpowiedzialność**: Persystencja komend do plików JSONL
126
- - **Format**: `{timestamp}.jsonl` - rotacja co godzinę
127
- - **Zawartość**: pełny payload komendy z metadanymi
128
- - **Aktywacja**: przez ENV `COMMAND_BUS_LOG=./command-logs`
129
-
130
- #### 9. **AutoConfigOptimizer** (auto-optymalizacja)
131
- - **Odpowiedzialność**: Obliczanie optymalnego concurrency na podstawie zasobów systemowych
132
- - **Heurystyka**:
133
- - I/O-heavy workload (Redis/BullMQ)
134
- - `concurrency = CPU cores * 2 + (availableMemory / 512MB)`
135
- - Zakładane 512MB RAM per worker
136
- - **Aktywacja**: domyślnie włączone (ENV `COMMAND_BUS_AUTO_OPTIMIZE=false` wyłącza)
137
-
138
- #### 10. **RedisConnectionFactory** (fabryka połączeń Redis)
139
- - **Odpowiedzialność**: Tworzenie połączeń Redis z wbudowaną obsługą błędów i eventów
140
- - **Zgodność z SRP**: CommandBus nie zarządza bezpośrednio połączeniami
141
- - **Kluczowe funkcje**:
142
- - `create(options, name)` - tworzy połączenie z pełną obsługą eventów
143
- - `createForWorker(options, name)` - dodaje `maxRetriesPerRequest: null` dla BullMQ
144
- - Obsługa eventów: `error`, `close`, `reconnecting`, `connect`, `ready`
145
- - Formatowanie błędów `AggregateError` (Node.js dual-stack IPv4/IPv6)
146
- - **Event Handlers**:
147
- - `on('error')` - logowanie błędów połączenia (zapobiega `Unhandled error event`)
148
- - `on('close')` - informacja o zamknięciu połączenia
149
- - `on('reconnecting')` - informacja o ponownym łączeniu
150
- - `on('connect')` / `on('ready')` - potwierdzenie nawiązania połączenia
151
-
152
- ### Flow Przepływu Danych
153
-
154
- #### Flow 1: dispatch() - Fire-and-forget
86
+ interface IStreamConsumer {
87
+ consume(streamName: string, groupName: string, handler: ConsumerHandler): Promise<void>;
88
+ }
155
89
 
156
- ```
157
- User Code
158
-
159
- ├─→ 1. commandBus.dispatch(command)
160
-
161
- ├─→ 2. PayloadCompressionService.compressCommand(command)
162
- │ └─→ Jeśli payload >1KB → gzip → base64 → __compressed: true
163
-
164
- ├─→ 3. QueueManager.getOrCreateQueue(commandName)
165
- │ └─→ Cache hit/miss → zwraca Queue
166
-
167
- ├─→ 4. queue.add(commandName, compressedCommand, options)
168
- │ └─→ BullMQ dodaje job do Redis
169
-
170
- └─→ 5. Promise<void> resolved (nie czekamy na wynik)
171
-
172
- Worker Side (asynchronicznie)
173
-
174
- ├─→ 6. Worker pobiera job z kolejki
175
-
176
- ├─→ 7. JobProcessor.process(job)
177
- │ ├─→ Dekompresja (jeśli __compressed)
178
- │ ├─→ Rekonstrukcja Date
179
- │ ├─→ Opcjonalne logowanie (CommandLogger)
180
- │ └─→ Wykonanie handlera
181
-
182
- └─→ 8. Worker kończy job (success/fail)
183
- ```
90
+ interface IRpcTransport {
91
+ rpcCall(commandName: string, data: Buffer, timeout: number): Promise<Buffer>;
92
+ rpcRespond(responseKey: string, data: Buffer, ttl: number): Promise<void>;
93
+ }
184
94
 
185
- #### Flow 2: call() - RPC przez Redis Pub/Sub
95
+ interface IClosable {
96
+ close(): Promise<void>;
97
+ }
186
98
 
187
- ```
188
- User Code
189
-
190
- ├─→ 1. commandBus.call(command, timeout)
191
-
192
- ├─→ 2. RpcCoordinator.registerCall(correlationId, commandName, timeout)
193
- │ ├─→ Oczekiwanie na gotowość shared subscriber (5s timeout)
194
- │ ├─→ Utworzenie Promise<T> dla odpowiedzi
195
- │ ├─→ Zapisanie pending call w Map
196
- │ └─→ Zwrócenie responsePromise (bez blokowania)
197
-
198
- ├─→ 3. PayloadCompressionService.compressCommand(command)
199
- │ └─→ Jeśli payload >1KB → gzip → __compressed: true
200
-
201
- ├─→ 4. RpcCoordinator.prepareRpcCommand(compressedCommand)
202
- │ └─→ Dodaje __rpcMetadata: { correlationId, responseChannel, timestamp }
203
-
204
- ├─→ 5. QueueManager.getOrCreateQueue(commandName)
205
- │ └─→ Cache hit/miss → zwraca Queue
206
-
207
- ├─→ 6. queue.add(commandName, commandWithMetadata, options)
208
- │ └─→ BullMQ dodaje job do Redis
209
-
210
- └─→ 7. await responsePromise (czeka na odpowiedź z Worker)
211
-
212
- Worker Side
213
-
214
- ├─→ 8. Worker pobiera job z kolejki
215
-
216
- ├─→ 9. JobProcessor.process(job)
217
- │ ├─→ Dekompresja payloadu (jeśli __compressed)
218
- │ ├─→ Rekonstrukcja Date
219
- │ ├─→ Wykonanie handlera → result
220
- │ │
221
- │ └─→ 10. JobProcessor.sendRpcResponse(rpcMetadata, result, null)
222
- │ ├─→ PayloadCompressionService.compress({ correlationId, result, error })
223
- │ ├─→ Wrapper: { data, compressed }
224
- │ └─→ redis.publish(responseChannel, JSON.stringify(wrapper))
225
-
226
- Shared Subscriber (RpcCoordinator)
227
-
228
- ├─→ 11. pmessage event → handleRpcMessage(channel, message)
229
- │ ├─→ Ekstraktuj correlationId z channel
230
- │ ├─→ Weryfikuj processInstanceId (process isolation)
231
- │ ├─→ Znajdź pending call w Map
232
- │ ├─→ PayloadCompressionService.decompress(wrapper.data, wrapper.compressed)
233
- │ ├─→ resolve(result) lub reject(error)
234
- │ └─→ Cleanup: clearTimeout, delete z Map
235
-
236
- └─→ 12. User Code otrzymuje wynik → Promise<T> resolved
99
+ // Pełny interfejs transportu — kompozycja segregowanych interfejsów
100
+ interface ITransport extends IStreamProducer, IStreamConsumer, IRpcTransport, IClosable {}
237
101
  ```
238
102
 
239
103
  ## Instalacja
@@ -244,1087 +108,404 @@ npm install pp-command-bus
244
108
 
245
109
  ### Wymagania
246
110
 
247
- - Node.js >= 14
248
- - Redis >= 5 lub DragonflyDB
249
- - TypeScript >= 4.5 (opcjonalnie)
250
-
251
- ## Szybki start
111
+ - **Node.js** >= 18
112
+ - **Redis** >= 6.2 lub **DragonflyDB** >= 1.0
113
+ - **TypeScript** >= 5.0 (opcjonalnie, pełne typowanie)
252
114
 
253
- ### 1. Konfiguracja
115
+ ### Zależności
254
116
 
255
- ```typescript
256
- import { CommandBus, CommandBusConfig, Command } from 'pp-command-bus';
117
+ | Pakiet | Opis |
118
+ |--------|------|
119
+ | `ioredis` | Klient Redis z pipelining, Cluster, Sentinel |
120
+ | `@msgpack/msgpack` | Binarna serializacja z extension types |
257
121
 
258
- // Konfiguracja CommandBus
259
- const config = new CommandBusConfig({
260
- redisUrl: 'redis://localhost:6379',
261
- logger: console, // ILogger interface
262
- logLevel: 'log', // debug | log | warn | error
263
- concurrency: 5,
264
- maxAttempts: 1,
265
- });
266
-
267
- // Utwórz instancję CommandBus
268
- const commandBus = new CommandBus(config);
269
- ```
122
+ ## Szybki start
270
123
 
271
- ### 2. Definiowanie komend
124
+ ### Definiowanie komendy
272
125
 
273
126
  ```typescript
274
- class CreateUserCommand extends Command<{ email: string; name: string }> {
275
- constructor(payload: { email: string; name: string }) {
127
+ import { Command } from 'pp-command-bus';
128
+
129
+ class CreateUserCommand extends Command<{ name: string; email: string }> {
130
+ constructor(payload: { name: string; email: string }) {
276
131
  super(payload);
277
132
  }
278
133
  }
279
134
  ```
280
135
 
281
- ### 3. Rejestracja handlerów
282
-
283
- ```typescript
284
- commandBus.handle(CreateUserCommand, async (command) => {
285
- const { name, email } = command.__payload;
286
- console.log(`Creating user: ${name} (${email})`);
287
-
288
- // Twoja logika biznesowa
289
- const user = await createUser(email, name);
290
-
291
- // Zwróć wynik (opcjonalnie)
292
- return { userId: user.id };
293
- });
294
- ```
295
-
296
- ### 4. Wysyłanie komend (Fire-and-forget)
297
-
298
- ```typescript
299
- const command = new CreateUserCommand({
300
- email: 'jan.kowalski@example.com',
301
- name: 'Jan Kowalski'
302
- });
303
-
304
- // Wyślij komendę bez oczekiwania na wynik
305
- await commandBus.dispatch(command);
306
- ```
307
-
308
- ### 5. RPC - wywołania synchroniczne
309
-
310
- ```typescript
311
- // Wywołaj komendę i poczekaj na wynik
312
- const result = await commandBus.call<{ userId: string }>(command, 5000); // timeout 5s
313
-
314
- console.log(`User created with ID: ${result.userId}`);
315
- ```
316
-
317
- ## Zmienne Środowiskowe
318
-
319
- Biblioteka wspiera konfigurację poprzez zmienne środowiskowe z prefiksem `COMMAND_BUS_` (z fallbackiem do starszych nazw `EVENT_BUS_*`):
320
-
321
- ### Kompletna Lista Zmiennych
322
-
323
- | Zmienna | Typ | Wartość Domyślna | Opis |
324
- |---------|-----|------------------|------|
325
- | `REDIS_URL` | string | `redis://localhost:6379` | URL połączenia Redis/DragonflyDB (wspiera username, password, db) |
326
- | `REDIS_RETRY_DELAY` | number | `5000` | Opóźnienie między próbami reconnect do Redis w milisekundach |
327
- | `REDIS_MAX_RETRIES` | number | `0` | Maksymalna liczba prób reconnect (0 = nieskończoność) |
328
- | `LOG_LEVEL` | enum | `log` | Poziom logowania: `debug`, `log`, `warn`, `error` |
329
- | `COMMAND_BUS_CONCURRENCY` | number | `1` (lub auto) | Liczba równoległych workerów do przetwarzania komend |
330
- | `COMMAND_BUS_MAX_ATTEMPTS` | number | `1` | Maksymalna liczba prób przetworzenia zadania |
331
- | `COMMAND_BUS_BACKOFF_DELAY` | number | `2000` | Opóźnienie między próbami w milisekundach |
332
- | `COMMAND_BUS_QUEUE_MODE` | enum | `fifo` | Tryb przetwarzania kolejki: `fifo` (First In First Out) lub `lifo` (Last In First Out) |
333
- | `COMMAND_BUS_LOG` | string | _(puste)_ | Ścieżka do katalogu logów komend (JSONL format, rotacja co godzinę) |
334
- | `COMMAND_BUS_AUTO_OPTIMIZE` | boolean | `true` | Włącz auto-optymalizację concurrency na podstawie CPU/RAM |
335
- | `COMMAND_BUS_COMPRESSION_THRESHOLD` | number | `1024` | Próg kompresji gzip dla payloadów RPC w bajtach (1KB domyślnie) |
336
-
337
- ### Fallback do Starszych Nazw
338
-
339
- Dla kompatybilności wstecznej obsługiwane są również prefiksy `EVENT_BUS_*`:
340
-
341
- - `EVENT_BUS_CONCURRENCY` → `COMMAND_BUS_CONCURRENCY`
342
- - `EVENT_BUS_MAX_ATTEMPTS` → `COMMAND_BUS_MAX_ATTEMPTS`
343
- - `EVENT_BUS_BACKOFF_DELAY` → `COMMAND_BUS_BACKOFF_DELAY`
344
- - `EVENT_BUS_QUEUE_MODE` → `COMMAND_BUS_QUEUE_MODE`
345
- - `EVENT_BUS_LOG` → `COMMAND_BUS_LOG`
346
-
347
- ### Przykład Konfiguracji .env
348
-
349
- ```bash
350
- # Połączenie Redis
351
- REDIS_URL=redis://username:password@localhost:6379/0
352
-
353
- # Strategia reconnect Redis (stałe opóźnienie)
354
- REDIS_RETRY_DELAY=5000 # 5 sekund między próbami (domyślnie)
355
- REDIS_MAX_RETRIES=0 # 0 = nieskończoność (domyślnie)
356
-
357
- # Poziom logowania
358
- LOG_LEVEL=log # debug | log | warn | error
359
-
360
- # Auto-optymalizacja (domyślnie włączona)
361
- COMMAND_BUS_AUTO_OPTIMIZE=true
362
-
363
- # Concurrency (opcjonalnie - auto-optymalizacja ustawi optymalną wartość)
364
- # COMMAND_BUS_CONCURRENCY=10
365
-
366
- # Retry i backoff
367
- COMMAND_BUS_MAX_ATTEMPTS=1 # Maksymalna liczba prób
368
- COMMAND_BUS_BACKOFF_DELAY=3000 # Opóźnienie między próbami (3s)
369
-
370
- # Tryb kolejki
371
- COMMAND_BUS_QUEUE_MODE=fifo # fifo lub lifo
372
-
373
- # Kompresja payloadów (próg w bajtach)
374
- COMMAND_BUS_COMPRESSION_THRESHOLD=2048 # 2KB (domyślnie 1KB)
375
-
376
- # Logowanie komend do plików
377
- COMMAND_BUS_LOG=./command-logs # Ścieżka do katalogu logów
378
- ```
379
-
380
- ### Priorytety Konfiguracji
381
-
382
- 1. **Parametry konstruktora** - najwyższy priorytet
383
- 2. **Zmienne środowiskowe** - średni priorytet
384
- 3. **Wartości domyślne** - najniższy priorytet
385
-
386
- ```typescript
387
- // Przykład: parametry konstruktora nadpisują ENV
388
- const config = new CommandBusConfig({
389
- redisUrl: 'redis://localhost:6379',
390
- concurrency: 20, // Nadpisuje COMMAND_BUS_CONCURRENCY
391
- autoOptimize: false, // Wyłącza auto-optymalizację
392
- });
393
- ```
394
-
395
- ## Konfiguracja zaawansowana
396
-
397
- ### Auto-optymalizacja Concurrency
398
-
399
- Auto-optymalizacja automatycznie oblicza optymalną wartość concurrency na podstawie zasobów systemowych:
400
-
401
- ```typescript
402
- // Auto-optymalizacja włączona (domyślnie)
403
- const config = new CommandBusConfig({
404
- redisUrl: 'redis://localhost:6379',
405
- autoOptimize: true, // Domyślnie true
406
- // concurrency zostanie obliczone jako: CPU cores * 2 + (availableMemory / 512MB)
407
- });
408
-
409
- // Wyłączenie auto-optymalizacji
410
- const config2 = new CommandBusConfig({
411
- redisUrl: 'redis://localhost:6379',
412
- autoOptimize: false,
413
- concurrency: 10, // Ręczna wartość
414
- });
415
- ```
416
-
417
- **Algorytm auto-optymalizacji**:
418
- ```
419
- concurrency = (CPU cores * 2) + Math.floor(availableMemory / 512MB)
420
-
421
- Przykład:
422
- - 8 CPU cores
423
- - 16GB RAM dostępne
424
- - concurrency = (8 * 2) + Math.floor(16384 / 512) = 16 + 32 = 48
425
- ```
426
-
427
- ### Kompresja Payloadów
428
-
429
- Automatyczna kompresja gzip dla payloadów RPC większych niż threshold:
430
-
431
- ```typescript
432
- const config = new CommandBusConfig({
433
- redisUrl: 'redis://localhost:6379',
434
- compressionThreshold: 2048, // 2KB (domyślnie 1KB)
435
- });
436
-
437
- // Przykład: payload 3KB zostanie automatycznie skompresowany
438
- const largeCommand = new ProcessReportCommand(largeData); // 3KB
439
- const result = await commandBus.call(largeCommand); // Automatyczna kompresja/dekompresja
440
- ```
441
-
442
- **Korzyści kompresji**:
443
- - Redukcja transferu danych przez Redis
444
- - Szybsze przesyłanie dużych payloadów
445
- - Niższe zużycie pamięci Redis
446
- - Transparent dla użytkownika (automatyczna dekompresja)
447
-
448
- ### Command Logging
449
-
450
- Logowanie komend do plików JSONL (rotacja co godzinę):
136
+ ### Konfiguracja i inicjalizacja
451
137
 
452
138
  ```typescript
453
- const config = new CommandBusConfig({
454
- redisUrl: 'redis://localhost:6379',
455
- commandLog: './command-logs', // Ścieżka do katalogu logów
456
- });
139
+ import { CommandBus, CommandBusConfig } from 'pp-command-bus';
457
140
 
458
- // Struktura plików:
459
- // ./command-logs/2025-01-27T10.jsonl
460
- // ./command-logs/2025-01-27T11.jsonl
461
- ```
462
-
463
- **Format JSONL (JSON Lines)**:
464
- ```json
465
- {"__name":"CreateUserCommand","__id":"uuid","__time":1706347200000,"email":"jan@example.com","name":"Jan"}
466
- {"__name":"ProcessOrderCommand","__id":"uuid","__time":1706347201000,"orderId":"12345"}
467
- ```
468
-
469
- ### Opcje Redis
470
-
471
- ```typescript
472
- const config = new CommandBusConfig({
473
- redisUrl: 'redis://username:password@localhost:6379/0',
474
- // Parsuje się do:
475
- // - host: localhost
476
- // - port: 6379
477
- // - username: username (opcjonalnie)
478
- // - password: password (opcjonalnie)
479
- // - db: 0 (opcjonalnie)
480
- });
481
- ```
482
-
483
- ### Concurrency i Retry
484
-
485
- ```typescript
486
- const config = new CommandBusConfig({
487
- redisUrl: 'redis://localhost:6379',
488
- concurrency: 10, // Liczba równoległych workerów (lub auto)
489
- maxAttempts: 5, // Maksymalna liczba prób przetworzenia zadania
490
- backoffDelay: 3000, // Opóźnienie między próbami (3s)
491
- });
492
- ```
493
-
494
- ### Tryb kolejki
495
-
496
- ```typescript
497
- const config = new CommandBusConfig({
498
- redisUrl: 'redis://localhost:6379',
499
- queueMode: 'fifo', // 'fifo' (First In First Out) lub 'lifo' (Last In First Out)
500
- });
501
- ```
502
-
503
- ### Custom Logger
504
-
505
- Logger jest automatycznie opakowywany przez wewnętrzny wrapper, który filtruje logi według `logLevel`:
506
-
507
- ```typescript
508
- // Prosty logger - używa console
509
141
  const config = new CommandBusConfig({
510
142
  redisUrl: 'redis://localhost:6379',
511
143
  logger: console,
512
- logLevel: 'log', // debug | log | warn | error
513
- });
514
-
515
- // Własny logger - musi implementować metody: log, error, warn, debug
516
- class MyLogger {
517
- log(message: string, ...args: unknown[]): void {
518
- // Twoja implementacja
519
- }
520
- error(message: string, ...args: unknown[]): void {
521
- // Twoja implementacja
522
- }
523
- warn(message: string, ...args: unknown[]): void {
524
- // Twoja implementacja
525
- }
526
- debug(message: string, ...args: unknown[]): void {
527
- // Twoja implementacja
528
- }
529
- }
530
-
531
- const config2 = new CommandBusConfig({
532
- redisUrl: 'redis://localhost:6379',
533
- logger: new MyLogger(),
534
- logLevel: 'debug', // Wszystkie poziomy
535
144
  });
536
- ```
537
-
538
- ## API Reference
539
-
540
- ### CommandBus
541
145
 
542
- #### `dispatch(command: Command): Promise<void>`
543
-
544
- Wysyła komendę do kolejki bez oczekiwania na wynik (fire-and-forget).
545
-
546
- **Flow**:
547
- 1. Kompresja payloadu (jeśli >threshold)
548
- 2. Pobranie/utworzenie kolejki z cache
549
- 3. Dodanie job do BullMQ
550
- 4. Natychmiastowy return
551
-
552
- ```typescript
553
- await commandBus.dispatch(new CreateUserCommand({
554
- email: 'email@example.com',
555
- name: 'Jan Kowalski'
556
- }));
146
+ const bus = new CommandBus(config);
557
147
  ```
558
148
 
559
- #### `call<T>(command: Command, timeout?: number): Promise<T>`
560
-
561
- Wywołuje komendę synchronicznie i czeka na odpowiedź przez Redis Pub/Sub (RPC). Domyślny timeout: 30000ms (30s).
562
-
563
- **Flow**:
564
- 1. Rejestracja pending call z timeoutem
565
- 2. Kompresja payloadu (jeśli >threshold)
566
- 3. Przygotowanie metadanych RPC (correlationId, responseChannel)
567
- 4. Dodanie job do BullMQ
568
- 5. Oczekiwanie na odpowiedź przez shared subscriber
569
- 6. Dekompresja odpowiedzi
570
- 7. Zwrócenie wyniku lub błędu
149
+ ### Rejestracja handlera (worker)
571
150
 
572
151
  ```typescript
573
- const result = await commandBus.call<{ userId: string }>(command, 5000);
574
- ```
575
-
576
- #### `handle<T>(commandClass, handler): void`
577
-
578
- Rejestruje handler dla określonej klasy komendy. Tylko jeden handler per typ komendy.
579
-
580
- **Automatyczne akcje**:
581
- 1. Rejestracja handlera w Map
582
- 2. Utworzenie workera BullMQ
583
- 3. Uruchomienie benchmarku dla optymalnego concurrency
584
- 4. Utworzenie metrics collector
585
- 5. Setup event handlers
586
-
587
- ```typescript
588
- commandBus.handle(CreateUserCommand, async (command) => {
589
- const { email, name } = command.__payload;
590
- const user = await createUser(email, name);
591
- return { userId: user.id };
592
- });
593
- ```
594
-
595
- #### `close(): Promise<void>`
596
-
597
- Zamyka wszystkie połączenia i workery z graceful shutdown.
598
-
599
- **Cleanup**:
600
- 1. Zamknięcie wszystkich workerów BullMQ
601
- 2. Zamknięcie wszystkich kolejek z cache
602
- 3. Zamknięcie RpcCoordinator (reject pending calls)
603
- 4. Zamknięcie 3 połączeń Redis (Queue, Worker, RPC)
604
-
605
- ```typescript
606
- await commandBus.close();
607
- ```
608
-
609
- ### Command
610
-
611
- Klasa bazowa dla wszystkich komend. Każda komenda dziedziczy po `Command<T>` gdzie `T` to typ danych biznesowych:
612
-
613
- ```typescript
614
- class MyCommand extends Command<{ data: string }> {
615
- constructor(payload: { data: string }) {
616
- super(payload);
617
- }
618
- }
619
-
620
- // Uzycie
621
- const cmd = new MyCommand({ data: 'hello' });
622
- console.log(cmd.__payload.data); // 'hello'
623
- ```
624
-
625
- #### Struktura Komendy
626
-
627
- Kazda komenda po serializacji ma nastepujaca strukture:
628
-
629
- ```typescript
630
- {
631
- "__name": "MyCommand", // Nazwa klasy komendy
632
- "__id": "550e8400-e29b-41d4-a716-446655440000", // UUID komendy
633
- "__time": 1706347200000, // Timestamp utworzenia (ms)
634
- "__payload": { // Dane biznesowe komendy
635
- "data": "hello"
636
- }
637
- }
638
- ```
639
-
640
- #### Opis Pol Komendy
641
-
642
- | Pole | Typ | Opis |
643
- |------|-----|------|
644
- | `__name` | `string` | Nazwa klasy komendy (automatycznie ustawiana z `constructor.name`). Uzywana do routowania do odpowiedniego handlera. |
645
- | `__id` | `string` | Unikalny identyfikator komendy (UUID v4). Generowany automatycznie przy tworzeniu komendy. Uzywany do korelacji RPC i logowania. |
646
- | `__time` | `number` | Timestamp utworzenia komendy w milisekundach (`Date.now()`). Uzywany do audytu i debugowania. |
647
- | `__payload` | `T` | Dane biznesowe komendy. Wszystkie dane specyficzne dla komendy powinny byc przechowywane tutaj. Typ `T` jest generyczny i definiowany przy tworzeniu klasy komendy. |
648
-
649
- #### Przyklad Definicji Komendy
650
-
651
- ```typescript
652
- // Komenda z prostym payloadem
653
- class CreateUserCommand extends Command<{ name: string; email: string }> {
654
- constructor(payload: { name: string; email: string }) {
655
- super(payload);
656
- }
657
- }
658
-
659
- // Komenda ze zlozonym payloadem
660
- class ProcessOrderCommand extends Command<{
661
- orderId: string;
662
- items: Array<{ productId: string; quantity: number }>;
663
- shippingAddress: {
664
- street: string;
665
- city: string;
666
- postalCode: string;
667
- };
668
- }> {
669
- constructor(payload: {
670
- orderId: string;
671
- items: Array<{ productId: string; quantity: number }>;
672
- shippingAddress: { street: string; city: string; postalCode: string };
673
- }) {
674
- super(payload);
675
- }
676
- }
677
-
678
- // Uzycie
679
- const createUser = new CreateUserCommand({
680
- name: 'Jan Kowalski',
681
- email: 'jan@example.com'
682
- });
683
-
684
- console.log(createUser.__name); // 'CreateUserCommand'
685
- console.log(createUser.__id); // '550e8400-e29b-41d4-...'
686
- console.log(createUser.__time); // 1706347200000
687
- console.log(createUser.__payload.name); // 'Jan Kowalski'
688
- console.log(createUser.__payload.email); // 'jan@example.com'
689
- ```
690
-
691
- #### Dostep do Danych w Handlerze
692
-
693
- ```typescript
694
- commandBus.handle(CreateUserCommand, async (command) => {
695
- // Dostep do danych biznesowych przez __payload
152
+ bus.handle(CreateUserCommand, async (command) => {
696
153
  const { name, email } = command.__payload;
697
-
698
- // Dostep do metadanych komendy
699
- console.log(`Processing command ${command.__id} (${command.__name})`);
700
- console.log(`Created at: ${new Date(command.__time).toISOString()}`);
701
-
702
154
  // Logika biznesowa
703
- const user = await createUser(name, email);
704
- return { userId: user.id };
155
+ await userService.create({ name, email });
156
+ return { userId: '123' };
705
157
  });
706
158
  ```
707
159
 
708
- **Metody statyczne**:
709
- - `reconstructDates(obj)` - rekonstrukcja obiektow Date z serializowanych danych (uzywane wewnetrznie przez JobProcessor)
710
-
711
- ### CommandBusConfig
712
-
713
- Konfiguracja CommandBus z opcjami:
714
-
715
- ```typescript
716
- interface CommandBusConfigOptions {
717
- redisUrl?: string; // URL Redis (domyślnie: 'redis://localhost:6379' lub REDIS_URL)
718
- logger?: ILogger; // Logger (domyślnie console)
719
- logLevel?: 'debug' | 'log' | 'warn' | 'error'; // Poziom logowania (domyślnie 'log')
720
- concurrency?: number; // Liczba workerów (domyślnie 1 lub auto)
721
- maxAttempts?: number; // Maksymalna liczba prób (domyślnie 1)
722
- backoffDelay?: number; // Opóźnienie między próbami w ms (domyślnie 2000)
723
- queueMode?: 'fifo' | 'lifo'; // Tryb kolejki (domyślnie 'fifo')
724
- commandLog?: string; // Ścieżka do katalogu logów komend (opcjonalnie)
725
- autoOptimize?: boolean; // Auto-optymalizacja concurrency (domyślnie true)
726
- compressionThreshold?: number; // Próg kompresji w bajtach (domyślnie 1024)
727
- }
728
- ```
729
-
730
- ## Przykłady Użycia
731
-
732
- ### Podstawowy przyklad z RPC
733
-
734
- ```typescript
735
- import { CommandBus, CommandBusConfig, Command } from 'pp-command-bus';
736
-
737
- // Definicja komendy z payloadem
738
- class CalculateCommand extends Command<{
739
- a: number;
740
- b: number;
741
- operation: 'add' | 'multiply';
742
- }> {
743
- constructor(payload: { a: number; b: number; operation: 'add' | 'multiply' }) {
744
- super(payload);
745
- }
746
- }
747
-
748
- // Konfiguracja
749
- const config = new CommandBusConfig({
750
- redisUrl: 'redis://localhost:6379',
751
- });
752
- const commandBus = new CommandBus(config);
753
-
754
- // Rejestracja handlera
755
- commandBus.handle(CalculateCommand, async (command) => {
756
- const { a, b, operation } = command.__payload;
757
- console.log(`Calculating: ${a} ${operation} ${b}`);
758
-
759
- switch (operation) {
760
- case 'add':
761
- return a + b;
762
- case 'multiply':
763
- return a * b;
764
- }
765
- });
766
-
767
- // Fire-and-forget
768
- await commandBus.dispatch(new CalculateCommand({ a: 5, b: 3, operation: 'add' }));
769
-
770
- // RPC - czekamy na wynik
771
- const result = await commandBus.call<number>(
772
- new CalculateCommand({ a: 5, b: 3, operation: 'multiply' }),
773
- 5000 // timeout 5s
774
- );
775
- console.log(`Result: ${result}`); // Result: 15
776
- ```
777
-
778
- ### Rownolegle wywolania RPC
160
+ ### Wysyłanie komendy (fire-and-forget)
779
161
 
780
162
  ```typescript
781
- // Wiele rownoległych RPC calls
782
- const [result1, result2, result3] = await Promise.all([
783
- commandBus.call<number>(new CalculateCommand({ a: 10, b: 5, operation: 'add' })),
784
- commandBus.call<UserInfo>(new GetUserInfoCommand({ userId: 'user-1' })),
785
- commandBus.call<ValidationResult>(new ValidateUserCommand({ email: 'jan@example.com', age: 30 })),
786
- ]);
787
-
788
- console.log('All results:', { result1, result2, result3 });
163
+ const cmd = new CreateUserCommand({ name: 'Jan Kowalski', email: 'jan@example.com' });
164
+ await bus.dispatch(cmd);
789
165
  ```
790
166
 
791
- ### Obsluga bledow
167
+ ### Wysyłanie batch
792
168
 
793
169
  ```typescript
794
- commandBus.handle(CreateUserCommand, async (command) => {
795
- try {
796
- const { email, name } = command.__payload;
797
-
798
- // Walidacja
799
- if (!email.includes('@')) {
800
- throw new Error('Invalid email format');
801
- }
802
-
803
- const user = await createUser(email, name);
804
- return { userId: user.id };
805
- } catch (error) {
806
- // Loguj blad
807
- console.error('Failed to create user:', error);
808
-
809
- // Rzuc blad - BullMQ sprobuje ponownie (maxAttempts)
810
- throw error;
811
- }
812
- });
813
-
814
- // Obsluga bledow w RPC
815
- try {
816
- const result = await commandBus.call(
817
- new CreateUserCommand({ email: 'invalid-email', name: 'Jan' })
818
- );
819
- } catch (error) {
820
- console.error('RPC failed:', error.message);
821
- }
170
+ const commands = [
171
+ new CreateUserCommand({ name: 'Anna Nowak', email: 'anna@example.com' }),
172
+ new CreateUserCommand({ name: 'Piotr Wiśniewski', email: 'piotr@example.com' }),
173
+ ];
174
+ await bus.dispatchBatch(commands);
822
175
  ```
823
176
 
824
- ### Graceful Shutdown
177
+ ### Wywołanie RPC (request/response)
825
178
 
826
179
  ```typescript
827
- process.on('SIGTERM', async () => {
828
- console.log('Shutting down CommandBus...');
829
- await commandBus.close();
830
- process.exit(0);
831
- });
832
-
833
- process.on('SIGINT', async () => {
834
- console.log('Shutting down CommandBus...');
835
- await commandBus.close();
836
- process.exit(0);
837
- });
180
+ const result = await bus.call<{ userId: string }>(cmd, 5000); // timeout 5s
181
+ console.log(result.userId); // '123'
838
182
  ```
839
183
 
840
- ## Zaawansowane Funkcje
841
-
842
- ### 1. Dynamiczne Concurrency
843
-
844
- WorkerOrchestrator automatycznie dostosowuje concurrency na podstawie metryk:
845
-
846
- - **Benchmark przy starcie** - optymalny concurrency dla każdego workera
847
- - **Event-driven metrics** - zbieranie metryk z workerów
848
- - **Dynamiczne dostosowanie** - +/-20% co 30s (cooldown)
849
- - **Limity** - min 10, max 2000
184
+ ### Zamykanie
850
185
 
851
186
  ```typescript
852
- // Automatyczne - benchmark ustali optymalną wartość
853
- const config = new CommandBusConfig({
854
- redisUrl: 'redis://localhost:6379',
855
- // concurrency zostanie ustalone przez benchmark (np. 15-20)
856
- });
187
+ await bus.close(); // Graceful shutdown czeka na aktywne zadania
857
188
  ```
858
189
 
859
- ### 2. Process Isolation w RPC
860
-
861
- Każdy proces Node.js ma unikalny UUID - odpowiedzi RPC są izolowane między procesami:
190
+ ## API
862
191
 
863
- ```
864
- Process A (UUID: abc-123):
865
- - Kanał: rpc:response:abc-123:*
866
- - Otrzymuje tylko swoje odpowiedzi
867
-
868
- Process B (UUID: def-456):
869
- - Kanał: rpc:response:def-456:*
870
- - Otrzymuje tylko swoje odpowiedzi
871
- ```
192
+ ### `CommandBus`
872
193
 
873
- ### 3. Shared Subscriber Pattern
194
+ | Metoda | Opis |
195
+ |--------|------|
196
+ | `dispatch(command)` | Wysyła komendę fire-and-forget (`XADD`) |
197
+ | `dispatchBatch(commands)` | Wysyła wiele komend w jednym pipeline |
198
+ | `call<T>(command, timeout?)` | RPC — wysyła i czeka na wynik (`XADD` + `BRPOP`). Timeout per wywołanie w ms (domyślnie `30000`) |
199
+ | `handle(CommandClass, handler)` | Rejestruje handler dla komendy |
200
+ | `close()` | Graceful shutdown — zamyka połączenia i czeka na aktywne zadania |
874
201
 
875
- Jeden shared subscriber dla wszystkich RPC calls zamiast N subskrybentów:
202
+ ### `Command<T>`
876
203
 
877
- **Tradycyjne podejście** (N subskrybentów):
878
- ```
879
- 1000 RPC calls → 1000 Redis subscriptions → duże obciążenie
880
- ```
881
-
882
- **Shared Subscriber** (1 subskrybent):
883
- ```
884
- 1000 RPC calls → 1 Redis pattern subscription → multiplexing w pamięci
885
- ```
204
+ Bazowa klasa abstrakcyjna dla komend:
886
205
 
887
- ### 4. Automatyczna Kompresja
888
-
889
- PayloadCompressionService automatycznie kompresuje duże payloady:
890
-
891
- ```typescript
892
- // Mała komenda (<1KB) - brak kompresji
893
- const smallCommand = new CreateUserCommand('jan@example.com', 'Jan');
894
- await commandBus.call(smallCommand); // Bez kompresji
206
+ | Pole | Typ | Opis |
207
+ |------|-----|------|
208
+ | `__name` | `string` | Nazwa komendy (nazwa klasy) |
209
+ | `__id` | `string` | UUID v4 |
210
+ | `__time` | `number` | Timestamp utworzenia (ms) |
211
+ | `__payload` | `T` | Dane biznesowe komendy |
895
212
 
896
- // Duża komenda (>1KB) - automatyczna kompresja
897
- const largeCommand = new ProcessReportCommand(largeData); // 5KB
898
- await commandBus.call(largeCommand); // Automatyczna kompresja gzip → base64
899
- ```
213
+ ## Konfiguracja
900
214
 
901
- **Flagi kompresji**:
902
- - `__compressed: true` - payload został skompresowany
903
- - Automatyczna dekompresja w JobProcessor
904
- - Transparent dla użytkownika
215
+ ### Zmienne środowiskowe
905
216
 
906
- ### 5. Command Logging
217
+ | Zmienna | Domyślna | Opis |
218
+ |---------|----------|------|
219
+ | `REDIS_URL` | `redis://localhost:6379` | URL połączenia Redis/DragonflyDB |
220
+ | `REDIS_RETRY_DELAY` | `5000` | Opóźnienie między próbami reconnect (ms) |
221
+ | `REDIS_MAX_RETRIES` | `0` | Maks. prób reconnect do Redis (`0` = nieskończoność) |
222
+ | `LOG_LEVEL` | `log` | Poziom logowania (`debug`, `log`, `warn`, `error`) |
223
+ | `COMMAND_BUS_CONCURRENCY` | `60 × CPU` | Maks. równoległych wiadomości per konsument (I/O-bound: default, CPU-bound: `availableParallelism()`) |
224
+ | `COMMAND_BUS_MAX_ATTEMPTS` | `3` | Maks. prób przetworzenia wiadomości |
225
+ | `COMMAND_BUS_LOG` | _(puste)_ | Ścieżka do katalogu logów komend |
226
+ | `COMMAND_BUS_POOL_SIZE` | `2 × CPU` | Rozmiar puli połączeń Redis |
227
+ | `COMMAND_BUS_MAX_CONCURRENT_RPC` | `50` | Maks. równoległych wywołań RPC |
228
+ | `COMMAND_BUS_BATCH_SIZE` | `100` | Wiadomości pobieranych w jednym `XREADGROUP` (mniej roundtripów = wyższy throughput) |
229
+ | `COMMAND_BUS_CLAIM_TIMEOUT` | `30000` | Czas (ms) po którym stalled wiadomość jest przejmowana |
230
+ | `COMMAND_BUS_MAX_RETAINED` | `10000` | Maks. wiadomości w strumieniu (`XTRIM ~`) |
907
231
 
908
- Persystencja wszystkich komend do plików JSONL:
232
+ ### Konfiguracja programowa
909
233
 
910
234
  ```typescript
911
235
  const config = new CommandBusConfig({
912
- commandLog: './command-logs',
913
- });
914
-
915
- // Każda komenda jest logowana do pliku:
916
- // ./command-logs/2025-01-27T10.jsonl
917
- ```
918
-
919
- **Use cases**:
920
- - Auditing i compliance
921
- - Replay komend
922
- - Debugging produkcyjnych problemów
923
- - Analiza przepływu komend
924
-
925
- ### 6. RPC Job Cancellation
926
-
927
- Automatyczne usuwanie niepodjętych jobów RPC przy timeout:
928
-
929
- ```
930
- RPC Timeout Flow:
931
-
932
- User Code RpcCoordinator Redis JobProcessor
933
- │ │ │ │
934
- │── call(cmd) ─────▶│ │ │
935
- │ │── registerCall ──▶│ │
936
- │ │── queue.add ─────▶│ │
937
- │ │ │ │
938
- │ [timeout] │ │ │
939
- │ │ │ │
940
- │ │── markCancelled ─▶│ SET rpc:cancelled:xxx
941
- │ │── tryRemoveJob ──▶│ (próba usunięcia)
942
- │◀── Error ─────────│ │ │
943
- │ │ │ │
944
- │ │ │ [job picked up] │
945
- │ │ │──────────────────▶│
946
- │ │ │ │── isCancelled?
947
- │ │ │◀──────────────────│ → true
948
- │ │ │ │── SKIP handler
949
- │ │ │ │── clearCancellation
950
- ```
951
-
952
- **Kluczowe cechy**:
953
- - **Dwufazowe anulowanie**: Flaga Redis + próba usunięcia joba z kolejki
954
- - **Graceful degradation**: Błędy Redis nie blokują przetwarzania
955
- - **TTL 24h**: Automatyczne wygaśnięcie kluczy cancellation
956
- - **Aktywne czyszczenie**: Klucze usuwane po przetworzeniu lub usunięciu joba
957
- - **Kompatybilność wsteczna**: Funkcjonalność jest opcjonalna
958
-
959
- **Korzyści**:
960
- - Oszczędność zasobów - nieprzetworzone joby nie obciążają workerów
961
- - Brak efektów ubocznych - handler nie wykonuje się dla timeout'owanych RPC
962
- - Lepsza diagnostyka - logi pokazują pominięte joby
963
-
964
- ## Best Practices
965
-
966
- ### 1. Używaj TypeScript
967
-
968
- Biblioteka jest napisana w TypeScript z strict mode. Wykorzystaj typy dla lepszego DX:
969
-
970
- ```typescript
971
- interface UserCreatedResult {
972
- userId: string;
973
- createdAt: Date;
974
- }
975
-
976
- const result = await commandBus.call<UserCreatedResult>(command);
977
- ```
978
-
979
- ### 2. Idempotentne handlery
980
-
981
- Handlery powinny byc idempotentne (wielokrotne wykonanie = ten sam rezultat):
982
-
983
- ```typescript
984
- commandBus.handle(CreateUserCommand, async (command) => {
985
- const { email, name } = command.__payload;
986
-
987
- // Sprawdz czy uzytkownik juz istnieje
988
- const existing = await findUserByEmail(email);
989
- if (existing) {
990
- return { userId: existing.id }; // Zwroc istniejacego
991
- }
992
-
993
- // Utworz nowego
994
- const user = await createUser(email, name);
995
- return { userId: user.id };
236
+ redisUrl: 'redis://localhost:6379',
237
+ logger: console,
238
+ logLevel: 'log', // debug | log | warn | error
239
+ redisRetryDelay: 5000, // Opóźnienie reconnect (ms)
240
+ redisMaxRetries: 0, // 0 = nieskończoność
241
+ concurrency: 960, // I/O-bound: 60 × CPU, CPU-bound: availableParallelism()
242
+ poolSize: 32, // Rozmiar puli połączeń Redis
243
+ maxConcurrentRpc: 100, // Maks. równoległych BRPOP
244
+ batchSize: 100, // Wiadomości per XREADGROUP
245
+ claimTimeout: 60000, // Stalled message timeout (ms)
246
+ maxRetained: 50000, // XTRIM ~ limit
247
+ maxAttempts: 5, // Maks. prób przetworzenia
996
248
  });
997
249
  ```
998
250
 
999
- ### 3. Monitorowanie RPC timeoutów
251
+ ## Flow danych
252
+
253
+ ### 1. Dispatch (fire-and-forget)
254
+
255
+ ```
256
+ Producer Redis Worker
257
+ │ │ │
258
+ │ 1. serialize(command) │ │
259
+ │ → MessagePack encode │ │
260
+ │ │ │
261
+ │ 2. RedisCodec.encode(buffer) │ │
262
+ │ → base64 string │ │
263
+ │ │ │
264
+ │ 3. XADD cmd:CreateUser * ──→ │ stream: cmd:CreateUser │
265
+ │ data <base64> │ ┌─────────────────────┐ │
266
+ │ │ │ msg-1: data=<b64> │ │
267
+ │ │ └─────────────────────┘ │
268
+ │ │ │
269
+ │ │ ←── 4. XREADGROUP GROUP │
270
+ │ │ workers consumer-1 │
271
+ │ │ COUNT 100 BLOCK 5000 │
272
+ │ │ STREAMS cmd:CreateUser > │
273
+ │ │ │
274
+ │ │ ──→ [msg-1, [data, <b64>]] │
275
+ │ │ │
276
+ │ │ 5. RedisCodec.decode │
277
+ │ │ 6. deserialize(buffer) │
278
+ │ │ 7. handler(command) │
279
+ │ │ │
280
+ │ │ ←── 8. XACK cmd:CreateUser │
281
+ │ │ workers msg-1 │
282
+ │ │ │
283
+ │ │ ←── 9. XTRIM cmd:CreateUser │
284
+ │ │ MAXLEN ~ 10000 │
285
+ │ │ (co batchSize wiadomości)│
286
+ ```
287
+
288
+ ### 2. RPC (request/response)
289
+
290
+ ```
291
+ Caller Redis Worker
292
+ │ │ │
293
+ │ 1. serialize(command) │ │
294
+ │ 2. Wrap w RpcEnvelope: │ │
295
+ │ { commandData, rpc: { │ │
296
+ │ correlationId, │ │
297
+ │ responseKey │ │
298
+ │ }} │ │
299
+ │ │ │
300
+ │ 3. XADD cmd:GetUser * ────→ │ stream: cmd:GetUser │
301
+ │ data <envelope> │ ┌─────────────────────┐ │
302
+ │ rpc 1 │ │ msg-1: data=<env> │ │
303
+ │ │ │ rpc=1 │ │
304
+ │ 4. BRPOP rpc:res:{uuid} ──→ │ └─────────────────────┘ │
305
+ │ timeout 30s │ │
306
+ │ (dedykowane połączenie │ ←── 5. XREADGROUP │
307
+ │ z RpcConnectionPool) │ │
308
+ │ │ 6. Wykryj marker rpc=1 │
309
+ │ │ 7. Rozpakuj envelope │
310
+ │ │ 8. handler(commandData) │
311
+ │ │ 9. serialize({result}) │
312
+ │ │ │
313
+ │ │ ←── 10. LPUSH rpc:res:{uuid}│
314
+ │ │ <result> │
315
+ │ │ EXPIRE rpc:res:{uuid} 60│
316
+ │ │ │
317
+ │ ←── result z BRPOP │ │
318
+ │ 11. deserialize(result) │ │
319
+ │ 12. DEL rpc:res:{uuid} │ │
320
+ ```
321
+
322
+ ### 3. Recovery + Dead Letter Queue
323
+
324
+ ```
325
+ PendingRecovery Redis
326
+ │ │
327
+ │ (setInterval co claimTimeout) │
328
+ │ │
329
+ │ 1. XPENDING cmd:CreateUser ─→│ Pending entries:
330
+ │ workers - + 10 │ [msg-5, consumer-1,
331
+ │ │ idle: 45000, delivery: 2]
332
+ │ │
333
+ │ if idle > claimTimeout │
334
+ │ AND delivery < maxAttempts: │
335
+ │ │
336
+ │ 2. XCLAIM cmd:CreateUser ──→ │ Przejęcie wiadomości
337
+ │ workers consumer-2 │
338
+ │ 30000 msg-5 │
339
+ │ │
340
+ │ 3. processMessage(msg-5) │ → handler() → XACK
341
+ │ │
342
+ │ if delivery >= maxAttempts: │
343
+ │ │
344
+ │ 4. XCLAIM msg-5 ───────────→ │
345
+ │ 5. XADD dlq:cmd:CreateUser → │ Dead Letter Queue
346
+ │ * ...fields │ ┌──────────────────────┐
347
+ │ original_stream │ │ msg: data + metadata │
348
+ │ cmd:CreateUser │ │ original_stream │
349
+ │ delivery_count 3 │ │ delivery_count: 3 │
350
+ │ 6. XACK cmd:CreateUser ────→ │ └──────────────────────┘
351
+ │ workers msg-5 │
352
+ ```
353
+
354
+ ## Komponenty
355
+
356
+ ### RedisStreamsTransport (fasada)
357
+
358
+ Kompozycja 4 komponentów implementująca `ITransport`:
359
+
360
+ | Komponent | Odpowiedzialność | Komendy Redis |
361
+ |-----------|------------------|---------------|
362
+ | **StreamProducer** | Enqueue / enqueueBatch | `XADD`, pipeline |
363
+ | **StreamConsumer** | Cykl życia konsumenta, deduplicacja | `XGROUP CREATE` |
364
+ | **RpcHandler** | Request/response RPC | `XADD`, `BRPOP`, `LPUSH`, `DEL` |
365
+ | **PendingRecovery** | Automatyczny retry + DLQ | `XPENDING`, `XCLAIM`, `XACK` |
366
+
367
+ ### StreamConsumer (koordynator)
368
+
369
+ StreamConsumer komponuje dwa podkomponenty:
370
+
371
+ | Podkomponent | Odpowiedzialność | LOC |
372
+ |--------------|------------------|-----|
373
+ | **MessageProcessor** | Parsowanie fields, walidacja, RPC detection, handler, XACK/XTRIM | ~147 |
374
+ | **ConsumerLoop** | while(running) loop, XREADGROUP BLOCK, concurrency limiter, backoff | ~120 |
375
+ | **StreamConsumer** | Koordynacja: compose, deduplicacja (Map z TTL sweep), lifecycle | ~188 |
376
+
377
+ ### Pule połączeń
378
+
379
+ | Pula | Wzorzec | Tworzenie | Przeznaczenie |
380
+ |------|---------|-----------|---------------|
381
+ | **RedisConnectionPool** | Round-robin, eager | Przy starcie (`size` połączeń) | Operacje nieblokujące: XADD, XACK, DEL, pipeline |
382
+ | **RpcConnectionPool** | Bounded, lazy | Przy `acquire()` (max `maxSize`) | Operacje blokujące: BRPOP |
1000
383
 
1001
- ```typescript
1002
- // Ustaw timeout dostosowany do czasu przetwarzania komendy
1003
- const shortCommand = new QuickCommand();
1004
- const result1 = await commandBus.call(shortCommand, 1000); // 1s dla szybkich operacji
1005
-
1006
- const longRunningCommand = new ProcessReportCommand();
1007
- const result2 = await commandBus.call(longRunningCommand, 60000); // 60s dla długich operacji
1008
- ```
384
+ **RedisConnectionPool** dodatkowo udostępnia `createDedicated()` — tworzy izolowane połączenie poza pulą, używane przez `ConsumerLoop` (XREADGROUP BLOCK blokuje socket).
1009
385
 
1010
- ### 4. Walidacja w handlerach
386
+ ### Serializacja
1011
387
 
1012
- ```typescript
1013
- commandBus.handle(CreateUserCommand, async (command) => {
1014
- const { email, name } = command.__payload;
1015
-
1016
- // Walidacja na poczatku
1017
- if (!email || !email.includes('@')) {
1018
- throw new Error('Invalid email');
1019
- }
1020
-
1021
- if (!name || name.length < 2) {
1022
- throw new Error('Invalid name');
1023
- }
1024
-
1025
- // Logika biznesowa
1026
- return await createUser(email, name);
1027
- });
388
+ | Komponent | Warstwa | Opis |
389
+ |-----------|---------|------|
390
+ | **MsgpackSerializer** | Aplikacja | MessagePack z Date extension type — 1 krok zamiast 7 |
391
+ | **RedisCodec** | Transport | Base64 encode/decode — chroni binarne dane przed korupcją UTF-8 przez ioredis |
392
+
393
+ ### Concurrency Limiter (ConsumerLoop)
394
+
395
+ ```
396
+ ┌─ slot 1: handler(msg-1) ──→ done → slot wolny
397
+ XREADGROUP ────→├─ slot 2: handler(msg-2) ──→ done → slot wolny
398
+ (COUNT=N) └─ slot 3: handler(msg-3) ──→ (trwa...)
399
+
400
+ czekam (Promise.race) ←────┘
401
+ zwolni się slot
1028
402
  ```
1029
403
 
1030
- ### 5. Logowanie kontekstu
404
+ - `concurrency` kontroluje liczbę slotów (równoległych handlerów)
405
+ - `batchSize` kontroluje ile wiadomości czytanych na raz
406
+ - Dostępne sloty = `concurrency - active.size`
407
+ - Nowy batch = `Math.min(batchSize, availableSlots)`
1031
408
 
1032
- ```typescript
1033
- commandBus.handle(CreateUserCommand, async (command) => {
1034
- const { email, name } = command.__payload;
1035
-
1036
- console.log('Processing CreateUserCommand', {
1037
- commandId: command.__id,
1038
- timestamp: command.__time,
1039
- email: email,
1040
- });
1041
-
1042
- // Logika biznesowa
1043
- const user = await createUser(email, name);
409
+ ### Deduplicacja (StreamConsumer)
1044
410
 
1045
- console.log('User created successfully', {
1046
- commandId: command.__id,
1047
- userId: user.id,
1048
- });
411
+ `Map<string, number>` messageId → timestamp:
1049
412
 
1050
- return { userId: user.id };
1051
- });
1052
- ```
413
+ - Przed przetworzeniem: `sweepStaleProcessing()` usuwa wpisy starsze niż `staleThreshold` (5 min)
414
+ - Duplikat: `processing.has(messageId) → return` (skip)
415
+ - Po przetworzeniu: `processing.delete(messageId)`
1053
416
 
1054
- ## Testing
417
+ Chroni przed podwójnym przetworzeniem gdy `PendingRecovery` przejmuje wiadomość która jest jeszcze aktywna w consumer loop.
1055
418
 
1056
- ### Unit testy
419
+ ### XTRIM (MessageProcessor)
1057
420
 
1058
- ```typescript
1059
- import { CommandBus, CommandBusConfig, Command } from 'pp-command-bus';
1060
-
1061
- // Definicja komendy testowej
1062
- class CreateUserCommand extends Command<{ email: string; name: string }> {
1063
- constructor(payload: { email: string; name: string }) {
1064
- super(payload);
1065
- }
1066
- }
421
+ Throttled per-stream: counter wiadomości na strumień, co `batchSize` wiadomości pipeline `XACK + XTRIM ~ maxRetained`.
1067
422
 
1068
- describe('User Commands', () => {
1069
- let commandBus: CommandBus;
1070
-
1071
- beforeAll(async () => {
1072
- const config = new CommandBusConfig({
1073
- redisUrl: 'redis://localhost:6379',
1074
- logger: console,
1075
- logLevel: 'error', // Tylko bledy w testach
1076
- });
1077
-
1078
- commandBus = new CommandBus(config);
1079
-
1080
- // Zarejestruj handler
1081
- commandBus.handle(CreateUserCommand, async (command) => {
1082
- return { userId: 'test-user-id' };
1083
- });
1084
- });
1085
-
1086
- afterAll(async () => {
1087
- await commandBus.close();
1088
- });
1089
-
1090
- it('should create user', async () => {
1091
- const command = new CreateUserCommand({
1092
- email: 'test@example.com',
1093
- name: 'Test User'
1094
- });
1095
- const result = await commandBus.call<{ userId: string }>(command, 5000);
1096
-
1097
- expect(result.userId).toBeDefined();
1098
- expect(result.userId).toBe('test-user-id');
1099
- });
1100
-
1101
- it('should handle multiple commands in parallel', async () => {
1102
- const commands = [
1103
- new CreateUserCommand({ email: 'user1@example.com', name: 'User 1' }),
1104
- new CreateUserCommand({ email: 'user2@example.com', name: 'User 2' }),
1105
- new CreateUserCommand({ email: 'user3@example.com', name: 'User 3' }),
1106
- ];
1107
-
1108
- const results = await Promise.all(
1109
- commands.map((cmd) => commandBus.call<{ userId: string }>(cmd, 5000))
1110
- );
1111
-
1112
- expect(results).toHaveLength(3);
1113
- results.forEach((result) => {
1114
- expect(result.userId).toBeDefined();
1115
- });
1116
- });
1117
- });
1118
423
  ```
1119
-
1120
- ## Troubleshooting
1121
-
1122
- ### Connection refused (Redis)
1123
-
1124
- Upewnij się że Redis działa:
1125
-
1126
- ```bash
1127
- redis-cli ping
1128
- # Powinno zwrócić: PONG
424
+ Strumień A: [1] [2] [3] ... [10] → pipeline XACK + XTRIM → reset counter
425
+ Strumień B: [1] [2] [3] → zwykły XACK (nie osiągnął batchSize)
1129
426
  ```
1130
427
 
1131
- ### Timeout na RPC call
1132
-
1133
- Zwiększ timeout lub sprawdź czy handler został zarejestrowany:
1134
-
1135
- ```typescript
1136
- // Zwiększ timeout
1137
- const result = await commandBus.call(command, 60000); // 60s
1138
-
1139
- // Sprawdź czy handler został zarejestrowany PRZED wywołaniem
1140
- commandBus.handle(MyCommand, async (command) => {
1141
- // Handler implementation
1142
- return { result: 'success' };
1143
- });
1144
-
1145
- // Teraz możesz wywołać komendę
1146
- const result = await commandBus.call(new MyCommand());
1147
- ```
1148
-
1149
- ### Handler nie został wywołany
1150
-
1151
- Upewnij się że handler został zarejestrowany przed wysłaniem komendy:
1152
-
1153
- ```typescript
1154
- // ❌ ŹLE - handler po dispatch
1155
- await commandBus.dispatch(new MyCommand());
1156
- commandBus.handle(MyCommand, async (cmd) => { ... }); // Za późno!
428
+ ### Graceful Shutdown
1157
429
 
1158
- // ✅ DOBRZE - handler przed dispatch
1159
- commandBus.handle(MyCommand, async (cmd) => { ... });
1160
- await commandBus.dispatch(new MyCommand()); // Teraz OK
1161
430
  ```
1162
-
1163
- ### Wysokie zużycie pamięci
1164
-
1165
- 1. **Wyłącz command logging** jeśli nie jest potrzebne
1166
- 2. **Zmniejsz concurrency** jeśli workery używają dużo pamięci
1167
- 3. **Zwiększ compressionThreshold** jeśli duże payloady powodują problemy
1168
-
1169
- ```typescript
1170
- const config = new CommandBusConfig({
1171
- commandLog: undefined, // Wyłącz logging
1172
- concurrency: 5, // Zmniejsz concurrency
1173
- compressionThreshold: 512, // Kompresuj już od 512B
1174
- });
431
+ close():
432
+ 1. consumer.running = false → propaguje do wszystkich ConsumerLoop
433
+ 2. recovery.stop() → clearInterval
434
+ 3. conn.disconnect() → przerywa XREADGROUP BLOCK
435
+ 4. await consumerLoopPromises → czeka na aktywne handlery
436
+ 5. pool.close() → quit() na connection pool
437
+ 6. rpcPool.close() → quit() available, disconnect() borrowed
1175
438
  ```
1176
439
 
1177
- ### Worker stalled
1178
-
1179
- Worker został zatrzymany (prawdopodobnie crashed). BullMQ automatycznie przeniesie job do innego workera.
1180
-
1181
- **Przyczyny**:
1182
- - Out of memory
1183
- - Uncaught exception w handlerze
1184
- - Timeout w handlerze
1185
-
1186
- **Rozwiązanie**:
1187
- - Dodaj try/catch w handlerze
1188
- - Zwiększ pamięć dla procesu
1189
- - Zmniejsz concurrency
1190
-
1191
- ## Struktura Projektu
440
+ ## Struktura projektu
1192
441
 
1193
442
  ```
1194
443
  src/
1195
- ├── command-bus/ # Główna logika CommandBus
1196
- ├── config/ # Konfiguracja CommandBus
1197
- ├── command-bus-config.ts
1198
- │ └── auto-config-optimizer.ts
1199
- │ ├── job/ # Przetwarzanie jobów i opcje
1200
- │ │ ├── job-processor.ts
1201
- │ └── job-options-builder.ts
1202
- │ ├── logging/ # Command logging do plików
1203
- │ │ └── command-logger.ts
1204
- │ ├── queue/ # Queue management i cache
1205
- │ │ └── queue-manager.ts
1206
- │ ├── rpc/ # RPC coordination
1207
- │ │ ├── rpc-coordinator.ts
1208
- │ │ ├── payload-compression.service.ts
1209
- │ │ └── rpc-job-cancellation.service.ts # Anulowanie jobów RPC przy timeout
1210
- ├── worker/ # Worker orchestration
1211
- ├── worker-orchestrator.ts
1212
- │ │ ├── worker-benchmark.ts
1213
- │ │ └── worker-metrics-collector.ts
1214
- │ ├── types/ # Typy TypeScript
1215
- │ │ └── index.ts
1216
- ├── command.ts # Klasa bazowa Command
1217
- └── index.ts # CommandBus główna klasa
1218
- ├── shared/ # Wspólne komponenty
1219
- │ ├── config/ # Base config z Redis
1220
- │ └── base-config.ts
1221
- │ ├── logging/ # Logger wrapper z poziomami
1222
- │ │ ├── logger.ts
1223
- │ │ └── log-level.ts
1224
- │ ├── redis/ # Fabryka połączeń Redis (SRP)
1225
- │ │ ├── redis-connection-factory.ts # Tworzenie połączeń z event handlers
1226
- │ │ ├── redis-error-formatter.ts # Formatowanie błędów (AggregateError)
1227
- └── index.ts # Eksporty modułu
1228
- └── types.ts # Współdzielone typy
1229
- ├── examples/ # Przykłady użycia
1230
- ├── rpc.demo.ts # Demo RPC calls
1231
- │ ├── rpc-throughput.demo.ts # Demo wydajności RPC
1232
- │ ├── rpc-compression.demo.ts # Demo kompresji
1233
- │ └── rpc-resilience.demo.ts # Demo reconnect/failover (5 min test)
1234
- └── index.ts # Główny export pakietu
1235
- ```
1236
-
1237
- ## Migracja z pp-event-bus 1.x
1238
-
1239
- Jeśli używałeś Command Bus z pakietu `pp-event-bus` w wersji 1.x:
1240
-
1241
- ### Przed:
1242
-
1243
- ```typescript
1244
- import { CommandBus, Command, CommandBusConfig } from 'pp-event-bus';
1245
- ```
1246
-
1247
- ### Po:
1248
-
1249
- ```bash
1250
- npm install pp-command-bus
1251
- ```
1252
-
1253
- ```typescript
1254
- import { CommandBus, Command, CommandBusConfig } from 'pp-command-bus';
1255
- ```
1256
-
1257
- **Pełna zgodność API** - jedyna zmiana to źródło importu.
1258
-
1259
- ## Wersjonowanie i Releases
1260
-
1261
- Projekt używa [Semantic Versioning](https://semver.org/lang/pl/) oraz automatycznych release'ów dzięki [semantic-release](https://github.com/semantic-release/semantic-release).
1262
-
1263
- ### Konwencja commitów
1264
-
1265
- Używamy [Conventional Commits](https://www.conventionalcommits.org/):
444
+ ├── index.ts # Eksporty publiczne
445
+ ├── command-bus/
446
+ │ ├── index.ts # CommandBus (fasada wyższego poziomu)
447
+ ├── command.ts # Bazowa klasa Command<T>
448
+ │ ├── config/
449
+ │ │ └── command-bus-config.ts # Konfiguracja (env + params)
450
+ ├── transport/
451
+ ├── transport.interface.ts # Segregowane interfejsy ISP
452
+ │ │ ├── redis-streams-transport.ts # Fasada transportu (kompozycja 4 komponentów)
453
+ ├── stream-producer.ts # XADD enqueue / enqueueBatch
454
+ │ │ ├── stream-consumer.ts # Koordynator: lifecycle, dedup, compose
455
+ ├── message-processor.ts # Parse, validate, XACK/XTRIM, RPC detect
456
+ │ │ ├── consumer-loop.ts # XREADGROUP loop, concurrency limiter
457
+ │ │ ├── rpc-handler.ts # BRPOP/LPUSH request/response
458
+ │ │ ├── pending-recovery.ts # XPENDING/XCLAIM + Dead Letter Queue
459
+ │ └── redis-codec.ts # Base64 encode/decode
460
+ │ ├── serialization/
461
+ │ │ ├── serializer.interface.ts # ISerializer
462
+ │ │ └── msgpack-serializer.ts # MessagePack z Date extension
463
+ │ ├── logging/
464
+ │ │ └── command-logger.ts # Opcjonalny logger komend do plików
465
+ └── types/
466
+ └── index.ts # Typy (CommandPayload, CommandHandler)
467
+ ├── shared/
468
+ │ ├── types.ts # ILogger, TDict, TCallableAsync
469
+ ├── redis/
470
+ ├── connection-pool.ts # Round-robin eager pool
471
+ │ │ ├── rpc-connection-pool.ts # Bounded lazy pool (BRPOP)
472
+ │ │ └── redis-error-formatter.ts # Formatowanie błędów Redis
473
+ │ ├── logging/
474
+ │ │ ├── logger.ts # Logger implementacja
475
+ │ │ └── log-level.ts # Poziomy logowania
476
+ │ └── utils/
477
+ └── error-utils.ts # getErrorMessage()
478
+ └── examples/
479
+ ├── rpc.demo.ts # Demo RPC call
480
+ └── rpc-throughput.demo.ts # Demo throughput
481
+ ```
482
+
483
+ ## Testowanie
1266
484
 
1267
485
  ```bash
1268
- # Nowa funkcjonalność (minor release)
1269
- feat: dodano wsparcie dla delayed commands
486
+ # Wszystkie testy
487
+ npm test
1270
488
 
1271
- # Poprawka błędu (patch release)
1272
- fix: naprawiono memory leak w RPC
489
+ # Z coverage
490
+ npm run test:coverage
1273
491
 
1274
- # Breaking change (major release)
1275
- feat!: zmieniono API CommandBusConfig
492
+ # Lint
493
+ npm run lint
1276
494
 
1277
- # Inne typy (patch release)
1278
- docs: zaktualizowano dokumentację API
1279
- perf: zoptymalizowano przetwarzanie komend
1280
- refactor: uproszczono kod RPC handler
1281
- test: dodano testy dla command logging
495
+ # Format
496
+ npm run format:check
1282
497
  ```
1283
498
 
1284
- ### Typy commitów
1285
-
1286
- - `feat:` - nowa funkcjonalność (→ minor release)
1287
- - `fix:` - poprawka błędu (→ patch release)
1288
- - `perf:` - optymalizacja wydajności (→ patch release)
1289
- - `docs:` - zmiany w dokumentacji (→ patch release)
1290
- - `style:` - formatowanie kodu (→ patch release)
1291
- - `refactor:` - refaktoryzacja (→ patch release)
1292
- - `test:` - dodanie/poprawka testów (→ patch release)
1293
- - `build:` - zmiany w buildzie/zależnościach (→ patch release)
1294
- - `ci:` - zmiany w CI/CD (→ patch release)
1295
- - `chore:` - inne zmiany (bez release)
1296
- - `!` lub `BREAKING CHANGE:` - breaking change (→ major release)
1297
-
1298
- ## Dokumentacja
1299
-
1300
- ### Architektura
1301
-
1302
- Szczegółowa dokumentacja architektury systemu, wzorców projektowych i zasad SOLID:
1303
- - [ARCHITECTURE.md](src/command-bus/docs/ARCHITECTURE.md) - Kompletny opis architektury, diagramy komponentów, przepływy danych
1304
-
1305
- ### Komponenty
1306
-
1307
- - **QueueManager** - zarządzanie kolejkami BullMQ i cache kolejek
1308
- - **WorkerOrchestrator** - orkiestracja workerów, dynamiczne concurrency, benchmark
1309
- - **JobProcessor** - przetwarzanie jobów i wykonanie handlerów
1310
- - **RpcCoordinator** - zarządzanie wywołaniami RPC przez Redis Pub/Sub
1311
- - **RpcJobCancellationService** - anulowanie niepodjętych jobów RPC przy timeout
1312
- - **JobOptionsBuilder** - konfiguracja opcji dla jobów BullMQ
1313
- - **CommandLogger** - persystencja komend do plików JSONL
1314
- - **PayloadCompressionService** - automatyczna kompresja gzip
1315
- - **AutoConfigOptimizer** - optymalizacja concurrency na podstawie zasobów
1316
-
1317
- ## Licencja
499
+ ### Architektura testów
1318
500
 
1319
- MIT
501
+ | Typ | Pliki | Opis |
502
+ |-----|-------|------|
503
+ | Unit | `*.spec.ts` per komponent | Izolowane testy każdego komponentu |
504
+ | Integracja | `redis-streams-transport.spec.ts` | Testy fasady z mockami |
505
+ | Integracja | `command-bus.spec.ts` | Testy CommandBus end-to-end z mockami |
1320
506
 
1321
- ## Autorzy
1322
- - Mariusz Lejkowski <m.lejkowski@polskiepolisy.pl>
507
+ Pokrycie: **193 testów**, zero timing-dependent `setTimeout`, `jest.useFakeTimers()` dla backoff.
1323
508
 
1324
- ## Linki
509
+ ## Changelog
1325
510
 
1326
- - [GitLab Repository](https://gitlab.polskiepolisy.pl/lib/pp-command-bus)
1327
- - [Issue Tracker](https://gitlab.polskiepolisy.pl/lib/pp-command-bus/issues)
1328
- - [BullMQ Documentation](https://docs.bullmq.io/)
1329
- - [Semantic Versioning](https://semver.org/lang/pl/)
1330
- - [Conventional Commits](https://www.conventionalcommits.org/)
511
+ Pełny changelog dostępny w [CHANGELOG.md](CHANGELOG.md).