@vytches/ddd-messaging 0.27.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/dist/index.cjs CHANGED
@@ -1 +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;
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 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 {
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 g {
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: h.getUUID(),
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 || n.NORMAL
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 g.createMessage(e, t, {
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 g.createMessage(e, t, {
69
+ return c.createMessage(e, t, {
51
70
  ...s,
52
- priority: n.HIGH
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), g.createMessage(
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 m {
94
+ class w {
76
95
  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,
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
- n.CRITICAL,
83
- n.HIGH,
84
- n.NORMAL,
85
- n.LOW
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 d(
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 d(async () => {
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, c) => setTimeout(
189
- () => c(new Error("Message processing timeout")),
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 ? (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})`));
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 f {
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 M {
292
+ class S {
254
293
  constructor(e, t = {}) {
255
- this.logger = l.create("OutboxService"), this.repository = e, this.options = {
256
- defaultPriority: t.defaultPriority ?? n.NORMAL,
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 = g.createMessage(e, t, {
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: c }) => g.createMessage(r, a, {
318
+ ({ messageType: r, payload: a, options: n }) => c.createMessage(r, a, {
280
319
  priority: this.options.defaultPriority,
281
- metadata: { ...this.options.defaultMetadata, ...c?.metadata },
282
- ...c
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 = g.createDelayedMessage(e, t, s, {
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
- }), c = await this.repository.scheduleMessage(a, a.processAfter);
334
+ }), n = await this.repository.scheduleMessage(a, a.processAfter);
296
335
  return this.logger.debug(
297
- `Scheduled message ${c} of type ${e} for processing in ${s}ms`
298
- ), c;
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: n.HIGH
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: n.CRITICAL
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 = g.createFromIntegrationEvent(
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
- f as EventBusOutboxHandler,
405
- y as IOutboxRepository,
406
- n as MessagePriority,
443
+ M as EventBusOutboxHandler,
444
+ m as IOutboxRepository,
445
+ g as MessagePriority,
407
446
  o as MessageStatus,
408
- g as OutboxMessageFactory,
409
- m as OutboxProcessor,
410
- M as OutboxService
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.27.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": "^2.0.0",
40
- "@vytches/ddd-testing": "0.27.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.27.0"
54
+ "@vytches/ddd-events": "0.28.0"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "vite build",