@vytches/ddd-messaging 0.27.0 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -1041
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +227 -77
- package/dist/outbox/index.d.ts +1 -0
- package/dist/outbox/outbox-hooks.d.ts +24 -0
- package/dist/outbox/outbox-processor.d.ts +103 -3
- package/dist/outbox/outbox-repository.interface.d.ts +41 -1
- package/package.json +3 -4
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 n=(i=>(i.PENDING="PENDING",i.PROCESSING="PROCESSING",i.PROCESSED="PROCESSED",i.FAILED="FAILED",i))(n||{}),g=(i=>(i.LOW="low",i.NORMAL="normal",i.HIGH="high",i.CRITICAL="critical",i))(g||{});class p{scheduleRetry(e,t){return Promise.resolve()}resetStaleProcessing(e){return Promise.resolve(0)}}class l{static createMessage(e,t,s){const r={id:d.LibUtils.getUUID(),messageType:e,payload:t,metadata:s?.metadata||{},status:n.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 l.createMessage(e,t,{...r,processAfter:a})}static createHighPriorityMessage(e,t,s){return l.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),l.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:o,multiplier:h,maxDelay:c}=t.retryBackoff;if(o<=0||h<=0||c<=0)throw new RangeError(`OutboxProcessor: retryBackoff fields must be positive numbers (initial=${o}, multiplier=${h}, maxDelay=${c})`)}const r=t.messageTimeout??3e4,a=t.crashRecoveryThresholdMs??Math.max(3e5,r);if(a<r)throw new RangeError(`OutboxProcessor: crashRecoveryThresholdMs (${a}) must be ≥ messageTimeout (${r}) to avoid resetting in-flight messages`);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:r,enableLogging:t.enableLogging??!1,retryBackoff:t.retryBackoff,messageTypes:t.messageTypes,crashRecoveryIntervalMs:t.crashRecoveryIntervalMs,crashRecoveryThresholdMs:a,adaptiveRepoll:t.adaptiveRepoll??!1,adaptiveRepollMaxConsecutive:t.adaptiveRepollMaxConsecutive??5,adaptiveRepollPauseMs:t.adaptiveRepollPauseMs??50,startupJitterMs:t.startupJitterMs??0,hooks:t.hooks}}safelyInvokeHook(e,t){if(this.options.hooks)try{t()}catch(s){const r=s instanceof Error?s.message:String(s);this.logger.error(`Outbox hook ${e} threw and was ignored: ${r}`)}}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");const e=this.options.startupJitterMs>0?Math.floor(Math.random()*this.options.startupJitterMs):0;this.scheduleProcessing(0,this.options.processingInterval+e),this.scheduleCrashRecovery()}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.crashRecoveryTimer&&(clearTimeout(this.crashRecoveryTimer),this.crashRecoveryTimer=void 0),this.logger.info("Stopped outbox processor")}async processBatch(){const e=this.options.batchSize;if(!this.isRunning)return{processed:0,batchSize:e};const[t,s]=await d.safeRun(()=>this.repository.getUnprocessedMessages(e,this.options.priorityOrder,this.options.messageTypes));if(t){const c=t instanceof Error?t.message:String(t);return this.logger.error(`Error retrieving messages: ${c}`),{processed:0,batchSize:e}}const r=s;if(!r||!Array.isArray(r)||r.length===0)return this.logger.debug("No pending messages to process"),{processed:0,batchSize:e};this.logger.info(`Processing ${r.length} messages`);const a=Date.now(),h=(await Promise.allSettled(r.map(c=>this.processMessage(c)))).filter(c=>c.status==="rejected"||c.status==="fulfilled"&&c.value===!1).length;return this.logger.info(`Completed processing batch of ${r.length} messages`),this.safelyInvokeHook("onBatchComplete",()=>this.options.hooks?.onBatchComplete?.({processed:r.length,failed:h,batchSize:e,durationMs:Date.now()-a})),{processed:r.length,batchSize:e}}async processMessage(e){const[t]=await d.safeRun(async()=>{await this.repository.updateStatus(e.id,n.PROCESSING),await this.buildPipeline(e)(e),await this.repository.updateStatus(e.id,n.PROCESSED),this.logger.debug(`Successfully processed message ${e.id}`)});return t?(await this.handleMessageError(e,t),!1):!0}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,o)=>setTimeout(()=>o(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}`),this.safelyInvokeHook("onMessageFailed",()=>this.options.hooks?.onMessageFailed?.(e,t,s)),s>=this.options.maxRetries)await this.repository.updateStatus(e.id,n.FAILED,t),this.logger.error(`Message ${e.id} marked as failed after ${s} attempts`),this.safelyInvokeHook("onPermanentFailure",()=>this.options.hooks?.onPermanentFailure?.(e,t));else if(this.options.retryBackoff){const{initial:r,multiplier:a,maxDelay:o}=this.options.retryBackoff,h=Math.min(r*Math.pow(a,s-1),o),c=new Date(Date.now()+h);await this.repository.updateStatus(e.id,n.PENDING),await this.repository.scheduleRetry(e.id,c),this.logger.info(`Message ${e.id} scheduled for retry in ${h}ms (attempt ${s})`)}else await this.repository.updateStatus(e.id,n.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(e=0,t=this.options.processingInterval){this.isRunning&&(this.processingTimer=setTimeout(async()=>{const s=await this.processBatch();if(this.options.adaptiveRepoll&&s.processed>0&&s.processed>=s.batchSize){const a=e+1,o=a>this.options.adaptiveRepollMaxConsecutive?this.options.adaptiveRepollPauseMs:0;this.scheduleProcessing(a,o)}else this.scheduleProcessing(0,this.options.processingInterval)},t))}scheduleCrashRecovery(){!this.isRunning||this.options.crashRecoveryIntervalMs===void 0||(this.crashRecoveryTimer=setTimeout(async()=>{await this.runCrashRecovery(),this.scheduleCrashRecovery()},this.options.crashRecoveryIntervalMs))}async runCrashRecovery(){const e=new Date(Date.now()-this.options.crashRecoveryThresholdMs),[t,s]=await d.safeRun(()=>this.repository.resetStaleProcessing(e));if(t){const r=t instanceof Error?t.message:String(t);this.logger.error(`Crash recovery failed: ${r}`);return}s&&s>0&&this.logger.warn(`Crash recovery reset ${s} stale PROCESSING message(s) to PENDING`)}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=l.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:o})=>l.createMessage(r,a,{priority:this.options.defaultPriority,metadata:{...this.options.defaultMetadata,...o?.metadata},...o})),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=l.createDelayedMessage(e,t,s,{priority:this.options.defaultPriority,metadata:{...this.options.defaultMetadata,...r?.metadata},...r}),o=await this.repository.scheduleMessage(a,a.processAfter);return this.logger.debug(`Scheduled message ${o} of type ${e} for processing in ${s}ms`),o}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=l.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,n.PENDING),this.logger.info(`Reset message ${e} to pending for retry`)}async retryMessages(e){await this.repository.updateStatusBatch(e,n.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,n.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(n.PENDING),totalProcessing:e.getMessageCountByStatus(n.PROCESSING),totalProcessed:e.getMessageCountByStatus(n.PROCESSED),totalFailed:e.getMessageCountByStatus(n.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=n;exports.OutboxMessageFactory=l;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, OutboxProcessorHooks, 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,49 @@
|
|
|
1
|
-
import { Logger as
|
|
2
|
-
import { LibUtils as
|
|
3
|
-
var
|
|
4
|
-
class
|
|
1
|
+
import { Logger as u } from "@vytches/ddd-logging";
|
|
2
|
+
import { LibUtils as p, safeRun as d } from "@vytches/ddd-utils";
|
|
3
|
+
var n = /* @__PURE__ */ ((i) => (i.PENDING = "PENDING", i.PROCESSING = "PROCESSING", i.PROCESSED = "PROCESSED", i.FAILED = "FAILED", i))(n || {}), 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
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resets messages stuck in `PROCESSING` back to `PENDING` so they can be
|
|
26
|
+
* re-dispatched. Used for crash recovery: if a worker dies mid-batch, its
|
|
27
|
+
* in-flight messages remain `PROCESSING` forever without this.
|
|
28
|
+
*
|
|
29
|
+
* Default implementation is a no-op returning `0` — repositories that do not
|
|
30
|
+
* track a per-message "processing since" timestamp simply opt out. Override
|
|
31
|
+
* in concrete repositories, e.g.:
|
|
32
|
+
* `UPDATE outbox_messages SET status='PENDING' WHERE status='PROCESSING' AND updated_at < olderThan`.
|
|
33
|
+
*
|
|
34
|
+
* **Safety:** implementations MUST only reset messages whose processing
|
|
35
|
+
* started strictly before `olderThan`. `OutboxProcessor` always passes a past
|
|
36
|
+
* timestamp (`now - crashRecoveryThresholdMs`, with the threshold validated to
|
|
37
|
+
* be `>= messageTimeout`), so legitimately in-flight messages are never reset.
|
|
38
|
+
*
|
|
39
|
+
* @param olderThan Reset only `PROCESSING` messages last touched before this instant
|
|
40
|
+
* @returns Number of messages reset to `PENDING`
|
|
41
|
+
*/
|
|
42
|
+
resetStaleProcessing(e) {
|
|
43
|
+
return Promise.resolve(0);
|
|
44
|
+
}
|
|
5
45
|
}
|
|
6
|
-
class
|
|
46
|
+
class h {
|
|
7
47
|
/**
|
|
8
48
|
* Creates a new outbox message
|
|
9
49
|
* @param messageType Type of the message
|
|
@@ -13,14 +53,14 @@ class g {
|
|
|
13
53
|
*/
|
|
14
54
|
static createMessage(e, t, s) {
|
|
15
55
|
const r = {
|
|
16
|
-
id:
|
|
56
|
+
id: p.getUUID(),
|
|
17
57
|
messageType: e,
|
|
18
58
|
payload: t,
|
|
19
59
|
metadata: s?.metadata || {},
|
|
20
|
-
status:
|
|
60
|
+
status: n.PENDING,
|
|
21
61
|
attempts: 0,
|
|
22
62
|
createdAt: /* @__PURE__ */ new Date(),
|
|
23
|
-
priority: s?.priority ||
|
|
63
|
+
priority: s?.priority || g.NORMAL
|
|
24
64
|
};
|
|
25
65
|
return s?.processAfter && (r.processAfter = s.processAfter), r;
|
|
26
66
|
}
|
|
@@ -34,7 +74,7 @@ class g {
|
|
|
34
74
|
*/
|
|
35
75
|
static createDelayedMessage(e, t, s, r) {
|
|
36
76
|
const a = new Date(Date.now() + s);
|
|
37
|
-
return
|
|
77
|
+
return h.createMessage(e, t, {
|
|
38
78
|
...r,
|
|
39
79
|
processAfter: a
|
|
40
80
|
});
|
|
@@ -47,9 +87,9 @@ class g {
|
|
|
47
87
|
* @returns High priority outbox message
|
|
48
88
|
*/
|
|
49
89
|
static createHighPriorityMessage(e, t, s) {
|
|
50
|
-
return
|
|
90
|
+
return h.createMessage(e, t, {
|
|
51
91
|
...s,
|
|
52
|
-
priority:
|
|
92
|
+
priority: g.HIGH
|
|
53
93
|
});
|
|
54
94
|
}
|
|
55
95
|
/**
|
|
@@ -65,29 +105,67 @@ class g {
|
|
|
65
105
|
...t?.metadata || {}
|
|
66
106
|
}
|
|
67
107
|
};
|
|
68
|
-
return t?.priority && (s.priority = t.priority), t?.processAfter && (s.processAfter = t.processAfter),
|
|
108
|
+
return t?.priority && (s.priority = t.priority), t?.processAfter && (s.processAfter = t.processAfter), h.createMessage(
|
|
69
109
|
`integration_event:${e.eventName}`,
|
|
70
110
|
e.payload,
|
|
71
111
|
s
|
|
72
112
|
);
|
|
73
113
|
}
|
|
74
114
|
}
|
|
75
|
-
class
|
|
115
|
+
class v {
|
|
76
116
|
constructor(e, t = {}) {
|
|
77
|
-
this.handlers = /* @__PURE__ */ new Map(), this.middlewares = [], this.isRunning = !1, this.logger =
|
|
78
|
-
|
|
117
|
+
this.handlers = /* @__PURE__ */ new Map(), this.middlewares = [], this.isRunning = !1, this.logger = u.create("OutboxProcessor");
|
|
118
|
+
const s = t.batchSize ?? 10;
|
|
119
|
+
if (s > 1e4)
|
|
120
|
+
throw new RangeError(`OutboxProcessor: batchSize must be ≤ 10,000, got ${s}`);
|
|
121
|
+
if (t.retryBackoff) {
|
|
122
|
+
const { initial: o, multiplier: l, maxDelay: c } = t.retryBackoff;
|
|
123
|
+
if (o <= 0 || l <= 0 || c <= 0)
|
|
124
|
+
throw new RangeError(
|
|
125
|
+
`OutboxProcessor: retryBackoff fields must be positive numbers (initial=${o}, multiplier=${l}, maxDelay=${c})`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const r = t.messageTimeout ?? 3e4, a = t.crashRecoveryThresholdMs ?? Math.max(3e5, r);
|
|
129
|
+
if (a < r)
|
|
130
|
+
throw new RangeError(
|
|
131
|
+
`OutboxProcessor: crashRecoveryThresholdMs (${a}) must be ≥ messageTimeout (${r}) to avoid resetting in-flight messages`
|
|
132
|
+
);
|
|
133
|
+
this.repository = e, this.options = {
|
|
134
|
+
batchSize: s,
|
|
79
135
|
maxRetries: t.maxRetries ?? 3,
|
|
80
136
|
processingInterval: t.processingInterval ?? 5e3,
|
|
81
137
|
priorityOrder: t.priorityOrder ?? [
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
138
|
+
g.CRITICAL,
|
|
139
|
+
g.HIGH,
|
|
140
|
+
g.NORMAL,
|
|
141
|
+
g.LOW
|
|
86
142
|
],
|
|
87
|
-
messageTimeout:
|
|
88
|
-
enableLogging: t.enableLogging ?? !1
|
|
143
|
+
messageTimeout: r,
|
|
144
|
+
enableLogging: t.enableLogging ?? !1,
|
|
145
|
+
retryBackoff: t.retryBackoff,
|
|
146
|
+
messageTypes: t.messageTypes,
|
|
147
|
+
crashRecoveryIntervalMs: t.crashRecoveryIntervalMs,
|
|
148
|
+
crashRecoveryThresholdMs: a,
|
|
149
|
+
adaptiveRepoll: t.adaptiveRepoll ?? !1,
|
|
150
|
+
adaptiveRepollMaxConsecutive: t.adaptiveRepollMaxConsecutive ?? 5,
|
|
151
|
+
adaptiveRepollPauseMs: t.adaptiveRepollPauseMs ?? 50,
|
|
152
|
+
startupJitterMs: t.startupJitterMs ?? 0,
|
|
153
|
+
hooks: t.hooks
|
|
89
154
|
};
|
|
90
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Invokes an observability hook in isolation. A throwing hook is logged and
|
|
158
|
+
* swallowed so it can never break the processing loop (mitigation T3).
|
|
159
|
+
*/
|
|
160
|
+
safelyInvokeHook(e, t) {
|
|
161
|
+
if (this.options.hooks)
|
|
162
|
+
try {
|
|
163
|
+
t();
|
|
164
|
+
} catch (s) {
|
|
165
|
+
const r = s instanceof Error ? s.message : String(s);
|
|
166
|
+
this.logger.error(`Outbox hook ${e} threw and was ignored: ${r}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
91
169
|
/**
|
|
92
170
|
* Registers a message handler for a specific message type
|
|
93
171
|
*/
|
|
@@ -108,7 +186,9 @@ class m {
|
|
|
108
186
|
this.logger.warn("Processor is already running");
|
|
109
187
|
return;
|
|
110
188
|
}
|
|
111
|
-
this.isRunning = !0, this.logger.info("Starting outbox processor")
|
|
189
|
+
this.isRunning = !0, this.logger.info("Starting outbox processor");
|
|
190
|
+
const e = this.options.startupJitterMs > 0 ? Math.floor(Math.random() * this.options.startupJitterMs) : 0;
|
|
191
|
+
this.scheduleProcessing(0, this.options.processingInterval + e), this.scheduleCrashRecovery();
|
|
112
192
|
}
|
|
113
193
|
/**
|
|
114
194
|
* Stops the processor
|
|
@@ -118,7 +198,7 @@ class m {
|
|
|
118
198
|
this.logger.warn("Processor is not running");
|
|
119
199
|
return;
|
|
120
200
|
}
|
|
121
|
-
this.isRunning = !1, this.processingTimer && (clearTimeout(this.processingTimer), this.processingTimer = void 0), this.logger.info("Stopped outbox processor");
|
|
201
|
+
this.isRunning = !1, this.processingTimer && (clearTimeout(this.processingTimer), this.processingTimer = void 0), this.crashRecoveryTimer && (clearTimeout(this.crashRecoveryTimer), this.crashRecoveryTimer = void 0), this.logger.info("Stopped outbox processor");
|
|
122
202
|
}
|
|
123
203
|
/**
|
|
124
204
|
* Processes a single batch of pending messages.
|
|
@@ -141,38 +221,57 @@ class m {
|
|
|
141
221
|
* **Quiescence:** if there are no pending messages, returns immediately
|
|
142
222
|
* without invoking handlers.
|
|
143
223
|
*
|
|
224
|
+
* @returns `{ processed, batchSize }` — `processed` is how many messages were
|
|
225
|
+
* dispatched this batch, `batchSize` the configured limit. A full batch
|
|
226
|
+
* (`processed >= batchSize`) signals a likely backlog. Callers that ignore
|
|
227
|
+
* the return value are unaffected (the value was previously `void`).
|
|
144
228
|
* @public
|
|
145
229
|
* @stable
|
|
146
230
|
* @since 0.1.0
|
|
147
231
|
*/
|
|
148
232
|
async processBatch() {
|
|
233
|
+
const e = this.options.batchSize;
|
|
149
234
|
if (!this.isRunning)
|
|
150
|
-
return;
|
|
151
|
-
const [
|
|
152
|
-
() => this.repository.getUnprocessedMessages(
|
|
235
|
+
return { processed: 0, batchSize: e };
|
|
236
|
+
const [t, s] = await d(
|
|
237
|
+
() => this.repository.getUnprocessedMessages(
|
|
238
|
+
e,
|
|
239
|
+
this.options.priorityOrder,
|
|
240
|
+
this.options.messageTypes
|
|
241
|
+
)
|
|
153
242
|
);
|
|
154
|
-
if (
|
|
155
|
-
const
|
|
156
|
-
this.logger.error(`Error retrieving messages: ${
|
|
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;
|
|
243
|
+
if (t) {
|
|
244
|
+
const c = t instanceof Error ? t.message : String(t);
|
|
245
|
+
return this.logger.error(`Error retrieving messages: ${c}`), { processed: 0, batchSize: e };
|
|
163
246
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
247
|
+
const r = s;
|
|
248
|
+
if (!r || !Array.isArray(r) || r.length === 0)
|
|
249
|
+
return this.logger.debug("No pending messages to process"), { processed: 0, batchSize: e };
|
|
250
|
+
this.logger.info(`Processing ${r.length} messages`);
|
|
251
|
+
const a = Date.now(), l = (await Promise.allSettled(
|
|
252
|
+
r.map((c) => this.processMessage(c))
|
|
253
|
+
)).filter(
|
|
254
|
+
(c) => c.status === "rejected" || c.status === "fulfilled" && c.value === !1
|
|
255
|
+
).length;
|
|
256
|
+
return this.logger.info(`Completed processing batch of ${r.length} messages`), this.safelyInvokeHook(
|
|
257
|
+
"onBatchComplete",
|
|
258
|
+
() => this.options.hooks?.onBatchComplete?.({
|
|
259
|
+
processed: r.length,
|
|
260
|
+
failed: l,
|
|
261
|
+
batchSize: e,
|
|
262
|
+
durationMs: Date.now() - a
|
|
263
|
+
})
|
|
264
|
+
), { processed: r.length, batchSize: e };
|
|
167
265
|
}
|
|
168
266
|
/**
|
|
169
|
-
* Processes a single message through the middleware pipeline
|
|
267
|
+
* Processes a single message through the middleware pipeline.
|
|
268
|
+
* @returns `true` if the message was processed successfully, `false` if it failed.
|
|
170
269
|
*/
|
|
171
270
|
async processMessage(e) {
|
|
172
271
|
const [t] = await d(async () => {
|
|
173
|
-
await this.repository.updateStatus(e.id,
|
|
272
|
+
await this.repository.updateStatus(e.id, n.PROCESSING), await this.buildPipeline(e)(e), await this.repository.updateStatus(e.id, n.PROCESSED), this.logger.debug(`Successfully processed message ${e.id}`);
|
|
174
273
|
});
|
|
175
|
-
t
|
|
274
|
+
return t ? (await this.handleMessageError(e, t), !1) : !0;
|
|
176
275
|
}
|
|
177
276
|
/**
|
|
178
277
|
* Builds the middleware pipeline for message processing
|
|
@@ -185,8 +284,8 @@ class m {
|
|
|
185
284
|
await Promise.race([
|
|
186
285
|
t.handle(r),
|
|
187
286
|
new Promise(
|
|
188
|
-
(a,
|
|
189
|
-
() =>
|
|
287
|
+
(a, o) => setTimeout(
|
|
288
|
+
() => o(new Error("Message processing timeout")),
|
|
190
289
|
this.options.messageTimeout
|
|
191
290
|
)
|
|
192
291
|
)
|
|
@@ -200,19 +299,70 @@ class m {
|
|
|
200
299
|
async handleMessageError(e, t) {
|
|
201
300
|
try {
|
|
202
301
|
const s = await this.repository.incrementAttempt(e.id);
|
|
203
|
-
this.logger.warn(`Message ${e.id} failed, attempt ${s}: ${t?.message}`),
|
|
302
|
+
if (this.logger.warn(`Message ${e.id} failed, attempt ${s}: ${t?.message}`), this.safelyInvokeHook(
|
|
303
|
+
"onMessageFailed",
|
|
304
|
+
() => this.options.hooks?.onMessageFailed?.(e, t, s)
|
|
305
|
+
), s >= this.options.maxRetries)
|
|
306
|
+
await this.repository.updateStatus(e.id, n.FAILED, t), this.logger.error(`Message ${e.id} marked as failed after ${s} attempts`), this.safelyInvokeHook(
|
|
307
|
+
"onPermanentFailure",
|
|
308
|
+
() => this.options.hooks?.onPermanentFailure?.(e, t)
|
|
309
|
+
);
|
|
310
|
+
else if (this.options.retryBackoff) {
|
|
311
|
+
const { initial: r, multiplier: a, maxDelay: o } = this.options.retryBackoff, l = Math.min(r * Math.pow(a, s - 1), o), c = new Date(Date.now() + l);
|
|
312
|
+
await this.repository.updateStatus(e.id, n.PENDING), await this.repository.scheduleRetry(e.id, c), this.logger.info(
|
|
313
|
+
`Message ${e.id} scheduled for retry in ${l}ms (attempt ${s})`
|
|
314
|
+
);
|
|
315
|
+
} else
|
|
316
|
+
await this.repository.updateStatus(e.id, n.PENDING), this.logger.info(`Message ${e.id} scheduled for retry (attempt ${s + 1})`);
|
|
204
317
|
} catch (s) {
|
|
205
318
|
const r = s instanceof Error ? s.message : String(s);
|
|
206
319
|
this.logger.error(`Error updating message status: ${r}`);
|
|
207
320
|
}
|
|
208
321
|
}
|
|
209
322
|
/**
|
|
210
|
-
* Schedules the next processing cycle
|
|
323
|
+
* Schedules the next processing cycle.
|
|
324
|
+
*
|
|
325
|
+
* With `adaptiveRepoll` enabled, a full batch re-polls immediately (`delay 0`)
|
|
326
|
+
* to drain a backlog — until `adaptiveRepollMaxConsecutive` consecutive full
|
|
327
|
+
* batches trip the livelock guard, after which an `adaptiveRepollPauseMs`
|
|
328
|
+
* pause is inserted. Any non-full batch resets the cadence to
|
|
329
|
+
* `processingInterval`.
|
|
330
|
+
*
|
|
331
|
+
* @param consecutiveFullBatches running count of back-to-back full batches
|
|
332
|
+
* @param delay milliseconds to wait before the next batch
|
|
211
333
|
*/
|
|
212
|
-
scheduleProcessing() {
|
|
334
|
+
scheduleProcessing(e = 0, t = this.options.processingInterval) {
|
|
213
335
|
this.isRunning && (this.processingTimer = setTimeout(async () => {
|
|
214
|
-
await this.processBatch()
|
|
215
|
-
|
|
336
|
+
const s = await this.processBatch();
|
|
337
|
+
if (this.options.adaptiveRepoll && s.processed > 0 && s.processed >= s.batchSize) {
|
|
338
|
+
const a = e + 1, o = a > this.options.adaptiveRepollMaxConsecutive ? this.options.adaptiveRepollPauseMs : 0;
|
|
339
|
+
this.scheduleProcessing(a, o);
|
|
340
|
+
} else
|
|
341
|
+
this.scheduleProcessing(0, this.options.processingInterval);
|
|
342
|
+
}, t));
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Schedules the next crash-recovery cycle. No-op unless
|
|
346
|
+
* `crashRecoveryIntervalMs` was configured.
|
|
347
|
+
*/
|
|
348
|
+
scheduleCrashRecovery() {
|
|
349
|
+
!this.isRunning || this.options.crashRecoveryIntervalMs === void 0 || (this.crashRecoveryTimer = setTimeout(async () => {
|
|
350
|
+
await this.runCrashRecovery(), this.scheduleCrashRecovery();
|
|
351
|
+
}, this.options.crashRecoveryIntervalMs));
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Resets messages orphaned in `PROCESSING` (older than
|
|
355
|
+
* `crashRecoveryThresholdMs`) back to `PENDING` via the repository. Errors are
|
|
356
|
+
* logged, never thrown — crash recovery must not crash the processor.
|
|
357
|
+
*/
|
|
358
|
+
async runCrashRecovery() {
|
|
359
|
+
const e = new Date(Date.now() - this.options.crashRecoveryThresholdMs), [t, s] = await d(() => this.repository.resetStaleProcessing(e));
|
|
360
|
+
if (t) {
|
|
361
|
+
const r = t instanceof Error ? t.message : String(t);
|
|
362
|
+
this.logger.error(`Crash recovery failed: ${r}`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
s && s > 0 && this.logger.warn(`Crash recovery reset ${s} stale PROCESSING message(s) to PENDING`);
|
|
216
366
|
}
|
|
217
367
|
/**
|
|
218
368
|
* Logs messages if logging is enabled
|
|
@@ -234,7 +384,7 @@ class m {
|
|
|
234
384
|
};
|
|
235
385
|
}
|
|
236
386
|
}
|
|
237
|
-
class
|
|
387
|
+
class M {
|
|
238
388
|
constructor(e) {
|
|
239
389
|
this.eventBus = e;
|
|
240
390
|
}
|
|
@@ -250,10 +400,10 @@ class f {
|
|
|
250
400
|
await this.eventBus.publish(s);
|
|
251
401
|
}
|
|
252
402
|
}
|
|
253
|
-
class
|
|
403
|
+
class R {
|
|
254
404
|
constructor(e, t = {}) {
|
|
255
|
-
this.logger =
|
|
256
|
-
defaultPriority: t.defaultPriority ??
|
|
405
|
+
this.logger = u.create("OutboxService"), this.repository = e, this.options = {
|
|
406
|
+
defaultPriority: t.defaultPriority ?? g.NORMAL,
|
|
257
407
|
defaultMetadata: t.defaultMetadata ?? {},
|
|
258
408
|
autoCleanup: t.autoCleanup ?? !1,
|
|
259
409
|
cleanupAfterDays: t.cleanupAfterDays ?? 7,
|
|
@@ -264,7 +414,7 @@ class M {
|
|
|
264
414
|
* Saves a message to the outbox
|
|
265
415
|
*/
|
|
266
416
|
async saveMessage(e, t, s) {
|
|
267
|
-
const r =
|
|
417
|
+
const r = h.createMessage(e, t, {
|
|
268
418
|
priority: this.options.defaultPriority,
|
|
269
419
|
metadata: { ...this.options.defaultMetadata, ...s?.metadata },
|
|
270
420
|
...s
|
|
@@ -276,10 +426,10 @@ class M {
|
|
|
276
426
|
*/
|
|
277
427
|
async saveMessages(e) {
|
|
278
428
|
const t = e.map(
|
|
279
|
-
({ messageType: r, payload: a, options:
|
|
429
|
+
({ messageType: r, payload: a, options: o }) => h.createMessage(r, a, {
|
|
280
430
|
priority: this.options.defaultPriority,
|
|
281
|
-
metadata: { ...this.options.defaultMetadata, ...
|
|
282
|
-
...
|
|
431
|
+
metadata: { ...this.options.defaultMetadata, ...o?.metadata },
|
|
432
|
+
...o
|
|
283
433
|
})
|
|
284
434
|
), s = await this.repository.saveBatch(t);
|
|
285
435
|
return this.logger.debug(`Saved batch of ${e.length} messages`), s;
|
|
@@ -288,14 +438,14 @@ class M {
|
|
|
288
438
|
* Schedules a message for delayed processing
|
|
289
439
|
*/
|
|
290
440
|
async scheduleMessage(e, t, s, r) {
|
|
291
|
-
const a =
|
|
441
|
+
const a = h.createDelayedMessage(e, t, s, {
|
|
292
442
|
priority: this.options.defaultPriority,
|
|
293
443
|
metadata: { ...this.options.defaultMetadata, ...r?.metadata },
|
|
294
444
|
...r
|
|
295
|
-
}),
|
|
445
|
+
}), o = await this.repository.scheduleMessage(a, a.processAfter);
|
|
296
446
|
return this.logger.debug(
|
|
297
|
-
`Scheduled message ${
|
|
298
|
-
),
|
|
447
|
+
`Scheduled message ${o} of type ${e} for processing in ${s}ms`
|
|
448
|
+
), o;
|
|
299
449
|
}
|
|
300
450
|
/**
|
|
301
451
|
* Saves a high priority message to the outbox
|
|
@@ -303,7 +453,7 @@ class M {
|
|
|
303
453
|
async saveHighPriorityMessage(e, t, s) {
|
|
304
454
|
return this.saveMessage(e, t, {
|
|
305
455
|
...s,
|
|
306
|
-
priority:
|
|
456
|
+
priority: g.HIGH
|
|
307
457
|
});
|
|
308
458
|
}
|
|
309
459
|
/**
|
|
@@ -312,14 +462,14 @@ class M {
|
|
|
312
462
|
async saveCriticalMessage(e, t, s) {
|
|
313
463
|
return this.saveMessage(e, t, {
|
|
314
464
|
...s,
|
|
315
|
-
priority:
|
|
465
|
+
priority: g.CRITICAL
|
|
316
466
|
});
|
|
317
467
|
}
|
|
318
468
|
/**
|
|
319
469
|
* Converts a domain event to an outbox message and saves it
|
|
320
470
|
*/
|
|
321
471
|
async saveDomainEvent(e, t) {
|
|
322
|
-
const s =
|
|
472
|
+
const s = h.createFromIntegrationEvent(
|
|
323
473
|
{
|
|
324
474
|
eventName: e.eventName,
|
|
325
475
|
payload: e,
|
|
@@ -353,13 +503,13 @@ class M {
|
|
|
353
503
|
* Retries a failed message by resetting it to pending status
|
|
354
504
|
*/
|
|
355
505
|
async retryMessage(e) {
|
|
356
|
-
await this.repository.updateStatus(e,
|
|
506
|
+
await this.repository.updateStatus(e, n.PENDING), this.logger.info(`Reset message ${e} to pending for retry`);
|
|
357
507
|
}
|
|
358
508
|
/**
|
|
359
509
|
* Retries multiple failed messages
|
|
360
510
|
*/
|
|
361
511
|
async retryMessages(e) {
|
|
362
|
-
await this.repository.updateStatusBatch(e,
|
|
512
|
+
await this.repository.updateStatusBatch(e, n.PENDING), this.logger.info(`Reset ${e.length} messages to pending for retry`);
|
|
363
513
|
}
|
|
364
514
|
/**
|
|
365
515
|
* Cleans up old processed messages
|
|
@@ -369,7 +519,7 @@ class M {
|
|
|
369
519
|
s.setDate(s.getDate() - t);
|
|
370
520
|
const r = await this.repository.deleteByStatusAndAge(
|
|
371
521
|
s,
|
|
372
|
-
|
|
522
|
+
n.PROCESSED
|
|
373
523
|
);
|
|
374
524
|
return this.logger.info(`Cleaned up ${r} processed messages older than ${t} days`), r;
|
|
375
525
|
}
|
|
@@ -379,10 +529,10 @@ class M {
|
|
|
379
529
|
async getStats() {
|
|
380
530
|
const e = this.repository;
|
|
381
531
|
return e.getMessageCountByStatus ? {
|
|
382
|
-
totalPending: e.getMessageCountByStatus(
|
|
383
|
-
totalProcessing: e.getMessageCountByStatus(
|
|
384
|
-
totalProcessed: e.getMessageCountByStatus(
|
|
385
|
-
totalFailed: e.getMessageCountByStatus(
|
|
532
|
+
totalPending: e.getMessageCountByStatus(n.PENDING),
|
|
533
|
+
totalProcessing: e.getMessageCountByStatus(n.PROCESSING),
|
|
534
|
+
totalProcessed: e.getMessageCountByStatus(n.PROCESSED),
|
|
535
|
+
totalFailed: e.getMessageCountByStatus(n.FAILED)
|
|
386
536
|
} : {
|
|
387
537
|
totalPending: (await this.repository.getUnprocessedMessages(1e3)).length,
|
|
388
538
|
totalProcessing: 0,
|
|
@@ -401,11 +551,11 @@ class M {
|
|
|
401
551
|
}
|
|
402
552
|
}
|
|
403
553
|
export {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
554
|
+
M as EventBusOutboxHandler,
|
|
555
|
+
m as IOutboxRepository,
|
|
556
|
+
g as MessagePriority,
|
|
557
|
+
n as MessageStatus,
|
|
558
|
+
h as OutboxMessageFactory,
|
|
559
|
+
v as OutboxProcessor,
|
|
560
|
+
R as OutboxService
|
|
411
561
|
};
|
package/dist/outbox/index.d.ts
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { IOutboxMessage } from './outbox-interfaces';
|
|
2
|
+
/**
|
|
3
|
+
* Observability callbacks for {@link OutboxProcessor}. Use them to wire metrics
|
|
4
|
+
* (Prometheus counters, DataDog), alerting on permanent failures, or dead-letter
|
|
5
|
+
* routing — without coupling the processor to any specific telemetry stack.
|
|
6
|
+
*
|
|
7
|
+
* **Hooks must be synchronous and fast.** Each call is wrapped in a try/catch by
|
|
8
|
+
* the processor, so a throwing hook is logged and ignored (it never breaks the
|
|
9
|
+
* processing loop), but a slow hook still blocks the batch. Offload heavy work
|
|
10
|
+
* (network, disk) to a queue inside the hook.
|
|
11
|
+
*/
|
|
12
|
+
export interface OutboxProcessorHooks {
|
|
13
|
+
/** Fired once per processed batch with aggregate counts and wall-clock duration. */
|
|
14
|
+
onBatchComplete?(result: {
|
|
15
|
+
processed: number;
|
|
16
|
+
failed: number;
|
|
17
|
+
batchSize: number;
|
|
18
|
+
durationMs: number;
|
|
19
|
+
}): void;
|
|
20
|
+
/** Fired on every message failure, including ones that will be retried. */
|
|
21
|
+
onMessageFailed?(message: IOutboxMessage, error: Error, attempt: number): void;
|
|
22
|
+
/** Fired when a message is marked `FAILED` after exhausting `maxRetries`. */
|
|
23
|
+
onPermanentFailure?(message: IOutboxMessage, error: Error): void;
|
|
24
|
+
}
|