@vytches/ddd-messaging 0.26.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/LICENSE +21 -0
- package/LLMGUIDE.md +156 -0
- package/README.md +1113 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +411 -0
- package/dist/outbox/index.d.ts +5 -0
- package/dist/outbox/outbox-interfaces.d.ts +50 -0
- package/dist/outbox/outbox-message-factory.d.ts +39 -0
- package/dist/outbox/outbox-processor.d.ts +105 -0
- package/dist/outbox/outbox-repository.interface.d.ts +61 -0
- package/dist/outbox/outbox-service.d.ts +82 -0
- package/package.json +66 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const l=require("@vytches/ddd-logging"),d=require("@vytches/ddd-utils");var o=(i=>(i.PENDING="PENDING",i.PROCESSING="PROCESSING",i.PROCESSED="PROCESSED",i.FAILED="FAILED",i))(o||{}),n=(i=>(i.LOW="low",i.NORMAL="normal",i.HIGH="high",i.CRITICAL="critical",i))(n||{});class u{}class g{static createMessage(e,t,s){const r={id:d.LibUtils.getUUID(),messageType:e,payload:t,metadata:s?.metadata||{},status:o.PENDING,attempts:0,createdAt:new Date,priority:s?.priority||n.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:n.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 h{constructor(e,t={}){this.handlers=new Map,this.middlewares=[],this.isRunning=!1,this.logger=l.Logger.create("OutboxProcessor"),this.repository=e,this.options={batchSize:t.batchSize??10,maxRetries:t.maxRetries??3,processingInterval:t.processingInterval??5e3,priorityOrder:t.priorityOrder??[n.CRITICAL,n.HIGH,n.NORMAL,n.LOW],messageTimeout:t.messageTimeout??3e4,enableLogging:t.enableLogging??!1}}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"),this.scheduleProcessing()}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.logger.info("Stopped outbox processor")}async processBatch(){if(!this.isRunning)return;const[e,t]=await d.safeRun(()=>this.repository.getUnprocessedMessages(this.options.batchSize,this.options.priorityOrder));if(e){const a=e instanceof Error?e.message:String(e);this.logger.error(`Error retrieving messages: ${a}`);return}const s=t;if(!s||!Array.isArray(s)||s.length===0){this.logger.debug("No pending messages to process");return}this.logger.info(`Processing ${s.length} messages`);const r=s.map(a=>this.processMessage(a));await Promise.allSettled(r),this.logger.info(`Completed processing batch of ${s.length} messages`)}async processMessage(e){const[t]=await d.safeRun(async()=>{await this.repository.updateStatus(e.id,o.PROCESSING),await this.buildPipeline(e)(e),await this.repository.updateStatus(e.id,o.PROCESSED),this.logger.debug(`Successfully processed message ${e.id}`)});t&&await this.handleMessageError(e,t)}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,c)=>setTimeout(()=>c(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);this.logger.warn(`Message ${e.id} failed, attempt ${s}: ${t?.message}`),s>=this.options.maxRetries?(await this.repository.updateStatus(e.id,o.FAILED,t),this.logger.error(`Message ${e.id} marked as failed after ${s} attempts`)):(await this.repository.updateStatus(e.id,o.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(){this.isRunning&&(this.processingTimer=setTimeout(async()=>{await this.processBatch(),this.scheduleProcessing()},this.options.processingInterval))}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 p{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 y{constructor(e,t={}){this.logger=l.Logger.create("OutboxService"),this.repository=e,this.options={defaultPriority:t.defaultPriority??n.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:c})=>g.createMessage(r,a,{priority:this.options.defaultPriority,metadata:{...this.options.defaultMetadata,...c?.metadata},...c})),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}),c=await this.repository.scheduleMessage(a,a.processAfter);return this.logger.debug(`Scheduled message ${c} of type ${e} for processing in ${s}ms`),c}async saveHighPriorityMessage(e,t,s){return this.saveMessage(e,t,{...s,priority:n.HIGH})}async saveCriticalMessage(e,t,s){return this.saveMessage(e,t,{...s,priority:n.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,o.PENDING),this.logger.info(`Reset message ${e} to pending for retry`)}async retryMessages(e){await this.repository.updateStatusBatch(e,o.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,o.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(o.PENDING),totalProcessing:e.getMessageCountByStatus(o.PROCESSING),totalProcessed:e.getMessageCountByStatus(o.PROCESSED),totalFailed:e.getMessageCountByStatus(o.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=p;exports.IOutboxRepository=u;exports.MessagePriority=n;exports.MessageStatus=o;exports.OutboxMessageFactory=g;exports.OutboxProcessor=h;exports.OutboxService=y;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { IOutboxMessage, OutboxMessageOptions, IOutboxMessageHandler, OutboxMiddleware, OutboxProcessorOptions, OutboxServiceOptions, } from './outbox';
|
|
2
|
+
export { MessageStatus, MessagePriority } from './outbox';
|
|
3
|
+
export { IOutboxRepository, OutboxMessageFactory, OutboxProcessor, EventBusOutboxHandler, OutboxService, } from './outbox';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { Logger as l } from "@vytches/ddd-logging";
|
|
2
|
+
import { LibUtils as h, safeRun as d } from "@vytches/ddd-utils";
|
|
3
|
+
var o = /* @__PURE__ */ ((i) => (i.PENDING = "PENDING", i.PROCESSING = "PROCESSING", i.PROCESSED = "PROCESSED", i.FAILED = "FAILED", i))(o || {}), n = /* @__PURE__ */ ((i) => (i.LOW = "low", i.NORMAL = "normal", i.HIGH = "high", i.CRITICAL = "critical", i))(n || {});
|
|
4
|
+
class y {
|
|
5
|
+
}
|
|
6
|
+
class g {
|
|
7
|
+
/**
|
|
8
|
+
* Creates a new outbox message
|
|
9
|
+
* @param messageType Type of the message
|
|
10
|
+
* @param payload Message payload
|
|
11
|
+
* @param options Additional options
|
|
12
|
+
* @returns Outbox message
|
|
13
|
+
*/
|
|
14
|
+
static createMessage(e, t, s) {
|
|
15
|
+
const r = {
|
|
16
|
+
id: h.getUUID(),
|
|
17
|
+
messageType: e,
|
|
18
|
+
payload: t,
|
|
19
|
+
metadata: s?.metadata || {},
|
|
20
|
+
status: o.PENDING,
|
|
21
|
+
attempts: 0,
|
|
22
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
23
|
+
priority: s?.priority || n.NORMAL
|
|
24
|
+
};
|
|
25
|
+
return s?.processAfter && (r.processAfter = s.processAfter), r;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Creates a delayed outbox message
|
|
29
|
+
* @param messageType Type of the message
|
|
30
|
+
* @param payload Message payload
|
|
31
|
+
* @param delayMs Delay in milliseconds
|
|
32
|
+
* @param options Additional options
|
|
33
|
+
* @returns Outbox message scheduled for later processing
|
|
34
|
+
*/
|
|
35
|
+
static createDelayedMessage(e, t, s, r) {
|
|
36
|
+
const a = new Date(Date.now() + s);
|
|
37
|
+
return g.createMessage(e, t, {
|
|
38
|
+
...r,
|
|
39
|
+
processAfter: a
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Creates a high priority outbox message
|
|
44
|
+
* @param messageType Type of the message
|
|
45
|
+
* @param payload Message payload
|
|
46
|
+
* @param options Additional options
|
|
47
|
+
* @returns High priority outbox message
|
|
48
|
+
*/
|
|
49
|
+
static createHighPriorityMessage(e, t, s) {
|
|
50
|
+
return g.createMessage(e, t, {
|
|
51
|
+
...s,
|
|
52
|
+
priority: n.HIGH
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Creates an outbox message from an integration event
|
|
57
|
+
* @param event Integration event
|
|
58
|
+
* @param options Additional options
|
|
59
|
+
* @returns Outbox message containing the integration event
|
|
60
|
+
*/
|
|
61
|
+
static createFromIntegrationEvent(e, t) {
|
|
62
|
+
const s = {
|
|
63
|
+
metadata: {
|
|
64
|
+
...e.metadata || {},
|
|
65
|
+
...t?.metadata || {}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
return t?.priority && (s.priority = t.priority), t?.processAfter && (s.processAfter = t.processAfter), g.createMessage(
|
|
69
|
+
`integration_event:${e.eventName}`,
|
|
70
|
+
e.payload,
|
|
71
|
+
s
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
class m {
|
|
76
|
+
constructor(e, t = {}) {
|
|
77
|
+
this.handlers = /* @__PURE__ */ new Map(), this.middlewares = [], this.isRunning = !1, this.logger = l.create("OutboxProcessor"), this.repository = e, this.options = {
|
|
78
|
+
batchSize: t.batchSize ?? 10,
|
|
79
|
+
maxRetries: t.maxRetries ?? 3,
|
|
80
|
+
processingInterval: t.processingInterval ?? 5e3,
|
|
81
|
+
priorityOrder: t.priorityOrder ?? [
|
|
82
|
+
n.CRITICAL,
|
|
83
|
+
n.HIGH,
|
|
84
|
+
n.NORMAL,
|
|
85
|
+
n.LOW
|
|
86
|
+
],
|
|
87
|
+
messageTimeout: t.messageTimeout ?? 3e4,
|
|
88
|
+
enableLogging: t.enableLogging ?? !1
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Registers a message handler for a specific message type
|
|
93
|
+
*/
|
|
94
|
+
registerHandler(e, t) {
|
|
95
|
+
this.handlers.set(e, t), this.logger.debug(`Registered handler for message type: ${e}`);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Registers middleware for message processing pipeline
|
|
99
|
+
*/
|
|
100
|
+
use(e) {
|
|
101
|
+
this.middlewares.push(e), this.logger.debug("Registered middleware");
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Starts the processor with automatic interval processing
|
|
105
|
+
*/
|
|
106
|
+
start() {
|
|
107
|
+
if (this.isRunning) {
|
|
108
|
+
this.logger.warn("Processor is already running");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.isRunning = !0, this.logger.info("Starting outbox processor"), this.scheduleProcessing();
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Stops the processor
|
|
115
|
+
*/
|
|
116
|
+
stop() {
|
|
117
|
+
if (!this.isRunning) {
|
|
118
|
+
this.logger.warn("Processor is not running");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.isRunning = !1, this.processingTimer && (clearTimeout(this.processingTimer), this.processingTimer = void 0), this.logger.info("Stopped outbox processor");
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Processes a single batch of pending messages.
|
|
125
|
+
*
|
|
126
|
+
* **Dispatch model:** messages within a batch are dispatched **in parallel**
|
|
127
|
+
* via `Promise.allSettled`. A failure in message N does **not** block
|
|
128
|
+
* messages N+1..N+M. Each failure is isolated, retry-counted via
|
|
129
|
+
* `incrementAttempt`, and reset to `PENDING` (or marked `FAILED` after
|
|
130
|
+
* `maxRetries` attempts) — independently of sibling messages.
|
|
131
|
+
*
|
|
132
|
+
* **Ordering:** parallel dispatch means message processing order is
|
|
133
|
+
* **not guaranteed** within a batch, even if `priorityOrder` was used to
|
|
134
|
+
* select which messages to fetch. If you need strict ordering, set
|
|
135
|
+
* `batchSize: 1` (one message per batch = sequential by definition).
|
|
136
|
+
*
|
|
137
|
+
* **Throughput:** for N messages and dispatch latency L, total batch time
|
|
138
|
+
* is approximately L (parallel) rather than N*L (sequential). For 200
|
|
139
|
+
* messages at 10ms dispatch each: ~10ms parallel vs ~2s sequential.
|
|
140
|
+
*
|
|
141
|
+
* **Quiescence:** if there are no pending messages, returns immediately
|
|
142
|
+
* without invoking handlers.
|
|
143
|
+
*
|
|
144
|
+
* @public
|
|
145
|
+
* @stable
|
|
146
|
+
* @since 0.1.0
|
|
147
|
+
*/
|
|
148
|
+
async processBatch() {
|
|
149
|
+
if (!this.isRunning)
|
|
150
|
+
return;
|
|
151
|
+
const [e, t] = await d(
|
|
152
|
+
() => this.repository.getUnprocessedMessages(this.options.batchSize, this.options.priorityOrder)
|
|
153
|
+
);
|
|
154
|
+
if (e) {
|
|
155
|
+
const a = e instanceof Error ? e.message : String(e);
|
|
156
|
+
this.logger.error(`Error retrieving messages: ${a}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const s = t;
|
|
160
|
+
if (!s || !Array.isArray(s) || s.length === 0) {
|
|
161
|
+
this.logger.debug("No pending messages to process");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this.logger.info(`Processing ${s.length} messages`);
|
|
165
|
+
const r = s.map((a) => this.processMessage(a));
|
|
166
|
+
await Promise.allSettled(r), this.logger.info(`Completed processing batch of ${s.length} messages`);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Processes a single message through the middleware pipeline
|
|
170
|
+
*/
|
|
171
|
+
async processMessage(e) {
|
|
172
|
+
const [t] = await d(async () => {
|
|
173
|
+
await this.repository.updateStatus(e.id, o.PROCESSING), await this.buildPipeline(e)(e), await this.repository.updateStatus(e.id, o.PROCESSED), this.logger.debug(`Successfully processed message ${e.id}`);
|
|
174
|
+
});
|
|
175
|
+
t && await this.handleMessageError(e, t);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Builds the middleware pipeline for message processing
|
|
179
|
+
*/
|
|
180
|
+
buildPipeline(e) {
|
|
181
|
+
const t = this.handlers.get(e.messageType);
|
|
182
|
+
if (!t)
|
|
183
|
+
throw new Error(`No handler registered for message type: ${e.messageType}`);
|
|
184
|
+
const s = async (r) => {
|
|
185
|
+
await Promise.race([
|
|
186
|
+
t.handle(r),
|
|
187
|
+
new Promise(
|
|
188
|
+
(a, c) => setTimeout(
|
|
189
|
+
() => c(new Error("Message processing timeout")),
|
|
190
|
+
this.options.messageTimeout
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
]);
|
|
194
|
+
};
|
|
195
|
+
return this.middlewares.reduceRight((r, a) => a(r), s);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Handles errors during message processing
|
|
199
|
+
*/
|
|
200
|
+
async handleMessageError(e, t) {
|
|
201
|
+
try {
|
|
202
|
+
const s = await this.repository.incrementAttempt(e.id);
|
|
203
|
+
this.logger.warn(`Message ${e.id} failed, attempt ${s}: ${t?.message}`), s >= this.options.maxRetries ? (await this.repository.updateStatus(e.id, o.FAILED, t), this.logger.error(`Message ${e.id} marked as failed after ${s} attempts`)) : (await this.repository.updateStatus(e.id, o.PENDING), this.logger.info(`Message ${e.id} scheduled for retry (attempt ${s + 1})`));
|
|
204
|
+
} catch (s) {
|
|
205
|
+
const r = s instanceof Error ? s.message : String(s);
|
|
206
|
+
this.logger.error(`Error updating message status: ${r}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Schedules the next processing cycle
|
|
211
|
+
*/
|
|
212
|
+
scheduleProcessing() {
|
|
213
|
+
this.isRunning && (this.processingTimer = setTimeout(async () => {
|
|
214
|
+
await this.processBatch(), this.scheduleProcessing();
|
|
215
|
+
}, this.options.processingInterval));
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Logs messages if logging is enabled
|
|
219
|
+
*/
|
|
220
|
+
log(e) {
|
|
221
|
+
this.options.enableLogging && this.logger.info(e);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Gets processor statistics
|
|
225
|
+
*/
|
|
226
|
+
async getStats() {
|
|
227
|
+
return {
|
|
228
|
+
isRunning: this.isRunning,
|
|
229
|
+
batchSize: this.options.batchSize,
|
|
230
|
+
maxRetries: this.options.maxRetries,
|
|
231
|
+
processingInterval: this.options.processingInterval,
|
|
232
|
+
registeredHandlers: Array.from(this.handlers.keys()),
|
|
233
|
+
middlewareCount: this.middlewares.length
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
class f {
|
|
238
|
+
constructor(e) {
|
|
239
|
+
this.eventBus = e;
|
|
240
|
+
}
|
|
241
|
+
async handle(e) {
|
|
242
|
+
const s = {
|
|
243
|
+
eventName: e.messageType.startsWith("integration_event:") ? e.messageType.replace("integration_event:", "") : e.messageType,
|
|
244
|
+
eventId: e.id,
|
|
245
|
+
occurredOn: e.createdAt,
|
|
246
|
+
version: 1,
|
|
247
|
+
...e.payload,
|
|
248
|
+
...e.metadata
|
|
249
|
+
};
|
|
250
|
+
await this.eventBus.publish(s);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
class M {
|
|
254
|
+
constructor(e, t = {}) {
|
|
255
|
+
this.logger = l.create("OutboxService"), this.repository = e, this.options = {
|
|
256
|
+
defaultPriority: t.defaultPriority ?? n.NORMAL,
|
|
257
|
+
defaultMetadata: t.defaultMetadata ?? {},
|
|
258
|
+
autoCleanup: t.autoCleanup ?? !1,
|
|
259
|
+
cleanupAfterDays: t.cleanupAfterDays ?? 7,
|
|
260
|
+
enableLogging: t.enableLogging ?? !1
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Saves a message to the outbox
|
|
265
|
+
*/
|
|
266
|
+
async saveMessage(e, t, s) {
|
|
267
|
+
const r = g.createMessage(e, t, {
|
|
268
|
+
priority: this.options.defaultPriority,
|
|
269
|
+
metadata: { ...this.options.defaultMetadata, ...s?.metadata },
|
|
270
|
+
...s
|
|
271
|
+
}), a = await this.repository.saveMessage(r);
|
|
272
|
+
return this.logger.debug(`Saved message ${a} of type ${e}`), a;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Saves multiple messages to the outbox in a batch
|
|
276
|
+
*/
|
|
277
|
+
async saveMessages(e) {
|
|
278
|
+
const t = e.map(
|
|
279
|
+
({ messageType: r, payload: a, options: c }) => g.createMessage(r, a, {
|
|
280
|
+
priority: this.options.defaultPriority,
|
|
281
|
+
metadata: { ...this.options.defaultMetadata, ...c?.metadata },
|
|
282
|
+
...c
|
|
283
|
+
})
|
|
284
|
+
), s = await this.repository.saveBatch(t);
|
|
285
|
+
return this.logger.debug(`Saved batch of ${e.length} messages`), s;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Schedules a message for delayed processing
|
|
289
|
+
*/
|
|
290
|
+
async scheduleMessage(e, t, s, r) {
|
|
291
|
+
const a = g.createDelayedMessage(e, t, s, {
|
|
292
|
+
priority: this.options.defaultPriority,
|
|
293
|
+
metadata: { ...this.options.defaultMetadata, ...r?.metadata },
|
|
294
|
+
...r
|
|
295
|
+
}), c = await this.repository.scheduleMessage(a, a.processAfter);
|
|
296
|
+
return this.logger.debug(
|
|
297
|
+
`Scheduled message ${c} of type ${e} for processing in ${s}ms`
|
|
298
|
+
), c;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Saves a high priority message to the outbox
|
|
302
|
+
*/
|
|
303
|
+
async saveHighPriorityMessage(e, t, s) {
|
|
304
|
+
return this.saveMessage(e, t, {
|
|
305
|
+
...s,
|
|
306
|
+
priority: n.HIGH
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Saves a critical priority message to the outbox
|
|
311
|
+
*/
|
|
312
|
+
async saveCriticalMessage(e, t, s) {
|
|
313
|
+
return this.saveMessage(e, t, {
|
|
314
|
+
...s,
|
|
315
|
+
priority: n.CRITICAL
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Converts a domain event to an outbox message and saves it
|
|
320
|
+
*/
|
|
321
|
+
async saveDomainEvent(e, t) {
|
|
322
|
+
const s = g.createFromIntegrationEvent(
|
|
323
|
+
{
|
|
324
|
+
eventName: e.eventName,
|
|
325
|
+
payload: e,
|
|
326
|
+
metadata: {
|
|
327
|
+
eventId: e?.metadata?.eventId,
|
|
328
|
+
occurredOn: e?.metadata?.occurredOn,
|
|
329
|
+
version: e?.metadata?.version
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
priority: this.options.defaultPriority,
|
|
334
|
+
metadata: { ...this.options.defaultMetadata, ...t?.metadata },
|
|
335
|
+
...t
|
|
336
|
+
}
|
|
337
|
+
), r = await this.repository.saveMessage(s);
|
|
338
|
+
return this.logger.debug(`Saved domain event ${e.eventName} as message ${r}`), r;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Gets a message by ID
|
|
342
|
+
*/
|
|
343
|
+
async getMessage(e) {
|
|
344
|
+
return this.repository.getById(e);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Gets pending messages (for monitoring purposes)
|
|
348
|
+
*/
|
|
349
|
+
async getPendingMessages(e = 100) {
|
|
350
|
+
return this.repository.getUnprocessedMessages(e);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Retries a failed message by resetting it to pending status
|
|
354
|
+
*/
|
|
355
|
+
async retryMessage(e) {
|
|
356
|
+
await this.repository.updateStatus(e, o.PENDING), this.logger.info(`Reset message ${e} to pending for retry`);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Retries multiple failed messages
|
|
360
|
+
*/
|
|
361
|
+
async retryMessages(e) {
|
|
362
|
+
await this.repository.updateStatusBatch(e, o.PENDING), this.logger.info(`Reset ${e.length} messages to pending for retry`);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Cleans up old processed messages
|
|
366
|
+
*/
|
|
367
|
+
async cleanup(e) {
|
|
368
|
+
const t = e ?? this.options.cleanupAfterDays, s = /* @__PURE__ */ new Date();
|
|
369
|
+
s.setDate(s.getDate() - t);
|
|
370
|
+
const r = await this.repository.deleteByStatusAndAge(
|
|
371
|
+
s,
|
|
372
|
+
o.PROCESSED
|
|
373
|
+
);
|
|
374
|
+
return this.logger.info(`Cleaned up ${r} processed messages older than ${t} days`), r;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Gets outbox statistics
|
|
378
|
+
*/
|
|
379
|
+
async getStats() {
|
|
380
|
+
const e = this.repository;
|
|
381
|
+
return e.getMessageCountByStatus ? {
|
|
382
|
+
totalPending: e.getMessageCountByStatus(o.PENDING),
|
|
383
|
+
totalProcessing: e.getMessageCountByStatus(o.PROCESSING),
|
|
384
|
+
totalProcessed: e.getMessageCountByStatus(o.PROCESSED),
|
|
385
|
+
totalFailed: e.getMessageCountByStatus(o.FAILED)
|
|
386
|
+
} : {
|
|
387
|
+
totalPending: (await this.repository.getUnprocessedMessages(1e3)).length,
|
|
388
|
+
totalProcessing: 0,
|
|
389
|
+
// Would need separate query
|
|
390
|
+
totalProcessed: 0,
|
|
391
|
+
// Would need separate query
|
|
392
|
+
totalFailed: 0
|
|
393
|
+
// Would need separate query
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Logs messages if logging is enabled
|
|
398
|
+
*/
|
|
399
|
+
log(e) {
|
|
400
|
+
this.options.enableLogging && this.logger.info(e);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
export {
|
|
404
|
+
f as EventBusOutboxHandler,
|
|
405
|
+
y as IOutboxRepository,
|
|
406
|
+
n as MessagePriority,
|
|
407
|
+
o as MessageStatus,
|
|
408
|
+
g as OutboxMessageFactory,
|
|
409
|
+
m as OutboxProcessor,
|
|
410
|
+
M as OutboxService
|
|
411
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare enum MessageStatus {
|
|
2
|
+
PENDING = "PENDING",
|
|
3
|
+
PROCESSING = "PROCESSING",
|
|
4
|
+
PROCESSED = "PROCESSED",
|
|
5
|
+
FAILED = "FAILED"
|
|
6
|
+
}
|
|
7
|
+
export declare enum MessagePriority {
|
|
8
|
+
LOW = "low",
|
|
9
|
+
NORMAL = "normal",
|
|
10
|
+
HIGH = "high",
|
|
11
|
+
CRITICAL = "critical"
|
|
12
|
+
}
|
|
13
|
+
export interface IOutboxMessage<T = unknown> {
|
|
14
|
+
/** Unique identifier for the message */
|
|
15
|
+
id: string;
|
|
16
|
+
/** Type of message */
|
|
17
|
+
messageType: string;
|
|
18
|
+
/** Message payload */
|
|
19
|
+
payload: T;
|
|
20
|
+
/** Additional metadata */
|
|
21
|
+
metadata: Record<string, unknown>;
|
|
22
|
+
/** Processing status */
|
|
23
|
+
status: MessageStatus;
|
|
24
|
+
/** Processing attempts count */
|
|
25
|
+
attempts: number;
|
|
26
|
+
/** When the message was created */
|
|
27
|
+
createdAt: Date;
|
|
28
|
+
/** When to process the message (for delayed processing) */
|
|
29
|
+
processAfter?: Date;
|
|
30
|
+
/** Priority level */
|
|
31
|
+
priority?: MessagePriority;
|
|
32
|
+
/** Last error message (if any) */
|
|
33
|
+
lastError?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface OutboxMessageOptions {
|
|
36
|
+
/** When to process the message (for delayed processing) */
|
|
37
|
+
processAfter?: Date;
|
|
38
|
+
/** Priority level */
|
|
39
|
+
priority?: MessagePriority;
|
|
40
|
+
/** Additional metadata */
|
|
41
|
+
metadata?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
export interface IOutboxMessageHandler<T = unknown> {
|
|
44
|
+
/**
|
|
45
|
+
* Handles a message from the outbox
|
|
46
|
+
* @param message Message to handle
|
|
47
|
+
*/
|
|
48
|
+
handle(message: IOutboxMessage<T>): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
export type OutboxMiddleware = (next: (message: IOutboxMessage) => Promise<void>) => (message: IOutboxMessage) => Promise<void>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { IOutboxMessage, OutboxMessageOptions } from './outbox-interfaces';
|
|
2
|
+
export declare class OutboxMessageFactory {
|
|
3
|
+
/**
|
|
4
|
+
* Creates a new outbox message
|
|
5
|
+
* @param messageType Type of the message
|
|
6
|
+
* @param payload Message payload
|
|
7
|
+
* @param options Additional options
|
|
8
|
+
* @returns Outbox message
|
|
9
|
+
*/
|
|
10
|
+
static createMessage<T = unknown>(messageType: string, payload: T, options?: OutboxMessageOptions): IOutboxMessage<T>;
|
|
11
|
+
/**
|
|
12
|
+
* Creates a delayed outbox message
|
|
13
|
+
* @param messageType Type of the message
|
|
14
|
+
* @param payload Message payload
|
|
15
|
+
* @param delayMs Delay in milliseconds
|
|
16
|
+
* @param options Additional options
|
|
17
|
+
* @returns Outbox message scheduled for later processing
|
|
18
|
+
*/
|
|
19
|
+
static createDelayedMessage<T = unknown>(messageType: string, payload: T, delayMs: number, options?: OutboxMessageOptions): IOutboxMessage<T>;
|
|
20
|
+
/**
|
|
21
|
+
* Creates a high priority outbox message
|
|
22
|
+
* @param messageType Type of the message
|
|
23
|
+
* @param payload Message payload
|
|
24
|
+
* @param options Additional options
|
|
25
|
+
* @returns High priority outbox message
|
|
26
|
+
*/
|
|
27
|
+
static createHighPriorityMessage<T = unknown>(messageType: string, payload: T, options?: OutboxMessageOptions): IOutboxMessage<T>;
|
|
28
|
+
/**
|
|
29
|
+
* Creates an outbox message from an integration event
|
|
30
|
+
* @param event Integration event
|
|
31
|
+
* @param options Additional options
|
|
32
|
+
* @returns Outbox message containing the integration event
|
|
33
|
+
*/
|
|
34
|
+
static createFromIntegrationEvent<T = unknown>(event: {
|
|
35
|
+
eventName: string;
|
|
36
|
+
payload?: T;
|
|
37
|
+
metadata?: Record<string, unknown>;
|
|
38
|
+
}, options?: OutboxMessageOptions): IOutboxMessage<T>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { IEventBus } from '../../../contracts/src/index.ts';
|
|
2
|
+
import { IOutboxMessage, IOutboxMessageHandler, OutboxMiddleware, MessagePriority } from './outbox-interfaces';
|
|
3
|
+
import { IOutboxRepository } from './outbox-repository.interface';
|
|
4
|
+
export interface OutboxProcessorOptions {
|
|
5
|
+
/** Maximum number of messages to process in one batch */
|
|
6
|
+
batchSize?: number;
|
|
7
|
+
/** Maximum retry attempts for failed messages */
|
|
8
|
+
maxRetries?: number;
|
|
9
|
+
/** Processing interval in milliseconds */
|
|
10
|
+
processingInterval?: number;
|
|
11
|
+
/** Priority order for message processing */
|
|
12
|
+
priorityOrder?: MessagePriority[];
|
|
13
|
+
/** Timeout for processing a single message */
|
|
14
|
+
messageTimeout?: number;
|
|
15
|
+
/** Enable logging */
|
|
16
|
+
enableLogging?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare class OutboxProcessor {
|
|
19
|
+
private readonly repository;
|
|
20
|
+
private readonly options;
|
|
21
|
+
private readonly handlers;
|
|
22
|
+
private readonly middlewares;
|
|
23
|
+
private isRunning;
|
|
24
|
+
private processingTimer?;
|
|
25
|
+
private readonly logger;
|
|
26
|
+
constructor(repository: IOutboxRepository, options?: OutboxProcessorOptions);
|
|
27
|
+
/**
|
|
28
|
+
* Registers a message handler for a specific message type
|
|
29
|
+
*/
|
|
30
|
+
registerHandler(messageType: string, handler: IOutboxMessageHandler): void;
|
|
31
|
+
/**
|
|
32
|
+
* Registers middleware for message processing pipeline
|
|
33
|
+
*/
|
|
34
|
+
use(middleware: OutboxMiddleware): void;
|
|
35
|
+
/**
|
|
36
|
+
* Starts the processor with automatic interval processing
|
|
37
|
+
*/
|
|
38
|
+
start(): void;
|
|
39
|
+
/**
|
|
40
|
+
* Stops the processor
|
|
41
|
+
*/
|
|
42
|
+
stop(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Processes a single batch of pending messages.
|
|
45
|
+
*
|
|
46
|
+
* **Dispatch model:** messages within a batch are dispatched **in parallel**
|
|
47
|
+
* via `Promise.allSettled`. A failure in message N does **not** block
|
|
48
|
+
* messages N+1..N+M. Each failure is isolated, retry-counted via
|
|
49
|
+
* `incrementAttempt`, and reset to `PENDING` (or marked `FAILED` after
|
|
50
|
+
* `maxRetries` attempts) — independently of sibling messages.
|
|
51
|
+
*
|
|
52
|
+
* **Ordering:** parallel dispatch means message processing order is
|
|
53
|
+
* **not guaranteed** within a batch, even if `priorityOrder` was used to
|
|
54
|
+
* select which messages to fetch. If you need strict ordering, set
|
|
55
|
+
* `batchSize: 1` (one message per batch = sequential by definition).
|
|
56
|
+
*
|
|
57
|
+
* **Throughput:** for N messages and dispatch latency L, total batch time
|
|
58
|
+
* is approximately L (parallel) rather than N*L (sequential). For 200
|
|
59
|
+
* messages at 10ms dispatch each: ~10ms parallel vs ~2s sequential.
|
|
60
|
+
*
|
|
61
|
+
* **Quiescence:** if there are no pending messages, returns immediately
|
|
62
|
+
* without invoking handlers.
|
|
63
|
+
*
|
|
64
|
+
* @public
|
|
65
|
+
* @stable
|
|
66
|
+
* @since 0.1.0
|
|
67
|
+
*/
|
|
68
|
+
processBatch(): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Processes a single message through the middleware pipeline
|
|
71
|
+
*/
|
|
72
|
+
private processMessage;
|
|
73
|
+
/**
|
|
74
|
+
* Builds the middleware pipeline for message processing
|
|
75
|
+
*/
|
|
76
|
+
private buildPipeline;
|
|
77
|
+
/**
|
|
78
|
+
* Handles errors during message processing
|
|
79
|
+
*/
|
|
80
|
+
private handleMessageError;
|
|
81
|
+
/**
|
|
82
|
+
* Schedules the next processing cycle
|
|
83
|
+
*/
|
|
84
|
+
private scheduleProcessing;
|
|
85
|
+
/**
|
|
86
|
+
* Logs messages if logging is enabled
|
|
87
|
+
*/
|
|
88
|
+
private log;
|
|
89
|
+
/**
|
|
90
|
+
* Gets processor statistics
|
|
91
|
+
*/
|
|
92
|
+
getStats(): Promise<{
|
|
93
|
+
isRunning: boolean;
|
|
94
|
+
batchSize: number;
|
|
95
|
+
maxRetries: number;
|
|
96
|
+
processingInterval: number;
|
|
97
|
+
registeredHandlers: string[];
|
|
98
|
+
middlewareCount: number;
|
|
99
|
+
}>;
|
|
100
|
+
}
|
|
101
|
+
export declare class EventBusOutboxHandler implements IOutboxMessageHandler {
|
|
102
|
+
private readonly eventBus;
|
|
103
|
+
constructor(eventBus: IEventBus);
|
|
104
|
+
handle(message: IOutboxMessage): Promise<void>;
|
|
105
|
+
}
|