@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 +21 -0
- package/LLMGUIDE.md +156 -0
- package/README.md +1113 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +411 -0
- package/dist/outbox/index.d.ts +5 -0
- package/dist/outbox/outbox-interfaces.d.ts +50 -0
- package/dist/outbox/outbox-message-factory.d.ts +39 -0
- package/dist/outbox/outbox-processor.d.ts +105 -0
- package/dist/outbox/outbox-repository.interface.d.ts +61 -0
- package/dist/outbox/outbox-service.d.ts +82 -0
- package/package.json +66 -0
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.
|