@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/README.md
ADDED
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
# @vytches/ddd-messaging
|
|
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
|
+
[](https://badge.fury.io/js/%40vytches%2Fddd-messaging)
|
|
14
|
+
[](https://www.typescriptlang.org/)
|
|
15
|
+
[](https://opensource.org/licenses/MIT)
|
|
16
|
+
|
|
17
|
+
> **Enterprise-grade messaging patterns with outbox pattern and saga
|
|
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
|
|
26
|
+
|
|
27
|
+
- [Installation](#installation)
|
|
28
|
+
- [Key Features](#key-features)
|
|
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)
|
|
41
|
+
|
|
42
|
+
## 🚀 Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# npm
|
|
46
|
+
npm install @vytches/ddd-messaging
|
|
47
|
+
|
|
48
|
+
# yarn
|
|
49
|
+
yarn add @vytches/ddd-messaging
|
|
50
|
+
|
|
51
|
+
# pnpm
|
|
52
|
+
pnpm add @vytches/ddd-messaging
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Peer Dependencies
|
|
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
|
|
128
|
+
|
|
129
|
+
Sagas orchestrate long-running business processes with compensation support:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
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
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import {
|
|
171
|
+
OutboxService,
|
|
172
|
+
OutboxProcessor,
|
|
173
|
+
OutboxMessageFactory,
|
|
174
|
+
MessagePriority,
|
|
175
|
+
} from '@vytches/ddd-messaging';
|
|
176
|
+
|
|
177
|
+
// Create outbox service
|
|
178
|
+
const outboxService = new OutboxService({
|
|
179
|
+
maxRetries: 3,
|
|
180
|
+
retryDelay: 1000,
|
|
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
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
canHandle(event: IDomainEvent): boolean {
|
|
289
|
+
return ['OrderCreated', 'PaymentProcessed', 'InventoryReserved'].includes(
|
|
290
|
+
event.eventType
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private async refundPayment(
|
|
295
|
+
context: ISagaExecutionContext
|
|
296
|
+
): Promise<ISagaActionResult> {
|
|
297
|
+
// Implement refund logic
|
|
298
|
+
return { success: true };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private async releaseInventory(
|
|
302
|
+
context: ISagaExecutionContext
|
|
303
|
+
): Promise<ISagaActionResult> {
|
|
304
|
+
// Implement inventory release logic
|
|
305
|
+
return { success: true };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Saga Orchestration
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import {
|
|
314
|
+
SagaOrchestrator,
|
|
315
|
+
InMemorySagaRepository,
|
|
316
|
+
} from '@vytches/ddd-messaging';
|
|
317
|
+
|
|
318
|
+
// Setup saga infrastructure
|
|
319
|
+
const sagaRepository = new InMemorySagaRepository();
|
|
320
|
+
const orchestrator = new SagaOrchestrator(sagaRepository, {
|
|
321
|
+
maxConcurrentExecutions: 50,
|
|
322
|
+
enableMetrics: true,
|
|
323
|
+
enableAutoRetry: true,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Register saga definition
|
|
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
|
+
|
|
362
|
+
const factory = new OutboxMessageFactory();
|
|
363
|
+
|
|
364
|
+
// Create immediate message
|
|
365
|
+
const message = factory.createMessage({
|
|
366
|
+
messageType: 'UserRegistered',
|
|
367
|
+
payload: { userId: '123', email: 'user@example.com' },
|
|
368
|
+
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
|
+
});
|
|
434
|
+
|
|
435
|
+
await processor.start();
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## 🎭 Saga Pattern
|
|
439
|
+
|
|
440
|
+
### Saga Definition
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
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
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
class OrderProcessingSaga extends BaseSaga {
|
|
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
|
+
}
|
|
519
|
+
|
|
520
|
+
protected markFailed(error: string, step: string): void {
|
|
521
|
+
this.updateState({
|
|
522
|
+
status: SagaStatus.FAILED,
|
|
523
|
+
error: {
|
|
524
|
+
message: error,
|
|
525
|
+
step,
|
|
526
|
+
timestamp: new Date(),
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Saga Repository
|
|
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
|
|
572
|
+
|
|
573
|
+
### Built-in Middleware
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
import {
|
|
577
|
+
RetryMiddleware,
|
|
578
|
+
CircuitBreakerMiddleware,
|
|
579
|
+
PerformanceMonitoringMiddleware,
|
|
580
|
+
SecurityMiddleware,
|
|
581
|
+
} from '@vytches/ddd-messaging';
|
|
582
|
+
|
|
583
|
+
// Retry middleware
|
|
584
|
+
const retryMiddleware = new RetryMiddleware({
|
|
585
|
+
maxAttempts: 3,
|
|
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
|
+
}
|
|
655
|
+
}
|
|
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
|
+
|
|
687
|
+
throw error;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
private async startCompensation(
|
|
692
|
+
context: ISagaExecutionContext
|
|
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 };
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
### Outbox Error Handling
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
import { OutboxProcessor } from '@vytches/ddd-messaging';
|
|
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,
|
|
743
|
+
});
|
|
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
|
+
}
|
|
864
|
+
}
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### CQRS Integration
|
|
868
|
+
|
|
869
|
+
```typescript
|
|
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
|
+
```
|
|
1102
|
+
|
|
1103
|
+
## 📄 License
|
|
1104
|
+
|
|
1105
|
+
This project is licensed under the MIT License - see the
|
|
1106
|
+
[LICENSE](../../LICENSE) file for details.
|
|
1107
|
+
|
|
1108
|
+
---
|
|
1109
|
+
|
|
1110
|
+
**Part of the VytchesDDD Enterprise Suite**
|
|
1111
|
+
|
|
1112
|
+
For more information about the complete VytchesDDD ecosystem, visit our
|
|
1113
|
+
[main documentation](../../README.md).
|