@vytches/ddd-messaging 0.27.0 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -1041
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +227 -77
- package/dist/outbox/index.d.ts +1 -0
- package/dist/outbox/outbox-hooks.d.ts +24 -0
- package/dist/outbox/outbox-processor.d.ts +103 -3
- package/dist/outbox/outbox-repository.interface.d.ts +41 -1
- package/package.json +3 -4
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { IEventBus } from '../../../contracts/src/index.ts';
|
|
2
2
|
import { IOutboxMessage, IOutboxMessageHandler, OutboxMiddleware, MessagePriority } from './outbox-interfaces';
|
|
3
3
|
import { IOutboxRepository } from './outbox-repository.interface';
|
|
4
|
+
import { OutboxProcessorHooks } from './outbox-hooks';
|
|
5
|
+
export interface RetryBackoffConfig {
|
|
6
|
+
/** Initial delay in milliseconds before first retry (default: 1000) */
|
|
7
|
+
initial: number;
|
|
8
|
+
/** Multiplier applied per subsequent attempt (default: 2) */
|
|
9
|
+
multiplier: number;
|
|
10
|
+
/** Maximum delay cap in milliseconds (default: 300_000 — 5 min) */
|
|
11
|
+
maxDelay: number;
|
|
12
|
+
}
|
|
4
13
|
export interface OutboxProcessorOptions {
|
|
5
14
|
/** Maximum number of messages to process in one batch */
|
|
6
15
|
batchSize?: number;
|
|
@@ -14,6 +23,63 @@ export interface OutboxProcessorOptions {
|
|
|
14
23
|
messageTimeout?: number;
|
|
15
24
|
/** Enable logging */
|
|
16
25
|
enableLogging?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Exponential backoff for retries.
|
|
28
|
+
* When set, failed messages are scheduled for future retry via
|
|
29
|
+
* `IOutboxRepository.scheduleRetry()` instead of immediately re-queued.
|
|
30
|
+
* If the repository's `scheduleRetry` is a no-op, messages fall back to
|
|
31
|
+
* immediate PENDING reset (legacy behavior).
|
|
32
|
+
* If omitted, immediate retry is used for all messages.
|
|
33
|
+
*/
|
|
34
|
+
retryBackoff?: RetryBackoffConfig;
|
|
35
|
+
/**
|
|
36
|
+
* Optional allow-list of message types to process. When set, only messages
|
|
37
|
+
* of these types are fetched from the repository (passed through to
|
|
38
|
+
* `getUnprocessedMessages`). Lets multiple processors partition the outbox
|
|
39
|
+
* by type. Omit to process all types.
|
|
40
|
+
*/
|
|
41
|
+
messageTypes?: string[];
|
|
42
|
+
/**
|
|
43
|
+
* When set, the processor periodically calls
|
|
44
|
+
* `IOutboxRepository.resetStaleProcessing()` every N ms to recover messages
|
|
45
|
+
* orphaned in `PROCESSING` by a crashed worker. Omit to disable crash recovery.
|
|
46
|
+
*/
|
|
47
|
+
crashRecoveryIntervalMs?: number;
|
|
48
|
+
/**
|
|
49
|
+
* How old (in ms) a `PROCESSING` message must be before crash recovery resets
|
|
50
|
+
* it to `PENDING`. Must be `>= messageTimeout` so legitimately in-flight
|
|
51
|
+
* messages are never reset. Defaults to `max(300_000, messageTimeout)`.
|
|
52
|
+
* Setting this below `messageTimeout` throws a `RangeError` from the constructor.
|
|
53
|
+
*/
|
|
54
|
+
crashRecoveryThresholdMs?: number;
|
|
55
|
+
/**
|
|
56
|
+
* When `true`, after a **full** batch (every fetched slot used) the processor
|
|
57
|
+
* re-polls immediately instead of waiting `processingInterval`, draining a
|
|
58
|
+
* backlog faster. Defaults to `false` (opt-in — no behavior change).
|
|
59
|
+
*/
|
|
60
|
+
adaptiveRepoll?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Livelock guard for `adaptiveRepoll`: after this many consecutive full
|
|
63
|
+
* batches the processor inserts an `adaptiveRepollPauseMs` pause instead of
|
|
64
|
+
* re-polling immediately, yielding the event loop. Defaults to `5`.
|
|
65
|
+
*/
|
|
66
|
+
adaptiveRepollMaxConsecutive?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Pause (ms) inserted once the `adaptiveRepollMaxConsecutive` guard trips.
|
|
69
|
+
* Defaults to `50`.
|
|
70
|
+
*/
|
|
71
|
+
adaptiveRepollPauseMs?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Random delay (0..N ms) added before the first poll on `start()`, to
|
|
74
|
+
* de-synchronize many processors that boot simultaneously. Defaults to `0`.
|
|
75
|
+
*/
|
|
76
|
+
startupJitterMs?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Observability callbacks (batch completion, message failure, permanent
|
|
79
|
+
* failure). Each invocation is isolated in a try/catch — a throwing hook is
|
|
80
|
+
* logged and never breaks the processing loop. Omit to disable.
|
|
81
|
+
*/
|
|
82
|
+
hooks?: OutboxProcessorHooks;
|
|
17
83
|
}
|
|
18
84
|
export declare class OutboxProcessor {
|
|
19
85
|
private readonly repository;
|
|
@@ -22,8 +88,14 @@ export declare class OutboxProcessor {
|
|
|
22
88
|
private readonly middlewares;
|
|
23
89
|
private isRunning;
|
|
24
90
|
private processingTimer?;
|
|
91
|
+
private crashRecoveryTimer?;
|
|
25
92
|
private readonly logger;
|
|
26
93
|
constructor(repository: IOutboxRepository, options?: OutboxProcessorOptions);
|
|
94
|
+
/**
|
|
95
|
+
* Invokes an observability hook in isolation. A throwing hook is logged and
|
|
96
|
+
* swallowed so it can never break the processing loop (mitigation T3).
|
|
97
|
+
*/
|
|
98
|
+
private safelyInvokeHook;
|
|
27
99
|
/**
|
|
28
100
|
* Registers a message handler for a specific message type
|
|
29
101
|
*/
|
|
@@ -61,13 +133,21 @@ export declare class OutboxProcessor {
|
|
|
61
133
|
* **Quiescence:** if there are no pending messages, returns immediately
|
|
62
134
|
* without invoking handlers.
|
|
63
135
|
*
|
|
136
|
+
* @returns `{ processed, batchSize }` — `processed` is how many messages were
|
|
137
|
+
* dispatched this batch, `batchSize` the configured limit. A full batch
|
|
138
|
+
* (`processed >= batchSize`) signals a likely backlog. Callers that ignore
|
|
139
|
+
* the return value are unaffected (the value was previously `void`).
|
|
64
140
|
* @public
|
|
65
141
|
* @stable
|
|
66
142
|
* @since 0.1.0
|
|
67
143
|
*/
|
|
68
|
-
processBatch(): Promise<
|
|
144
|
+
processBatch(): Promise<{
|
|
145
|
+
processed: number;
|
|
146
|
+
batchSize: number;
|
|
147
|
+
}>;
|
|
69
148
|
/**
|
|
70
|
-
* Processes a single message through the middleware pipeline
|
|
149
|
+
* Processes a single message through the middleware pipeline.
|
|
150
|
+
* @returns `true` if the message was processed successfully, `false` if it failed.
|
|
71
151
|
*/
|
|
72
152
|
private processMessage;
|
|
73
153
|
/**
|
|
@@ -79,9 +159,29 @@ export declare class OutboxProcessor {
|
|
|
79
159
|
*/
|
|
80
160
|
private handleMessageError;
|
|
81
161
|
/**
|
|
82
|
-
* Schedules the next processing cycle
|
|
162
|
+
* Schedules the next processing cycle.
|
|
163
|
+
*
|
|
164
|
+
* With `adaptiveRepoll` enabled, a full batch re-polls immediately (`delay 0`)
|
|
165
|
+
* to drain a backlog — until `adaptiveRepollMaxConsecutive` consecutive full
|
|
166
|
+
* batches trip the livelock guard, after which an `adaptiveRepollPauseMs`
|
|
167
|
+
* pause is inserted. Any non-full batch resets the cadence to
|
|
168
|
+
* `processingInterval`.
|
|
169
|
+
*
|
|
170
|
+
* @param consecutiveFullBatches running count of back-to-back full batches
|
|
171
|
+
* @param delay milliseconds to wait before the next batch
|
|
83
172
|
*/
|
|
84
173
|
private scheduleProcessing;
|
|
174
|
+
/**
|
|
175
|
+
* Schedules the next crash-recovery cycle. No-op unless
|
|
176
|
+
* `crashRecoveryIntervalMs` was configured.
|
|
177
|
+
*/
|
|
178
|
+
private scheduleCrashRecovery;
|
|
179
|
+
/**
|
|
180
|
+
* Resets messages orphaned in `PROCESSING` (older than
|
|
181
|
+
* `crashRecoveryThresholdMs`) back to `PENDING` via the repository. Errors are
|
|
182
|
+
* logged, never thrown — crash recovery must not crash the processor.
|
|
183
|
+
*/
|
|
184
|
+
private runCrashRecovery;
|
|
85
185
|
/**
|
|
86
186
|
* Logs messages if logging is enabled
|
|
87
187
|
*/
|
|
@@ -16,9 +16,13 @@ export declare abstract class IOutboxRepository {
|
|
|
16
16
|
* Gets unprocessed messages from the outbox
|
|
17
17
|
* @param limit Maximum number of messages to retrieve
|
|
18
18
|
* @param priorityOrder Order of priority for processing (default: [CRITICAL, HIGH, NORMAL, LOW])
|
|
19
|
+
* @param messageTypes Optional allow-list of message types to fetch. When
|
|
20
|
+
* provided, only messages whose `messageType` is in this list are returned
|
|
21
|
+
* (e.g. `WHERE message_type IN (...)`). Omitting it fetches all types —
|
|
22
|
+
* this parameter is optional and backward-compatible.
|
|
19
23
|
* @returns Unprocessed messages
|
|
20
24
|
*/
|
|
21
|
-
abstract getUnprocessedMessages(limit?: number, priorityOrder?: MessagePriority[]): Promise<IOutboxMessage[]>;
|
|
25
|
+
abstract getUnprocessedMessages(limit?: number, priorityOrder?: MessagePriority[], messageTypes?: string[]): Promise<IOutboxMessage[]>;
|
|
22
26
|
/**
|
|
23
27
|
* Gets specific message by ID
|
|
24
28
|
* @param id Message ID
|
|
@@ -58,4 +62,40 @@ export declare abstract class IOutboxRepository {
|
|
|
58
62
|
* @returns ID of the saved message
|
|
59
63
|
*/
|
|
60
64
|
abstract scheduleMessage<T = unknown>(message: IOutboxMessage<T>, processAfter: Date): Promise<string>;
|
|
65
|
+
/**
|
|
66
|
+
* Schedules a retry for a failed message at a specific future time.
|
|
67
|
+
*
|
|
68
|
+
* Default implementation is a no-op — repositories that do not support
|
|
69
|
+
* delayed processing will fall back to immediate retry (PENDING reset).
|
|
70
|
+
* Override this in concrete repositories to enable exponential backoff.
|
|
71
|
+
*
|
|
72
|
+
* **Multi-worker note:** `OutboxProcessor` calls `updateStatus(PENDING)` immediately
|
|
73
|
+
* before this method. In multi-worker environments, override this to perform
|
|
74
|
+
* an atomic `UPDATE ... SET status=PENDING, process_after=?` in a single statement,
|
|
75
|
+
* and ensure `getUnprocessedMessages` filters `WHERE process_after IS NULL OR process_after <= NOW()`
|
|
76
|
+
* to prevent duplicate dispatch during the window between the two calls.
|
|
77
|
+
*
|
|
78
|
+
* @param id Message ID
|
|
79
|
+
* @param processAfter When to retry the message
|
|
80
|
+
*/
|
|
81
|
+
scheduleRetry(_id: string, _processAfter: Date): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Resets messages stuck in `PROCESSING` back to `PENDING` so they can be
|
|
84
|
+
* re-dispatched. Used for crash recovery: if a worker dies mid-batch, its
|
|
85
|
+
* in-flight messages remain `PROCESSING` forever without this.
|
|
86
|
+
*
|
|
87
|
+
* Default implementation is a no-op returning `0` — repositories that do not
|
|
88
|
+
* track a per-message "processing since" timestamp simply opt out. Override
|
|
89
|
+
* in concrete repositories, e.g.:
|
|
90
|
+
* `UPDATE outbox_messages SET status='PENDING' WHERE status='PROCESSING' AND updated_at < olderThan`.
|
|
91
|
+
*
|
|
92
|
+
* **Safety:** implementations MUST only reset messages whose processing
|
|
93
|
+
* started strictly before `olderThan`. `OutboxProcessor` always passes a past
|
|
94
|
+
* timestamp (`now - crashRecoveryThresholdMs`, with the threshold validated to
|
|
95
|
+
* be `>= messageTimeout`), so legitimately in-flight messages are never reset.
|
|
96
|
+
*
|
|
97
|
+
* @param olderThan Reset only `PROCESSING` messages last touched before this instant
|
|
98
|
+
* @returns Number of messages reset to `PENDING`
|
|
99
|
+
*/
|
|
100
|
+
resetStaleProcessing(_olderThan: Date): Promise<number>;
|
|
61
101
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vytches/ddd-messaging",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.1",
|
|
4
4
|
"description": "Outbox pattern and reliable message delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -36,8 +36,7 @@
|
|
|
36
36
|
"typescript": "^5.0.0",
|
|
37
37
|
"vite": "^7.3.2",
|
|
38
38
|
"vite-plugin-dts": "^4.0.0",
|
|
39
|
-
"vitest": "^
|
|
40
|
-
"@vytches/ddd-testing": "0.27.0"
|
|
39
|
+
"vitest": "^4.0.0"
|
|
41
40
|
},
|
|
42
41
|
"nx": {
|
|
43
42
|
"tags": [
|
|
@@ -51,7 +50,7 @@
|
|
|
51
50
|
"access": "public"
|
|
52
51
|
},
|
|
53
52
|
"dependencies": {
|
|
54
|
-
"@vytches/ddd-events": "0.
|
|
53
|
+
"@vytches/ddd-events": "0.29.1"
|
|
55
54
|
},
|
|
56
55
|
"scripts": {
|
|
57
56
|
"build": "vite build",
|