@vytches/ddd-messaging 0.29.2 → 0.30.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/LLMGUIDE.md +137 -36
- package/README.md +86 -71
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +61 -28
- package/dist/outbox/outbox-interfaces.d.ts +19 -0
- package/dist/outbox/outbox-processor.d.ts +19 -0
- package/dist/outbox/outbox-repository.interface.d.ts +13 -8
- package/package.json +2 -2
package/LLMGUIDE.md
CHANGED
|
@@ -14,71 +14,88 @@ True to the library's philosophy: **no database adapter ships with the package**
|
|
|
14
14
|
|
|
15
15
|
```typescript
|
|
16
16
|
import {
|
|
17
|
-
OutboxService,
|
|
18
17
|
OutboxProcessor,
|
|
19
18
|
OutboxMessageFactory,
|
|
20
19
|
EventBusOutboxHandler,
|
|
21
|
-
MessageStatus,
|
|
22
20
|
MessagePriority,
|
|
23
21
|
type IOutboxRepository,
|
|
22
|
+
type IOutboxMessageHandler,
|
|
24
23
|
} from '@vytches/ddd-messaging';
|
|
25
24
|
|
|
26
|
-
// 1. Implement the repository for your storage
|
|
27
|
-
class PgOutboxRepository
|
|
28
|
-
async
|
|
25
|
+
// 1. Implement the repository for your storage
|
|
26
|
+
class PgOutboxRepository extends IOutboxRepository {
|
|
27
|
+
async saveMessage(message) {
|
|
29
28
|
/* INSERT INTO outbox ... */
|
|
30
29
|
}
|
|
31
|
-
async
|
|
32
|
-
/*
|
|
30
|
+
async saveBatch(messages) {
|
|
31
|
+
/* INSERT INTO outbox ... */
|
|
32
|
+
}
|
|
33
|
+
async getUnprocessedMessages(limit, priorityOrder, messageTypes) {
|
|
34
|
+
// IMPORTANT: order by priorityOrder array index, NOT alphabetically
|
|
35
|
+
// See "Priority ordering in repository implementations" below
|
|
36
|
+
}
|
|
37
|
+
async getById(id) {
|
|
38
|
+
/* SELECT ... */
|
|
33
39
|
}
|
|
34
|
-
async
|
|
40
|
+
async updateStatus(id, status, error?) {
|
|
35
41
|
/* UPDATE ... */
|
|
36
42
|
}
|
|
37
|
-
async
|
|
43
|
+
async updateStatusBatch(ids, status) {
|
|
38
44
|
/* UPDATE ... */
|
|
39
45
|
}
|
|
40
|
-
|
|
46
|
+
async incrementAttempt(id) {
|
|
47
|
+
/* UPDATE ... RETURNING attempts */
|
|
48
|
+
}
|
|
49
|
+
async deleteByStatusAndAge(olderThan, status) {
|
|
50
|
+
/* DELETE ... */
|
|
51
|
+
}
|
|
52
|
+
async scheduleMessage(message, processAfter) {
|
|
53
|
+
/* INSERT ... */
|
|
54
|
+
}
|
|
41
55
|
}
|
|
42
56
|
|
|
43
|
-
// 2. Wire up the
|
|
57
|
+
// 2. Wire up the processor
|
|
44
58
|
const repo = new PgOutboxRepository(db);
|
|
45
|
-
const factory = new OutboxMessageFactory();
|
|
46
59
|
const handler = new EventBusOutboxHandler(integrationBus);
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
|
|
61
|
+
const processor = new OutboxProcessor(repo, {
|
|
49
62
|
batchSize: 50,
|
|
63
|
+
processingInterval: 1_000,
|
|
64
|
+
retryBackoff: { initial: 1_000, multiplier: 2, maxDelay: 300_000 },
|
|
50
65
|
});
|
|
66
|
+
processor.registerHandler('order.placed', handler);
|
|
67
|
+
processor.start();
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// 3. In a transaction with your aggregate save
|
|
69
|
+
// 3. Enqueue in the same transaction as your aggregate save
|
|
70
|
+
const factory = new OutboxMessageFactory();
|
|
55
71
|
await db.transaction(async tx => {
|
|
56
72
|
await orderRepo.save(order, tx);
|
|
57
|
-
await
|
|
58
|
-
integrationEvent,
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
await repo.saveMessage(
|
|
74
|
+
factory.create(integrationEvent, {
|
|
75
|
+
priority: MessagePriority.HIGH,
|
|
76
|
+
})
|
|
61
77
|
);
|
|
62
78
|
});
|
|
63
79
|
```
|
|
64
80
|
|
|
65
81
|
## Key API
|
|
66
82
|
|
|
67
|
-
| Export | Kind | Description
|
|
68
|
-
| ------------------------ | --------- |
|
|
69
|
-
| `IOutboxMessage` | interface | The shape of a stored outbox row
|
|
70
|
-
| `IOutboxRepository` | interface | **You implement this**: persistence for outbox messages
|
|
71
|
-
| `IOutboxMessageHandler` | interface | How to dispatch a stored message (default: publish to event bus)
|
|
72
|
-
| `OutboxMessageOptions` | type | `{ priority, dedupKey, headers }` per-message overrides
|
|
73
|
-
| `OutboxMiddleware` | type | Hook for cross-cutting concerns (tracing, encryption)
|
|
74
|
-
| `OutboxProcessorOptions` | type | `{ pollIntervalMs, batchSize, maxRetries, ... }`
|
|
75
|
-
| `OutboxServiceOptions` | type | Top-level config bag
|
|
76
|
-
| `MessageStatus` | enum | `Pending`, `Dispatched`, `Failed`, `DeadLettered`
|
|
77
|
-
| `MessagePriority` | enum | `Low`, `Normal`, `High`, `Critical`
|
|
78
|
-
| `OutboxMessageFactory` | class | Build `IOutboxMessage` from an integration event
|
|
79
|
-
| `OutboxProcessor` | class | Polls repo, dispatches via handler, updates status
|
|
80
|
-
| `EventBusOutboxHandler` | class | Default handler — publishes via an `IIntegrationEventBus`
|
|
81
|
-
| `OutboxService` | class | Facade combining factory + processor for the application
|
|
83
|
+
| Export | Kind | Description |
|
|
84
|
+
| ------------------------ | --------- | ------------------------------------------------------------------------------- |
|
|
85
|
+
| `IOutboxMessage` | interface | The shape of a stored outbox row |
|
|
86
|
+
| `IOutboxRepository` | interface | **You implement this**: persistence for outbox messages |
|
|
87
|
+
| `IOutboxMessageHandler` | interface | How to dispatch a stored message (default: publish to event bus) |
|
|
88
|
+
| `OutboxMessageOptions` | type | `{ priority, dedupKey, headers }` per-message overrides |
|
|
89
|
+
| `OutboxMiddleware` | type | Hook for cross-cutting concerns (tracing, encryption) |
|
|
90
|
+
| `OutboxProcessorOptions` | type | `{ pollIntervalMs, batchSize, maxRetries, ... }` |
|
|
91
|
+
| `OutboxServiceOptions` | type | Top-level config bag |
|
|
92
|
+
| `MessageStatus` | enum | `Pending`, `Dispatched`, `Failed`, `DeadLettered` |
|
|
93
|
+
| `MessagePriority` | enum | `Low`, `Normal`, `High`, `Critical` |
|
|
94
|
+
| `OutboxMessageFactory` | class | Build `IOutboxMessage` from an integration event |
|
|
95
|
+
| `OutboxProcessor` | class | Polls repo, dispatches via handler, updates status |
|
|
96
|
+
| `EventBusOutboxHandler` | class | Default handler — publishes via an `IIntegrationEventBus` |
|
|
97
|
+
| `OutboxService` | class | Facade combining factory + processor for the application |
|
|
98
|
+
| `comparePriority` | function | Sorts `MessagePriority` values by a given order array (safe for partial arrays) |
|
|
82
99
|
|
|
83
100
|
## Patterns
|
|
84
101
|
|
|
@@ -142,6 +159,90 @@ The library does **not** ship broker-specific adapters — that would force
|
|
|
142
159
|
adopters to bring in BullMQ/Kafka/etc. as transitive dependencies. Write the
|
|
143
160
|
~10-line adapter yourself; the library guarantees the interface contract.
|
|
144
161
|
|
|
162
|
+
### Fan-out: one default handler for N event types
|
|
163
|
+
|
|
164
|
+
When you want a single handler to process all (or most) outbox message types —
|
|
165
|
+
for example, routing every integration event to a BullMQ per-context queue — use
|
|
166
|
+
`registerDefaultHandler` instead of registering the same handler 50 times.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
class FanOutHandler implements IOutboxMessageHandler {
|
|
170
|
+
constructor(private readonly queues: Map<string, Queue>) {}
|
|
171
|
+
|
|
172
|
+
async handle(message: IOutboxMessage): Promise<void> {
|
|
173
|
+
// RECOMMENDED: validate against a known-types set so new event types
|
|
174
|
+
// added to the domain are caught early rather than silently dispatched.
|
|
175
|
+
if (!KNOWN_EVENT_TYPES.has(message.messageType)) {
|
|
176
|
+
throw new Error(`Unknown event type: ${message.messageType}`);
|
|
177
|
+
}
|
|
178
|
+
const queue =
|
|
179
|
+
this.queues.get(message.messageType) ?? this.queues.get('default');
|
|
180
|
+
await queue?.add(message.messageType, message.payload, {
|
|
181
|
+
jobId: message.id,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const processor = new OutboxProcessor(repo, { batchSize: 200 });
|
|
187
|
+
processor.registerDefaultHandler(new FanOutHandler(queues));
|
|
188
|
+
processor.start();
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Security warning:** `registerDefaultHandler` removes the implicit type
|
|
192
|
+
allowlist that exists when only `registerHandler` is used. Before VP-008, an
|
|
193
|
+
unknown `messageType` would always throw — an implicit safety net. After
|
|
194
|
+
registering a default handler, ALL future message types (including ones added
|
|
195
|
+
later by mistake or injected via a compromised storage layer) will be dispatched
|
|
196
|
+
automatically. Always validate `message.messageType` against an explicit
|
|
197
|
+
known-types set inside the default handler.
|
|
198
|
+
|
|
199
|
+
**Interaction with `messageTypes` option:** if
|
|
200
|
+
`OutboxProcessorOptions.messageTypes` is set, the default handler will only see
|
|
201
|
+
messages whose type passes that filter. To catch every type, leave
|
|
202
|
+
`messageTypes` unset.
|
|
203
|
+
|
|
204
|
+
**PII and new event types:** when a new domain event type carrying PII is added,
|
|
205
|
+
review access controls on all downstream queues — the default handler will
|
|
206
|
+
dispatch it to all configured destinations without any per-type gating.
|
|
207
|
+
|
|
208
|
+
### Priority ordering in repository implementations
|
|
209
|
+
|
|
210
|
+
`IOutboxRepository.getUnprocessedMessages` receives a `priorityOrder` array such
|
|
211
|
+
as `[CRITICAL, HIGH, NORMAL, LOW]`. Repository implementations **MUST** order by
|
|
212
|
+
the array index, not by raw column value. String comparison on the `priority`
|
|
213
|
+
column (`ORDER BY priority DESC`) silently inverts the intent:
|
|
214
|
+
`'normal' > 'low' > 'high' > 'critical'` alphabetically.
|
|
215
|
+
|
|
216
|
+
Correct SQL using a `CASE` expression:
|
|
217
|
+
|
|
218
|
+
```sql
|
|
219
|
+
ORDER BY
|
|
220
|
+
CASE priority
|
|
221
|
+
WHEN 'critical' THEN 0
|
|
222
|
+
WHEN 'high' THEN 1
|
|
223
|
+
WHEN 'normal' THEN 2
|
|
224
|
+
WHEN 'low' THEN 3
|
|
225
|
+
ELSE 4 -- unknown priorities sort last
|
|
226
|
+
END
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Or use the exported `comparePriority` helper in in-process sorting:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import { comparePriority } from '@vytches/ddd-messaging';
|
|
233
|
+
|
|
234
|
+
// Sort an in-memory array highest-priority-first:
|
|
235
|
+
messages.sort((a, b) =>
|
|
236
|
+
comparePriority(
|
|
237
|
+
a.priority ?? MessagePriority.NORMAL,
|
|
238
|
+
b.priority ?? MessagePriority.NORMAL
|
|
239
|
+
)
|
|
240
|
+
);
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`comparePriority` is safe with partial or custom order arrays — values absent
|
|
244
|
+
from the array sort last rather than accidentally first.
|
|
245
|
+
|
|
145
246
|
## Anti-Patterns
|
|
146
247
|
|
|
147
248
|
- **Do not enqueue outbox messages outside the aggregate save transaction** — if
|
package/README.md
CHANGED
|
@@ -17,74 +17,101 @@ pnpm add @vytches/ddd-messaging
|
|
|
17
17
|
|
|
18
18
|
## What's included
|
|
19
19
|
|
|
20
|
-
| Export | Kind | Description
|
|
21
|
-
| ------------------------ | -------------- |
|
|
22
|
-
| `MessageStatus` | enum | `PENDING \| PROCESSING \| PROCESSED \| FAILED`
|
|
23
|
-
| `MessagePriority` | enum | `
|
|
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
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
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 via registered handlers |
|
|
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, backoff, hooks…) |
|
|
33
|
+
| `OutboxProcessorHooks` | interface | Observability callbacks: batch complete, message failed, permanent fail |
|
|
34
|
+
| `OutboxServiceOptions` | interface | Service configuration |
|
|
35
|
+
| `OutboxMessageOptions` | interface | Per-message options (priority, processAfter…) |
|
|
36
|
+
| `RetryBackoffConfig` | interface | Exponential backoff configuration for the processor |
|
|
37
|
+
| `comparePriority` | function | Compares two `MessagePriority` values by a given order array |
|
|
36
38
|
|
|
37
39
|
## Quick start
|
|
38
40
|
|
|
39
41
|
```typescript
|
|
40
42
|
import {
|
|
41
|
-
OutboxService,
|
|
42
43
|
OutboxProcessor,
|
|
43
44
|
OutboxMessageFactory,
|
|
44
45
|
EventBusOutboxHandler,
|
|
45
46
|
MessagePriority,
|
|
47
|
+
type IOutboxRepository,
|
|
46
48
|
} from '@vytches/ddd-messaging';
|
|
47
|
-
import { UnifiedEventBus } from '@vytches/ddd-events';
|
|
48
49
|
|
|
49
|
-
// Implement IOutboxRepository for your storage (e.g. PostgreSQL)
|
|
50
|
+
// 1. Implement IOutboxRepository for your storage (e.g. PostgreSQL)
|
|
50
51
|
class PostgresOutboxRepository extends IOutboxRepository {
|
|
51
|
-
async
|
|
52
|
-
/* ... */
|
|
52
|
+
async saveMessage(message) {
|
|
53
|
+
/* INSERT INTO outbox ... */
|
|
53
54
|
}
|
|
54
|
-
async
|
|
55
|
-
/* ... */
|
|
55
|
+
async saveBatch(messages) {
|
|
56
|
+
/* INSERT INTO outbox (batch) ... */
|
|
56
57
|
}
|
|
57
|
-
async
|
|
58
|
-
|
|
58
|
+
async getUnprocessedMessages(limit, priorityOrder, messageTypes) {
|
|
59
|
+
// ORDER by priorityOrder array index — do NOT use ORDER BY priority string column
|
|
60
|
+
/* SELECT ... WHERE status = 'PENDING' ORDER BY CASE priority ... */
|
|
59
61
|
}
|
|
60
|
-
async
|
|
61
|
-
/* ... */
|
|
62
|
+
async getById(id) {
|
|
63
|
+
/* SELECT ... */
|
|
64
|
+
}
|
|
65
|
+
async updateStatus(id, status, error?) {
|
|
66
|
+
/* UPDATE ... */
|
|
67
|
+
}
|
|
68
|
+
async updateStatusBatch(ids, status) {
|
|
69
|
+
/* UPDATE ... */
|
|
70
|
+
}
|
|
71
|
+
async incrementAttempt(id) {
|
|
72
|
+
/* UPDATE ... RETURNING attempts */
|
|
73
|
+
}
|
|
74
|
+
async deleteByStatusAndAge(olderThan, status) {
|
|
75
|
+
/* DELETE ... */
|
|
76
|
+
}
|
|
77
|
+
async scheduleMessage(message, processAfter) {
|
|
78
|
+
/* INSERT ... */
|
|
62
79
|
}
|
|
63
80
|
}
|
|
64
81
|
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
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,
|
|
82
|
+
// 2. Wire up the processor
|
|
83
|
+
const repo = new PostgresOutboxRepository(db);
|
|
84
|
+
const processor = new OutboxProcessor(repo, {
|
|
73
85
|
batchSize: 50,
|
|
86
|
+
processingInterval: 5_000,
|
|
74
87
|
maxRetries: 3,
|
|
88
|
+
retryBackoff: { initial: 1_000, multiplier: 2, maxDelay: 300_000 },
|
|
75
89
|
});
|
|
76
90
|
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
// Register a type-specific handler
|
|
92
|
+
processor.registerHandler('OrderCreated', new EventBusOutboxHandler(eventBus));
|
|
93
|
+
|
|
94
|
+
// Or a catch-all default handler for uniform fan-out
|
|
95
|
+
processor.registerDefaultHandler({
|
|
96
|
+
async handle(message) {
|
|
97
|
+
await myQueue.add(message.messageType, message.payload);
|
|
98
|
+
},
|
|
83
99
|
});
|
|
84
|
-
await service.store(message);
|
|
85
100
|
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
processor.start();
|
|
102
|
+
|
|
103
|
+
// 3. Enqueue in the same transaction as your aggregate save
|
|
104
|
+
const factory = new OutboxMessageFactory();
|
|
105
|
+
await db.transaction(async tx => {
|
|
106
|
+
await orderRepo.save(order, tx);
|
|
107
|
+
await repo.saveMessage(
|
|
108
|
+
factory.create({
|
|
109
|
+
messageType: 'OrderCreated',
|
|
110
|
+
payload: { orderId: order.id },
|
|
111
|
+
priority: MessagePriority.HIGH,
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
});
|
|
88
115
|
```
|
|
89
116
|
|
|
90
117
|
## Custom message handler
|
|
@@ -102,41 +129,29 @@ class KafkaMessageHandler implements IOutboxMessageHandler {
|
|
|
102
129
|
});
|
|
103
130
|
}
|
|
104
131
|
}
|
|
132
|
+
|
|
133
|
+
processor.registerHandler('OrderCreated', new KafkaMessageHandler());
|
|
105
134
|
```
|
|
106
135
|
|
|
107
|
-
##
|
|
136
|
+
## Default (catch-all) handler
|
|
108
137
|
|
|
109
|
-
|
|
138
|
+
For uniform dispatch — e.g. routing all event types to a single queue — use
|
|
139
|
+
`registerDefaultHandler` instead of registering per-type:
|
|
110
140
|
|
|
111
141
|
```typescript
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
} from '@vytches/ddd-messaging';
|
|
117
|
-
|
|
118
|
-
class MyOutboxRepository extends IOutboxRepository {
|
|
119
|
-
async save(message: IOutboxMessage): Promise<void> {
|
|
120
|
-
await db.outbox.insert(message);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async findPendingMessages(limit: number): Promise<IOutboxMessage[]> {
|
|
124
|
-
return db.outbox.findWhere({ status: MessageStatus.PENDING }, limit);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async markAsProcessed(id: string): Promise<void> {
|
|
128
|
-
await db.outbox.update(id, { status: MessageStatus.PROCESSED });
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async markAsFailed(id: string, error: string): Promise<void> {
|
|
132
|
-
await db.outbox.update(id, {
|
|
133
|
-
status: MessageStatus.FAILED,
|
|
134
|
-
lastError: error,
|
|
142
|
+
processor.registerDefaultHandler({
|
|
143
|
+
async handle(message) {
|
|
144
|
+
await fanOutQueue.add(message.messageType, message.payload, {
|
|
145
|
+
jobId: message.id,
|
|
135
146
|
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
147
|
+
},
|
|
148
|
+
});
|
|
138
149
|
```
|
|
139
150
|
|
|
151
|
+
> **Note:** Registering a default handler removes the implicit per-type
|
|
152
|
+
> allowlist. Validate `message.messageType` inside the handler if you need an
|
|
153
|
+
> explicit allowlist.
|
|
154
|
+
|
|
140
155
|
## Package boundaries
|
|
141
156
|
|
|
142
157
|
`@vytches/ddd-messaging` depends on:
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const u=require("@vytches/ddd-logging"),d=require("@vytches/ddd-utils");var n=(i=>(i.PENDING="PENDING",i.PROCESSING="PROCESSING",i.PROCESSED="PROCESSED",i.FAILED="FAILED",i))(n||{}),g=(i=>(i.LOW="low",i.NORMAL="normal",i.HIGH="high",i.CRITICAL="critical",i))(g||{});class p{scheduleRetry(e,t){return Promise.resolve()}resetStaleProcessing(e){return Promise.resolve(0)}}class l{static createMessage(e,t,s){const r={id:d.LibUtils.getUUID(),messageType:e,payload:t,metadata:s?.metadata||{},status:n.PENDING,attempts:0,createdAt:new Date,priority:s?.priority||g.NORMAL};return s?.processAfter&&(r.processAfter=s.processAfter),r}static createDelayedMessage(e,t,s,r){const a=new Date(Date.now()+s);return l.createMessage(e,t,{...r,processAfter:a})}static createHighPriorityMessage(e,t,s){return l.createMessage(e,t,{...s,priority:g.HIGH})}static createFromIntegrationEvent(e,t){const s={metadata:{...e.metadata||{},...t?.metadata||{}}};return t?.priority&&(s.priority=t.priority),t?.processAfter&&(s.processAfter=t.processAfter),l.createMessage(`integration_event:${e.eventName}`,e.payload,s)}}class y{constructor(e,t={}){this.handlers=new Map,this.middlewares=[],this.isRunning=!1,this.logger=u.Logger.create("OutboxProcessor");const s=t.batchSize??10;if(s>1e4)throw new RangeError(`OutboxProcessor: batchSize must be ≤ 10,000, got ${s}`);if(t.retryBackoff){const{initial:o,multiplier:h,maxDelay:c}=t.retryBackoff;if(o<=0||h<=0||c<=0)throw new RangeError(`OutboxProcessor: retryBackoff fields must be positive numbers (initial=${o}, multiplier=${h}, maxDelay=${c})`)}const r=t.messageTimeout??3e4,a=t.crashRecoveryThresholdMs??Math.max(3e5,r);if(a<r)throw new RangeError(`OutboxProcessor: crashRecoveryThresholdMs (${a}) must be ≥ messageTimeout (${r}) to avoid resetting in-flight messages`);this.repository=e,this.options={batchSize:s,maxRetries:t.maxRetries??3,processingInterval:t.processingInterval??5e3,priorityOrder:t.priorityOrder??[g.CRITICAL,g.HIGH,g.NORMAL,g.LOW],messageTimeout:r,enableLogging:t.enableLogging??!1,retryBackoff:t.retryBackoff,messageTypes:t.messageTypes,crashRecoveryIntervalMs:t.crashRecoveryIntervalMs,crashRecoveryThresholdMs:a,adaptiveRepoll:t.adaptiveRepoll??!1,adaptiveRepollMaxConsecutive:t.adaptiveRepollMaxConsecutive??5,adaptiveRepollPauseMs:t.adaptiveRepollPauseMs??50,startupJitterMs:t.startupJitterMs??0,hooks:t.hooks}}safelyInvokeHook(e,t){if(this.options.hooks)try{t()}catch(s){const r=s instanceof Error?s.message:String(s);this.logger.error(`Outbox hook ${e} threw and was ignored: ${r}`)}}registerHandler(e,t){this.handlers.set(e,t),this.logger.debug(`Registered handler for message type: ${e}`)}use(e){this.middlewares.push(e),this.logger.debug("Registered middleware")}start(){if(this.isRunning){this.logger.warn("Processor is already running");return}this.isRunning=!0,this.logger.info("Starting outbox processor");const e=this.options.startupJitterMs>0?Math.floor(Math.random()*this.options.startupJitterMs):0;this.scheduleProcessing(0,this.options.processingInterval+e),this.scheduleCrashRecovery()}stop(){if(!this.isRunning){this.logger.warn("Processor is not running");return}this.isRunning=!1,this.processingTimer&&(clearTimeout(this.processingTimer),this.processingTimer=void 0),this.crashRecoveryTimer&&(clearTimeout(this.crashRecoveryTimer),this.crashRecoveryTimer=void 0),this.logger.info("Stopped outbox processor")}async processBatch(){const e=this.options.batchSize;if(!this.isRunning)return{processed:0,batchSize:e};const[t,s]=await d.safeRun(()=>this.repository.getUnprocessedMessages(e,this.options.priorityOrder,this.options.messageTypes));if(t){const c=t instanceof Error?t.message:String(t);return this.logger.error(`Error retrieving messages: ${c}`),{processed:0,batchSize:e}}const r=s;if(!r||!Array.isArray(r)||r.length===0)return this.logger.debug("No pending messages to process"),{processed:0,batchSize:e};this.logger.info(`Processing ${r.length} messages`);const a=Date.now(),h=(await Promise.allSettled(r.map(c=>this.processMessage(c)))).filter(c=>c.status==="rejected"||c.status==="fulfilled"&&c.value===!1).length;return this.logger.info(`Completed processing batch of ${r.length} messages`),this.safelyInvokeHook("onBatchComplete",()=>this.options.hooks?.onBatchComplete?.({processed:r.length,failed:h,batchSize:e,durationMs:Date.now()-a})),{processed:r.length,batchSize:e}}async processMessage(e){const[t]=await d.safeRun(async()=>{await this.repository.updateStatus(e.id,n.PROCESSING),await this.buildPipeline(e)(e),await this.repository.updateStatus(e.id,n.PROCESSED),this.logger.debug(`Successfully processed message ${e.id}`)});return t?(await this.handleMessageError(e,t),!1):!0}buildPipeline(e){const t=this.handlers.get(e.messageType);if(!t)throw new Error(`No handler registered for message type: ${e.messageType}`);const s=async r=>{await Promise.race([t.handle(r),new Promise((a,o)=>setTimeout(()=>o(new Error("Message processing timeout")),this.options.messageTimeout))])};return this.middlewares.reduceRight((r,a)=>a(r),s)}async handleMessageError(e,t){try{const s=await this.repository.incrementAttempt(e.id);if(this.logger.warn(`Message ${e.id} failed, attempt ${s}: ${t?.message}`),this.safelyInvokeHook("onMessageFailed",()=>this.options.hooks?.onMessageFailed?.(e,t,s)),s>=this.options.maxRetries)await this.repository.updateStatus(e.id,n.FAILED,t),this.logger.error(`Message ${e.id} marked as failed after ${s} attempts`),this.safelyInvokeHook("onPermanentFailure",()=>this.options.hooks?.onPermanentFailure?.(e,t));else if(this.options.retryBackoff){const{initial:r,multiplier:a,maxDelay:o}=this.options.retryBackoff,h=Math.min(r*Math.pow(a,s-1),o),c=new Date(Date.now()+h);await this.repository.updateStatus(e.id,n.PENDING),await this.repository.scheduleRetry(e.id,c),this.logger.info(`Message ${e.id} scheduled for retry in ${h}ms (attempt ${s})`)}else await this.repository.updateStatus(e.id,n.PENDING),this.logger.info(`Message ${e.id} scheduled for retry (attempt ${s+1})`)}catch(s){const r=s instanceof Error?s.message:String(s);this.logger.error(`Error updating message status: ${r}`)}}scheduleProcessing(e=0,t=this.options.processingInterval){this.isRunning&&(this.processingTimer=setTimeout(async()=>{const s=await this.processBatch();if(this.options.adaptiveRepoll&&s.processed>0&&s.processed>=s.batchSize){const a=e+1,o=a>this.options.adaptiveRepollMaxConsecutive?this.options.adaptiveRepollPauseMs:0;this.scheduleProcessing(a,o)}else this.scheduleProcessing(0,this.options.processingInterval)},t))}scheduleCrashRecovery(){!this.isRunning||this.options.crashRecoveryIntervalMs===void 0||(this.crashRecoveryTimer=setTimeout(async()=>{await this.runCrashRecovery(),this.scheduleCrashRecovery()},this.options.crashRecoveryIntervalMs))}async runCrashRecovery(){const e=new Date(Date.now()-this.options.crashRecoveryThresholdMs),[t,s]=await d.safeRun(()=>this.repository.resetStaleProcessing(e));if(t){const r=t instanceof Error?t.message:String(t);this.logger.error(`Crash recovery failed: ${r}`);return}s&&s>0&&this.logger.warn(`Crash recovery reset ${s} stale PROCESSING message(s) to PENDING`)}log(e){this.options.enableLogging&&this.logger.info(e)}async getStats(){return{isRunning:this.isRunning,batchSize:this.options.batchSize,maxRetries:this.options.maxRetries,processingInterval:this.options.processingInterval,registeredHandlers:Array.from(this.handlers.keys()),middlewareCount:this.middlewares.length}}}class f{constructor(e){this.eventBus=e}async handle(e){const s={eventName:e.messageType.startsWith("integration_event:")?e.messageType.replace("integration_event:",""):e.messageType,eventId:e.id,occurredOn:e.createdAt,version:1,...e.payload,...e.metadata};await this.eventBus.publish(s)}}class m{constructor(e,t={}){this.logger=u.Logger.create("OutboxService"),this.repository=e,this.options={defaultPriority:t.defaultPriority??g.NORMAL,defaultMetadata:t.defaultMetadata??{},autoCleanup:t.autoCleanup??!1,cleanupAfterDays:t.cleanupAfterDays??7,enableLogging:t.enableLogging??!1}}async saveMessage(e,t,s){const r=l.createMessage(e,t,{priority:this.options.defaultPriority,metadata:{...this.options.defaultMetadata,...s?.metadata},...s}),a=await this.repository.saveMessage(r);return this.logger.debug(`Saved message ${a} of type ${e}`),a}async saveMessages(e){const t=e.map(({messageType:r,payload:a,options:o})=>l.createMessage(r,a,{priority:this.options.defaultPriority,metadata:{...this.options.defaultMetadata,...o?.metadata},...o})),s=await this.repository.saveBatch(t);return this.logger.debug(`Saved batch of ${e.length} messages`),s}async scheduleMessage(e,t,s,r){const a=l.createDelayedMessage(e,t,s,{priority:this.options.defaultPriority,metadata:{...this.options.defaultMetadata,...r?.metadata},...r}),o=await this.repository.scheduleMessage(a,a.processAfter);return this.logger.debug(`Scheduled message ${o} of type ${e} for processing in ${s}ms`),o}async saveHighPriorityMessage(e,t,s){return this.saveMessage(e,t,{...s,priority:g.HIGH})}async saveCriticalMessage(e,t,s){return this.saveMessage(e,t,{...s,priority:g.CRITICAL})}async saveDomainEvent(e,t){const s=l.createFromIntegrationEvent({eventName:e.eventName,payload:e,metadata:{eventId:e?.metadata?.eventId,occurredOn:e?.metadata?.occurredOn,version:e?.metadata?.version}},{priority:this.options.defaultPriority,metadata:{...this.options.defaultMetadata,...t?.metadata},...t}),r=await this.repository.saveMessage(s);return this.logger.debug(`Saved domain event ${e.eventName} as message ${r}`),r}async getMessage(e){return this.repository.getById(e)}async getPendingMessages(e=100){return this.repository.getUnprocessedMessages(e)}async retryMessage(e){await this.repository.updateStatus(e,n.PENDING),this.logger.info(`Reset message ${e} to pending for retry`)}async retryMessages(e){await this.repository.updateStatusBatch(e,n.PENDING),this.logger.info(`Reset ${e.length} messages to pending for retry`)}async cleanup(e){const t=e??this.options.cleanupAfterDays,s=new Date;s.setDate(s.getDate()-t);const r=await this.repository.deleteByStatusAndAge(s,n.PROCESSED);return this.logger.info(`Cleaned up ${r} processed messages older than ${t} days`),r}async getStats(){const e=this.repository;return e.getMessageCountByStatus?{totalPending:e.getMessageCountByStatus(n.PENDING),totalProcessing:e.getMessageCountByStatus(n.PROCESSING),totalProcessed:e.getMessageCountByStatus(n.PROCESSED),totalFailed:e.getMessageCountByStatus(n.FAILED)}:{totalPending:(await this.repository.getUnprocessedMessages(1e3)).length,totalProcessing:0,totalProcessed:0,totalFailed:0}}log(e){this.options.enableLogging&&this.logger.info(e)}}exports.EventBusOutboxHandler=f;exports.IOutboxRepository=p;exports.MessagePriority=g;exports.MessageStatus=n;exports.OutboxMessageFactory=l;exports.OutboxProcessor=y;exports.OutboxService=m;
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const u=require("@vytches/ddd-logging"),d=require("@vytches/ddd-utils");var n=(i=>(i.PENDING="PENDING",i.PROCESSING="PROCESSING",i.PROCESSED="PROCESSED",i.FAILED="FAILED",i))(n||{}),l=(i=>(i.LOW="low",i.NORMAL="normal",i.HIGH="high",i.CRITICAL="critical",i))(l||{});const p=["critical","high","normal","low"];function y(i,e,t=p){const s=t.includes(i)?t.indexOf(i):t.length,r=t.includes(e)?t.indexOf(e):t.length;return s-r}class f{scheduleRetry(e,t){return Promise.resolve()}resetStaleProcessing(e){return Promise.resolve(0)}}class g{static createMessage(e,t,s){const r={id:d.LibUtils.getUUID(),messageType:e,payload:t,metadata:s?.metadata||{},status:n.PENDING,attempts:0,createdAt:new Date,priority:s?.priority||l.NORMAL};return s?.processAfter&&(r.processAfter=s.processAfter),r}static createDelayedMessage(e,t,s,r){const a=new Date(Date.now()+s);return g.createMessage(e,t,{...r,processAfter:a})}static createHighPriorityMessage(e,t,s){return g.createMessage(e,t,{...s,priority:l.HIGH})}static createFromIntegrationEvent(e,t){const s={metadata:{...e.metadata||{},...t?.metadata||{}}};return t?.priority&&(s.priority=t.priority),t?.processAfter&&(s.processAfter=t.processAfter),g.createMessage(`integration_event:${e.eventName}`,e.payload,s)}}class m{constructor(e,t={}){this.handlers=new Map,this.middlewares=[],this.isRunning=!1,this.logger=u.Logger.create("OutboxProcessor");const s=t.batchSize??10;if(s>1e4)throw new RangeError(`OutboxProcessor: batchSize must be ≤ 10,000, got ${s}`);if(t.retryBackoff){const{initial:o,multiplier:h,maxDelay:c}=t.retryBackoff;if(o<=0||h<=0||c<=0)throw new RangeError(`OutboxProcessor: retryBackoff fields must be positive numbers (initial=${o}, multiplier=${h}, maxDelay=${c})`)}const r=t.messageTimeout??3e4,a=t.crashRecoveryThresholdMs??Math.max(3e5,r);if(a<r)throw new RangeError(`OutboxProcessor: crashRecoveryThresholdMs (${a}) must be ≥ messageTimeout (${r}) to avoid resetting in-flight messages`);this.repository=e,this.options={batchSize:s,maxRetries:t.maxRetries??3,processingInterval:t.processingInterval??5e3,priorityOrder:t.priorityOrder??[l.CRITICAL,l.HIGH,l.NORMAL,l.LOW],messageTimeout:r,enableLogging:t.enableLogging??!1,retryBackoff:t.retryBackoff,messageTypes:t.messageTypes,crashRecoveryIntervalMs:t.crashRecoveryIntervalMs,crashRecoveryThresholdMs:a,adaptiveRepoll:t.adaptiveRepoll??!1,adaptiveRepollMaxConsecutive:t.adaptiveRepollMaxConsecutive??5,adaptiveRepollPauseMs:t.adaptiveRepollPauseMs??50,startupJitterMs:t.startupJitterMs??0,hooks:t.hooks}}safelyInvokeHook(e,t){if(this.options.hooks)try{t()}catch(s){const r=s instanceof Error?s.message:String(s);this.logger.error(`Outbox hook ${e} threw and was ignored: ${r}`)}}registerHandler(e,t){this.handlers.set(e,t),this.logger.debug(`Registered handler for message type: ${e}`)}registerDefaultHandler(e){this.defaultHandler=e}use(e){this.middlewares.push(e),this.logger.debug("Registered middleware")}start(){if(this.isRunning){this.logger.warn("Processor is already running");return}this.isRunning=!0,this.logger.info("Starting outbox processor");const e=this.options.startupJitterMs>0?Math.floor(Math.random()*this.options.startupJitterMs):0;this.scheduleProcessing(0,this.options.processingInterval+e),this.scheduleCrashRecovery()}stop(){if(!this.isRunning){this.logger.warn("Processor is not running");return}this.isRunning=!1,this.processingTimer&&(clearTimeout(this.processingTimer),this.processingTimer=void 0),this.crashRecoveryTimer&&(clearTimeout(this.crashRecoveryTimer),this.crashRecoveryTimer=void 0),this.logger.info("Stopped outbox processor")}async processBatch(){const e=this.options.batchSize;if(!this.isRunning)return{processed:0,batchSize:e};const[t,s]=await d.safeRun(()=>this.repository.getUnprocessedMessages(e,this.options.priorityOrder,this.options.messageTypes));if(t){const c=t instanceof Error?t.message:String(t);return this.logger.error(`Error retrieving messages: ${c}`),{processed:0,batchSize:e}}const r=s;if(!r||!Array.isArray(r)||r.length===0)return this.logger.debug("No pending messages to process"),{processed:0,batchSize:e};this.logger.info(`Processing ${r.length} messages`);const a=Date.now(),h=(await Promise.allSettled(r.map(c=>this.processMessage(c)))).filter(c=>c.status==="rejected"||c.status==="fulfilled"&&c.value===!1).length;return this.logger.info(`Completed processing batch of ${r.length} messages`),this.safelyInvokeHook("onBatchComplete",()=>this.options.hooks?.onBatchComplete?.({processed:r.length,failed:h,batchSize:e,durationMs:Date.now()-a})),{processed:r.length,batchSize:e}}async processMessage(e){const[t]=await d.safeRun(async()=>{await this.repository.updateStatus(e.id,n.PROCESSING),await this.buildPipeline(e)(e),await this.repository.updateStatus(e.id,n.PROCESSED),this.logger.debug(`Successfully processed message ${e.id}`)});return t?(await this.handleMessageError(e,t),!1):!0}buildPipeline(e){const t=this.handlers.get(e.messageType)??this.defaultHandler;if(!t)throw new Error(`No handler registered for message type: ${e.messageType}`);this.handlers.has(e.messageType)||this.logger.debug(`Default handler used for message type: ${e.messageType}`);const s=async r=>{await Promise.race([t.handle(r),new Promise((a,o)=>setTimeout(()=>o(new Error("Message processing timeout")),this.options.messageTimeout))])};return this.middlewares.reduceRight((r,a)=>a(r),s)}async handleMessageError(e,t){try{const s=await this.repository.incrementAttempt(e.id);if(this.logger.warn(`Message ${e.id} failed, attempt ${s}: ${t?.message}`),this.safelyInvokeHook("onMessageFailed",()=>this.options.hooks?.onMessageFailed?.(e,t,s)),s>=this.options.maxRetries)await this.repository.updateStatus(e.id,n.FAILED,t),this.logger.error(`Message ${e.id} marked as failed after ${s} attempts`),this.safelyInvokeHook("onPermanentFailure",()=>this.options.hooks?.onPermanentFailure?.(e,t));else if(this.options.retryBackoff){const{initial:r,multiplier:a,maxDelay:o}=this.options.retryBackoff,h=Math.min(r*Math.pow(a,s-1),o),c=new Date(Date.now()+h);await this.repository.updateStatus(e.id,n.PENDING),await this.repository.scheduleRetry(e.id,c),this.logger.info(`Message ${e.id} scheduled for retry in ${h}ms (attempt ${s})`)}else await this.repository.updateStatus(e.id,n.PENDING),this.logger.info(`Message ${e.id} scheduled for retry (attempt ${s+1})`)}catch(s){const r=s instanceof Error?s.message:String(s);this.logger.error(`Error updating message status: ${r}`)}}scheduleProcessing(e=0,t=this.options.processingInterval){this.isRunning&&(this.processingTimer=setTimeout(async()=>{const s=await this.processBatch();if(this.options.adaptiveRepoll&&s.processed>0&&s.processed>=s.batchSize){const a=e+1,o=a>this.options.adaptiveRepollMaxConsecutive?this.options.adaptiveRepollPauseMs:0;this.scheduleProcessing(a,o)}else this.scheduleProcessing(0,this.options.processingInterval)},t))}scheduleCrashRecovery(){!this.isRunning||this.options.crashRecoveryIntervalMs===void 0||(this.crashRecoveryTimer=setTimeout(async()=>{await this.runCrashRecovery(),this.scheduleCrashRecovery()},this.options.crashRecoveryIntervalMs))}async runCrashRecovery(){const e=new Date(Date.now()-this.options.crashRecoveryThresholdMs),[t,s]=await d.safeRun(()=>this.repository.resetStaleProcessing(e));if(t){const r=t instanceof Error?t.message:String(t);this.logger.error(`Crash recovery failed: ${r}`);return}s&&s>0&&this.logger.warn(`Crash recovery reset ${s} stale PROCESSING message(s) to PENDING`)}log(e){this.options.enableLogging&&this.logger.info(e)}async getStats(){return{isRunning:this.isRunning,batchSize:this.options.batchSize,maxRetries:this.options.maxRetries,processingInterval:this.options.processingInterval,registeredHandlers:Array.from(this.handlers.keys()),middlewareCount:this.middlewares.length,hasDefaultHandler:this.defaultHandler!==void 0}}}class v{constructor(e){this.eventBus=e}async handle(e){const s={eventName:e.messageType.startsWith("integration_event:")?e.messageType.replace("integration_event:",""):e.messageType,eventId:e.id,occurredOn:e.createdAt,version:1,...e.payload,...e.metadata};await this.eventBus.publish(s)}}class M{constructor(e,t={}){this.logger=u.Logger.create("OutboxService"),this.repository=e,this.options={defaultPriority:t.defaultPriority??l.NORMAL,defaultMetadata:t.defaultMetadata??{},autoCleanup:t.autoCleanup??!1,cleanupAfterDays:t.cleanupAfterDays??7,enableLogging:t.enableLogging??!1}}async saveMessage(e,t,s){const r=g.createMessage(e,t,{priority:this.options.defaultPriority,metadata:{...this.options.defaultMetadata,...s?.metadata},...s}),a=await this.repository.saveMessage(r);return this.logger.debug(`Saved message ${a} of type ${e}`),a}async saveMessages(e){const t=e.map(({messageType:r,payload:a,options:o})=>g.createMessage(r,a,{priority:this.options.defaultPriority,metadata:{...this.options.defaultMetadata,...o?.metadata},...o})),s=await this.repository.saveBatch(t);return this.logger.debug(`Saved batch of ${e.length} messages`),s}async scheduleMessage(e,t,s,r){const a=g.createDelayedMessage(e,t,s,{priority:this.options.defaultPriority,metadata:{...this.options.defaultMetadata,...r?.metadata},...r}),o=await this.repository.scheduleMessage(a,a.processAfter);return this.logger.debug(`Scheduled message ${o} of type ${e} for processing in ${s}ms`),o}async saveHighPriorityMessage(e,t,s){return this.saveMessage(e,t,{...s,priority:l.HIGH})}async saveCriticalMessage(e,t,s){return this.saveMessage(e,t,{...s,priority:l.CRITICAL})}async saveDomainEvent(e,t){const s=g.createFromIntegrationEvent({eventName:e.eventName,payload:e,metadata:{eventId:e?.metadata?.eventId,occurredOn:e?.metadata?.occurredOn,version:e?.metadata?.version}},{priority:this.options.defaultPriority,metadata:{...this.options.defaultMetadata,...t?.metadata},...t}),r=await this.repository.saveMessage(s);return this.logger.debug(`Saved domain event ${e.eventName} as message ${r}`),r}async getMessage(e){return this.repository.getById(e)}async getPendingMessages(e=100){return this.repository.getUnprocessedMessages(e)}async retryMessage(e){await this.repository.updateStatus(e,n.PENDING),this.logger.info(`Reset message ${e} to pending for retry`)}async retryMessages(e){await this.repository.updateStatusBatch(e,n.PENDING),this.logger.info(`Reset ${e.length} messages to pending for retry`)}async cleanup(e){const t=e??this.options.cleanupAfterDays,s=new Date;s.setDate(s.getDate()-t);const r=await this.repository.deleteByStatusAndAge(s,n.PROCESSED);return this.logger.info(`Cleaned up ${r} processed messages older than ${t} days`),r}async getStats(){const e=this.repository;return e.getMessageCountByStatus?{totalPending:e.getMessageCountByStatus(n.PENDING),totalProcessing:e.getMessageCountByStatus(n.PROCESSING),totalProcessed:e.getMessageCountByStatus(n.PROCESSED),totalFailed:e.getMessageCountByStatus(n.FAILED)}:{totalPending:(await this.repository.getUnprocessedMessages(1e3)).length,totalProcessing:0,totalProcessed:0,totalFailed:0}}log(e){this.options.enableLogging&&this.logger.info(e)}}exports.EventBusOutboxHandler=v;exports.IOutboxRepository=f;exports.MessagePriority=l;exports.MessageStatus=n;exports.OutboxMessageFactory=g;exports.OutboxProcessor=m;exports.OutboxService=M;exports.comparePriority=y;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export type { IOutboxMessage, OutboxMessageOptions, IOutboxMessageHandler, OutboxMiddleware, OutboxProcessorOptions, OutboxProcessorHooks, RetryBackoffConfig, OutboxServiceOptions, } from './outbox';
|
|
2
2
|
export { MessageStatus, MessagePriority } from './outbox';
|
|
3
|
+
export { comparePriority } from './outbox';
|
|
3
4
|
export { IOutboxRepository, OutboxMessageFactory, OutboxProcessor, EventBusOutboxHandler, OutboxService, } from './outbox';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { Logger as u } from "@vytches/ddd-logging";
|
|
2
2
|
import { LibUtils as p, safeRun as d } from "@vytches/ddd-utils";
|
|
3
|
-
var n = /* @__PURE__ */ ((i) => (i.PENDING = "PENDING", i.PROCESSING = "PROCESSING", i.PROCESSED = "PROCESSED", i.FAILED = "FAILED", i))(n || {}),
|
|
4
|
-
|
|
3
|
+
var n = /* @__PURE__ */ ((i) => (i.PENDING = "PENDING", i.PROCESSING = "PROCESSING", i.PROCESSED = "PROCESSED", i.FAILED = "FAILED", i))(n || {}), l = /* @__PURE__ */ ((i) => (i.LOW = "low", i.NORMAL = "normal", i.HIGH = "high", i.CRITICAL = "critical", i))(l || {});
|
|
4
|
+
const y = [
|
|
5
|
+
"critical",
|
|
6
|
+
"high",
|
|
7
|
+
"normal",
|
|
8
|
+
"low"
|
|
9
|
+
/* LOW */
|
|
10
|
+
];
|
|
11
|
+
function v(i, e, t = y) {
|
|
12
|
+
const s = t.includes(i) ? t.indexOf(i) : t.length, r = t.includes(e) ? t.indexOf(e) : t.length;
|
|
13
|
+
return s - r;
|
|
14
|
+
}
|
|
15
|
+
class M {
|
|
5
16
|
/**
|
|
6
17
|
* Schedules a retry for a failed message at a specific future time.
|
|
7
18
|
*
|
|
@@ -60,7 +71,7 @@ class h {
|
|
|
60
71
|
status: n.PENDING,
|
|
61
72
|
attempts: 0,
|
|
62
73
|
createdAt: /* @__PURE__ */ new Date(),
|
|
63
|
-
priority: s?.priority ||
|
|
74
|
+
priority: s?.priority || l.NORMAL
|
|
64
75
|
};
|
|
65
76
|
return s?.processAfter && (r.processAfter = s.processAfter), r;
|
|
66
77
|
}
|
|
@@ -89,7 +100,7 @@ class h {
|
|
|
89
100
|
static createHighPriorityMessage(e, t, s) {
|
|
90
101
|
return h.createMessage(e, t, {
|
|
91
102
|
...s,
|
|
92
|
-
priority:
|
|
103
|
+
priority: l.HIGH
|
|
93
104
|
});
|
|
94
105
|
}
|
|
95
106
|
/**
|
|
@@ -112,17 +123,17 @@ class h {
|
|
|
112
123
|
);
|
|
113
124
|
}
|
|
114
125
|
}
|
|
115
|
-
class
|
|
126
|
+
class R {
|
|
116
127
|
constructor(e, t = {}) {
|
|
117
128
|
this.handlers = /* @__PURE__ */ new Map(), this.middlewares = [], this.isRunning = !1, this.logger = u.create("OutboxProcessor");
|
|
118
129
|
const s = t.batchSize ?? 10;
|
|
119
130
|
if (s > 1e4)
|
|
120
131
|
throw new RangeError(`OutboxProcessor: batchSize must be ≤ 10,000, got ${s}`);
|
|
121
132
|
if (t.retryBackoff) {
|
|
122
|
-
const { initial: o, multiplier:
|
|
123
|
-
if (o <= 0 ||
|
|
133
|
+
const { initial: o, multiplier: g, maxDelay: c } = t.retryBackoff;
|
|
134
|
+
if (o <= 0 || g <= 0 || c <= 0)
|
|
124
135
|
throw new RangeError(
|
|
125
|
-
`OutboxProcessor: retryBackoff fields must be positive numbers (initial=${o}, multiplier=${
|
|
136
|
+
`OutboxProcessor: retryBackoff fields must be positive numbers (initial=${o}, multiplier=${g}, maxDelay=${c})`
|
|
126
137
|
);
|
|
127
138
|
}
|
|
128
139
|
const r = t.messageTimeout ?? 3e4, a = t.crashRecoveryThresholdMs ?? Math.max(3e5, r);
|
|
@@ -135,10 +146,10 @@ class v {
|
|
|
135
146
|
maxRetries: t.maxRetries ?? 3,
|
|
136
147
|
processingInterval: t.processingInterval ?? 5e3,
|
|
137
148
|
priorityOrder: t.priorityOrder ?? [
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
149
|
+
l.CRITICAL,
|
|
150
|
+
l.HIGH,
|
|
151
|
+
l.NORMAL,
|
|
152
|
+
l.LOW
|
|
142
153
|
],
|
|
143
154
|
messageTimeout: r,
|
|
144
155
|
enableLogging: t.enableLogging ?? !1,
|
|
@@ -172,6 +183,25 @@ class v {
|
|
|
172
183
|
registerHandler(e, t) {
|
|
173
184
|
this.handlers.set(e, t), this.logger.debug(`Registered handler for message type: ${e}`);
|
|
174
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Registers a catch-all handler invoked when no type-specific handler matches.
|
|
188
|
+
*
|
|
189
|
+
* NOTE: Registering a default handler removes the implicit type allowlist —
|
|
190
|
+
* all future message types, including ones added by mistake or injected via
|
|
191
|
+
* compromised storage, will be dispatched to this handler. Validate
|
|
192
|
+
* `message.messageType` inside the handler against a known-types set if your
|
|
193
|
+
* domain requires an explicit allowlist.
|
|
194
|
+
*
|
|
195
|
+
* NOTE: When `messageTypes` filter option is set, the default handler handles
|
|
196
|
+
* only types that pass through that filter. To handle all types, leave
|
|
197
|
+
* `messageTypes` unset.
|
|
198
|
+
*
|
|
199
|
+
* Calling this method a second time silently replaces the previous default
|
|
200
|
+
* handler (idempotent).
|
|
201
|
+
*/
|
|
202
|
+
registerDefaultHandler(e) {
|
|
203
|
+
this.defaultHandler = e;
|
|
204
|
+
}
|
|
175
205
|
/**
|
|
176
206
|
* Registers middleware for message processing pipeline
|
|
177
207
|
*/
|
|
@@ -248,7 +278,7 @@ class v {
|
|
|
248
278
|
if (!r || !Array.isArray(r) || r.length === 0)
|
|
249
279
|
return this.logger.debug("No pending messages to process"), { processed: 0, batchSize: e };
|
|
250
280
|
this.logger.info(`Processing ${r.length} messages`);
|
|
251
|
-
const a = Date.now(),
|
|
281
|
+
const a = Date.now(), g = (await Promise.allSettled(
|
|
252
282
|
r.map((c) => this.processMessage(c))
|
|
253
283
|
)).filter(
|
|
254
284
|
(c) => c.status === "rejected" || c.status === "fulfilled" && c.value === !1
|
|
@@ -257,7 +287,7 @@ class v {
|
|
|
257
287
|
"onBatchComplete",
|
|
258
288
|
() => this.options.hooks?.onBatchComplete?.({
|
|
259
289
|
processed: r.length,
|
|
260
|
-
failed:
|
|
290
|
+
failed: g,
|
|
261
291
|
batchSize: e,
|
|
262
292
|
durationMs: Date.now() - a
|
|
263
293
|
})
|
|
@@ -277,9 +307,10 @@ class v {
|
|
|
277
307
|
* Builds the middleware pipeline for message processing
|
|
278
308
|
*/
|
|
279
309
|
buildPipeline(e) {
|
|
280
|
-
const t = this.handlers.get(e.messageType);
|
|
310
|
+
const t = this.handlers.get(e.messageType) ?? this.defaultHandler;
|
|
281
311
|
if (!t)
|
|
282
312
|
throw new Error(`No handler registered for message type: ${e.messageType}`);
|
|
313
|
+
this.handlers.has(e.messageType) || this.logger.debug(`Default handler used for message type: ${e.messageType}`);
|
|
283
314
|
const s = async (r) => {
|
|
284
315
|
await Promise.race([
|
|
285
316
|
t.handle(r),
|
|
@@ -308,9 +339,9 @@ class v {
|
|
|
308
339
|
() => this.options.hooks?.onPermanentFailure?.(e, t)
|
|
309
340
|
);
|
|
310
341
|
else if (this.options.retryBackoff) {
|
|
311
|
-
const { initial: r, multiplier: a, maxDelay: o } = this.options.retryBackoff,
|
|
342
|
+
const { initial: r, multiplier: a, maxDelay: o } = this.options.retryBackoff, g = Math.min(r * Math.pow(a, s - 1), o), c = new Date(Date.now() + g);
|
|
312
343
|
await this.repository.updateStatus(e.id, n.PENDING), await this.repository.scheduleRetry(e.id, c), this.logger.info(
|
|
313
|
-
`Message ${e.id} scheduled for retry in ${
|
|
344
|
+
`Message ${e.id} scheduled for retry in ${g}ms (attempt ${s})`
|
|
314
345
|
);
|
|
315
346
|
} else
|
|
316
347
|
await this.repository.updateStatus(e.id, n.PENDING), this.logger.info(`Message ${e.id} scheduled for retry (attempt ${s + 1})`);
|
|
@@ -380,11 +411,12 @@ class v {
|
|
|
380
411
|
maxRetries: this.options.maxRetries,
|
|
381
412
|
processingInterval: this.options.processingInterval,
|
|
382
413
|
registeredHandlers: Array.from(this.handlers.keys()),
|
|
383
|
-
middlewareCount: this.middlewares.length
|
|
414
|
+
middlewareCount: this.middlewares.length,
|
|
415
|
+
hasDefaultHandler: this.defaultHandler !== void 0
|
|
384
416
|
};
|
|
385
417
|
}
|
|
386
418
|
}
|
|
387
|
-
class
|
|
419
|
+
class w {
|
|
388
420
|
constructor(e) {
|
|
389
421
|
this.eventBus = e;
|
|
390
422
|
}
|
|
@@ -400,10 +432,10 @@ class M {
|
|
|
400
432
|
await this.eventBus.publish(s);
|
|
401
433
|
}
|
|
402
434
|
}
|
|
403
|
-
class
|
|
435
|
+
class P {
|
|
404
436
|
constructor(e, t = {}) {
|
|
405
437
|
this.logger = u.create("OutboxService"), this.repository = e, this.options = {
|
|
406
|
-
defaultPriority: t.defaultPriority ??
|
|
438
|
+
defaultPriority: t.defaultPriority ?? l.NORMAL,
|
|
407
439
|
defaultMetadata: t.defaultMetadata ?? {},
|
|
408
440
|
autoCleanup: t.autoCleanup ?? !1,
|
|
409
441
|
cleanupAfterDays: t.cleanupAfterDays ?? 7,
|
|
@@ -453,7 +485,7 @@ class R {
|
|
|
453
485
|
async saveHighPriorityMessage(e, t, s) {
|
|
454
486
|
return this.saveMessage(e, t, {
|
|
455
487
|
...s,
|
|
456
|
-
priority:
|
|
488
|
+
priority: l.HIGH
|
|
457
489
|
});
|
|
458
490
|
}
|
|
459
491
|
/**
|
|
@@ -462,7 +494,7 @@ class R {
|
|
|
462
494
|
async saveCriticalMessage(e, t, s) {
|
|
463
495
|
return this.saveMessage(e, t, {
|
|
464
496
|
...s,
|
|
465
|
-
priority:
|
|
497
|
+
priority: l.CRITICAL
|
|
466
498
|
});
|
|
467
499
|
}
|
|
468
500
|
/**
|
|
@@ -551,11 +583,12 @@ class R {
|
|
|
551
583
|
}
|
|
552
584
|
}
|
|
553
585
|
export {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
586
|
+
w as EventBusOutboxHandler,
|
|
587
|
+
M as IOutboxRepository,
|
|
588
|
+
l as MessagePriority,
|
|
557
589
|
n as MessageStatus,
|
|
558
590
|
h as OutboxMessageFactory,
|
|
559
|
-
|
|
560
|
-
|
|
591
|
+
R as OutboxProcessor,
|
|
592
|
+
P as OutboxService,
|
|
593
|
+
v as comparePriority
|
|
561
594
|
};
|
|
@@ -48,3 +48,22 @@ export interface IOutboxMessageHandler<T = unknown> {
|
|
|
48
48
|
handle(message: IOutboxMessage<T>): Promise<void>;
|
|
49
49
|
}
|
|
50
50
|
export type OutboxMiddleware = (next: (message: IOutboxMessage) => Promise<void>) => (message: IOutboxMessage) => Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Compares two MessagePriority values for sorting purposes.
|
|
53
|
+
*
|
|
54
|
+
* Returns a negative number if `a` should be processed before `b`,
|
|
55
|
+
* positive if `b` before `a`, 0 if equal.
|
|
56
|
+
*
|
|
57
|
+
* @param order - Processing order array (first element = highest priority).
|
|
58
|
+
* Defaults to [CRITICAL, HIGH, NORMAL, LOW].
|
|
59
|
+
* Values absent from the array sort LAST (treated as lowest priority).
|
|
60
|
+
* Passing a partial array is safe — missing values are not promoted.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* // Sort messages by priority (highest first):
|
|
64
|
+
* messages.sort((a, b) => comparePriority(a.priority, b.priority));
|
|
65
|
+
*
|
|
66
|
+
* // Custom order:
|
|
67
|
+
* messages.sort((a, b) => comparePriority(a.priority, b.priority, ['high', 'critical', 'normal', 'low']));
|
|
68
|
+
*/
|
|
69
|
+
export declare function comparePriority(a: MessagePriority, b: MessagePriority, order?: MessagePriority[]): number;
|
|
@@ -85,6 +85,7 @@ export declare class OutboxProcessor {
|
|
|
85
85
|
private readonly repository;
|
|
86
86
|
private readonly options;
|
|
87
87
|
private readonly handlers;
|
|
88
|
+
private defaultHandler?;
|
|
88
89
|
private readonly middlewares;
|
|
89
90
|
private isRunning;
|
|
90
91
|
private processingTimer?;
|
|
@@ -100,6 +101,23 @@ export declare class OutboxProcessor {
|
|
|
100
101
|
* Registers a message handler for a specific message type
|
|
101
102
|
*/
|
|
102
103
|
registerHandler(messageType: string, handler: IOutboxMessageHandler): void;
|
|
104
|
+
/**
|
|
105
|
+
* Registers a catch-all handler invoked when no type-specific handler matches.
|
|
106
|
+
*
|
|
107
|
+
* NOTE: Registering a default handler removes the implicit type allowlist —
|
|
108
|
+
* all future message types, including ones added by mistake or injected via
|
|
109
|
+
* compromised storage, will be dispatched to this handler. Validate
|
|
110
|
+
* `message.messageType` inside the handler against a known-types set if your
|
|
111
|
+
* domain requires an explicit allowlist.
|
|
112
|
+
*
|
|
113
|
+
* NOTE: When `messageTypes` filter option is set, the default handler handles
|
|
114
|
+
* only types that pass through that filter. To handle all types, leave
|
|
115
|
+
* `messageTypes` unset.
|
|
116
|
+
*
|
|
117
|
+
* Calling this method a second time silently replaces the previous default
|
|
118
|
+
* handler (idempotent).
|
|
119
|
+
*/
|
|
120
|
+
registerDefaultHandler(handler: IOutboxMessageHandler): void;
|
|
103
121
|
/**
|
|
104
122
|
* Registers middleware for message processing pipeline
|
|
105
123
|
*/
|
|
@@ -196,6 +214,7 @@ export declare class OutboxProcessor {
|
|
|
196
214
|
processingInterval: number;
|
|
197
215
|
registeredHandlers: string[];
|
|
198
216
|
middlewareCount: number;
|
|
217
|
+
hasDefaultHandler: boolean;
|
|
199
218
|
}>;
|
|
200
219
|
}
|
|
201
220
|
export declare class EventBusOutboxHandler implements IOutboxMessageHandler {
|
|
@@ -13,14 +13,19 @@ export declare abstract class IOutboxRepository {
|
|
|
13
13
|
*/
|
|
14
14
|
abstract saveBatch<T = unknown>(messages: IOutboxMessage<T>[]): Promise<string[]>;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* @param
|
|
19
|
-
* @param
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* this
|
|
23
|
-
*
|
|
16
|
+
* Returns unprocessed messages up to the given limit.
|
|
17
|
+
*
|
|
18
|
+
* @param limit - Maximum number of messages to return.
|
|
19
|
+
* @param priorityOrder - Ordered array of MessagePriority values defining
|
|
20
|
+
* processing order (first element = highest priority, processed first).
|
|
21
|
+
* Implementations MUST sort results by the position of each message's
|
|
22
|
+
* priority in this array — do NOT use ORDER BY priority directly on the
|
|
23
|
+
* string column. Alphabetical sort inverts the intent: 'critical' sorts
|
|
24
|
+
* after 'high' alphabetically, causing CRITICAL messages to be processed
|
|
25
|
+
* last. Use comparePriority() or array index-based sorting instead.
|
|
26
|
+
* @param messageTypes - Optional allow-list of message types to return.
|
|
27
|
+
* When provided, only messages whose messageType is in this array are
|
|
28
|
+
* returned. When absent, all message types are returned.
|
|
24
29
|
*/
|
|
25
30
|
abstract getUnprocessedMessages(limit?: number, priorityOrder?: MessagePriority[], messageTypes?: string[]): Promise<IOutboxMessage[]>;
|
|
26
31
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vytches/ddd-messaging",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Outbox pattern and reliable message delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@vytches/ddd-events": "0.
|
|
53
|
+
"@vytches/ddd-events": "0.30.0"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "vite build",
|