@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IEventBus } from '
|
|
1
|
+
import { IEventBus } from '@vytches/ddd-contracts';
|
|
2
2
|
import { IOutboxMessage, IOutboxMessageHandler, OutboxMiddleware, MessagePriority } from './outbox-interfaces';
|
|
3
3
|
import { IOutboxRepository } from './outbox-repository.interface';
|
|
4
4
|
import { OutboxProcessorHooks } from './outbox-hooks';
|
|
@@ -80,16 +80,42 @@ export interface OutboxProcessorOptions {
|
|
|
80
80
|
* logged and never breaks the processing loop. Omit to disable.
|
|
81
81
|
*/
|
|
82
82
|
hooks?: OutboxProcessorHooks;
|
|
83
|
+
/**
|
|
84
|
+
* Opt-in: when `true`, `processBatch` calls `IOutboxRepository.claimBatch()`
|
|
85
|
+
* instead of `getUnprocessedMessages()`, and `processMessage` skips its own
|
|
86
|
+
* (now redundant) `updateStatus(id, PROCESSING)` call for messages obtained
|
|
87
|
+
* this way — the repository's `claimBatch` is responsible for the
|
|
88
|
+
* PROCESSING mark.
|
|
89
|
+
*
|
|
90
|
+
* **Correctness depends on the repository.** `claimBatch` is an optional
|
|
91
|
+
* method on `IOutboxRepository` with a non-atomic default implementation
|
|
92
|
+
* (read then mark, two steps). Setting `useClaimBatch: true` against a
|
|
93
|
+
* repository that has NOT overridden `claimBatch` with a real atomic
|
|
94
|
+
* claim (e.g. `SELECT ... FOR UPDATE SKIP LOCKED`) provides **no**
|
|
95
|
+
* additional safety over the default `getUnprocessedMessages` path — it
|
|
96
|
+
* only prevents duplicate dispatch when the repository's `claimBatch` is
|
|
97
|
+
* genuinely atomic.
|
|
98
|
+
*
|
|
99
|
+
* If the repository does not implement `claimBatch` at all (the method is
|
|
100
|
+
* `undefined`, e.g. a hand-rolled repository predating this option), the
|
|
101
|
+
* processor logs a warning once and falls back to
|
|
102
|
+
* `getUnprocessedMessages` for that batch.
|
|
103
|
+
*
|
|
104
|
+
* Defaults to `false` — no behavior change for existing consumers (AC#5).
|
|
105
|
+
*/
|
|
106
|
+
useClaimBatch?: boolean;
|
|
83
107
|
}
|
|
84
108
|
export declare class OutboxProcessor {
|
|
85
109
|
private readonly repository;
|
|
86
110
|
private readonly options;
|
|
87
111
|
private readonly handlers;
|
|
112
|
+
private defaultHandler?;
|
|
88
113
|
private readonly middlewares;
|
|
89
114
|
private isRunning;
|
|
90
115
|
private processingTimer?;
|
|
91
116
|
private crashRecoveryTimer?;
|
|
92
|
-
|
|
117
|
+
/** Guards the one-time fallback warning when `useClaimBatch` is set but the repository has no `claimBatch`. */
|
|
118
|
+
private warnedMissingClaimBatch;
|
|
93
119
|
constructor(repository: IOutboxRepository, options?: OutboxProcessorOptions);
|
|
94
120
|
/**
|
|
95
121
|
* Invokes an observability hook in isolation. A throwing hook is logged and
|
|
@@ -100,6 +126,22 @@ export declare class OutboxProcessor {
|
|
|
100
126
|
* Registers a message handler for a specific message type
|
|
101
127
|
*/
|
|
102
128
|
registerHandler(messageType: string, handler: IOutboxMessageHandler): void;
|
|
129
|
+
/**
|
|
130
|
+
* Registers a catch-all handler invoked when no type-specific handler matches.
|
|
131
|
+
*
|
|
132
|
+
* NOTE: Registering a default handler removes the implicit type allowlist —
|
|
133
|
+
* all future message types, including ones added by mistake or injected via
|
|
134
|
+
* compromised storage, will be dispatched to this handler. Validate
|
|
135
|
+
* `message.messageType` inside the handler against a known-types set if your
|
|
136
|
+
* domain requires an explicit allowlist.
|
|
137
|
+
*
|
|
138
|
+
* NOTE: When `messageTypes` filter option is set, the default handler handles
|
|
139
|
+
* only types that pass through that filter. To handle all types, leave
|
|
140
|
+
* `messageTypes` unset.
|
|
141
|
+
*
|
|
142
|
+
* Second/subsequent call emits a warning and replaces the previous handler.
|
|
143
|
+
*/
|
|
144
|
+
registerDefaultHandler(handler: IOutboxMessageHandler): void;
|
|
103
145
|
/**
|
|
104
146
|
* Registers middleware for message processing pipeline
|
|
105
147
|
*/
|
|
@@ -147,6 +189,9 @@ export declare class OutboxProcessor {
|
|
|
147
189
|
}>;
|
|
148
190
|
/**
|
|
149
191
|
* Processes a single message through the middleware pipeline.
|
|
192
|
+
* @param alreadyClaimed When `true`, the message was already marked
|
|
193
|
+
* `PROCESSING` by `IOutboxRepository.claimBatch()` — the redundant
|
|
194
|
+
* `updateStatus(id, PROCESSING)` call is skipped.
|
|
150
195
|
* @returns `true` if the message was processed successfully, `false` if it failed.
|
|
151
196
|
*/
|
|
152
197
|
private processMessage;
|
|
@@ -182,10 +227,6 @@ export declare class OutboxProcessor {
|
|
|
182
227
|
* logged, never thrown — crash recovery must not crash the processor.
|
|
183
228
|
*/
|
|
184
229
|
private runCrashRecovery;
|
|
185
|
-
/**
|
|
186
|
-
* Logs messages if logging is enabled
|
|
187
|
-
*/
|
|
188
|
-
private log;
|
|
189
230
|
/**
|
|
190
231
|
* Gets processor statistics
|
|
191
232
|
*/
|
|
@@ -196,6 +237,7 @@ export declare class OutboxProcessor {
|
|
|
196
237
|
processingInterval: number;
|
|
197
238
|
registeredHandlers: string[];
|
|
198
239
|
middlewareCount: number;
|
|
240
|
+
hasDefaultHandler: boolean;
|
|
199
241
|
}>;
|
|
200
242
|
}
|
|
201
243
|
export declare class EventBusOutboxHandler implements IOutboxMessageHandler {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IOutboxMessage, MessagePriority
|
|
1
|
+
import { MessageStatus, IOutboxMessage, MessagePriority } from './outbox-interfaces';
|
|
2
2
|
export declare abstract class IOutboxRepository {
|
|
3
3
|
/**
|
|
4
4
|
* Saves a message to the outbox
|
|
@@ -13,16 +13,73 @@ export declare abstract class IOutboxRepository {
|
|
|
13
13
|
*/
|
|
14
14
|
abstract saveBatch<T = unknown>(messages: IOutboxMessage<T>[]): Promise<string[]>;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
16
|
+
* Returns unprocessed messages up to the given limit.
|
|
17
|
+
*
|
|
18
|
+
* **Not a claim.** This is a pure read — it does not mark returned messages
|
|
19
|
+
* as `PROCESSING` or otherwise reserve them. `OutboxProcessor` calls
|
|
20
|
+
* `updateStatus(id, PROCESSING)` for each message *after* this read
|
|
21
|
+
* returns, which means there is a window between "read" and "claimed"
|
|
22
|
+
* during which two concurrent processors (or two calls racing on the same
|
|
23
|
+
* processor) can both read the same PENDING message and both dispatch it.
|
|
24
|
+
* Multi-worker deployments that need "exactly one processor claims each
|
|
25
|
+
* message" should implement {@link claimBatch} with an atomic
|
|
26
|
+
* read+mark (e.g. `SELECT ... FOR UPDATE SKIP LOCKED` or a single
|
|
27
|
+
* `UPDATE ... WHERE status='PENDING' RETURNING *` statement / compare-
|
|
28
|
+
* and-swap on `status`) rather than relying on this method plus a
|
|
29
|
+
* follow-up `updateStatus`.
|
|
30
|
+
*
|
|
31
|
+
* Regardless of claim strategy, handlers should be idempotent to side
|
|
32
|
+
* effects — transient failures or duplicate-claim races may cause a
|
|
33
|
+
* handler to run more than once.
|
|
34
|
+
*
|
|
35
|
+
* @param limit - Maximum number of messages to return.
|
|
36
|
+
* @param priorityOrder - Ordered array of MessagePriority values defining
|
|
37
|
+
* processing order (first element = highest priority, processed first).
|
|
38
|
+
* Implementations MUST sort results by the position of each message's
|
|
39
|
+
* priority in this array — do NOT use ORDER BY priority directly on the
|
|
40
|
+
* string column. Alphabetical sort inverts the intent: 'critical' sorts
|
|
41
|
+
* after 'high' alphabetically, causing CRITICAL messages to be processed
|
|
42
|
+
* last. Use comparePriority() or array index-based sorting instead.
|
|
43
|
+
* @param messageTypes - Optional allow-list of message types to return.
|
|
44
|
+
* When provided, only messages whose messageType is in this array are
|
|
45
|
+
* returned. When absent, all message types are returned.
|
|
24
46
|
*/
|
|
25
47
|
abstract getUnprocessedMessages(limit?: number, priorityOrder?: MessagePriority[], messageTypes?: string[]): Promise<IOutboxMessage[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Atomically claims a batch of unprocessed messages: reads PENDING
|
|
50
|
+
* messages *and* marks them `PROCESSING` in the same operation, so two
|
|
51
|
+
* concurrent callers (two processor instances, or two racing calls on one
|
|
52
|
+
* instance) never both receive the same message.
|
|
53
|
+
*
|
|
54
|
+
* **Optional, and non-atomic by default.** This method is declared with
|
|
55
|
+
* `?` — it is NOT part of the required abstract contract, so existing
|
|
56
|
+
* `IOutboxRepository` subclasses keep compiling unmodified (AC#5). The
|
|
57
|
+
* default implementation provided here is a convenience shim only: it
|
|
58
|
+
* calls {@link getUnprocessedMessages} and then `updateStatusBatch(...,
|
|
59
|
+
* PROCESSING)` as two separate steps. **That is NOT atomic** — it has the
|
|
60
|
+
* exact same read-then-mark race window as calling
|
|
61
|
+
* `getUnprocessedMessages` + `updateStatus` manually, and is safe only for
|
|
62
|
+
* a single-worker deployment. Override `claimBatch` in a concrete
|
|
63
|
+
* repository (e.g. backed by `SELECT ... FOR UPDATE SKIP LOCKED`, or a
|
|
64
|
+
* single atomic `UPDATE ... WHERE status='PENDING' LIMIT n RETURNING *`)
|
|
65
|
+
* to get real multi-worker/multi-processor safety.
|
|
66
|
+
*
|
|
67
|
+
* `OutboxProcessor` only calls this method when
|
|
68
|
+
* `OutboxProcessorOptions.useClaimBatch` is explicitly set to `true`
|
|
69
|
+
* (opt-in, default `false`) — see AC#1/AC#2. When enabled, the processor
|
|
70
|
+
* skips its own redundant `updateStatus(id, PROCESSING)` call for messages
|
|
71
|
+
* returned by `claimBatch`, since claiming already performed that mark.
|
|
72
|
+
*
|
|
73
|
+
* Handlers should be idempotent to side effects regardless — transient
|
|
74
|
+
* failures or duplicate-claim races (e.g. with the non-atomic default
|
|
75
|
+
* implementation) may still cause a handler to run more than once.
|
|
76
|
+
*
|
|
77
|
+
* @param limit - Maximum number of messages to claim.
|
|
78
|
+
* @param priorityOrder - See {@link getUnprocessedMessages}.
|
|
79
|
+
* @param messageTypes - See {@link getUnprocessedMessages}.
|
|
80
|
+
* @returns Messages that were claimed (already marked `PROCESSING`).
|
|
81
|
+
*/
|
|
82
|
+
claimBatch?(limit?: number, priorityOrder?: MessagePriority[], messageTypes?: string[]): Promise<IOutboxMessage[]>;
|
|
26
83
|
/**
|
|
27
84
|
* Gets specific message by ID
|
|
28
85
|
* @param id Message ID
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IDomainEvent } from '
|
|
1
|
+
import { IDomainEvent } from '@vytches/ddd-contracts';
|
|
2
2
|
import { IOutboxMessage, OutboxMessageOptions, MessagePriority } from './outbox-interfaces';
|
|
3
3
|
import { IOutboxRepository } from './outbox-repository.interface';
|
|
4
4
|
export interface OutboxServiceOptions {
|
|
@@ -16,7 +16,6 @@ export interface OutboxServiceOptions {
|
|
|
16
16
|
export declare class OutboxService {
|
|
17
17
|
private readonly repository;
|
|
18
18
|
private readonly options;
|
|
19
|
-
private readonly logger;
|
|
20
19
|
constructor(repository: IOutboxRepository, options?: OutboxServiceOptions);
|
|
21
20
|
/**
|
|
22
21
|
* Saves a message to the outbox
|
|
@@ -75,8 +74,4 @@ export declare class OutboxService {
|
|
|
75
74
|
totalProcessed: number;
|
|
76
75
|
totalFailed: number;
|
|
77
76
|
}>;
|
|
78
|
-
/**
|
|
79
|
-
* Logs messages if logging is enabled
|
|
80
|
-
*/
|
|
81
|
-
private log;
|
|
82
77
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vytches/ddd-messaging",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0-alpha.0",
|
|
4
4
|
"description": "Outbox pattern and reliable message delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"LLMGUIDE.md",
|
|
19
19
|
"README.md",
|
|
20
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"LICENSE"
|
|
21
22
|
],
|
|
22
23
|
"keywords": [
|
|
23
24
|
"ddd",
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
"typescript": "^5.0.0",
|
|
37
38
|
"vite": "^7.3.2",
|
|
38
39
|
"vite-plugin-dts": "^4.0.0",
|
|
39
|
-
"vitest": "^4.
|
|
40
|
+
"vitest": "^4.1.0"
|
|
40
41
|
},
|
|
41
42
|
"nx": {
|
|
42
43
|
"tags": [
|
|
@@ -50,7 +51,9 @@
|
|
|
50
51
|
"access": "public"
|
|
51
52
|
},
|
|
52
53
|
"dependencies": {
|
|
53
|
-
"@vytches/ddd-
|
|
54
|
+
"@vytches/ddd-contracts": "0.31.0-alpha.0",
|
|
55
|
+
"@vytches/ddd-utils": "0.31.0-alpha.0",
|
|
56
|
+
"@vytches/ddd-events": "0.31.0-alpha.0"
|
|
54
57
|
},
|
|
55
58
|
"scripts": {
|
|
56
59
|
"build": "vite build",
|