@vytches/ddd-messaging 0.26.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 VytchesDDD
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/LLMGUIDE.md ADDED
@@ -0,0 +1,156 @@
1
+ # @vytches/ddd-messaging - LLM Guide
2
+
3
+ ## Purpose
4
+
5
+ Outbox pattern implementation for at-least-once delivery of integration events.
6
+ Provides the storage-agnostic interfaces and the orchestration logic (processor,
7
+ factory, service); consumers plug in their own `IOutboxRepository` (Postgres,
8
+ Redis, in-memory, etc.).
9
+
10
+ True to the library's philosophy: **no database adapter ships with the package**
11
+ — bring your own storage.
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import {
17
+ OutboxService,
18
+ OutboxProcessor,
19
+ OutboxMessageFactory,
20
+ EventBusOutboxHandler,
21
+ MessageStatus,
22
+ MessagePriority,
23
+ type IOutboxRepository,
24
+ } from '@vytches/ddd-messaging';
25
+
26
+ // 1. Implement the repository for your storage (example sketch)
27
+ class PgOutboxRepository implements IOutboxRepository {
28
+ async save(message) {
29
+ /* INSERT INTO outbox ... */
30
+ }
31
+ async findPending(limit) {
32
+ /* SELECT ... WHERE status = 'pending' */
33
+ }
34
+ async markDispatched(id) {
35
+ /* UPDATE ... */
36
+ }
37
+ async markFailed(id, error) {
38
+ /* UPDATE ... */
39
+ }
40
+ // ...
41
+ }
42
+
43
+ // 2. Wire up the outbox service
44
+ const repo = new PgOutboxRepository(db);
45
+ const factory = new OutboxMessageFactory();
46
+ const handler = new EventBusOutboxHandler(integrationBus);
47
+ const processor = new OutboxProcessor(repo, handler, {
48
+ pollIntervalMs: 1_000,
49
+ batchSize: 50,
50
+ });
51
+
52
+ const outbox = new OutboxService(repo, factory, processor);
53
+
54
+ // 3. In a transaction with your aggregate save
55
+ await db.transaction(async tx => {
56
+ await orderRepo.save(order, tx);
57
+ await outbox.enqueue(
58
+ integrationEvent,
59
+ { priority: MessagePriority.High },
60
+ tx
61
+ );
62
+ });
63
+ ```
64
+
65
+ ## Key API
66
+
67
+ | Export | Kind | Description |
68
+ | ------------------------ | --------- | ---------------------------------------------------------------- |
69
+ | `IOutboxMessage` | interface | The shape of a stored outbox row |
70
+ | `IOutboxRepository` | interface | **You implement this**: persistence for outbox messages |
71
+ | `IOutboxMessageHandler` | interface | How to dispatch a stored message (default: publish to event bus) |
72
+ | `OutboxMessageOptions` | type | `{ priority, dedupKey, headers }` per-message overrides |
73
+ | `OutboxMiddleware` | type | Hook for cross-cutting concerns (tracing, encryption) |
74
+ | `OutboxProcessorOptions` | type | `{ pollIntervalMs, batchSize, maxRetries, ... }` |
75
+ | `OutboxServiceOptions` | type | Top-level config bag |
76
+ | `MessageStatus` | enum | `Pending`, `Dispatched`, `Failed`, `DeadLettered` |
77
+ | `MessagePriority` | enum | `Low`, `Normal`, `High`, `Critical` |
78
+ | `OutboxMessageFactory` | class | Build `IOutboxMessage` from an integration event |
79
+ | `OutboxProcessor` | class | Polls repo, dispatches via handler, updates status |
80
+ | `EventBusOutboxHandler` | class | Default handler — publishes via an `IIntegrationEventBus` |
81
+ | `OutboxService` | class | Facade combining factory + processor for the application |
82
+
83
+ ## Patterns
84
+
85
+ ### Transactional outbox
86
+
87
+ Always enqueue the outbox row in **the same transaction** as the aggregate save.
88
+ This is the whole point — the outbox row and the state change commit atomically.
89
+ The processor then picks up pending rows asynchronously.
90
+
91
+ ```typescript
92
+ await db.transaction(async tx => {
93
+ await orderRepo.save(order, tx);
94
+ // critical: same tx
95
+ await outboxRepo.save(factory.fromEvent(orderPlacedEvent), tx);
96
+ });
97
+ ```
98
+
99
+ ### Idempotent consumers
100
+
101
+ Set a deduplication key on each outbox message so consumers can ignore
102
+ re-deliveries (which **will** happen with at-least-once).
103
+
104
+ ```typescript
105
+ await outbox.enqueue(event, { dedupKey: `order-${orderId}-placed` });
106
+ ```
107
+
108
+ ### Dispatch model — parallel within batch
109
+
110
+ `OutboxProcessor.processBatch()` dispatches all messages in the batch **in
111
+ parallel** via `Promise.allSettled`. This is intentional and contractual:
112
+
113
+ - A failure in message N does **not** block messages N+1..N+M
114
+ - Each message has independent retry/fail tracking via the repository
115
+ - For 200 messages at 10ms dispatch each: ~10ms parallel vs ~2s sequential
116
+
117
+ If you need strict ordering within a stream, set `batchSize: 1` (one message per
118
+ batch = sequential by definition). Note: parallel dispatch means the **execution
119
+ order** within a batch is not guaranteed, even when messages were selected via
120
+ `priorityOrder`.
121
+
122
+ For high-volume use cases where the batch is consistently full, consider
123
+ implementing the adapter pattern via `IOutboxMessageHandler` to dispatch to your
124
+ existing message broker (BullMQ, Kafka, RabbitMQ — your code, no library
125
+ dependency added).
126
+
127
+ ```typescript
128
+ class BullMQOutboxHandler implements IOutboxMessageHandler {
129
+ constructor(private readonly fanOut: MyFanOutService) {}
130
+ async handle(message: IOutboxMessage): Promise<void> {
131
+ await this.fanOut.dispatch(message.payload);
132
+ }
133
+ }
134
+
135
+ const processor = new OutboxProcessor(repo, new BullMQOutboxHandler(fanOut), {
136
+ pollIntervalMs: 2_000,
137
+ batchSize: 200,
138
+ });
139
+ ```
140
+
141
+ The library does **not** ship broker-specific adapters — that would force
142
+ adopters to bring in BullMQ/Kafka/etc. as transitive dependencies. Write the
143
+ ~10-line adapter yourself; the library guarantees the interface contract.
144
+
145
+ ## Anti-Patterns
146
+
147
+ - **Do not enqueue outbox messages outside the aggregate save transaction** — if
148
+ the save commits but the outbox enqueue fails, the integration event is lost
149
+ forever.
150
+ - **Do not assume exactly-once delivery** — outbox is at-least-once. Make
151
+ consumers idempotent.
152
+ - **Do not use the outbox for synchronous request/response** — it is for
153
+ fire-and-forget integration events. Use a different abstraction (RPC, query
154
+ handler) for sync flows.
155
+ - **Do not skip the dead-letter path** — messages that exceed retry budget must
156
+ land somewhere observable, or you'll silently lose data.