@vytches/ddd-messaging 0.29.3 → 0.31.0-alpha.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 +225 -136
- package/dist/outbox/outbox-interfaces.d.ts +19 -0
- package/dist/outbox/outbox-processor.d.ts +48 -6
- package/dist/outbox/outbox-repository.interface.d.ts +66 -9
- package/dist/outbox/outbox-service.d.ts +1 -6
- package/package.json +7 -4
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 p=require("@vytches/ddd-utils"),c=require("@vytches/ddd-contracts/internal");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 m=["critical","high","normal","low"];function f(i,e,t=m){const s=t.includes(i)?t.indexOf(i):t.length,a=t.includes(e)?t.indexOf(e):t.length;return s-a}class v{async claimBatch(e,t,s){const a=await this.getUnprocessedMessages(e,t,s);return a.length===0?a:(await this.updateStatusBatch(a.map(r=>r.id),n.PROCESSING),a.map(r=>({...r,status:n.PROCESSING})))}scheduleRetry(e,t){return Promise.resolve()}resetStaleProcessing(e){return Promise.resolve(0)}}class h{static createMessage(e,t,s){const a={id:p.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&&(a.processAfter=s.processAfter),a}static createDelayedMessage(e,t,s,a){const r=new Date(Date.now()+s);return h.createMessage(e,t,{...a,processAfter:r})}static createHighPriorityMessage(e,t,s){return h.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),h.createMessage(`integration_event:${e.eventName}`,e.payload,s)}}class M{constructor(e,t={}){this.handlers=new Map,this.middlewares=[],this.isRunning=!1,this.warnedMissingClaimBatch=!1;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:u,maxDelay:d}=t.retryBackoff;if(o<=0||u<=0||d<=0)throw new RangeError(`OutboxProcessor: retryBackoff fields must be positive numbers (initial=${o}, multiplier=${u}, maxDelay=${d})`)}const a=t.messageTimeout??3e4,r=t.crashRecoveryThresholdMs??Math.max(3e5,a);if(r<a)throw new RangeError(`OutboxProcessor: crashRecoveryThresholdMs (${r}) must be ≥ messageTimeout (${a}) 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:a,enableLogging:t.enableLogging??!1,retryBackoff:t.retryBackoff,messageTypes:t.messageTypes,crashRecoveryIntervalMs:t.crashRecoveryIntervalMs,crashRecoveryThresholdMs:r,adaptiveRepoll:t.adaptiveRepoll??!1,adaptiveRepollMaxConsecutive:t.adaptiveRepollMaxConsecutive??5,adaptiveRepollPauseMs:t.adaptiveRepollPauseMs??50,startupJitterMs:t.startupJitterMs??0,hooks:t.hooks,useClaimBatch:t.useClaimBatch??!1}}safelyInvokeHook(e,t){if(this.options.hooks)try{t()}catch(s){c.internalLogger.error(`OutboxProcessor: hook ${e} threw and was ignored`,s instanceof Error?s:void 0,{hookName:e})}}registerHandler(e,t){this.handlers.set(e,t)}registerDefaultHandler(e){this.defaultHandler&&c.internalLogger.warn("registerDefaultHandler: replacing existing default handler — previous handler discarded"),this.defaultHandler=e}use(e){this.middlewares.push(e)}start(){if(this.isRunning){c.internalLogger.warn("OutboxProcessor: processor is already running");return}this.isRunning=!0;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){c.internalLogger.warn("OutboxProcessor: 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)}async processBatch(){const e=this.options.batchSize;if(!this.isRunning)return{processed:0,batchSize:e};const t=this.repository.claimBatch,s=this.options.useClaimBatch===!0&&typeof t=="function";this.options.useClaimBatch&&!s&&!this.warnedMissingClaimBatch&&(this.warnedMissingClaimBatch=!0,c.internalLogger.warn("OutboxProcessor: useClaimBatch is enabled but the repository does not implement claimBatch() — falling back to getUnprocessedMessages (no atomic-claim protection)"));const[a,r]=await p.safeRun(()=>s&&t?t.call(this.repository,e,this.options.priorityOrder,this.options.messageTypes):this.repository.getUnprocessedMessages(e,this.options.priorityOrder,this.options.messageTypes));if(a)return c.internalLogger.error("OutboxProcessor: error retrieving messages",a instanceof Error?a:void 0),{processed:0,batchSize:e};const o=r;if(!o||!Array.isArray(o)||o.length===0)return{processed:0,batchSize:e};const u=Date.now(),y=(await Promise.allSettled(o.map(g=>this.processMessage(g,s)))).filter(g=>g.status==="rejected"||g.status==="fulfilled"&&g.value===!1).length;return this.safelyInvokeHook("onBatchComplete",()=>this.options.hooks?.onBatchComplete?.({processed:o.length,failed:y,batchSize:e,durationMs:Date.now()-u})),{processed:o.length,batchSize:e}}async processMessage(e,t=!1){const[s]=await p.safeRun(async()=>{t||await this.repository.updateStatus(e.id,n.PROCESSING),await this.buildPipeline(e)(e),await this.repository.updateStatus(e.id,n.PROCESSED)});return s?(await this.handleMessageError(e,s),!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}`);const s=async a=>{let r;try{await Promise.race([t.handle(a),new Promise((o,u)=>{r=setTimeout(()=>u(new Error("Message processing timeout")),this.options.messageTimeout)})])}finally{r!==void 0&&clearTimeout(r)}};return this.middlewares.reduceRight((a,r)=>r(a),s)}async handleMessageError(e,t){try{const s=await this.repository.incrementAttempt(e.id);if(c.internalLogger.warn(`OutboxProcessor: message ${e.id} failed, attempt ${s}: ${p.LibUtils.sanitizeLogMessage(t?.message??String(t))}`),this.safelyInvokeHook("onMessageFailed",()=>this.options.hooks?.onMessageFailed?.(e,t,s)),s>=this.options.maxRetries)await this.repository.updateStatus(e.id,n.FAILED,t),c.internalLogger.error("OutboxProcessor: message permanently failed",t,{messageId:e.id,attempts:s}),this.safelyInvokeHook("onPermanentFailure",()=>this.options.hooks?.onPermanentFailure?.(e,t));else if(this.options.retryBackoff){const{initial:a,multiplier:r,maxDelay:o}=this.options.retryBackoff,u=Math.min(a*Math.pow(r,s-1),o),d=new Date(Date.now()+u);await this.repository.updateStatus(e.id,n.PENDING),await this.repository.scheduleRetry(e.id,d)}else await this.repository.updateStatus(e.id,n.PENDING)}catch(s){c.internalLogger.error("OutboxProcessor: error updating message status",s instanceof Error?s:void 0,{messageId:e.id})}}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 r=e+1,o=r>this.options.adaptiveRepollMaxConsecutive?this.options.adaptiveRepollPauseMs:0;this.scheduleProcessing(r,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 p.safeRun(()=>this.repository.resetStaleProcessing(e));if(t){c.internalLogger.error("OutboxProcessor: crash recovery failed",t instanceof Error?t:void 0);return}s&&s>0&&c.internalLogger.warn(`OutboxProcessor: crash recovery reset ${s} stale PROCESSING message(s) to PENDING`)}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 R{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 P{constructor(e,t={}){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 a=h.createMessage(e,t,{priority:this.options.defaultPriority,metadata:{...this.options.defaultMetadata,...s?.metadata},...s});return await this.repository.saveMessage(a)}async saveMessages(e){const t=e.map(({messageType:a,payload:r,options:o})=>h.createMessage(a,r,{priority:this.options.defaultPriority,metadata:{...this.options.defaultMetadata,...o?.metadata},...o}));return await this.repository.saveBatch(t)}async scheduleMessage(e,t,s,a){const r=h.createDelayedMessage(e,t,s,{priority:this.options.defaultPriority,metadata:{...this.options.defaultMetadata,...a?.metadata},...a});return await this.repository.scheduleMessage(r,r.processAfter)}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=h.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});return await this.repository.saveMessage(s)}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)}async retryMessages(e){await this.repository.updateStatusBatch(e,n.PENDING)}async cleanup(e){const t=e??this.options.cleanupAfterDays,s=new Date;return s.setDate(s.getDate()-t),await this.repository.deleteByStatusAndAge(s,n.PROCESSED)}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}}}exports.EventBusOutboxHandler=R;exports.IOutboxRepository=v;exports.MessagePriority=l;exports.MessageStatus=n;exports.OutboxMessageFactory=h;exports.OutboxProcessor=M;exports.OutboxService=P;exports.comparePriority=f;
|
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';
|