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.
- package/README.md +400 -1219
- package/dist/command-bus/command-bus.spec.js +138 -359
- package/dist/command-bus/command-bus.spec.js.map +1 -1
- package/dist/command-bus/command.d.ts +3 -4
- package/dist/command-bus/command.js +3 -32
- package/dist/command-bus/command.js.map +1 -1
- package/dist/command-bus/config/command-bus-config.d.ts +75 -21
- package/dist/command-bus/config/command-bus-config.js +99 -58
- package/dist/command-bus/config/command-bus-config.js.map +1 -1
- package/dist/command-bus/config/command-bus-config.spec.js +174 -100
- package/dist/command-bus/config/command-bus-config.spec.js.map +1 -1
- package/dist/command-bus/index.d.ts +39 -52
- package/dist/command-bus/index.js +133 -126
- package/dist/command-bus/index.js.map +1 -1
- package/dist/command-bus/logging/command-logger.d.ts +2 -0
- package/dist/command-bus/logging/command-logger.js +7 -0
- package/dist/command-bus/logging/command-logger.js.map +1 -1
- package/dist/command-bus/logging/command-logger.spec.js +36 -0
- package/dist/command-bus/logging/command-logger.spec.js.map +1 -1
- package/dist/command-bus/serialization/index.d.ts +6 -0
- package/dist/command-bus/serialization/index.js +9 -0
- package/dist/command-bus/serialization/index.js.map +1 -0
- package/dist/command-bus/serialization/msgpack-serializer.d.ts +26 -0
- package/dist/command-bus/serialization/msgpack-serializer.js +70 -0
- package/dist/command-bus/serialization/msgpack-serializer.js.map +1 -0
- package/dist/command-bus/serialization/msgpack-serializer.spec.js +223 -0
- package/dist/command-bus/serialization/msgpack-serializer.spec.js.map +1 -0
- package/dist/command-bus/serialization/serializer.interface.d.ts +21 -0
- package/dist/command-bus/serialization/serializer.interface.js +3 -0
- package/dist/command-bus/serialization/serializer.interface.js.map +1 -0
- package/dist/command-bus/transport/consumer-loop.d.ts +45 -0
- package/dist/command-bus/transport/consumer-loop.js +90 -0
- package/dist/command-bus/transport/consumer-loop.js.map +1 -0
- package/dist/command-bus/transport/consumer-loop.spec.js +216 -0
- package/dist/command-bus/transport/consumer-loop.spec.js.map +1 -0
- package/dist/command-bus/transport/index.d.ts +21 -0
- package/dist/command-bus/transport/index.js +23 -0
- package/dist/command-bus/transport/index.js.map +1 -0
- package/dist/command-bus/transport/message-processor.d.ts +59 -0
- package/dist/command-bus/transport/message-processor.js +111 -0
- package/dist/command-bus/transport/message-processor.js.map +1 -0
- package/dist/command-bus/transport/message-processor.spec.js +185 -0
- package/dist/command-bus/transport/message-processor.spec.js.map +1 -0
- package/dist/command-bus/transport/pending-recovery.d.ts +54 -0
- package/dist/command-bus/transport/pending-recovery.js +139 -0
- package/dist/command-bus/transport/pending-recovery.js.map +1 -0
- package/dist/command-bus/transport/pending-recovery.spec.js +176 -0
- package/dist/command-bus/transport/pending-recovery.spec.js.map +1 -0
- package/dist/command-bus/transport/redis-codec.d.ts +24 -0
- package/dist/command-bus/transport/redis-codec.js +33 -0
- package/dist/command-bus/transport/redis-codec.js.map +1 -0
- package/dist/command-bus/transport/redis-codec.spec.js +53 -0
- package/dist/command-bus/transport/redis-codec.spec.js.map +1 -0
- package/dist/command-bus/transport/redis-streams-transport.d.ts +91 -0
- package/dist/command-bus/transport/redis-streams-transport.js +134 -0
- package/dist/command-bus/transport/redis-streams-transport.js.map +1 -0
- package/dist/command-bus/transport/redis-streams-transport.spec.js +420 -0
- package/dist/command-bus/transport/redis-streams-transport.spec.js.map +1 -0
- package/dist/command-bus/transport/rpc-handler.d.ts +39 -0
- package/dist/command-bus/transport/rpc-handler.js +87 -0
- package/dist/command-bus/transport/rpc-handler.js.map +1 -0
- package/dist/command-bus/transport/rpc-handler.spec.js +157 -0
- package/dist/command-bus/transport/rpc-handler.spec.js.map +1 -0
- package/dist/command-bus/transport/stream-consumer.d.ts +89 -0
- package/dist/command-bus/transport/stream-consumer.js +181 -0
- package/dist/command-bus/transport/stream-consumer.js.map +1 -0
- package/dist/command-bus/transport/stream-consumer.spec.js +284 -0
- package/dist/command-bus/transport/stream-consumer.spec.js.map +1 -0
- package/dist/command-bus/transport/stream-producer.d.ts +23 -0
- package/dist/command-bus/transport/stream-producer.js +70 -0
- package/dist/command-bus/transport/stream-producer.js.map +1 -0
- package/dist/command-bus/transport/stream-producer.spec.js +125 -0
- package/dist/command-bus/transport/stream-producer.spec.js.map +1 -0
- package/dist/command-bus/transport/transport.interface.d.ts +87 -0
- package/dist/command-bus/transport/transport.interface.js +3 -0
- package/dist/command-bus/transport/transport.interface.js.map +1 -0
- package/dist/command-bus/types/index.d.ts +0 -84
- package/dist/examples/rpc.demo.js +1 -1
- package/dist/examples/rpc.demo.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/pp-command-bus-2.0.0.tgz +0 -0
- package/dist/shared/redis/connection-pool.d.ts +54 -0
- package/dist/shared/redis/connection-pool.js +117 -0
- package/dist/shared/redis/connection-pool.js.map +1 -0
- package/dist/shared/redis/connection-pool.spec.js +114 -0
- package/dist/shared/redis/connection-pool.spec.js.map +1 -0
- package/dist/shared/redis/index.d.ts +5 -3
- package/dist/shared/redis/index.js +6 -4
- package/dist/shared/redis/index.js.map +1 -1
- package/dist/shared/redis/rpc-connection-pool.d.ts +61 -0
- package/dist/shared/redis/rpc-connection-pool.js +154 -0
- package/dist/shared/redis/rpc-connection-pool.js.map +1 -0
- package/dist/shared/redis/rpc-connection-pool.spec.d.ts +1 -0
- package/dist/shared/redis/rpc-connection-pool.spec.js +173 -0
- package/dist/shared/redis/rpc-connection-pool.spec.js.map +1 -0
- package/dist/shared/types.d.ts +0 -4
- package/dist/shared/utils/error-utils.d.ts +8 -0
- package/dist/shared/utils/error-utils.js +14 -0
- package/dist/shared/utils/error-utils.js.map +1 -0
- package/package.json +12 -12
- package/dist/command-bus/config/auto-config-optimizer.d.ts +0 -35
- package/dist/command-bus/config/auto-config-optimizer.js +0 -52
- package/dist/command-bus/config/auto-config-optimizer.js.map +0 -1
- package/dist/command-bus/config/auto-config-optimizer.spec.js +0 -42
- package/dist/command-bus/config/auto-config-optimizer.spec.js.map +0 -1
- package/dist/command-bus/job/index.d.ts +0 -6
- package/dist/command-bus/job/index.js +0 -15
- package/dist/command-bus/job/index.js.map +0 -1
- package/dist/command-bus/job/job-options-builder.d.ts +0 -21
- package/dist/command-bus/job/job-options-builder.js +0 -58
- package/dist/command-bus/job/job-options-builder.js.map +0 -1
- package/dist/command-bus/job/job-options-builder.spec.js +0 -156
- package/dist/command-bus/job/job-options-builder.spec.js.map +0 -1
- package/dist/command-bus/job/job-processor.d.ts +0 -39
- package/dist/command-bus/job/job-processor.js +0 -203
- package/dist/command-bus/job/job-processor.js.map +0 -1
- package/dist/command-bus/job/job-processor.spec.js +0 -436
- package/dist/command-bus/job/job-processor.spec.js.map +0 -1
- package/dist/command-bus/queue/index.d.ts +0 -5
- package/dist/command-bus/queue/index.js +0 -13
- package/dist/command-bus/queue/index.js.map +0 -1
- package/dist/command-bus/queue/queue-manager.d.ts +0 -56
- package/dist/command-bus/queue/queue-manager.js +0 -163
- package/dist/command-bus/queue/queue-manager.js.map +0 -1
- package/dist/command-bus/queue/queue-manager.spec.js +0 -371
- package/dist/command-bus/queue/queue-manager.spec.js.map +0 -1
- package/dist/command-bus/rpc/index.d.ts +0 -11
- package/dist/command-bus/rpc/index.js +0 -19
- package/dist/command-bus/rpc/index.js.map +0 -1
- package/dist/command-bus/rpc/payload-compression.service.d.ts +0 -50
- package/dist/command-bus/rpc/payload-compression.service.js +0 -215
- package/dist/command-bus/rpc/payload-compression.service.js.map +0 -1
- package/dist/command-bus/rpc/payload-compression.service.spec.js +0 -376
- package/dist/command-bus/rpc/payload-compression.service.spec.js.map +0 -1
- package/dist/command-bus/rpc/rpc-coordinator.d.ts +0 -96
- package/dist/command-bus/rpc/rpc-coordinator.js +0 -500
- package/dist/command-bus/rpc/rpc-coordinator.js.map +0 -1
- package/dist/command-bus/rpc/rpc-coordinator.spec.js +0 -621
- package/dist/command-bus/rpc/rpc-coordinator.spec.js.map +0 -1
- package/dist/command-bus/rpc/rpc-job-cancellation.service.d.ts +0 -82
- package/dist/command-bus/rpc/rpc-job-cancellation.service.js +0 -180
- package/dist/command-bus/rpc/rpc-job-cancellation.service.js.map +0 -1
- package/dist/command-bus/rpc/rpc-job-cancellation.service.spec.js +0 -286
- package/dist/command-bus/rpc/rpc-job-cancellation.service.spec.js.map +0 -1
- package/dist/command-bus/worker/index.d.ts +0 -10
- package/dist/command-bus/worker/index.js +0 -19
- package/dist/command-bus/worker/index.js.map +0 -1
- package/dist/command-bus/worker/worker-benchmark.d.ts +0 -71
- package/dist/command-bus/worker/worker-benchmark.js +0 -202
- package/dist/command-bus/worker/worker-benchmark.js.map +0 -1
- package/dist/command-bus/worker/worker-benchmark.spec.js +0 -310
- package/dist/command-bus/worker/worker-benchmark.spec.js.map +0 -1
- package/dist/command-bus/worker/worker-metrics-collector.d.ts +0 -98
- package/dist/command-bus/worker/worker-metrics-collector.js +0 -242
- package/dist/command-bus/worker/worker-metrics-collector.js.map +0 -1
- package/dist/command-bus/worker/worker-orchestrator.d.ts +0 -70
- package/dist/command-bus/worker/worker-orchestrator.js +0 -339
- package/dist/command-bus/worker/worker-orchestrator.js.map +0 -1
- package/dist/command-bus/worker/worker-orchestrator.spec.js +0 -712
- package/dist/command-bus/worker/worker-orchestrator.spec.js.map +0 -1
- package/dist/examples/auto-config.demo.d.ts +0 -9
- package/dist/examples/auto-config.demo.js +0 -106
- package/dist/examples/auto-config.demo.js.map +0 -1
- package/dist/examples/rpc-compression.demo.d.ts +0 -5
- package/dist/examples/rpc-compression.demo.js +0 -358
- package/dist/examples/rpc-compression.demo.js.map +0 -1
- package/dist/examples/rpc-resilience.demo.d.ts +0 -15
- package/dist/examples/rpc-resilience.demo.js +0 -233
- package/dist/examples/rpc-resilience.demo.js.map +0 -1
- package/dist/pp-command-bus-1.5.0.tgz +0 -0
- package/dist/shared/config/base-config.d.ts +0 -54
- package/dist/shared/config/base-config.js +0 -114
- package/dist/shared/config/base-config.js.map +0 -1
- package/dist/shared/config/base-config.spec.js +0 -204
- package/dist/shared/config/base-config.spec.js.map +0 -1
- package/dist/shared/config/index.d.ts +0 -1
- package/dist/shared/config/index.js +0 -9
- package/dist/shared/config/index.js.map +0 -1
- package/dist/shared/redis/redis-connection-factory.d.ts +0 -66
- package/dist/shared/redis/redis-connection-factory.js +0 -113
- package/dist/shared/redis/redis-connection-factory.js.map +0 -1
- /package/dist/command-bus/{config/auto-config-optimizer.spec.d.ts → serialization/msgpack-serializer.spec.d.ts} +0 -0
- /package/dist/command-bus/{job/job-options-builder.spec.d.ts → transport/consumer-loop.spec.d.ts} +0 -0
- /package/dist/command-bus/{job/job-processor.spec.d.ts → transport/message-processor.spec.d.ts} +0 -0
- /package/dist/command-bus/{queue/queue-manager.spec.d.ts → transport/pending-recovery.spec.d.ts} +0 -0
- /package/dist/command-bus/{rpc/payload-compression.service.spec.d.ts → transport/redis-codec.spec.d.ts} +0 -0
- /package/dist/command-bus/{rpc/rpc-coordinator.spec.d.ts → transport/redis-streams-transport.spec.d.ts} +0 -0
- /package/dist/command-bus/{rpc/rpc-job-cancellation.service.spec.d.ts → transport/rpc-handler.spec.d.ts} +0 -0
- /package/dist/command-bus/{worker/worker-benchmark.spec.d.ts → transport/stream-consumer.spec.d.ts} +0 -0
- /package/dist/command-bus/{worker/worker-orchestrator.spec.d.ts → transport/stream-producer.spec.d.ts} +0 -0
- /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
|
-
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
│
|
|
31
|
-
|
|
32
|
-
│
|
|
33
|
-
│
|
|
34
|
-
|
|
35
|
-
│
|
|
36
|
-
│
|
|
37
|
-
│
|
|
38
|
-
│
|
|
39
|
-
│ │
|
|
40
|
-
│
|
|
41
|
-
│
|
|
42
|
-
│
|
|
43
|
-
│
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
95
|
+
interface IClosable {
|
|
96
|
+
close(): Promise<void>;
|
|
97
|
+
}
|
|
186
98
|
|
|
187
|
-
|
|
188
|
-
|
|
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 >=
|
|
248
|
-
- Redis >=
|
|
249
|
-
- TypeScript >=
|
|
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
|
-
###
|
|
115
|
+
### Zależności
|
|
254
116
|
|
|
255
|
-
|
|
256
|
-
|
|
117
|
+
| Pakiet | Opis |
|
|
118
|
+
|--------|------|
|
|
119
|
+
| `ioredis` | Klient Redis z pipelining, Cluster, Sentinel |
|
|
120
|
+
| `@msgpack/msgpack` | Binarna serializacja z extension types |
|
|
257
121
|
|
|
258
|
-
|
|
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
|
-
###
|
|
124
|
+
### Definiowanie komendy
|
|
272
125
|
|
|
273
126
|
```typescript
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
704
|
-
return { userId:
|
|
155
|
+
await userService.create({ name, email });
|
|
156
|
+
return { userId: '123' };
|
|
705
157
|
});
|
|
706
158
|
```
|
|
707
159
|
|
|
708
|
-
|
|
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
|
-
|
|
782
|
-
|
|
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
|
-
###
|
|
167
|
+
### Wysyłanie batch
|
|
792
168
|
|
|
793
169
|
```typescript
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
###
|
|
177
|
+
### Wywołanie RPC (request/response)
|
|
825
178
|
|
|
826
179
|
```typescript
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
+
### `Command<T>`
|
|
876
203
|
|
|
877
|
-
|
|
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
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
897
|
-
const largeCommand = new ProcessReportCommand(largeData); // 5KB
|
|
898
|
-
await commandBus.call(largeCommand); // Automatyczna kompresja gzip → base64
|
|
899
|
-
```
|
|
213
|
+
## Konfiguracja
|
|
900
214
|
|
|
901
|
-
|
|
902
|
-
- `__compressed: true` - payload został skompresowany
|
|
903
|
-
- Automatyczna dekompresja w JobProcessor
|
|
904
|
-
- Transparent dla użytkownika
|
|
215
|
+
### Zmienne środowiskowe
|
|
905
216
|
|
|
906
|
-
|
|
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
|
-
|
|
232
|
+
### Konfiguracja programowa
|
|
909
233
|
|
|
910
234
|
```typescript
|
|
911
235
|
const config = new CommandBusConfig({
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
386
|
+
### Serializacja
|
|
1011
387
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
+
aż zwolni się slot
|
|
1028
402
|
```
|
|
1029
403
|
|
|
1030
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1046
|
-
commandId: command.__id,
|
|
1047
|
-
userId: user.id,
|
|
1048
|
-
});
|
|
411
|
+
`Map<string, number>` — messageId → timestamp:
|
|
1049
412
|
|
|
1050
|
-
|
|
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
|
-
|
|
417
|
+
Chroni przed podwójnym przetworzeniem gdy `PendingRecovery` przejmuje wiadomość która jest jeszcze aktywna w consumer loop.
|
|
1055
418
|
|
|
1056
|
-
###
|
|
419
|
+
### XTRIM (MessageProcessor)
|
|
1057
420
|
|
|
1058
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
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
|
-
|
|
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
|
-
├──
|
|
1196
|
-
|
|
1197
|
-
│
|
|
1198
|
-
│
|
|
1199
|
-
│ ├──
|
|
1200
|
-
│ │
|
|
1201
|
-
│
|
|
1202
|
-
│ ├──
|
|
1203
|
-
│ │
|
|
1204
|
-
│ ├──
|
|
1205
|
-
│ │
|
|
1206
|
-
│ ├──
|
|
1207
|
-
│ │ ├──
|
|
1208
|
-
│ │ ├──
|
|
1209
|
-
│ │
|
|
1210
|
-
│
|
|
1211
|
-
│
|
|
1212
|
-
│ │ ├──
|
|
1213
|
-
│ │ └──
|
|
1214
|
-
│ ├──
|
|
1215
|
-
│ │ └──
|
|
1216
|
-
│
|
|
1217
|
-
│
|
|
1218
|
-
├── shared/
|
|
1219
|
-
│ ├──
|
|
1220
|
-
│
|
|
1221
|
-
│ ├──
|
|
1222
|
-
│ │ ├──
|
|
1223
|
-
│ │ └──
|
|
1224
|
-
│ ├──
|
|
1225
|
-
│ │ ├──
|
|
1226
|
-
│ │
|
|
1227
|
-
│
|
|
1228
|
-
│
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
-
#
|
|
1269
|
-
|
|
486
|
+
# Wszystkie testy
|
|
487
|
+
npm test
|
|
1270
488
|
|
|
1271
|
-
#
|
|
1272
|
-
|
|
489
|
+
# Z coverage
|
|
490
|
+
npm run test:coverage
|
|
1273
491
|
|
|
1274
|
-
#
|
|
1275
|
-
|
|
492
|
+
# Lint
|
|
493
|
+
npm run lint
|
|
1276
494
|
|
|
1277
|
-
#
|
|
1278
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
1322
|
-
- Mariusz Lejkowski <m.lejkowski@polskiepolisy.pl>
|
|
507
|
+
Pokrycie: **193 testów**, zero timing-dependent `setTimeout`, `jest.useFakeTimers()` dla backoff.
|
|
1323
508
|
|
|
1324
|
-
##
|
|
509
|
+
## Changelog
|
|
1325
510
|
|
|
1326
|
-
|
|
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).
|