@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 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
  [![npm version](https://badge.fury.io/js/%40vytches%2Fddd-messaging.svg)](https://badge.fury.io/js/%40vytches%2Fddd-messaging)
14
4
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue?logo=typescript)](https://www.typescriptlang.org/)
15
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
16
6
 
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
7
+ > **Transactional Outbox pattern for reliable domain event delivery**
26
8
 
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)
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
- ## 🚀 Installation
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
- ### 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
18
+ ## What's included
128
19
 
129
- Sagas orchestrate long-running business processes with compensation support:
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
- ```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
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
- // 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
- }
49
+ // Implement IOutboxRepository for your storage (e.g. PostgreSQL)
50
+ class PostgresOutboxRepository extends IOutboxRepository {
51
+ async save(message) {
52
+ /* ... */
286
53
  }
287
-
288
- canHandle(event: IDomainEvent): boolean {
289
- return ['OrderCreated', 'PaymentProcessed', 'InventoryReserved'].includes(
290
- event.eventType
291
- );
54
+ async findPending(limit) {
55
+ /* ... */
292
56
  }
293
-
294
- private async refundPayment(
295
- context: ISagaExecutionContext
296
- ): Promise<ISagaActionResult> {
297
- // Implement refund logic
298
- return { success: true };
57
+ async markProcessed(id) {
58
+ /* ... */
299
59
  }
300
-
301
- private async releaseInventory(
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
- ### Saga Orchestration
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
- // Setup saga infrastructure
319
- const sagaRepository = new InMemorySagaRepository();
320
- const orchestrator = new SagaOrchestrator(sagaRepository, {
321
- maxConcurrentExecutions: 50,
322
- enableMetrics: true,
323
- enableAutoRetry: true,
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
- // 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
-
77
+ // Store a message (call this inside your aggregate save transaction)
362
78
  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' },
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
- ## 🎭 Saga Pattern
439
-
440
- ### Saga Definition
90
+ ## Custom message handler
441
91
 
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
92
+ Implement `IOutboxMessageHandler` to route messages any way you like:
488
93
 
489
94
  ```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
- }
95
+ import { IOutboxMessageHandler, IOutboxMessage } from '@vytches/ddd-messaging';
519
96
 
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
- },
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
- ### 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
107
+ ## Implement your repository
572
108
 
573
- ### Built-in Middleware
109
+ Extend `IOutboxRepository` and provide persistence:
574
110
 
575
111
  ```typescript
576
112
  import {
577
- RetryMiddleware,
578
- CircuitBreakerMiddleware,
579
- PerformanceMonitoringMiddleware,
580
- SecurityMiddleware,
113
+ IOutboxRepository,
114
+ IOutboxMessage,
115
+ MessageStatus,
581
116
  } from '@vytches/ddd-messaging';
582
117
 
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
- }
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
- throw error;
688
- }
123
+ async findPendingMessages(limit: number): Promise<IOutboxMessage[]> {
124
+ return db.outbox.findWhere({ status: MessageStatus.PENDING }, limit);
689
125
  }
690
126
 
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 };
127
+ async markAsProcessed(id: string): Promise<void> {
128
+ await db.outbox.update(id, { status: MessageStatus.PROCESSED });
713
129
  }
714
- }
715
- ```
716
130
 
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,
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
- ### CQRS Integration
140
+ ## Package boundaries
868
141
 
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
- ```
142
+ `@vytches/ddd-messaging` depends on:
1102
143
 
1103
- ## 📄 License
144
+ - `@vytches/ddd-contracts` — `IDomainEvent`, `IEventBus`
145
+ - `@vytches/ddd-logging` — internal structured logging
1104
146
 
1105
- This project is licensed under the MIT License - see the
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
- **Part of the VytchesDDD Enterprise Suite**
152
+ ## License
1111
153
 
1112
- For more information about the complete VytchesDDD ecosystem, visit our
1113
- [main documentation](../../README.md).
154
+ MIT