@vytches/ddd-messaging 0.27.0 → 0.28.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 +82 -1041
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +84 -45
- package/dist/outbox/outbox-processor.d.ts +17 -0
- package/dist/outbox/outbox-repository.interface.d.ts +17 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,1113 +1,154 @@
|
|
|
1
1
|
# @vytches/ddd-messaging
|
|
2
2
|
|
|
3
|
-
<!-- LLM-METADATA
|
|
4
|
-
Package: @vytches/ddd-messaging
|
|
5
|
-
Category: Integration
|
|
6
|
-
Purpose: Outbox pattern and Saga orchestration for reliable message delivery and long-running business processes
|
|
7
|
-
Dependencies: @vytches/ddd-core, @vytches/ddd-events, @vytches/ddd-logging
|
|
8
|
-
Complexity: High
|
|
9
|
-
DDD Patterns: Outbox Pattern, Saga Pattern, Message Passing, Event-Driven Architecture
|
|
10
|
-
Integration Points: @vytches/ddd-events, @vytches/ddd-repositories, @vytches/ddd-logging, @vytches/ddd-di
|
|
11
|
-
-->
|
|
12
|
-
|
|
13
3
|
[](https://badge.fury.io/js/%40vytches%2Fddd-messaging)
|
|
14
4
|
[](https://www.typescriptlang.org/)
|
|
15
5
|
[](https://opensource.org/licenses/MIT)
|
|
16
6
|
|
|
17
|
-
> **
|
|
18
|
-
> orchestration for reliable message delivery**
|
|
19
|
-
|
|
20
|
-
Complete messaging solution with outbox pattern for reliable message delivery,
|
|
21
|
-
saga orchestration for long-running business processes, and comprehensive
|
|
22
|
-
middleware support. Designed for distributed systems requiring guaranteed
|
|
23
|
-
message delivery and complex workflow orchestration.
|
|
24
|
-
|
|
25
|
-
## 📋 Table of Contents
|
|
7
|
+
> **Transactional Outbox pattern for reliable domain event delivery**
|
|
26
8
|
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
- [Core Concepts](#core-concepts)
|
|
30
|
-
- [Quick Start](#quick-start)
|
|
31
|
-
- [Outbox Pattern](#outbox-pattern)
|
|
32
|
-
- [Saga Pattern](#saga-pattern)
|
|
33
|
-
- [Message Processing](#message-processing)
|
|
34
|
-
- [Middleware](#middleware)
|
|
35
|
-
- [Error Handling](#error-handling)
|
|
36
|
-
- [Monitoring](#monitoring)
|
|
37
|
-
- [Integration Patterns](#integration-patterns)
|
|
38
|
-
- [Testing](#testing)
|
|
39
|
-
- [Best Practices](#best-practices)
|
|
40
|
-
- [Contributing](#contributing)
|
|
9
|
+
Provides the Outbox pattern — store-then-publish guarantee for domain events.
|
|
10
|
+
This package contains **only Outbox** infrastructure. There is no Saga support.
|
|
41
11
|
|
|
42
|
-
##
|
|
12
|
+
## Installation
|
|
43
13
|
|
|
44
14
|
```bash
|
|
45
|
-
# npm
|
|
46
|
-
npm install @vytches/ddd-messaging
|
|
47
|
-
|
|
48
|
-
# yarn
|
|
49
|
-
yarn add @vytches/ddd-messaging
|
|
50
|
-
|
|
51
|
-
# pnpm
|
|
52
15
|
pnpm add @vytches/ddd-messaging
|
|
53
16
|
```
|
|
54
17
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
# Required for full functionality
|
|
59
|
-
npm install @vytches/ddd-core @vytches/ddd-events @vytches/ddd-logging
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## ✨ Key Features
|
|
63
|
-
|
|
64
|
-
### Outbox Pattern
|
|
65
|
-
|
|
66
|
-
- **Reliable Delivery**: Guaranteed message delivery with transactional outbox
|
|
67
|
-
- **Priority Processing**: Configurable message priorities
|
|
68
|
-
(LOW/NORMAL/HIGH/CRITICAL)
|
|
69
|
-
- **Delayed Messages**: Support for scheduled message processing
|
|
70
|
-
- **Batch Operations**: Efficient bulk message handling
|
|
71
|
-
|
|
72
|
-
### Saga Orchestration
|
|
73
|
-
|
|
74
|
-
- **Long-running Processes**: Complete saga orchestration for complex business
|
|
75
|
-
workflows
|
|
76
|
-
- **Compensation Logic**: Automatic compensation for failed transactions
|
|
77
|
-
- **State Management**: Persistent saga state with optimistic concurrency
|
|
78
|
-
control
|
|
79
|
-
- **Correlation Support**: Event correlation for distributed process tracking
|
|
80
|
-
|
|
81
|
-
### Enterprise Features
|
|
82
|
-
|
|
83
|
-
- **Middleware Pipeline**: Extensible message processing pipeline
|
|
84
|
-
- **Retry Logic**: Configurable retry policies with exponential backoff
|
|
85
|
-
- **Circuit Breaker**: Fault tolerance with circuit breaker pattern
|
|
86
|
-
- **Monitoring**: Comprehensive metrics and observability
|
|
87
|
-
|
|
88
|
-
## 🎯 Core Concepts
|
|
89
|
-
|
|
90
|
-
### Outbox Pattern
|
|
91
|
-
|
|
92
|
-
The outbox pattern ensures reliable message delivery by storing messages in a
|
|
93
|
-
transactional outbox:
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
// Message interface
|
|
97
|
-
interface IOutboxMessage<T = unknown> {
|
|
98
|
-
id: string;
|
|
99
|
-
messageType: string;
|
|
100
|
-
payload: T;
|
|
101
|
-
metadata: Record<string, unknown>;
|
|
102
|
-
status: MessageStatus;
|
|
103
|
-
attempts: number;
|
|
104
|
-
createdAt: Date;
|
|
105
|
-
processAfter?: Date;
|
|
106
|
-
priority?: MessagePriority;
|
|
107
|
-
lastError?: string;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Message statuses
|
|
111
|
-
enum MessageStatus {
|
|
112
|
-
PENDING = 'PENDING',
|
|
113
|
-
PROCESSING = 'PROCESSING',
|
|
114
|
-
PROCESSED = 'PROCESSED',
|
|
115
|
-
FAILED = 'FAILED',
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Priority levels
|
|
119
|
-
enum MessagePriority {
|
|
120
|
-
LOW = 'low',
|
|
121
|
-
NORMAL = 'normal',
|
|
122
|
-
HIGH = 'high',
|
|
123
|
-
CRITICAL = 'critical',
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### Saga Pattern
|
|
18
|
+
## What's included
|
|
128
19
|
|
|
129
|
-
|
|
20
|
+
| Export | Kind | Description |
|
|
21
|
+
| ------------------------ | -------------- | --------------------------------------------------------------- |
|
|
22
|
+
| `MessageStatus` | enum | `PENDING \| PROCESSING \| PROCESSED \| FAILED` |
|
|
23
|
+
| `MessagePriority` | enum | `LOW \| NORMAL \| HIGH \| CRITICAL` |
|
|
24
|
+
| `OutboxMessageFactory` | class | Creates `IOutboxMessage` instances |
|
|
25
|
+
| `OutboxProcessor` | class | Polls the repository and dispatches messages |
|
|
26
|
+
| `EventBusOutboxHandler` | class | `IOutboxMessageHandler` that publishes to an `IEventBus` |
|
|
27
|
+
| `OutboxService` | class | High-level facade: store, schedule, and coordinate processing |
|
|
28
|
+
| `IOutboxRepository` | abstract class | Base repository contract — extend this for your storage backend |
|
|
29
|
+
| `IOutboxMessage` | interface | Message shape with id, payload, status, priority, timestamps |
|
|
30
|
+
| `IOutboxMessageHandler` | interface | Single-method handler: `handle(message): Promise<void>` |
|
|
31
|
+
| `OutboxMiddleware` | type | Middleware signature for the processor pipeline |
|
|
32
|
+
| `OutboxProcessorOptions` | interface | Processor configuration (interval, batch size, retries…) |
|
|
33
|
+
| `OutboxServiceOptions` | interface | Service configuration |
|
|
34
|
+
| `OutboxMessageOptions` | interface | Per-message options (priority, processAfter…) |
|
|
35
|
+
| `RetryBackoffConfig` | interface | Exponential backoff configuration for the processor |
|
|
130
36
|
|
|
131
|
-
|
|
132
|
-
// Saga interface
|
|
133
|
-
interface ISaga {
|
|
134
|
-
readonly sagaId: string;
|
|
135
|
-
readonly sagaType: string;
|
|
136
|
-
readonly status: SagaStatus;
|
|
137
|
-
readonly state: ISagaState;
|
|
138
|
-
|
|
139
|
-
handleEvent(
|
|
140
|
-
event: IDomainEvent,
|
|
141
|
-
context: ISagaExecutionContext
|
|
142
|
-
): Promise<ISagaActionResult>;
|
|
143
|
-
compensate(
|
|
144
|
-
stepName: string,
|
|
145
|
-
context: ISagaExecutionContext
|
|
146
|
-
): Promise<ISagaActionResult>;
|
|
147
|
-
canHandle(event: IDomainEvent): boolean;
|
|
148
|
-
getCorrelationData(): Record<string, unknown>;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Saga statuses
|
|
152
|
-
enum SagaStatus {
|
|
153
|
-
STARTED = 'STARTED',
|
|
154
|
-
EXECUTING = 'EXECUTING',
|
|
155
|
-
WAITING = 'WAITING',
|
|
156
|
-
COMPLETED = 'COMPLETED',
|
|
157
|
-
FAILED = 'FAILED',
|
|
158
|
-
COMPENSATING = 'COMPENSATING',
|
|
159
|
-
COMPENSATED = 'COMPENSATED',
|
|
160
|
-
CANCELLED = 'CANCELLED',
|
|
161
|
-
TIMED_OUT = 'TIMED_OUT',
|
|
162
|
-
}
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
## 🚀 Quick Start
|
|
166
|
-
|
|
167
|
-
### Basic Outbox Usage
|
|
37
|
+
## Quick start
|
|
168
38
|
|
|
169
39
|
```typescript
|
|
170
40
|
import {
|
|
171
41
|
OutboxService,
|
|
172
42
|
OutboxProcessor,
|
|
173
43
|
OutboxMessageFactory,
|
|
44
|
+
EventBusOutboxHandler,
|
|
174
45
|
MessagePriority,
|
|
175
46
|
} from '@vytches/ddd-messaging';
|
|
47
|
+
import { UnifiedEventBus } from '@vytches/ddd-events';
|
|
176
48
|
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
batchSize: 100,
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// Create and store a message
|
|
185
|
-
const messageFactory = new OutboxMessageFactory();
|
|
186
|
-
const message = messageFactory.createMessage({
|
|
187
|
-
messageType: 'OrderCreated',
|
|
188
|
-
payload: { orderId: '123', customerId: '456' },
|
|
189
|
-
priority: MessagePriority.HIGH,
|
|
190
|
-
metadata: { source: 'OrderService' },
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
await outboxService.storeMessage(message);
|
|
194
|
-
|
|
195
|
-
// Process messages
|
|
196
|
-
const processor = new OutboxProcessor(outboxService, {
|
|
197
|
-
processingInterval: 5000,
|
|
198
|
-
maxConcurrency: 10,
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
await processor.start();
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
### Basic Saga Usage
|
|
205
|
-
|
|
206
|
-
```typescript
|
|
207
|
-
import {
|
|
208
|
-
BaseSaga,
|
|
209
|
-
SagaOrchestrator,
|
|
210
|
-
InMemorySagaRepository,
|
|
211
|
-
Saga,
|
|
212
|
-
SagaEventHandler,
|
|
213
|
-
} from '@vytches/ddd-messaging';
|
|
214
|
-
|
|
215
|
-
// Define a saga
|
|
216
|
-
@Saga('OrderProcessingSaga')
|
|
217
|
-
class OrderProcessingSaga extends BaseSaga {
|
|
218
|
-
constructor(sagaId: string, correlationId: string) {
|
|
219
|
-
super(
|
|
220
|
-
{
|
|
221
|
-
sagaId,
|
|
222
|
-
sagaType: 'OrderProcessingSaga',
|
|
223
|
-
status: SagaStatus.STARTED,
|
|
224
|
-
currentStep: 'ProcessPayment',
|
|
225
|
-
stepData: {},
|
|
226
|
-
compensationData: {},
|
|
227
|
-
correlationId,
|
|
228
|
-
metadata: {},
|
|
229
|
-
createdAt: new Date(),
|
|
230
|
-
updatedAt: new Date(),
|
|
231
|
-
timeoutAt: undefined,
|
|
232
|
-
version: 1,
|
|
233
|
-
},
|
|
234
|
-
'OrderProcessingSaga'
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
@SagaEventHandler('OrderCreated')
|
|
239
|
-
async handleOrderCreated(
|
|
240
|
-
event: IDomainEvent,
|
|
241
|
-
context: ISagaExecutionContext
|
|
242
|
-
): Promise<ISagaActionResult> {
|
|
243
|
-
// Process order creation
|
|
244
|
-
this.updateState({
|
|
245
|
-
currentStep: 'ProcessPayment',
|
|
246
|
-
stepData: { orderId: event.payload.orderId },
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
return {
|
|
250
|
-
success: true,
|
|
251
|
-
commands: [{ type: 'ProcessPayment', payload: event.payload }],
|
|
252
|
-
events: [{ eventType: 'PaymentRequested', payload: event.payload }],
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
@SagaEventHandler('PaymentProcessed')
|
|
257
|
-
async handlePaymentProcessed(
|
|
258
|
-
event: IDomainEvent,
|
|
259
|
-
context: ISagaExecutionContext
|
|
260
|
-
): Promise<ISagaActionResult> {
|
|
261
|
-
// Process payment completion
|
|
262
|
-
this.updateState({
|
|
263
|
-
currentStep: 'ReserveInventory',
|
|
264
|
-
stepData: { ...this.state.stepData, paymentId: event.payload.paymentId },
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
return {
|
|
268
|
-
success: true,
|
|
269
|
-
commands: [{ type: 'ReserveInventory', payload: event.payload }],
|
|
270
|
-
completesSaga: false,
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async compensate(
|
|
275
|
-
stepName: string,
|
|
276
|
-
context: ISagaExecutionContext
|
|
277
|
-
): Promise<ISagaActionResult> {
|
|
278
|
-
switch (stepName) {
|
|
279
|
-
case 'ProcessPayment':
|
|
280
|
-
return await this.refundPayment(context);
|
|
281
|
-
case 'ReserveInventory':
|
|
282
|
-
return await this.releaseInventory(context);
|
|
283
|
-
default:
|
|
284
|
-
return { success: true };
|
|
285
|
-
}
|
|
49
|
+
// Implement IOutboxRepository for your storage (e.g. PostgreSQL)
|
|
50
|
+
class PostgresOutboxRepository extends IOutboxRepository {
|
|
51
|
+
async save(message) {
|
|
52
|
+
/* ... */
|
|
286
53
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return ['OrderCreated', 'PaymentProcessed', 'InventoryReserved'].includes(
|
|
290
|
-
event.eventType
|
|
291
|
-
);
|
|
54
|
+
async findPending(limit) {
|
|
55
|
+
/* ... */
|
|
292
56
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
context: ISagaExecutionContext
|
|
296
|
-
): Promise<ISagaActionResult> {
|
|
297
|
-
// Implement refund logic
|
|
298
|
-
return { success: true };
|
|
57
|
+
async markProcessed(id) {
|
|
58
|
+
/* ... */
|
|
299
59
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
context: ISagaExecutionContext
|
|
303
|
-
): Promise<ISagaActionResult> {
|
|
304
|
-
// Implement inventory release logic
|
|
305
|
-
return { success: true };
|
|
60
|
+
async markFailed(id, error) {
|
|
61
|
+
/* ... */
|
|
306
62
|
}
|
|
307
63
|
}
|
|
308
|
-
```
|
|
309
64
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
```typescript
|
|
313
|
-
import {
|
|
314
|
-
SagaOrchestrator,
|
|
315
|
-
InMemorySagaRepository,
|
|
316
|
-
} from '@vytches/ddd-messaging';
|
|
65
|
+
const repository = new PostgresOutboxRepository();
|
|
66
|
+
const eventBus = new UnifiedEventBus();
|
|
317
67
|
|
|
318
|
-
//
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
68
|
+
// Service stores messages, processor dispatches them
|
|
69
|
+
const service = new OutboxService(repository);
|
|
70
|
+
const handler = new EventBusOutboxHandler(eventBus);
|
|
71
|
+
const processor = new OutboxProcessor(repository, handler, {
|
|
72
|
+
processingInterval: 5_000,
|
|
73
|
+
batchSize: 50,
|
|
74
|
+
maxRetries: 3,
|
|
324
75
|
});
|
|
325
76
|
|
|
326
|
-
//
|
|
327
|
-
const orderSagaDefinition = {
|
|
328
|
-
sagaType: 'OrderProcessingSaga',
|
|
329
|
-
displayName: 'Order Processing Workflow',
|
|
330
|
-
description: 'Handles complete order processing with compensation',
|
|
331
|
-
startEvents: ['OrderCreated'],
|
|
332
|
-
defaultTimeout: 3600000, // 1 hour
|
|
333
|
-
maxInstances: 100,
|
|
334
|
-
steps: [],
|
|
335
|
-
createInstance: async (event, context) =>
|
|
336
|
-
new OrderProcessingSaga(generateId(), event.correlationId || generateId()),
|
|
337
|
-
getCorrelationData: event => ({ orderId: event.payload.orderId }),
|
|
338
|
-
validate: () => [],
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
orchestrator.registerSagaDefinition(orderSagaDefinition);
|
|
342
|
-
|
|
343
|
-
// Process events
|
|
344
|
-
const event = createOrderCreatedEvent();
|
|
345
|
-
const context = {
|
|
346
|
-
correlationId: 'order-123',
|
|
347
|
-
userId: 'user-456',
|
|
348
|
-
metadata: {},
|
|
349
|
-
timestamp: new Date(),
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
const results = await orchestrator.processEvent(event, context);
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
## 📦 Outbox Pattern
|
|
356
|
-
|
|
357
|
-
### Message Creation
|
|
358
|
-
|
|
359
|
-
```typescript
|
|
360
|
-
import { OutboxMessageFactory, MessagePriority } from '@vytches/ddd-messaging';
|
|
361
|
-
|
|
77
|
+
// Store a message (call this inside your aggregate save transaction)
|
|
362
78
|
const factory = new OutboxMessageFactory();
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
messageType: 'UserRegistered',
|
|
367
|
-
payload: { userId: '123', email: 'user@example.com' },
|
|
79
|
+
const message = factory.create({
|
|
80
|
+
messageType: 'OrderCreated',
|
|
81
|
+
payload: { orderId: '123' },
|
|
368
82
|
priority: MessagePriority.HIGH,
|
|
369
|
-
metadata: { source: 'UserService' },
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
// Create delayed message
|
|
373
|
-
const delayedMessage = factory.createMessage({
|
|
374
|
-
messageType: 'SendWelcomeEmail',
|
|
375
|
-
payload: { userId: '123' },
|
|
376
|
-
processAfter: new Date(Date.now() + 60000), // 1 minute delay
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
// Create batch of messages
|
|
380
|
-
const messages = factory.createBatch([
|
|
381
|
-
{ messageType: 'Event1', payload: { data: 'test1' } },
|
|
382
|
-
{ messageType: 'Event2', payload: { data: 'test2' } },
|
|
383
|
-
{ messageType: 'Event3', payload: { data: 'test3' } },
|
|
384
|
-
]);
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
### Custom Message Handler
|
|
388
|
-
|
|
389
|
-
```typescript
|
|
390
|
-
import { IOutboxMessageHandler, IOutboxMessage } from '@vytches/ddd-messaging';
|
|
391
|
-
|
|
392
|
-
class EmailMessageHandler implements IOutboxMessageHandler {
|
|
393
|
-
async handle(message: IOutboxMessage): Promise<void> {
|
|
394
|
-
switch (message.messageType) {
|
|
395
|
-
case 'SendWelcomeEmail':
|
|
396
|
-
await this.sendWelcomeEmail(message.payload);
|
|
397
|
-
break;
|
|
398
|
-
case 'SendPasswordResetEmail':
|
|
399
|
-
await this.sendPasswordResetEmail(message.payload);
|
|
400
|
-
break;
|
|
401
|
-
default:
|
|
402
|
-
throw new Error(`Unknown message type: ${message.messageType}`);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
private async sendWelcomeEmail(payload: any): Promise<void> {
|
|
407
|
-
// Email sending logic
|
|
408
|
-
console.log('Sending welcome email to:', payload.email);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
private async sendPasswordResetEmail(payload: any): Promise<void> {
|
|
412
|
-
// Password reset email logic
|
|
413
|
-
console.log('Sending password reset email to:', payload.email);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
### Processing Configuration
|
|
419
|
-
|
|
420
|
-
```typescript
|
|
421
|
-
import { OutboxProcessor, OutboxService } from '@vytches/ddd-messaging';
|
|
422
|
-
|
|
423
|
-
const processor = new OutboxProcessor(outboxService, {
|
|
424
|
-
processingInterval: 5000, // Process every 5 seconds
|
|
425
|
-
maxConcurrency: 10, // Process up to 10 messages concurrently
|
|
426
|
-
maxRetries: 3, // Retry failed messages up to 3 times
|
|
427
|
-
retryDelay: 1000, // Wait 1 second between retries
|
|
428
|
-
batchSize: 100, // Process 100 messages per batch
|
|
429
|
-
enableMetrics: true, // Enable performance metrics
|
|
430
|
-
errorHandler: (error, message) => {
|
|
431
|
-
console.error('Message processing failed:', error);
|
|
432
|
-
},
|
|
433
83
|
});
|
|
84
|
+
await service.store(message);
|
|
434
85
|
|
|
86
|
+
// Start polling
|
|
435
87
|
await processor.start();
|
|
436
88
|
```
|
|
437
89
|
|
|
438
|
-
##
|
|
439
|
-
|
|
440
|
-
### Saga Definition
|
|
90
|
+
## Custom message handler
|
|
441
91
|
|
|
442
|
-
|
|
443
|
-
import {
|
|
444
|
-
BaseSaga,
|
|
445
|
-
SagaDefinition,
|
|
446
|
-
SagaStep,
|
|
447
|
-
SagaStatus,
|
|
448
|
-
} from '@vytches/ddd-messaging';
|
|
449
|
-
|
|
450
|
-
const orderProcessingDefinition = new SagaDefinition({
|
|
451
|
-
sagaType: 'OrderProcessingSaga',
|
|
452
|
-
displayName: 'Order Processing Workflow',
|
|
453
|
-
description:
|
|
454
|
-
'Comprehensive order processing with payment, inventory, and shipping',
|
|
455
|
-
startEvents: ['OrderCreated'],
|
|
456
|
-
defaultTimeout: 3600000,
|
|
457
|
-
maxInstances: 1000,
|
|
458
|
-
steps: [
|
|
459
|
-
new SagaStep({
|
|
460
|
-
name: 'ProcessPayment',
|
|
461
|
-
displayName: 'Process Payment',
|
|
462
|
-
compensatable: true,
|
|
463
|
-
timeout: 30000,
|
|
464
|
-
triggerEvents: ['OrderCreated'],
|
|
465
|
-
completionEvents: ['PaymentProcessed', 'PaymentFailed'],
|
|
466
|
-
}),
|
|
467
|
-
new SagaStep({
|
|
468
|
-
name: 'ReserveInventory',
|
|
469
|
-
displayName: 'Reserve Inventory',
|
|
470
|
-
compensatable: true,
|
|
471
|
-
timeout: 15000,
|
|
472
|
-
triggerEvents: ['PaymentProcessed'],
|
|
473
|
-
completionEvents: ['InventoryReserved', 'InventoryNotAvailable'],
|
|
474
|
-
}),
|
|
475
|
-
new SagaStep({
|
|
476
|
-
name: 'ArrangeShipping',
|
|
477
|
-
displayName: 'Arrange Shipping',
|
|
478
|
-
compensatable: true,
|
|
479
|
-
timeout: 60000,
|
|
480
|
-
triggerEvents: ['InventoryReserved'],
|
|
481
|
-
completionEvents: ['ShippingArranged', 'ShippingFailed'],
|
|
482
|
-
}),
|
|
483
|
-
],
|
|
484
|
-
});
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
### Saga State Management
|
|
92
|
+
Implement `IOutboxMessageHandler` to route messages any way you like:
|
|
488
93
|
|
|
489
94
|
```typescript
|
|
490
|
-
|
|
491
|
-
protected updateState(updates: Partial<ISagaState>): void {
|
|
492
|
-
this._state = {
|
|
493
|
-
...this._state,
|
|
494
|
-
...updates,
|
|
495
|
-
updatedAt: new Date(),
|
|
496
|
-
version: this._state.version + 1,
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
protected addCompensationData(
|
|
501
|
-
stepName: string,
|
|
502
|
-
data: Record<string, unknown>
|
|
503
|
-
): void {
|
|
504
|
-
this._state.compensationData[stepName] = data;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
protected getCompensationData(
|
|
508
|
-
stepName: string
|
|
509
|
-
): Record<string, unknown> | undefined {
|
|
510
|
-
return this._state.compensationData[stepName];
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
protected markCompleted(): void {
|
|
514
|
-
this.updateState({
|
|
515
|
-
status: SagaStatus.COMPLETED,
|
|
516
|
-
currentStep: 'COMPLETED',
|
|
517
|
-
});
|
|
518
|
-
}
|
|
95
|
+
import { IOutboxMessageHandler, IOutboxMessage } from '@vytches/ddd-messaging';
|
|
519
96
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
step,
|
|
526
|
-
timestamp: new Date(),
|
|
527
|
-
},
|
|
97
|
+
class KafkaMessageHandler implements IOutboxMessageHandler {
|
|
98
|
+
async handle(message: IOutboxMessage): Promise<void> {
|
|
99
|
+
await kafkaProducer.send({
|
|
100
|
+
topic: message.messageType,
|
|
101
|
+
messages: [{ value: JSON.stringify(message.payload) }],
|
|
528
102
|
});
|
|
529
103
|
}
|
|
530
104
|
}
|
|
531
105
|
```
|
|
532
106
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
```typescript
|
|
536
|
-
import { InMemorySagaRepository } from '@vytches/ddd-messaging';
|
|
537
|
-
|
|
538
|
-
// Create repository with configuration
|
|
539
|
-
const repository = new InMemorySagaRepository({
|
|
540
|
-
enableOptimisticLocking: true,
|
|
541
|
-
enableAuditLog: true,
|
|
542
|
-
retentionPolicy: {
|
|
543
|
-
completedAfterDays: 30,
|
|
544
|
-
compensatedAfterDays: 60,
|
|
545
|
-
failedAfterDays: 90,
|
|
546
|
-
},
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
// Save saga state
|
|
550
|
-
await repository.save(sagaInstance);
|
|
551
|
-
|
|
552
|
-
// Find sagas by correlation
|
|
553
|
-
const relatedSagas = await repository.findByCorrelation({
|
|
554
|
-
orderId: 'order-123',
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
// Find timed out sagas
|
|
558
|
-
const timedOutSagas = await repository.findTimedOut(new Date());
|
|
559
|
-
|
|
560
|
-
// Query sagas with criteria
|
|
561
|
-
const queryResult = await repository.query({
|
|
562
|
-
sagaType: 'OrderProcessingSaga',
|
|
563
|
-
status: [SagaStatus.STARTED, SagaStatus.EXECUTING],
|
|
564
|
-
createdBetween: { start: yesterday, end: today },
|
|
565
|
-
limit: 50,
|
|
566
|
-
sortBy: 'createdAt',
|
|
567
|
-
sortOrder: 'desc',
|
|
568
|
-
});
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
## 🔧 Middleware
|
|
107
|
+
## Implement your repository
|
|
572
108
|
|
|
573
|
-
|
|
109
|
+
Extend `IOutboxRepository` and provide persistence:
|
|
574
110
|
|
|
575
111
|
```typescript
|
|
576
112
|
import {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
SecurityMiddleware,
|
|
113
|
+
IOutboxRepository,
|
|
114
|
+
IOutboxMessage,
|
|
115
|
+
MessageStatus,
|
|
581
116
|
} from '@vytches/ddd-messaging';
|
|
582
117
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
baseDelay: 1000,
|
|
587
|
-
maxDelay: 30000,
|
|
588
|
-
retryCondition: error => error.code === 'TRANSIENT_ERROR',
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
// Circuit breaker middleware
|
|
592
|
-
const circuitBreakerMiddleware = new CircuitBreakerMiddleware({
|
|
593
|
-
failureThreshold: 5,
|
|
594
|
-
resetTimeout: 60000,
|
|
595
|
-
monitoredServices: ['payment-service', 'inventory-service'],
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
// Performance monitoring
|
|
599
|
-
const performanceMiddleware = new PerformanceMonitoringMiddleware({
|
|
600
|
-
enableMetrics: true,
|
|
601
|
-
slowExecutionThreshold: 5000,
|
|
602
|
-
memoryUsageTracking: true,
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
// Security middleware
|
|
606
|
-
const securityMiddleware = new SecurityMiddleware({
|
|
607
|
-
validateCorrelationId: true,
|
|
608
|
-
requireAuthentication: true,
|
|
609
|
-
allowedOrigins: ['order-service', 'payment-service'],
|
|
610
|
-
});
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
### Custom Middleware
|
|
614
|
-
|
|
615
|
-
```typescript
|
|
616
|
-
import { BaseSagaMiddleware } from '@vytches/ddd-messaging';
|
|
617
|
-
|
|
618
|
-
class AuditMiddleware extends BaseSagaMiddleware {
|
|
619
|
-
async execute(
|
|
620
|
-
saga: ISaga,
|
|
621
|
-
event: IDomainEvent,
|
|
622
|
-
context: ISagaExecutionContext,
|
|
623
|
-
next: () => Promise<ISagaActionResult>
|
|
624
|
-
): Promise<ISagaActionResult> {
|
|
625
|
-
// Pre-execution audit
|
|
626
|
-
await this.auditService.logSagaExecution({
|
|
627
|
-
sagaId: saga.sagaId,
|
|
628
|
-
sagaType: saga.sagaType,
|
|
629
|
-
eventType: event.eventType,
|
|
630
|
-
correlationId: context.correlationId,
|
|
631
|
-
timestamp: new Date(),
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
try {
|
|
635
|
-
const result = await next();
|
|
636
|
-
|
|
637
|
-
// Post-execution audit
|
|
638
|
-
await this.auditService.logSagaResult({
|
|
639
|
-
sagaId: saga.sagaId,
|
|
640
|
-
success: result.success,
|
|
641
|
-
timestamp: new Date(),
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
return result;
|
|
645
|
-
} catch (error) {
|
|
646
|
-
// Error audit
|
|
647
|
-
await this.auditService.logSagaError({
|
|
648
|
-
sagaId: saga.sagaId,
|
|
649
|
-
error: error.message,
|
|
650
|
-
timestamp: new Date(),
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
throw error;
|
|
654
|
-
}
|
|
118
|
+
class MyOutboxRepository extends IOutboxRepository {
|
|
119
|
+
async save(message: IOutboxMessage): Promise<void> {
|
|
120
|
+
await db.outbox.insert(message);
|
|
655
121
|
}
|
|
656
|
-
}
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
## 🏥 Error Handling
|
|
660
|
-
|
|
661
|
-
### Saga Error Handling
|
|
662
|
-
|
|
663
|
-
```typescript
|
|
664
|
-
import {
|
|
665
|
-
SagaError,
|
|
666
|
-
SagaExecutionError,
|
|
667
|
-
SagaCompensationError,
|
|
668
|
-
} from '@vytches/ddd-messaging';
|
|
669
|
-
|
|
670
|
-
class OrderProcessingSaga extends BaseSaga {
|
|
671
|
-
async handleEvent(
|
|
672
|
-
event: IDomainEvent,
|
|
673
|
-
context: ISagaExecutionContext
|
|
674
|
-
): Promise<ISagaActionResult> {
|
|
675
|
-
try {
|
|
676
|
-
return await this.processEvent(event, context);
|
|
677
|
-
} catch (error) {
|
|
678
|
-
if (error instanceof SagaExecutionError) {
|
|
679
|
-
// Handle execution errors
|
|
680
|
-
this.markFailed(error.message, this.state.currentStep);
|
|
681
|
-
|
|
682
|
-
if (error.requiresCompensation) {
|
|
683
|
-
return await this.startCompensation(context);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
122
|
|
|
687
|
-
|
|
688
|
-
}
|
|
123
|
+
async findPendingMessages(limit: number): Promise<IOutboxMessage[]> {
|
|
124
|
+
return db.outbox.findWhere({ status: MessageStatus.PENDING }, limit);
|
|
689
125
|
}
|
|
690
126
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
): Promise<ISagaActionResult> {
|
|
694
|
-
this.updateState({ status: SagaStatus.COMPENSATING });
|
|
695
|
-
|
|
696
|
-
// Execute compensation in reverse order
|
|
697
|
-
const completedSteps = this.getCompletedSteps();
|
|
698
|
-
|
|
699
|
-
for (const step of completedSteps.reverse()) {
|
|
700
|
-
try {
|
|
701
|
-
await this.compensateStep(step, context);
|
|
702
|
-
} catch (compensationError) {
|
|
703
|
-
throw new SagaCompensationError(
|
|
704
|
-
`Compensation failed for step ${step}`,
|
|
705
|
-
{ step, originalError: compensationError }
|
|
706
|
-
);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
this.updateState({ status: SagaStatus.COMPENSATED });
|
|
711
|
-
|
|
712
|
-
return { success: true };
|
|
127
|
+
async markAsProcessed(id: string): Promise<void> {
|
|
128
|
+
await db.outbox.update(id, { status: MessageStatus.PROCESSED });
|
|
713
129
|
}
|
|
714
|
-
}
|
|
715
|
-
```
|
|
716
130
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const processor = new OutboxProcessor(outboxService, {
|
|
723
|
-
errorHandler: async (error, message) => {
|
|
724
|
-
console.error(`Failed to process message ${message.id}:`, error);
|
|
725
|
-
|
|
726
|
-
// Custom error handling logic
|
|
727
|
-
if (error.code === 'RATE_LIMIT_EXCEEDED') {
|
|
728
|
-
// Reschedule message for later
|
|
729
|
-
message.processAfter = new Date(Date.now() + 60000);
|
|
730
|
-
await outboxService.updateMessage(message);
|
|
731
|
-
} else if (error.code === 'PERMANENT_FAILURE') {
|
|
732
|
-
// Move to dead letter queue
|
|
733
|
-
await deadLetterService.store(message, error);
|
|
734
|
-
}
|
|
735
|
-
},
|
|
736
|
-
deadLetterHandler: async (message, error) => {
|
|
737
|
-
// Handle messages that exceed retry limit
|
|
738
|
-
await deadLetterService.store(message, error);
|
|
739
|
-
await notificationService.notifyAdmins({
|
|
740
|
-
type: 'DEAD_LETTER_MESSAGE',
|
|
741
|
-
messageId: message.id,
|
|
742
|
-
error: error.message,
|
|
131
|
+
async markAsFailed(id: string, error: string): Promise<void> {
|
|
132
|
+
await db.outbox.update(id, {
|
|
133
|
+
status: MessageStatus.FAILED,
|
|
134
|
+
lastError: error,
|
|
743
135
|
});
|
|
744
|
-
},
|
|
745
|
-
});
|
|
746
|
-
```
|
|
747
|
-
|
|
748
|
-
## 📊 Monitoring
|
|
749
|
-
|
|
750
|
-
### Saga Metrics
|
|
751
|
-
|
|
752
|
-
```typescript
|
|
753
|
-
import { SagaOrchestrator } from '@vytches/ddd-messaging';
|
|
754
|
-
|
|
755
|
-
const orchestrator = new SagaOrchestrator(repository, {
|
|
756
|
-
enableMetrics: true,
|
|
757
|
-
metricsConfig: {
|
|
758
|
-
collectExecutionTimes: true,
|
|
759
|
-
collectMemoryUsage: true,
|
|
760
|
-
collectCorrelationStats: true,
|
|
761
|
-
},
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
// Get comprehensive statistics
|
|
765
|
-
const stats = await orchestrator.getStatistics();
|
|
766
|
-
console.log('Saga Statistics:', {
|
|
767
|
-
totalInstances: stats.totalInstances,
|
|
768
|
-
activeInstances: stats.activeInstances,
|
|
769
|
-
completedInstances: stats.completedInstances,
|
|
770
|
-
failedInstances: stats.failedInstances,
|
|
771
|
-
averageExecutionTime: stats.averageExecutionTime,
|
|
772
|
-
memoryUsage: stats.memoryUsage,
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
// Get metrics by saga type
|
|
776
|
-
const typeMetrics = await orchestrator.getMetricsBySagaType(
|
|
777
|
-
'OrderProcessingSaga'
|
|
778
|
-
);
|
|
779
|
-
console.log('Order Processing Metrics:', {
|
|
780
|
-
instanceCount: typeMetrics.instanceCount,
|
|
781
|
-
successRate: typeMetrics.successRate,
|
|
782
|
-
averageSteps: typeMetrics.averageSteps,
|
|
783
|
-
compensationRate: typeMetrics.compensationRate,
|
|
784
|
-
});
|
|
785
|
-
```
|
|
786
|
-
|
|
787
|
-
### Performance Monitoring
|
|
788
|
-
|
|
789
|
-
```typescript
|
|
790
|
-
import { PerformanceMonitoringMiddleware } from '@vytches/ddd-messaging';
|
|
791
|
-
|
|
792
|
-
const performanceMiddleware = new PerformanceMonitoringMiddleware({
|
|
793
|
-
enableMetrics: true,
|
|
794
|
-
slowExecutionThreshold: 5000,
|
|
795
|
-
memoryUsageTracking: true,
|
|
796
|
-
customMetrics: {
|
|
797
|
-
trackCorrelationCounts: true,
|
|
798
|
-
trackEventProcessingTimes: true,
|
|
799
|
-
trackCompensationRates: true,
|
|
800
|
-
},
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
// Get performance metrics
|
|
804
|
-
const metrics = performanceMiddleware.getMetrics();
|
|
805
|
-
console.log('Performance Metrics:', {
|
|
806
|
-
totalExecutions: metrics.totalExecutions,
|
|
807
|
-
averageExecutionTime: metrics.averageExecutionTime,
|
|
808
|
-
slowExecutions: metrics.slowExecutions,
|
|
809
|
-
memoryUsage: metrics.memoryUsage,
|
|
810
|
-
errorRate: metrics.errorRate,
|
|
811
|
-
});
|
|
812
|
-
```
|
|
813
|
-
|
|
814
|
-
## 🔗 Integration Patterns
|
|
815
|
-
|
|
816
|
-
### Event-Driven Integration
|
|
817
|
-
|
|
818
|
-
```typescript
|
|
819
|
-
import { EventBusOutboxHandler } from '@vytches/ddd-messaging';
|
|
820
|
-
import { UnifiedEventBus } from '@vytches/ddd-events';
|
|
821
|
-
|
|
822
|
-
// Integrate outbox with event bus
|
|
823
|
-
const eventBus = new UnifiedEventBus();
|
|
824
|
-
const outboxHandler = new EventBusOutboxHandler(eventBus);
|
|
825
|
-
|
|
826
|
-
// Register handler for specific message types
|
|
827
|
-
outboxHandler.registerHandler('DomainEvent', async message => {
|
|
828
|
-
const domainEvent = message.payload;
|
|
829
|
-
await eventBus.publish(domainEvent);
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
// Process outbox messages through event bus
|
|
833
|
-
const processor = new OutboxProcessor(outboxService, {
|
|
834
|
-
messageHandler: outboxHandler,
|
|
835
|
-
});
|
|
836
|
-
```
|
|
837
|
-
|
|
838
|
-
### Repository Integration
|
|
839
|
-
|
|
840
|
-
```typescript
|
|
841
|
-
import { OutboxService } from '@vytches/ddd-messaging';
|
|
842
|
-
import { IBaseRepository } from '@vytches/ddd-repositories';
|
|
843
|
-
|
|
844
|
-
class OrderRepository extends IBaseRepository<OrderAggregate> {
|
|
845
|
-
constructor(
|
|
846
|
-
private outboxService: OutboxService,
|
|
847
|
-
eventDispatcher: IEventDispatcher
|
|
848
|
-
) {
|
|
849
|
-
super(eventDispatcher);
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
async save(order: OrderAggregate): Promise<void> {
|
|
853
|
-
// Save aggregate
|
|
854
|
-
await super.save(order);
|
|
855
|
-
|
|
856
|
-
// Store events in outbox for reliable delivery
|
|
857
|
-
const events = order.getUncommittedEvents();
|
|
858
|
-
|
|
859
|
-
for (const event of events) {
|
|
860
|
-
const message = OutboxMessageFactory.fromDomainEvent(event);
|
|
861
|
-
await this.outboxService.storeMessage(message);
|
|
862
|
-
}
|
|
863
136
|
}
|
|
864
137
|
}
|
|
865
138
|
```
|
|
866
139
|
|
|
867
|
-
|
|
140
|
+
## Package boundaries
|
|
868
141
|
|
|
869
|
-
|
|
870
|
-
import { CommandHandler, QueryHandler } from '@vytches/ddd-cqrs';
|
|
871
|
-
import { SagaOrchestrator } from '@vytches/ddd-messaging';
|
|
872
|
-
|
|
873
|
-
@CommandHandler(ProcessOrderCommand)
|
|
874
|
-
class ProcessOrderHandler {
|
|
875
|
-
constructor(private sagaOrchestrator: SagaOrchestrator) {}
|
|
876
|
-
|
|
877
|
-
async execute(command: ProcessOrderCommand): Promise<void> {
|
|
878
|
-
// Create order processing saga
|
|
879
|
-
const event = new OrderCreatedEvent(command.orderData);
|
|
880
|
-
const context = {
|
|
881
|
-
correlationId: command.correlationId,
|
|
882
|
-
userId: command.userId,
|
|
883
|
-
metadata: { source: 'OrderService' },
|
|
884
|
-
timestamp: new Date(),
|
|
885
|
-
};
|
|
886
|
-
|
|
887
|
-
await this.sagaOrchestrator.processEvent(event, context);
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
```
|
|
891
|
-
|
|
892
|
-
## 🧪 Testing
|
|
893
|
-
|
|
894
|
-
### Saga Testing
|
|
895
|
-
|
|
896
|
-
```typescript
|
|
897
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
898
|
-
import { safeRun } from '@vytches/ddd-utils';
|
|
899
|
-
import { OrderProcessingSaga } from '../src/order-processing-saga';
|
|
900
|
-
import { SagaStatus } from '@vytches/ddd-messaging';
|
|
901
|
-
|
|
902
|
-
describe('OrderProcessingSaga', () => {
|
|
903
|
-
let saga: OrderProcessingSaga;
|
|
904
|
-
let context: ISagaExecutionContext;
|
|
905
|
-
|
|
906
|
-
beforeEach(() => {
|
|
907
|
-
saga = new OrderProcessingSaga('saga-123', 'correlation-456');
|
|
908
|
-
context = {
|
|
909
|
-
correlationId: 'correlation-456',
|
|
910
|
-
userId: 'user-123',
|
|
911
|
-
metadata: {},
|
|
912
|
-
timestamp: new Date(),
|
|
913
|
-
};
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
describe('handleOrderCreated', () => {
|
|
917
|
-
it('should transition to payment processing', async () => {
|
|
918
|
-
const event = createOrderCreatedEvent();
|
|
919
|
-
|
|
920
|
-
const [error, result] = await safeRun(
|
|
921
|
-
async () => await saga.handleEvent(event, context)
|
|
922
|
-
);
|
|
923
|
-
|
|
924
|
-
expect(error).toBeUndefined();
|
|
925
|
-
expect(result.success).toBe(true);
|
|
926
|
-
expect(saga.state.currentStep).toBe('ProcessPayment');
|
|
927
|
-
expect(result.commands).toHaveLength(1);
|
|
928
|
-
expect(result.commands[0].type).toBe('ProcessPayment');
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
it('should handle invalid event gracefully', async () => {
|
|
932
|
-
const invalidEvent = createInvalidEvent();
|
|
933
|
-
|
|
934
|
-
const [error] = await safeRun(
|
|
935
|
-
async () => await saga.handleEvent(invalidEvent, context)
|
|
936
|
-
);
|
|
937
|
-
|
|
938
|
-
expect(error).toBeInstanceOf(SagaEventProcessingError);
|
|
939
|
-
expect(error?.message).toContain('Cannot handle event');
|
|
940
|
-
});
|
|
941
|
-
});
|
|
942
|
-
|
|
943
|
-
describe('compensation', () => {
|
|
944
|
-
it('should execute compensation for completed steps', async () => {
|
|
945
|
-
// Setup saga with completed payment step
|
|
946
|
-
saga.updateState({
|
|
947
|
-
currentStep: 'ReserveInventory',
|
|
948
|
-
stepData: { paymentId: 'payment-123' },
|
|
949
|
-
compensationData: {
|
|
950
|
-
ProcessPayment: { paymentId: 'payment-123' },
|
|
951
|
-
},
|
|
952
|
-
});
|
|
953
|
-
|
|
954
|
-
const [error, result] = await safeRun(
|
|
955
|
-
async () => await saga.compensate('ProcessPayment', context)
|
|
956
|
-
);
|
|
957
|
-
|
|
958
|
-
expect(error).toBeUndefined();
|
|
959
|
-
expect(result.success).toBe(true);
|
|
960
|
-
});
|
|
961
|
-
});
|
|
962
|
-
});
|
|
963
|
-
```
|
|
964
|
-
|
|
965
|
-
### Outbox Testing
|
|
966
|
-
|
|
967
|
-
```typescript
|
|
968
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
969
|
-
import { safeRun } from '@vytches/ddd-utils';
|
|
970
|
-
import {
|
|
971
|
-
OutboxService,
|
|
972
|
-
OutboxMessageFactory,
|
|
973
|
-
MessageStatus,
|
|
974
|
-
} from '@vytches/ddd-messaging';
|
|
975
|
-
|
|
976
|
-
describe('OutboxService', () => {
|
|
977
|
-
let service: OutboxService;
|
|
978
|
-
let factory: OutboxMessageFactory;
|
|
979
|
-
|
|
980
|
-
beforeEach(() => {
|
|
981
|
-
service = new OutboxService();
|
|
982
|
-
factory = new OutboxMessageFactory();
|
|
983
|
-
});
|
|
984
|
-
|
|
985
|
-
describe('storeMessage', () => {
|
|
986
|
-
it('should store message successfully', async () => {
|
|
987
|
-
const message = factory.createMessage({
|
|
988
|
-
messageType: 'TestEvent',
|
|
989
|
-
payload: { data: 'test' },
|
|
990
|
-
});
|
|
991
|
-
|
|
992
|
-
const [error] = await safeRun(
|
|
993
|
-
async () => await service.storeMessage(message)
|
|
994
|
-
);
|
|
995
|
-
|
|
996
|
-
expect(error).toBeUndefined();
|
|
997
|
-
|
|
998
|
-
const stored = await service.getMessageById(message.id);
|
|
999
|
-
expect(stored).toBeDefined();
|
|
1000
|
-
expect(stored?.status).toBe(MessageStatus.PENDING);
|
|
1001
|
-
});
|
|
1002
|
-
|
|
1003
|
-
it('should handle duplicate message IDs', async () => {
|
|
1004
|
-
const message = factory.createMessage({
|
|
1005
|
-
messageType: 'TestEvent',
|
|
1006
|
-
payload: { data: 'test' },
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
await service.storeMessage(message);
|
|
1010
|
-
|
|
1011
|
-
const [duplicateError] = await safeRun(
|
|
1012
|
-
async () => await service.storeMessage(message)
|
|
1013
|
-
);
|
|
1014
|
-
|
|
1015
|
-
expect(duplicateError).toBeInstanceOf(OutboxError);
|
|
1016
|
-
expect(duplicateError?.message).toContain('already exists');
|
|
1017
|
-
});
|
|
1018
|
-
});
|
|
1019
|
-
|
|
1020
|
-
describe('processMessages', () => {
|
|
1021
|
-
it('should process pending messages', async () => {
|
|
1022
|
-
const messages = [
|
|
1023
|
-
factory.createMessage({
|
|
1024
|
-
messageType: 'Event1',
|
|
1025
|
-
payload: { data: '1' },
|
|
1026
|
-
}),
|
|
1027
|
-
factory.createMessage({
|
|
1028
|
-
messageType: 'Event2',
|
|
1029
|
-
payload: { data: '2' },
|
|
1030
|
-
}),
|
|
1031
|
-
];
|
|
1032
|
-
|
|
1033
|
-
for (const message of messages) {
|
|
1034
|
-
await service.storeMessage(message);
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
const processed = await service.processPendingMessages();
|
|
1038
|
-
expect(processed).toBe(2);
|
|
1039
|
-
});
|
|
1040
|
-
});
|
|
1041
|
-
});
|
|
1042
|
-
```
|
|
1043
|
-
|
|
1044
|
-
## 🎯 Best Practices
|
|
1045
|
-
|
|
1046
|
-
### Saga Design
|
|
1047
|
-
|
|
1048
|
-
1. **Keep Sagas Focused**: Each saga should handle a single business process
|
|
1049
|
-
2. **Design for Compensation**: Always plan compensation logic during design
|
|
1050
|
-
3. **Use Correlation IDs**: Properly correlate events and commands
|
|
1051
|
-
4. **Handle Timeouts**: Set appropriate timeouts for long-running processes
|
|
1052
|
-
5. **Monitor Saga Health**: Implement comprehensive monitoring and alerting
|
|
1053
|
-
|
|
1054
|
-
### Message Design
|
|
1055
|
-
|
|
1056
|
-
1. **Idempotent Operations**: Design message handlers to be idempotent
|
|
1057
|
-
2. **Small Message Payloads**: Keep message payloads small and focused
|
|
1058
|
-
3. **Structured Metadata**: Use consistent metadata structure
|
|
1059
|
-
4. **Priority Classification**: Properly classify message priorities
|
|
1060
|
-
5. **Error Context**: Include sufficient context for error handling
|
|
1061
|
-
|
|
1062
|
-
### Performance Optimization
|
|
1063
|
-
|
|
1064
|
-
1. **Batch Processing**: Process messages in batches when possible
|
|
1065
|
-
2. **Connection Pooling**: Use connection pooling for database operations
|
|
1066
|
-
3. **Async Processing**: Leverage async processing for better throughput
|
|
1067
|
-
4. **Resource Management**: Properly manage resources and connections
|
|
1068
|
-
5. **Monitoring**: Continuously monitor performance metrics
|
|
1069
|
-
|
|
1070
|
-
### Error Handling
|
|
1071
|
-
|
|
1072
|
-
1. **Graceful Degradation**: Design for graceful failure handling
|
|
1073
|
-
2. **Retry Logic**: Implement appropriate retry strategies
|
|
1074
|
-
3. **Dead Letter Queues**: Use dead letter queues for failed messages
|
|
1075
|
-
4. **Compensation Patterns**: Implement proper compensation patterns
|
|
1076
|
-
5. **Alerting**: Set up proper alerting for critical failures
|
|
1077
|
-
|
|
1078
|
-
## 🤝 Contributing
|
|
1079
|
-
|
|
1080
|
-
We welcome contributions! Please see our
|
|
1081
|
-
[Contributing Guide](../../CONTRIBUTING.md) for details.
|
|
1082
|
-
|
|
1083
|
-
### Development Setup
|
|
1084
|
-
|
|
1085
|
-
```bash
|
|
1086
|
-
# Clone repository
|
|
1087
|
-
git clone https://github.com/vytches/ddd.git
|
|
1088
|
-
cd ddd
|
|
1089
|
-
|
|
1090
|
-
# Install dependencies
|
|
1091
|
-
pnpm install
|
|
1092
|
-
|
|
1093
|
-
# Run tests
|
|
1094
|
-
pnpm test
|
|
1095
|
-
|
|
1096
|
-
# Build package
|
|
1097
|
-
pnpm build
|
|
1098
|
-
|
|
1099
|
-
# Run messaging-specific tests
|
|
1100
|
-
pnpm test:packages:messaging
|
|
1101
|
-
```
|
|
142
|
+
`@vytches/ddd-messaging` depends on:
|
|
1102
143
|
|
|
1103
|
-
|
|
144
|
+
- `@vytches/ddd-contracts` — `IDomainEvent`, `IEventBus`
|
|
145
|
+
- `@vytches/ddd-logging` — internal structured logging
|
|
1104
146
|
|
|
1105
|
-
|
|
1106
|
-
[LICENSE](../../LICENSE) file for details.
|
|
147
|
+
## Note on Sagas
|
|
1107
148
|
|
|
1108
|
-
|
|
149
|
+
This package has **no Saga support**. For long-running process orchestration,
|
|
150
|
+
use a dedicated library (e.g. Temporal, Conductor).
|
|
1109
151
|
|
|
1110
|
-
|
|
152
|
+
## License
|
|
1111
153
|
|
|
1112
|
-
|
|
1113
|
-
[main documentation](../../README.md).
|
|
154
|
+
MIT
|