@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.
@@ -1,4 +1,4 @@
1
- import { IEventBus } from '../../../contracts/src/index.ts';
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
- private readonly logger;
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, MessageStatus } from './outbox-interfaces';
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
- * Gets unprocessed messages from the outbox
17
- * @param limit Maximum number of messages to retrieve
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.
23
- * @returns Unprocessed messages
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 '../../../contracts/src/index.ts';
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.29.3",
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.0.0"
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-events": "0.29.3"
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",