@vytches/ddd-messaging 0.26.0 → 0.28.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/README.md +82 -1041
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +84 -45
- package/dist/outbox/outbox-processor.d.ts +17 -0
- package/dist/outbox/outbox-repository.interface.d.ts +17 -0
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const u=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||{}),g=(i=>(i.LOW="low",i.NORMAL="normal",i.HIGH="high",i.CRITICAL="critical",i))(g||{});class p{scheduleRetry(e,t){return Promise.resolve()}}class c{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||g.NORMAL};return s?.processAfter&&(r.processAfter=s.processAfter),r}static createDelayedMessage(e,t,s,r){const a=new Date(Date.now()+s);return c.createMessage(e,t,{...r,processAfter:a})}static createHighPriorityMessage(e,t,s){return c.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),c.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:r,multiplier:a,maxDelay:n}=t.retryBackoff;if(r<=0||a<=0||n<=0)throw new RangeError(`OutboxProcessor: retryBackoff fields must be positive numbers (initial=${r}, multiplier=${a}, maxDelay=${n})`)}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:t.messageTimeout??3e4,enableLogging:t.enableLogging??!1,retryBackoff:t.retryBackoff}}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,n)=>setTimeout(()=>n(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}`),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`);else if(this.options.retryBackoff){const{initial:r,multiplier:a,maxDelay:n}=this.options.retryBackoff,l=Math.min(r*Math.pow(a,s-1),n),h=new Date(Date.now()+l);await this.repository.updateStatus(e.id,o.PENDING),await this.repository.scheduleRetry(e.id,h),this.logger.info(`Message ${e.id} scheduled for retry in ${l}ms (attempt ${s})`)}else 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 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=c.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:n})=>c.createMessage(r,a,{priority:this.options.defaultPriority,metadata:{...this.options.defaultMetadata,...n?.metadata},...n})),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=c.createDelayedMessage(e,t,s,{priority:this.options.defaultPriority,metadata:{...this.options.defaultMetadata,...r?.metadata},...r}),n=await this.repository.scheduleMessage(a,a.processAfter);return this.logger.debug(`Scheduled message ${n} of type ${e} for processing in ${s}ms`),n}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=c.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=f;exports.IOutboxRepository=p;exports.MessagePriority=g;exports.MessageStatus=o;exports.OutboxMessageFactory=c;exports.OutboxProcessor=y;exports.OutboxService=m;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export type { IOutboxMessage, OutboxMessageOptions, IOutboxMessageHandler, OutboxMiddleware, OutboxProcessorOptions, OutboxServiceOptions, } from './outbox';
|
|
1
|
+
export type { IOutboxMessage, OutboxMessageOptions, IOutboxMessageHandler, OutboxMiddleware, OutboxProcessorOptions, RetryBackoffConfig, OutboxServiceOptions, } from './outbox';
|
|
2
2
|
export { MessageStatus, MessagePriority } from './outbox';
|
|
3
3
|
export { IOutboxRepository, OutboxMessageFactory, OutboxProcessor, EventBusOutboxHandler, OutboxService, } from './outbox';
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
|
-
import { Logger as
|
|
2
|
-
import { LibUtils as
|
|
3
|
-
var o = /* @__PURE__ */ ((i) => (i.PENDING = "PENDING", i.PROCESSING = "PROCESSING", i.PROCESSED = "PROCESSED", i.FAILED = "FAILED", i))(o || {}),
|
|
4
|
-
class
|
|
1
|
+
import { Logger as h } from "@vytches/ddd-logging";
|
|
2
|
+
import { LibUtils as p, safeRun as l } from "@vytches/ddd-utils";
|
|
3
|
+
var o = /* @__PURE__ */ ((i) => (i.PENDING = "PENDING", i.PROCESSING = "PROCESSING", i.PROCESSED = "PROCESSED", i.FAILED = "FAILED", i))(o || {}), g = /* @__PURE__ */ ((i) => (i.LOW = "low", i.NORMAL = "normal", i.HIGH = "high", i.CRITICAL = "critical", i))(g || {});
|
|
4
|
+
class m {
|
|
5
|
+
/**
|
|
6
|
+
* Schedules a retry for a failed message at a specific future time.
|
|
7
|
+
*
|
|
8
|
+
* Default implementation is a no-op — repositories that do not support
|
|
9
|
+
* delayed processing will fall back to immediate retry (PENDING reset).
|
|
10
|
+
* Override this in concrete repositories to enable exponential backoff.
|
|
11
|
+
*
|
|
12
|
+
* **Multi-worker note:** `OutboxProcessor` calls `updateStatus(PENDING)` immediately
|
|
13
|
+
* before this method. In multi-worker environments, override this to perform
|
|
14
|
+
* an atomic `UPDATE ... SET status=PENDING, process_after=?` in a single statement,
|
|
15
|
+
* and ensure `getUnprocessedMessages` filters `WHERE process_after IS NULL OR process_after <= NOW()`
|
|
16
|
+
* to prevent duplicate dispatch during the window between the two calls.
|
|
17
|
+
*
|
|
18
|
+
* @param id Message ID
|
|
19
|
+
* @param processAfter When to retry the message
|
|
20
|
+
*/
|
|
21
|
+
scheduleRetry(e, t) {
|
|
22
|
+
return Promise.resolve();
|
|
23
|
+
}
|
|
5
24
|
}
|
|
6
|
-
class
|
|
25
|
+
class c {
|
|
7
26
|
/**
|
|
8
27
|
* Creates a new outbox message
|
|
9
28
|
* @param messageType Type of the message
|
|
@@ -13,14 +32,14 @@ class g {
|
|
|
13
32
|
*/
|
|
14
33
|
static createMessage(e, t, s) {
|
|
15
34
|
const r = {
|
|
16
|
-
id:
|
|
35
|
+
id: p.getUUID(),
|
|
17
36
|
messageType: e,
|
|
18
37
|
payload: t,
|
|
19
38
|
metadata: s?.metadata || {},
|
|
20
39
|
status: o.PENDING,
|
|
21
40
|
attempts: 0,
|
|
22
41
|
createdAt: /* @__PURE__ */ new Date(),
|
|
23
|
-
priority: s?.priority ||
|
|
42
|
+
priority: s?.priority || g.NORMAL
|
|
24
43
|
};
|
|
25
44
|
return s?.processAfter && (r.processAfter = s.processAfter), r;
|
|
26
45
|
}
|
|
@@ -34,7 +53,7 @@ class g {
|
|
|
34
53
|
*/
|
|
35
54
|
static createDelayedMessage(e, t, s, r) {
|
|
36
55
|
const a = new Date(Date.now() + s);
|
|
37
|
-
return
|
|
56
|
+
return c.createMessage(e, t, {
|
|
38
57
|
...r,
|
|
39
58
|
processAfter: a
|
|
40
59
|
});
|
|
@@ -47,9 +66,9 @@ class g {
|
|
|
47
66
|
* @returns High priority outbox message
|
|
48
67
|
*/
|
|
49
68
|
static createHighPriorityMessage(e, t, s) {
|
|
50
|
-
return
|
|
69
|
+
return c.createMessage(e, t, {
|
|
51
70
|
...s,
|
|
52
|
-
priority:
|
|
71
|
+
priority: g.HIGH
|
|
53
72
|
});
|
|
54
73
|
}
|
|
55
74
|
/**
|
|
@@ -65,27 +84,39 @@ class g {
|
|
|
65
84
|
...t?.metadata || {}
|
|
66
85
|
}
|
|
67
86
|
};
|
|
68
|
-
return t?.priority && (s.priority = t.priority), t?.processAfter && (s.processAfter = t.processAfter),
|
|
87
|
+
return t?.priority && (s.priority = t.priority), t?.processAfter && (s.processAfter = t.processAfter), c.createMessage(
|
|
69
88
|
`integration_event:${e.eventName}`,
|
|
70
89
|
e.payload,
|
|
71
90
|
s
|
|
72
91
|
);
|
|
73
92
|
}
|
|
74
93
|
}
|
|
75
|
-
class
|
|
94
|
+
class w {
|
|
76
95
|
constructor(e, t = {}) {
|
|
77
|
-
this.handlers = /* @__PURE__ */ new Map(), this.middlewares = [], this.isRunning = !1, this.logger =
|
|
78
|
-
|
|
96
|
+
this.handlers = /* @__PURE__ */ new Map(), this.middlewares = [], this.isRunning = !1, this.logger = h.create("OutboxProcessor");
|
|
97
|
+
const s = t.batchSize ?? 10;
|
|
98
|
+
if (s > 1e4)
|
|
99
|
+
throw new RangeError(`OutboxProcessor: batchSize must be ≤ 10,000, got ${s}`);
|
|
100
|
+
if (t.retryBackoff) {
|
|
101
|
+
const { initial: r, multiplier: a, maxDelay: n } = t.retryBackoff;
|
|
102
|
+
if (r <= 0 || a <= 0 || n <= 0)
|
|
103
|
+
throw new RangeError(
|
|
104
|
+
`OutboxProcessor: retryBackoff fields must be positive numbers (initial=${r}, multiplier=${a}, maxDelay=${n})`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
this.repository = e, this.options = {
|
|
108
|
+
batchSize: s,
|
|
79
109
|
maxRetries: t.maxRetries ?? 3,
|
|
80
110
|
processingInterval: t.processingInterval ?? 5e3,
|
|
81
111
|
priorityOrder: t.priorityOrder ?? [
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
112
|
+
g.CRITICAL,
|
|
113
|
+
g.HIGH,
|
|
114
|
+
g.NORMAL,
|
|
115
|
+
g.LOW
|
|
86
116
|
],
|
|
87
117
|
messageTimeout: t.messageTimeout ?? 3e4,
|
|
88
|
-
enableLogging: t.enableLogging ?? !1
|
|
118
|
+
enableLogging: t.enableLogging ?? !1,
|
|
119
|
+
retryBackoff: t.retryBackoff
|
|
89
120
|
};
|
|
90
121
|
}
|
|
91
122
|
/**
|
|
@@ -148,7 +179,7 @@ class m {
|
|
|
148
179
|
async processBatch() {
|
|
149
180
|
if (!this.isRunning)
|
|
150
181
|
return;
|
|
151
|
-
const [e, t] = await
|
|
182
|
+
const [e, t] = await l(
|
|
152
183
|
() => this.repository.getUnprocessedMessages(this.options.batchSize, this.options.priorityOrder)
|
|
153
184
|
);
|
|
154
185
|
if (e) {
|
|
@@ -169,7 +200,7 @@ class m {
|
|
|
169
200
|
* Processes a single message through the middleware pipeline
|
|
170
201
|
*/
|
|
171
202
|
async processMessage(e) {
|
|
172
|
-
const [t] = await
|
|
203
|
+
const [t] = await l(async () => {
|
|
173
204
|
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
205
|
});
|
|
175
206
|
t && await this.handleMessageError(e, t);
|
|
@@ -185,8 +216,8 @@ class m {
|
|
|
185
216
|
await Promise.race([
|
|
186
217
|
t.handle(r),
|
|
187
218
|
new Promise(
|
|
188
|
-
(a,
|
|
189
|
-
() =>
|
|
219
|
+
(a, n) => setTimeout(
|
|
220
|
+
() => n(new Error("Message processing timeout")),
|
|
190
221
|
this.options.messageTimeout
|
|
191
222
|
)
|
|
192
223
|
)
|
|
@@ -200,7 +231,15 @@ class m {
|
|
|
200
231
|
async handleMessageError(e, t) {
|
|
201
232
|
try {
|
|
202
233
|
const s = await this.repository.incrementAttempt(e.id);
|
|
203
|
-
this.logger.warn(`Message ${e.id} failed, attempt ${s}: ${t?.message}`), s >= this.options.maxRetries
|
|
234
|
+
if (this.logger.warn(`Message ${e.id} failed, attempt ${s}: ${t?.message}`), s >= this.options.maxRetries)
|
|
235
|
+
await this.repository.updateStatus(e.id, o.FAILED, t), this.logger.error(`Message ${e.id} marked as failed after ${s} attempts`);
|
|
236
|
+
else if (this.options.retryBackoff) {
|
|
237
|
+
const { initial: r, multiplier: a, maxDelay: n } = this.options.retryBackoff, d = Math.min(r * Math.pow(a, s - 1), n), u = new Date(Date.now() + d);
|
|
238
|
+
await this.repository.updateStatus(e.id, o.PENDING), await this.repository.scheduleRetry(e.id, u), this.logger.info(
|
|
239
|
+
`Message ${e.id} scheduled for retry in ${d}ms (attempt ${s})`
|
|
240
|
+
);
|
|
241
|
+
} else
|
|
242
|
+
await this.repository.updateStatus(e.id, o.PENDING), this.logger.info(`Message ${e.id} scheduled for retry (attempt ${s + 1})`);
|
|
204
243
|
} catch (s) {
|
|
205
244
|
const r = s instanceof Error ? s.message : String(s);
|
|
206
245
|
this.logger.error(`Error updating message status: ${r}`);
|
|
@@ -234,7 +273,7 @@ class m {
|
|
|
234
273
|
};
|
|
235
274
|
}
|
|
236
275
|
}
|
|
237
|
-
class
|
|
276
|
+
class M {
|
|
238
277
|
constructor(e) {
|
|
239
278
|
this.eventBus = e;
|
|
240
279
|
}
|
|
@@ -250,10 +289,10 @@ class f {
|
|
|
250
289
|
await this.eventBus.publish(s);
|
|
251
290
|
}
|
|
252
291
|
}
|
|
253
|
-
class
|
|
292
|
+
class S {
|
|
254
293
|
constructor(e, t = {}) {
|
|
255
|
-
this.logger =
|
|
256
|
-
defaultPriority: t.defaultPriority ??
|
|
294
|
+
this.logger = h.create("OutboxService"), this.repository = e, this.options = {
|
|
295
|
+
defaultPriority: t.defaultPriority ?? g.NORMAL,
|
|
257
296
|
defaultMetadata: t.defaultMetadata ?? {},
|
|
258
297
|
autoCleanup: t.autoCleanup ?? !1,
|
|
259
298
|
cleanupAfterDays: t.cleanupAfterDays ?? 7,
|
|
@@ -264,7 +303,7 @@ class M {
|
|
|
264
303
|
* Saves a message to the outbox
|
|
265
304
|
*/
|
|
266
305
|
async saveMessage(e, t, s) {
|
|
267
|
-
const r =
|
|
306
|
+
const r = c.createMessage(e, t, {
|
|
268
307
|
priority: this.options.defaultPriority,
|
|
269
308
|
metadata: { ...this.options.defaultMetadata, ...s?.metadata },
|
|
270
309
|
...s
|
|
@@ -276,10 +315,10 @@ class M {
|
|
|
276
315
|
*/
|
|
277
316
|
async saveMessages(e) {
|
|
278
317
|
const t = e.map(
|
|
279
|
-
({ messageType: r, payload: a, options:
|
|
318
|
+
({ messageType: r, payload: a, options: n }) => c.createMessage(r, a, {
|
|
280
319
|
priority: this.options.defaultPriority,
|
|
281
|
-
metadata: { ...this.options.defaultMetadata, ...
|
|
282
|
-
...
|
|
320
|
+
metadata: { ...this.options.defaultMetadata, ...n?.metadata },
|
|
321
|
+
...n
|
|
283
322
|
})
|
|
284
323
|
), s = await this.repository.saveBatch(t);
|
|
285
324
|
return this.logger.debug(`Saved batch of ${e.length} messages`), s;
|
|
@@ -288,14 +327,14 @@ class M {
|
|
|
288
327
|
* Schedules a message for delayed processing
|
|
289
328
|
*/
|
|
290
329
|
async scheduleMessage(e, t, s, r) {
|
|
291
|
-
const a =
|
|
330
|
+
const a = c.createDelayedMessage(e, t, s, {
|
|
292
331
|
priority: this.options.defaultPriority,
|
|
293
332
|
metadata: { ...this.options.defaultMetadata, ...r?.metadata },
|
|
294
333
|
...r
|
|
295
|
-
}),
|
|
334
|
+
}), n = await this.repository.scheduleMessage(a, a.processAfter);
|
|
296
335
|
return this.logger.debug(
|
|
297
|
-
`Scheduled message ${
|
|
298
|
-
),
|
|
336
|
+
`Scheduled message ${n} of type ${e} for processing in ${s}ms`
|
|
337
|
+
), n;
|
|
299
338
|
}
|
|
300
339
|
/**
|
|
301
340
|
* Saves a high priority message to the outbox
|
|
@@ -303,7 +342,7 @@ class M {
|
|
|
303
342
|
async saveHighPriorityMessage(e, t, s) {
|
|
304
343
|
return this.saveMessage(e, t, {
|
|
305
344
|
...s,
|
|
306
|
-
priority:
|
|
345
|
+
priority: g.HIGH
|
|
307
346
|
});
|
|
308
347
|
}
|
|
309
348
|
/**
|
|
@@ -312,14 +351,14 @@ class M {
|
|
|
312
351
|
async saveCriticalMessage(e, t, s) {
|
|
313
352
|
return this.saveMessage(e, t, {
|
|
314
353
|
...s,
|
|
315
|
-
priority:
|
|
354
|
+
priority: g.CRITICAL
|
|
316
355
|
});
|
|
317
356
|
}
|
|
318
357
|
/**
|
|
319
358
|
* Converts a domain event to an outbox message and saves it
|
|
320
359
|
*/
|
|
321
360
|
async saveDomainEvent(e, t) {
|
|
322
|
-
const s =
|
|
361
|
+
const s = c.createFromIntegrationEvent(
|
|
323
362
|
{
|
|
324
363
|
eventName: e.eventName,
|
|
325
364
|
payload: e,
|
|
@@ -401,11 +440,11 @@ class M {
|
|
|
401
440
|
}
|
|
402
441
|
}
|
|
403
442
|
export {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
443
|
+
M as EventBusOutboxHandler,
|
|
444
|
+
m as IOutboxRepository,
|
|
445
|
+
g as MessagePriority,
|
|
407
446
|
o as MessageStatus,
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
447
|
+
c as OutboxMessageFactory,
|
|
448
|
+
w as OutboxProcessor,
|
|
449
|
+
S as OutboxService
|
|
411
450
|
};
|
|
@@ -1,6 +1,14 @@
|
|
|
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
|
+
export interface RetryBackoffConfig {
|
|
5
|
+
/** Initial delay in milliseconds before first retry (default: 1000) */
|
|
6
|
+
initial: number;
|
|
7
|
+
/** Multiplier applied per subsequent attempt (default: 2) */
|
|
8
|
+
multiplier: number;
|
|
9
|
+
/** Maximum delay cap in milliseconds (default: 300_000 — 5 min) */
|
|
10
|
+
maxDelay: number;
|
|
11
|
+
}
|
|
4
12
|
export interface OutboxProcessorOptions {
|
|
5
13
|
/** Maximum number of messages to process in one batch */
|
|
6
14
|
batchSize?: number;
|
|
@@ -14,6 +22,15 @@ export interface OutboxProcessorOptions {
|
|
|
14
22
|
messageTimeout?: number;
|
|
15
23
|
/** Enable logging */
|
|
16
24
|
enableLogging?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Exponential backoff for retries.
|
|
27
|
+
* When set, failed messages are scheduled for future retry via
|
|
28
|
+
* `IOutboxRepository.scheduleRetry()` instead of immediately re-queued.
|
|
29
|
+
* If the repository's `scheduleRetry` is a no-op, messages fall back to
|
|
30
|
+
* immediate PENDING reset (legacy behavior).
|
|
31
|
+
* If omitted, immediate retry is used for all messages.
|
|
32
|
+
*/
|
|
33
|
+
retryBackoff?: RetryBackoffConfig;
|
|
17
34
|
}
|
|
18
35
|
export declare class OutboxProcessor {
|
|
19
36
|
private readonly repository;
|
|
@@ -58,4 +58,21 @@ export declare abstract class IOutboxRepository {
|
|
|
58
58
|
* @returns ID of the saved message
|
|
59
59
|
*/
|
|
60
60
|
abstract scheduleMessage<T = unknown>(message: IOutboxMessage<T>, processAfter: Date): Promise<string>;
|
|
61
|
+
/**
|
|
62
|
+
* Schedules a retry for a failed message at a specific future time.
|
|
63
|
+
*
|
|
64
|
+
* Default implementation is a no-op — repositories that do not support
|
|
65
|
+
* delayed processing will fall back to immediate retry (PENDING reset).
|
|
66
|
+
* Override this in concrete repositories to enable exponential backoff.
|
|
67
|
+
*
|
|
68
|
+
* **Multi-worker note:** `OutboxProcessor` calls `updateStatus(PENDING)` immediately
|
|
69
|
+
* before this method. In multi-worker environments, override this to perform
|
|
70
|
+
* an atomic `UPDATE ... SET status=PENDING, process_after=?` in a single statement,
|
|
71
|
+
* and ensure `getUnprocessedMessages` filters `WHERE process_after IS NULL OR process_after <= NOW()`
|
|
72
|
+
* to prevent duplicate dispatch during the window between the two calls.
|
|
73
|
+
*
|
|
74
|
+
* @param id Message ID
|
|
75
|
+
* @param processAfter When to retry the message
|
|
76
|
+
*/
|
|
77
|
+
scheduleRetry(_id: string, _processAfter: Date): Promise<void>;
|
|
61
78
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vytches/ddd-messaging",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Outbox pattern and reliable message delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -36,8 +36,8 @@
|
|
|
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.
|
|
39
|
+
"vitest": "^4.0.0",
|
|
40
|
+
"@vytches/ddd-testing": "0.28.0"
|
|
41
41
|
},
|
|
42
42
|
"nx": {
|
|
43
43
|
"tags": [
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@vytches/ddd-events": "0.
|
|
54
|
+
"@vytches/ddd-events": "0.28.0"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "vite build",
|