@dawntech/dispatcher 0.2.2 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- import{a as D}from"./chunk-UFUGEIFL.mjs";import{a as I}from"./chunk-QA6PFVGP.mjs";import{a as v}from"./chunk-CVPGUSFU.mjs";import{a as f,b as y}from"./chunk-4LYB64T2.mjs";import{b as w}from"./chunk-OXXLVJVC.mjs";import{a as S}from"./chunk-JD53PFR4.mjs";var b=S((A,k)=>{k.exports={name:"@dawntech/dispatcher",version:"0.2.2",description:"A TypeScript Node.js package for sending push messages in conversational chatbots on the Blip platform.",main:"dist/index.js",module:"dist/index.mjs",types:"dist/index.d.ts",scripts:{start:"node dist/index.js",build:"tsup","build:watch":"tsup --watch",dev:"tsx watch src/server.ts","test:basic":"tsx tests/integration/scenarios/1-basic-send.ts","test:contact":"tsx tests/integration/scenarios/2-contact-update.ts","test:schedule":"tsx tests/integration/scenarios/3-scheduling.ts","test:status":"tsx tests/integration/scenarios/4-status-config.ts","test:intent":"tsx tests/integration/scenarios/5-intent.ts","test:rate-limit":"tsx tests/integration/scenarios/6-rate-limiting.ts","test:high-load":"tsx tests/integration/scenarios/7-high-load.ts","test:retries":"tsx tests/integration/scenarios/8-retries.ts","test:expiry":"tsx tests/integration/scenarios/9-expiration.ts","test:cluster":"tsx tests/integration/scenarios/10-cluster.ts","test:monitor":"tsx tests/integration/scenarios/11-monitor.ts",test:"jest","test:watch":"jest --watch","test:coverage":"jest --coverage","test:blip-api":"tsx tests/blip-api.ts",clean:"rm -rf dist",setup:"bash scripts/setup.sh","docker:up":"docker-compose up -d","docker:down":"docker-compose down","docker:logs":"docker-compose logs -f","docker:redis-cli":"docker-compose exec redis redis-cli","docker:clean":"docker-compose down -v","docker:restart":"docker-compose restart","docker:tools":"docker-compose --profile tools up -d",format:'prettier --write "src/**/*.ts" "tests/**/*.ts"',"format:check":'prettier --check "src/**/*.ts" "tests/**/*.ts"',prepublishOnly:"npm run build"},packageManager:"npm@11.8.0",devDependencies:{"@types/body-parser":"^1.19.6","@types/cors":"^2.8.19","@types/debug":"^4.1.12","@types/express":"^5.0.6","@types/ioredis":"^4.28.10","@types/jest":"^29.5.14","@types/lodash":"^4.17.20","@types/node":"^20.19.24",husky:"^9.1.7",jest:"^29.7.0","lint-staged":"^16.2.6",prettier:"^3.6.2","ts-jest":"^29.2.5",tsup:"^8.5.1",tsx:"^4.7.0",typescript:"^5.3.0"},keywords:[],author:"",license:"ISC",engines:{node:">=24.0.0"},dependencies:{axios:"^1.13.1","body-parser":"^2.2.2",bullmq:"^5.67.1",cors:"^2.8.6",debug:"^4.4.3",dotenv:"^17.2.3",express:"^5.2.1",ioredis:"^5.9.2",lodash:"^4.17.21","rate-limiter-flexible":"^9.0.1",redis:"^5.9.0",uuid:"^13.0.0"}}});import{v4 as E}from"uuid";import{Queue as N,Worker as x}from"bullmq";var{version:T}=b(),o=w("Dispatcher"),M=class{constructor(t,e,i,s){this.callbacks={};this.descriptors=new Map;this.isRunning=!1;this.setupCompleted=!1;this.id=t,this.repository=e,this.redis=this.repository.redis,this.stateMachine=new D(this.id,this.repository,(r,n)=>{this.emit(r,n),this.descriptors.get(n.descriptorId)?.emit(r,n,this.api,this.id)}),this.api=new I(i.contract,i.key),this.queueName=`dispatcher-${this.id.replace(/:/g,"-")}`,this.maxRetries=s?.maxRetries??0,this.retryIntervals=s?.retryIntervals??[1*1e3,5*1e3,15*1e3],this.timeouts={pending:s?.timeouts?.pending??120*1e3,sending:s?.timeouts?.sending??120*1e3},this.retention=s?.retention??2880*60*1e3,this.pollingIntervals={scheduled:s?.pollingIntervals?.scheduled??30*1e3,pending:s?.pollingIntervals?.pending??10*1e3,sending:s?.pollingIntervals?.sending??10*1e3,delivered:s?.pollingIntervals?.delivered??1800*1e3,read:s?.pollingIntervals?.read??1800*1e3,queue:s?.pollingIntervals?.queue??1*1e3},this.timeoutTimer=null,this.query=new v(this.repository),this.queue=new N(this.queueName,{connection:this.redis,defaultJobOptions:{removeOnComplete:!0,removeOnFail:!0}}),this.worker=new x(this.queueName,async r=>{try{await this.processJob(r)}catch(n){throw o.error(`[Worker] Job ${r.name} failed`,n),n}},{connection:this.redis,concurrency:s?.batchSize||50,limiter:s?.rateLimits?.global?{max:s.rateLimits.global.points,duration:s.rateLimits.global.duration*1e3}:void 0}),this.worker.on("error",r=>o.error("[Worker] Error",r)),this.worker.on("failed",(r,n)=>o.error(`[Worker] Job ${r?.id} failed`,n))}async setup(){if(this.setupCompleted)return;await this.repository.setup();let t=await this.repository.getManifest();await this.repository.writeManifest({version:T,createdAt:t?.createdAt??new Date().toISOString(),updatedAt:new Date().toISOString()}),await this.queue.waitUntilReady(),this.isRunning=!0,this.setupCompleted=!0,this.startTimeoutMonitor(),o.info("[setup] Dispatcher started (BullMQ)",{queue:this.queueName})}async teardown(){this.isRunning=!1,this.timeoutTimer&&(clearInterval(this.timeoutTimer),this.timeoutTimer=null),await this.queue.close(),await this.worker.close(),await this.repository.teardown(),o.info("[teardown] Dispatcher stopped")}on(t,e){return this.callbacks[t]=e,this}async getMetrics(){let t={total:0,byState:{},byStatus:{},cumulative:{dispatched:await this.repository.getMetric("dispatched"),delivered:await this.repository.getMetric("delivered"),failed:await this.repository.getMetric("failed")}},e=Object.values(f);for(let s of e)t.byState[s]=await this.repository.countMessages({state:s});let i=Object.values(y);for(let s of i)t.byStatus[s]=await this.repository.countMessages({status:s});return t.total=Object.values(t.byState).reduce((s,r)=>s+(r||0),0),t}emit(t,e){this.callbacks[t]?.(e,this.api,this.id)}async send(t,e,i,s){this.descriptors.set(t.id,t);let r=t.toContactId(e),n=t.transform(i),c=new Date().toISOString(),a={messageId:E(),contactId:r,descriptorId:t.id,payload:n,status:"INIT",state:"INIT",createdAt:c,attempts:0,retries:this.maxRetries},l={...t.messageOptions,...s},{schedule:u,...m}=l;a.options=m,this.emit("dispatch",a),t.emit("dispatch",a,this.api,this.id);let p=this.calculateScheduledTime(u,l.shifts),h=0;if(p){a.scheduledTo=p,a.state="SCHEDULED";let g=new Date(p).getTime();h=Math.max(0,g-Date.now()),this.emit("scheduled",a),t.emit("scheduled",a,this.api,this.id),o.info("[send] message scheduled",{messageId:a.messageId,scheduledTo:p,delay:h})}else a.state="QUEUED",a.status="INIT",o.info("[send] message queued",{messageId:a.messageId});return a.expiresAt=new Date(Date.now()+(a.state==="SCHEDULED"?h+this.retention:this.retention)).toISOString(),await this.stateMachine.transition(a,a.state,a.status),await this.queue.add("send",{messageId:a.messageId},{jobId:a.messageId,delay:h,priority:1}),a}async cancel(t){let e=await this.repository.getMessage(t);if(!e)return o.warn("[cancel] message not found",{messageId:t}),!1;if(e.state==="FINAL")return o.warn("[cancel] message already final",{messageId:t,status:e.status}),!1;let i=await this.queue.getJob(t);return i&&(await i.remove(),o.info("[cancel] removed job from queue",{messageId:t})),await this.stateMachine.transition(e,"FINAL","CANCELED"),o.info("[cancel] message canceled",{messageId:t}),!0}async processJob(t){let{messageId:e}=t.data,i=await this.repository.getMessage(e);if(!i){o.warn(`[processJob] Message not found: ${e}`);return}let s=this.descriptors.get(i.descriptorId)||null;switch(t.name){case"send":await this.handleSendJob(i,s);break;case"check":await this.handleCheckJob(i,s);break;default:o.warn(`[processJob] Unknown job name: ${t.name}`)}}async handleSendJob(t,e){t.lastDispatchAttemptAt=new Date().toISOString(),await this.stateMachine.transition(t,"DISPATCHED","PENDING");try{await this.api.sendMessage(t.contactId,t.payload,t.messageId),await this.handlePostSendOperations(t,t.options),t.sentAt=new Date().toISOString(),await this.stateMachine.transition(t,"DISPATCHED","PENDING"),o.info("[handleSendJob] Message sent to API",{messageId:t.messageId}),await this.repository.incrementMetric("dispatched"),await this.queue.add("check",{messageId:t.messageId},{delay:this.pollingIntervals.pending,priority:5})}catch(i){let s=i instanceof Error?i:new Error(String(i));await this.handleDispatchFailure(t,e,s)}}async handlePostSendOperations(t,e={}){let i={...e.contact||{}};if(e.intent)if(typeof e.intent=="string")i.intent=e.intent;else{i.intent=e.intent.intent;let{intent:s,...r}=e.intent;Object.entries(r).forEach(([n,c])=>{c!=null&&(i[n]=typeof c=="object"?JSON.stringify(c):String(c))})}Object.keys(i).length>0&&await this.api.mergeContact(t.contactId,i),e.state&&await this.api.setState(t.contactId,e.state.botId,e.state.stateId)}async handleCheckJob(t,e){if(t.state!=="FINAL"){if(this.checkAndHandleTimeout(t)){await this.handleTimeout(t,e);return}try{let i=await this.api.getDispatchState(t.messageId,t.contactId);if(!i){await this.rescheduleCheck(t,this.pollingIntervals.pending);return}let s=this.pollingIntervals.pending,r=!1;switch(i){case"accepted":t.status!=="SENDING"&&(await this.stateMachine.transition(t,t.state,"SENDING"),r=!0),s=this.pollingIntervals.sending;break;case"received":case"consumed":if(await this.api.getMessageAfter(t.contactId,t.messageId)){let d="REPLIED";t.status!==d&&t.status!=="READ"&&(await this.repository.incrementMetric("delivered"),await this.stateMachine.transition(t,"FINAL","REPLIED"),r=!0);break}let c=i==="consumed"?"READ":"DELIVERED";t.status!==c&&(await this.repository.incrementMetric("delivered"),await this.stateMachine.transition(t,t.state,c),r=!0);let a=t.options?.finalStatus||"DELIVERED";this.getStatusRank(t.status)>=this.getStatusRank(a)?(await this.stateMachine.transition(t,"FINAL",t.status),r=!0):s=this.pollingIntervals.delivered;break;case"failed":await this.handleDispatchFailure(t,e,new Error("Dispatch failed from Gateway"));return}t.state!=="FINAL"&&await this.rescheduleCheck(t,s)}catch(i){o.error("[handleCheckJob] Error",i),await this.rescheduleCheck(t,this.pollingIntervals.pending)}}}checkAndHandleTimeout(t){let e=new Date;if(t.status==="PENDING"){let i=t.lastDispatchAttemptAt||t.sentAt||t.createdAt;if(e.getTime()-new Date(i).getTime()>this.timeouts.pending)return!0}return!!(t.status==="SENDING"&&t.acceptedAt&&e.getTime()-new Date(t.acceptedAt).getTime()>this.timeouts.sending)}async handleTimeout(t,e){await this.stateMachine.transition(t,"FINAL","FAILED",{error:"Timeout Exceeded"}),o.info("[handleTimeout] Message timed out",{messageId:t.messageId})}startTimeoutMonitor(){this.timeoutTimer||(this.timeoutTimer=setInterval(async()=>{try{let t=await this.repository.getMessages({status:"PENDING"}),e=await this.repository.getMessages({status:"SENDING"}),i=[...t,...e];for(let r of i)if(this.checkAndHandleTimeout(r)){let n=this.descriptors.get(r.descriptorId)||null;await this.handleTimeout(r,n)}let s=await this.repository.getRetentionMessages(100);if(s.length>0){o.debug("[CleanupMonitor] Cleaning up expired messages",{count:s.length});for(let r of s)await this.repository.deleteMessage(r)}}catch(t){o.error("[TimeoutMonitor] Error during scan",t)}},10*1e3))}async rescheduleCheck(t,e){await this.queue.add("check",{messageId:t.messageId},{delay:e,priority:5})}async handleDispatchFailure(t,e,i){if(t.attempts=(t.attempts??0)+1,t.error=i.message,o.error("[handleDispatchFailure]",{messageId:t.messageId,attempts:t.attempts,maxRetries:this.maxRetries,error:i.message}),t.attempts<=this.maxRetries){t.retries=this.maxRetries-t.attempts;let s=this.retryIntervals[t.attempts-1]||this.retryIntervals[this.retryIntervals.length-1];await this.stateMachine.transition(t,"SCHEDULED",t.status),this.emit("retry",t),e?.emit("retry",t,this.api,this.id),await this.queue.add("send",{messageId:t.messageId},{delay:s,priority:1}),o.info("[handleDispatchFailure] Rescheduled retry",{messageId:t.messageId,retryDelay:s})}else t.retries=0,await this.stateMachine.transition(t,"FINAL","FAILED"),await this.repository.incrementMetric("failed")}calculateScheduledTime(t,e){if(t)return t;if(!e||e.length===0)return;let i=new Date;return this.isWithinShifts(i,e)?void 0:this.findNextShiftTime(i,e)?.toISOString()}isWithinShifts(t,e){let i=t.getDay(),s=i===0?64:Math.pow(2,i-1);for(let r of e){if((r.days&s)===0)continue;let n=r.gmt||"-3",c=parseInt(n,10),a=new Date(t.getTime()-c*60*60*1e3),d=a.getHours()*60+a.getMinutes(),[l,u]=r.start.split(":").map(Number),[m,p]=r.end.split(":").map(Number),h=l*60+u,g=m*60+p;if(d>=h&&d<g)return!0}return!1}findNextShiftTime(t,e){for(let s=0;s<=7;s++){let r=new Date(t);r.setDate(r.getDate()+s);let n=r.getDay(),c=n===0?64:Math.pow(2,n-1),a=e.filter(d=>(d.days&c)!==0);if(a.length!==0){a.sort((d,l)=>{let[u,m]=d.start.split(":").map(Number),[p,h]=l.start.split(":").map(Number);return u*60+m-(p*60+h)});for(let d of a){let l=d.gmt||"-3",u=parseInt(l,10),[m,p]=d.start.split(":").map(Number),h=new Date(r);h.setHours(m,p,0,0);let g=new Date(h.getTime()+u*60*60*1e3);if(s===0){if(g>t)return g}else return g}}}}getStatusRank(t){return{INIT:0,PENDING:1,SENDING:2,DELIVERED:3,READ:4,REPLIED:5,FAILED:6,CANCELED:6}[t]||0}};export{M as a};
2
- //# sourceMappingURL=chunk-LKT4N6L5.mjs.map
1
+ import{a as D}from"./chunk-UFUGEIFL.mjs";import{a as I}from"./chunk-QA6PFVGP.mjs";import{a as v}from"./chunk-CVPGUSFU.mjs";import{a as f,b as y}from"./chunk-4LYB64T2.mjs";import{b as w}from"./chunk-OXXLVJVC.mjs";import{a as S}from"./chunk-JD53PFR4.mjs";var b=S((A,k)=>{k.exports={name:"@dawntech/dispatcher",version:"0.2.6",description:"A TypeScript Node.js package for sending push messages in conversational chatbots on the Blip platform.",main:"dist/index.js",module:"dist/index.mjs",types:"dist/index.d.ts",scripts:{start:"node dist/index.js",build:"tsup","build:watch":"tsup --watch",dev:"tsx watch src/server.ts","test:basic":"tsx tests/integration/scenarios/1-basic-send.ts","test:contact":"tsx tests/integration/scenarios/2-contact-update.ts","test:schedule":"tsx tests/integration/scenarios/3-scheduling.ts","test:status":"tsx tests/integration/scenarios/4-status-config.ts","test:intent":"tsx tests/integration/scenarios/5-intent.ts","test:rate-limit":"tsx tests/integration/scenarios/6-rate-limiting.ts","test:high-load":"tsx tests/integration/scenarios/7-high-load.ts","test:retries":"tsx tests/integration/scenarios/8-retries.ts","test:expiry":"tsx tests/integration/scenarios/9-expiration.ts","test:cluster":"tsx tests/integration/scenarios/10-cluster.ts","test:monitor":"tsx tests/integration/scenarios/11-monitor.ts",test:"jest","test:watch":"jest --watch","test:coverage":"jest --coverage","test:blip-api":"tsx tests/blip-api.ts",clean:"rm -rf dist",setup:"bash scripts/setup.sh","docker:up":"docker-compose up -d","docker:down":"docker-compose down","docker:logs":"docker-compose logs -f","docker:redis-cli":"docker-compose exec redis redis-cli","docker:clean":"docker-compose down -v","docker:restart":"docker-compose restart","docker:tools":"docker-compose --profile tools up -d",format:'prettier --write "src/**/*.ts" "tests/**/*.ts"',"format:check":'prettier --check "src/**/*.ts" "tests/**/*.ts"',prepublishOnly:"npm run build"},packageManager:"npm@11.8.0",devDependencies:{"@types/body-parser":"^1.19.6","@types/cors":"^2.8.19","@types/debug":"^4.1.12","@types/express":"^5.0.6","@types/ioredis":"^4.28.10","@types/jest":"^29.5.14","@types/lodash":"^4.17.20","@types/node":"^20.19.24",husky:"^9.1.7",jest:"^29.7.0","lint-staged":"^16.2.6",prettier:"^3.6.2","ts-jest":"^29.2.5",tsup:"^8.5.1",tsx:"^4.7.0",typescript:"^5.3.0"},keywords:[],author:"",license:"ISC",engines:{node:">=24.0.0"},dependencies:{axios:"^1.13.1","body-parser":"^2.2.2",bullmq:"^5.67.1",cors:"^2.8.6",debug:"^4.4.3",dotenv:"^17.2.3",express:"^5.2.1",ioredis:"^5.9.2",lodash:"^4.17.21","rate-limiter-flexible":"^9.0.1",redis:"^5.9.0",uuid:"^13.0.0"}}});import{v4 as E}from"uuid";import{Queue as N,Worker as x}from"bullmq";var{version:T}=b(),o=w("Dispatcher"),M=class{constructor(t,e,i,s){this.callbacks={};this.descriptors=new Map;this.isRunning=!1;this.setupCompleted=!1;this.id=t,this.repository=e,this.redis=this.repository.redis,this.stateMachine=new D(this.id,this.repository,(r,n)=>{this.emit(r,n),this.descriptors.get(n.descriptorId)?.emit(r,n,this.api,this.id)}),this.api=new I(i.contract,i.key),this.queueName=`dispatcher-${this.id.replace(/:/g,"-")}`,this.maxRetries=s?.maxRetries??0,this.retryIntervals=s?.retryIntervals??[1*1e3,5*1e3,15*1e3],this.timeouts={pending:s?.timeouts?.pending??120*1e3,sending:s?.timeouts?.sending??120*1e3},this.retention=s?.retention??2880*60*1e3,this.pollingIntervals={scheduled:s?.pollingIntervals?.scheduled??30*1e3,pending:s?.pollingIntervals?.pending??10*1e3,sending:s?.pollingIntervals?.sending??10*1e3,delivered:s?.pollingIntervals?.delivered??1800*1e3,read:s?.pollingIntervals?.read??1800*1e3,queue:s?.pollingIntervals?.queue??1*1e3},this.timeoutTimer=null,this.query=new v(this.repository),this.queue=new N(this.queueName,{connection:this.redis,defaultJobOptions:{removeOnComplete:!0,removeOnFail:!0}}),this.worker=new x(this.queueName,async r=>{try{await this.processJob(r)}catch(n){throw o.error(`[Worker] Job ${r.name} failed`,n),n}},{connection:this.redis,concurrency:s?.batchSize||50,limiter:s?.rateLimits?.global?{max:s.rateLimits.global.points,duration:s.rateLimits.global.duration*1e3}:void 0}),this.worker.on("error",r=>o.error("[Worker] Error",r)),this.worker.on("failed",(r,n)=>o.error(`[Worker] Job ${r?.id} failed`,n))}async setup(){if(this.setupCompleted)return;await this.repository.setup();let t=await this.repository.getManifest();await this.repository.writeManifest({version:T,createdAt:t?.createdAt??new Date().toISOString(),updatedAt:new Date().toISOString()}),await this.queue.waitUntilReady(),this.isRunning=!0,this.setupCompleted=!0,this.startTimeoutMonitor(),o.info("[setup] Dispatcher started (BullMQ)",{queue:this.queueName})}async teardown(){this.isRunning=!1,this.timeoutTimer&&(clearInterval(this.timeoutTimer),this.timeoutTimer=null),await this.queue.close(),await this.worker.close(),await this.repository.teardown(),o.info("[teardown] Dispatcher stopped")}on(t,e){return this.callbacks[t]=e,this}async getMetrics(){let t={total:0,byState:{},byStatus:{},cumulative:{dispatched:await this.repository.getMetric("dispatched"),delivered:await this.repository.getMetric("delivered"),failed:await this.repository.getMetric("failed")}},e=Object.values(f);for(let s of e)t.byState[s]=await this.repository.countMessages({state:s});let i=Object.values(y);for(let s of i)t.byStatus[s]=await this.repository.countMessages({status:s});return t.total=Object.values(t.byState).reduce((s,r)=>s+(r||0),0),t}emit(t,e){this.callbacks[t]?.(e,this.api,this.id)}async send(t,e,i,s){this.descriptors.set(t.id,t);let r=t.toContactId(e),n=t.transform(i),c=new Date().toISOString(),a={messageId:E(),contactId:r,descriptorId:t.id,payload:n,status:"INIT",state:"INIT",createdAt:c,attempts:0,retries:this.maxRetries},l={...t.messageOptions,...s},{schedule:u,...m}=l;a.options=m,this.emit("dispatch",a),t.emit("dispatch",a,this.api,this.id);let p=this.calculateScheduledTime(u,l.shifts),h=0;if(p){a.scheduledTo=p,a.state="SCHEDULED";let g=new Date(p).getTime();h=Math.max(0,g-Date.now()),this.emit("scheduled",a),t.emit("scheduled",a,this.api,this.id),o.info("[send] message scheduled",{messageId:a.messageId,scheduledTo:p,delay:h})}else a.state="QUEUED",a.status="INIT",o.info("[send] message queued",{messageId:a.messageId});return a.expiresAt=new Date(Date.now()+(a.state==="SCHEDULED"?h+this.retention:this.retention)).toISOString(),await this.stateMachine.transition(a,a.state,a.status),await this.queue.add("send",{messageId:a.messageId},{jobId:a.messageId,delay:h,priority:1}),a}async cancel(t){let e=await this.repository.getMessage(t);if(!e)return o.warn("[cancel] message not found",{messageId:t}),!1;if(e.state==="FINAL")return o.warn("[cancel] message already final",{messageId:t,status:e.status}),!1;let i=await this.queue.getJob(t);return i&&(await i.remove(),o.info("[cancel] removed job from queue",{messageId:t})),await this.stateMachine.transition(e,"FINAL","CANCELED"),o.info("[cancel] message canceled",{messageId:t}),!0}async processJob(t){let{messageId:e}=t.data,i=await this.repository.getMessage(e);if(!i){o.warn(`[processJob] Message not found: ${e}`);return}let s=this.descriptors.get(i.descriptorId)||null;switch(t.name){case"send":await this.handleSendJob(i,s);break;case"check":await this.handleCheckJob(i,s);break;default:o.warn(`[processJob] Unknown job name: ${t.name}`)}}async handleSendJob(t,e){t.lastDispatchAttemptAt=new Date().toISOString(),await this.stateMachine.transition(t,"DISPATCHED","PENDING");try{await this.api.sendMessage(t.contactId,t.payload,t.messageId),await this.handlePostSendOperations(t,t.options),t.sentAt=new Date().toISOString(),await this.stateMachine.transition(t,"DISPATCHED","PENDING"),o.info("[handleSendJob] Message sent to API",{messageId:t.messageId}),await this.repository.incrementMetric("dispatched"),await this.queue.add("check",{messageId:t.messageId},{delay:this.pollingIntervals.pending,priority:5})}catch(i){let s=i instanceof Error?i:new Error(String(i));await this.handleDispatchFailure(t,e,s)}}async handlePostSendOperations(t,e={}){let i={...e.contact||{}};if(e.intent)if(typeof e.intent=="string")i.intent=e.intent;else{i.intent=e.intent.intent;let{intent:s,...r}=e.intent;Object.entries(r).forEach(([n,c])=>{c!=null&&(i[n]=typeof c=="object"?JSON.stringify(c):String(c))})}Object.keys(i).length>0&&await this.api.mergeContact(t.contactId,i),e.state&&await this.api.setState(t.contactId,e.state.botId,e.state.stateId)}async handleCheckJob(t,e){if(t.state!=="FINAL"){if(this.checkAndHandleTimeout(t)){await this.handleTimeout(t,e);return}try{let i=await this.api.getDispatchState(t.messageId,t.contactId);if(!i){await this.rescheduleCheck(t,this.pollingIntervals.pending);return}let s=this.pollingIntervals.pending,r=!1;switch(i){case"accepted":t.status!=="SENDING"&&(await this.stateMachine.transition(t,t.state,"SENDING"),r=!0),s=this.pollingIntervals.sending;break;case"received":case"consumed":if(await this.api.getMessageAfter(t.contactId,t.messageId)){let d="REPLIED";t.status!==d&&t.status!=="READ"&&(await this.repository.incrementMetric("delivered"),await this.stateMachine.transition(t,"FINAL","REPLIED"),r=!0);break}let c=i==="consumed"?"READ":"DELIVERED";t.status!==c&&(await this.repository.incrementMetric("delivered"),await this.stateMachine.transition(t,t.state,c),r=!0);let a=t.options?.finalStatus||"DELIVERED";this.getStatusRank(t.status)>=this.getStatusRank(a)?(await this.stateMachine.transition(t,"FINAL",t.status),r=!0):s=this.pollingIntervals.delivered;break;case"failed":await this.handleDispatchFailure(t,e,new Error("Dispatch failed from Gateway"));return}t.state!=="FINAL"&&await this.rescheduleCheck(t,s)}catch(i){o.error("[handleCheckJob] Error",i),await this.rescheduleCheck(t,this.pollingIntervals.pending)}}}checkAndHandleTimeout(t){let e=new Date;if(t.status==="PENDING"){let i=t.lastDispatchAttemptAt||t.sentAt||t.createdAt;if(e.getTime()-new Date(i).getTime()>this.timeouts.pending)return!0}return!!(t.status==="SENDING"&&t.acceptedAt&&e.getTime()-new Date(t.acceptedAt).getTime()>this.timeouts.sending)}async handleTimeout(t,e){await this.stateMachine.transition(t,"FINAL","FAILED",{error:"Timeout Exceeded"}),o.info("[handleTimeout] Message timed out",{messageId:t.messageId})}startTimeoutMonitor(){this.timeoutTimer||(this.timeoutTimer=setInterval(async()=>{try{let t=await this.repository.getMessages({status:"PENDING"}),e=await this.repository.getMessages({status:"SENDING"}),i=[...t,...e];for(let r of i)if(this.checkAndHandleTimeout(r)){let n=this.descriptors.get(r.descriptorId)||null;await this.handleTimeout(r,n)}let s=await this.repository.getRetentionMessages(100);if(s.length>0){o.debug("[CleanupMonitor] Cleaning up expired messages",{count:s.length});for(let r of s)await this.repository.deleteMessage(r)}}catch(t){o.error("[TimeoutMonitor] Error during scan",t)}},10*1e3))}async rescheduleCheck(t,e){await this.queue.add("check",{messageId:t.messageId},{delay:e,priority:5})}async handleDispatchFailure(t,e,i){if(t.attempts=(t.attempts??0)+1,t.error=i.message,o.error("[handleDispatchFailure]",{messageId:t.messageId,attempts:t.attempts,maxRetries:this.maxRetries,error:i.message}),t.attempts<=this.maxRetries){t.retries=this.maxRetries-t.attempts;let s=this.retryIntervals[t.attempts-1]||this.retryIntervals[this.retryIntervals.length-1];await this.stateMachine.transition(t,"SCHEDULED",t.status),this.emit("retry",t),e?.emit("retry",t,this.api,this.id),await this.queue.add("send",{messageId:t.messageId},{delay:s,priority:1}),o.info("[handleDispatchFailure] Rescheduled retry",{messageId:t.messageId,retryDelay:s})}else t.retries=0,await this.stateMachine.transition(t,"FINAL","FAILED"),await this.repository.incrementMetric("failed")}calculateScheduledTime(t,e){if(t)return t;if(!e||e.length===0)return;let i=new Date;return this.isWithinShifts(i,e)?void 0:this.findNextShiftTime(i,e)?.toISOString()}isWithinShifts(t,e){let i=t.getDay(),s=i===0?64:Math.pow(2,i-1);for(let r of e){if((r.days&s)===0)continue;let n=r.gmt||"-3",c=parseInt(n,10),a=new Date(t.getTime()-c*60*60*1e3),d=a.getHours()*60+a.getMinutes(),[l,u]=r.start.split(":").map(Number),[m,p]=r.end.split(":").map(Number),h=l*60+u,g=m*60+p;if(d>=h&&d<g)return!0}return!1}findNextShiftTime(t,e){for(let s=0;s<=7;s++){let r=new Date(t);r.setDate(r.getDate()+s);let n=r.getDay(),c=n===0?64:Math.pow(2,n-1),a=e.filter(d=>(d.days&c)!==0);if(a.length!==0){a.sort((d,l)=>{let[u,m]=d.start.split(":").map(Number),[p,h]=l.start.split(":").map(Number);return u*60+m-(p*60+h)});for(let d of a){let l=d.gmt||"-3",u=parseInt(l,10),[m,p]=d.start.split(":").map(Number),h=new Date(r);h.setHours(m,p,0,0);let g=new Date(h.getTime()+u*60*60*1e3);if(s===0){if(g>t)return g}else return g}}}}getStatusRank(t){return{INIT:0,PENDING:1,SENDING:2,DELIVERED:3,READ:4,REPLIED:5,FAILED:6,CANCELED:6}[t]||0}};export{M as a};
2
+ //# sourceMappingURL=chunk-PAKGTHKD.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../package.json","../src/core/Dispatcher.ts"],"sourcesContent":["{\n \"name\": \"@dawntech/dispatcher\",\n \"version\": \"0.2.2\",\n \"description\": \"A TypeScript Node.js package for sending push messages in conversational chatbots on the Blip platform.\",\n \"main\": \"dist/index.js\",\n \"module\": \"dist/index.mjs\",\n \"types\": \"dist/index.d.ts\",\n \"scripts\": {\n \"start\": \"node dist/index.js\",\n \"build\": \"tsup\",\n \"build:watch\": \"tsup --watch\",\n \"dev\": \"tsx watch src/server.ts\",\n \"test:basic\": \"tsx tests/integration/scenarios/1-basic-send.ts\",\n \"test:contact\": \"tsx tests/integration/scenarios/2-contact-update.ts\",\n \"test:schedule\": \"tsx tests/integration/scenarios/3-scheduling.ts\",\n \"test:status\": \"tsx tests/integration/scenarios/4-status-config.ts\",\n \"test:intent\": \"tsx tests/integration/scenarios/5-intent.ts\",\n \"test:rate-limit\": \"tsx tests/integration/scenarios/6-rate-limiting.ts\",\n \"test:high-load\": \"tsx tests/integration/scenarios/7-high-load.ts\",\n \"test:retries\": \"tsx tests/integration/scenarios/8-retries.ts\",\n \"test:expiry\": \"tsx tests/integration/scenarios/9-expiration.ts\",\n \"test:cluster\": \"tsx tests/integration/scenarios/10-cluster.ts\",\n \"test:monitor\": \"tsx tests/integration/scenarios/11-monitor.ts\",\n \"test\": \"jest\",\n \"test:watch\": \"jest --watch\",\n \"test:coverage\": \"jest --coverage\",\n \"test:blip-api\": \"tsx tests/blip-api.ts\",\n \"clean\": \"rm -rf dist\",\n \"setup\": \"bash scripts/setup.sh\",\n \"docker:up\": \"docker-compose up -d\",\n \"docker:down\": \"docker-compose down\",\n \"docker:logs\": \"docker-compose logs -f\",\n \"docker:redis-cli\": \"docker-compose exec redis redis-cli\",\n \"docker:clean\": \"docker-compose down -v\",\n \"docker:restart\": \"docker-compose restart\",\n \"docker:tools\": \"docker-compose --profile tools up -d\",\n \"format\": \"prettier --write \\\"src/**/*.ts\\\" \\\"tests/**/*.ts\\\"\",\n \"format:check\": \"prettier --check \\\"src/**/*.ts\\\" \\\"tests/**/*.ts\\\"\",\n \"prepublishOnly\": \"npm run build\"\n },\n \"packageManager\": \"npm@11.8.0\",\n \"devDependencies\": {\n \"@types/body-parser\": \"^1.19.6\",\n \"@types/cors\": \"^2.8.19\",\n \"@types/debug\": \"^4.1.12\",\n \"@types/express\": \"^5.0.6\",\n \"@types/ioredis\": \"^4.28.10\",\n \"@types/jest\": \"^29.5.14\",\n \"@types/lodash\": \"^4.17.20\",\n \"@types/node\": \"^20.19.24\",\n \"husky\": \"^9.1.7\",\n \"jest\": \"^29.7.0\",\n \"lint-staged\": \"^16.2.6\",\n \"prettier\": \"^3.6.2\",\n \"ts-jest\": \"^29.2.5\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.7.0\",\n \"typescript\": \"^5.3.0\"\n },\n \"keywords\": [],\n \"author\": \"\",\n \"license\": \"ISC\",\n \"engines\": {\n \"node\": \">=24.0.0\"\n },\n \"dependencies\": {\n \"axios\": \"^1.13.1\",\n \"body-parser\": \"^2.2.2\",\n \"bullmq\": \"^5.67.1\",\n \"cors\": \"^2.8.6\",\n \"debug\": \"^4.4.3\",\n \"dotenv\": \"^17.2.3\",\n \"express\": \"^5.2.1\",\n \"ioredis\": \"^5.9.2\",\n \"lodash\": \"^4.17.21\",\n \"rate-limiter-flexible\": \"^9.0.1\",\n \"redis\": \"^5.9.0\",\n \"uuid\": \"^13.0.0\"\n }\n}\n","import { v4 as uuidv4 } from 'uuid';\nimport { Queue, Worker, Job, ConnectionOptions } from 'bullmq';\nimport IORedis from 'ioredis';\nimport { DispatcherDescriptor, CallbackMap } from './DispatcherDescriptor.js';\nimport { DispatcherRepository } from './DispatcherRepository.js';\nimport { DispatcherStateMachine } from './DispatcherStateMachine.js';\nimport { Blip } from './Blip.js';\nimport {\n Contact,\n Message,\n MessagePayload,\n MessageState,\n MessageStatus,\n MessageOptions,\n DispatchMessageOptions,\n DispatcherOptions,\n ConnectionConfig,\n Shift,\n DispatchState,\n CallbackEvent,\n DispatcherMetrics,\n QueryFilter,\n} from '../types/index.js';\nimport { getLogger } from '../utils/logger.js';\nimport { DispatcherQuery } from './DispatcherQuery.js';\n\n// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst { version: PACKAGE_VERSION } = require('../../package.json');\nconst logger = getLogger('Dispatcher');\n\nexport class Dispatcher {\n public readonly id: string;\n private repository: DispatcherRepository;\n private stateMachine: DispatcherStateMachine;\n\n private api: Blip;\n private callbacks: CallbackMap = {};\n private descriptors: Map<string, DispatcherDescriptor> = new Map();\n\n private queue: Queue;\n private worker: Worker;\n private redis: IORedis;\n private queueName: string;\n\n private maxRetries: number;\n private retryIntervals: number[];\n\n private timeouts: {\n pending: number;\n sending: number;\n };\n private retention: number;\n\n private timeoutTimer: NodeJS.Timeout | null;\n\n private pollingIntervals: {\n scheduled: number;\n pending: number;\n sending: number;\n delivered: number;\n read: number;\n queue: number;\n };\n\n private isRunning = false;\n private setupCompleted = false;\n public readonly query: DispatcherQuery;\n\n constructor(\n id: string,\n repository: DispatcherRepository,\n connection: ConnectionConfig,\n options?: DispatcherOptions\n ) {\n this.id = id;\n\n this.repository = repository;\n this.redis = this.repository.redis;\n\n // Initialize State Machine with emit callback\n this.stateMachine = new DispatcherStateMachine(this.id, this.repository, (event, message) => {\n this.emit(event, message);\n const descriptor = this.descriptors.get(message.descriptorId);\n descriptor?.emit(event, message, this.api, this.id);\n });\n\n this.api = new Blip(connection.contract, connection.key);\n this.queueName = `dispatcher-${this.id.replace(/:/g, '-')}`;\n\n this.maxRetries = options?.maxRetries ?? 0;\n this.retryIntervals = options?.retryIntervals ?? [1 * 1000, 5 * 1000, 15 * 1000];\n\n this.timeouts = {\n pending: options?.timeouts?.pending ?? 2 * 60 * 1000, // 2 minutes\n sending: options?.timeouts?.sending ?? 2 * 60 * 1000, // 2 minute\n };\n this.retention = options?.retention ?? 2 * 24 * 60 * 60 * 1000; // 2 days\n\n // Polling intervals for status checks (re-queue delays)\n this.pollingIntervals = {\n scheduled: options?.pollingIntervals?.scheduled ?? 30 * 1000,\n pending: options?.pollingIntervals?.pending ?? 10 * 1000,\n sending: options?.pollingIntervals?.sending ?? 10 * 1000,\n delivered: options?.pollingIntervals?.delivered ?? 30 * 60 * 1000,\n read: options?.pollingIntervals?.read ?? 30 * 60 * 1000,\n queue: options?.pollingIntervals?.queue ?? 1 * 1000,\n };\n this.timeoutTimer = null;\n this.query = new DispatcherQuery(this.repository);\n\n // Queue Configuration\n this.queue = new Queue(this.queueName, {\n connection: this.redis as ConnectionOptions,\n defaultJobOptions: {\n removeOnComplete: true, // Keep Redis clean\n removeOnFail: true, // We handle failures manually\n },\n });\n\n // Worker Configuration\n this.worker = new Worker(\n this.queueName,\n async (job: Job) => {\n try {\n await this.processJob(job);\n } catch (error) {\n logger.error(`[Worker] Job ${job.name} failed`, error);\n throw error; // Let BullMQ mark as failed\n }\n },\n {\n // @ts-ignore\n connection: this.redis,\n concurrency: options?.batchSize || 50, // Concurrency matches batch capability\n limiter: options?.rateLimits?.global\n ? {\n max: options.rateLimits.global.points,\n duration: options.rateLimits.global.duration * 1000,\n }\n : undefined, // Worker limiter is alternative to Queue limiter\n }\n );\n\n this.worker.on('error', (err) => logger.error('[Worker] Error', err));\n this.worker.on('failed', (job, err) => logger.error(`[Worker] Job ${job?.id} failed`, err));\n }\n\n async setup(): Promise<void> {\n if (this.setupCompleted) return;\n await this.repository.setup();\n\n // Write/update manifest\n const existing = await this.repository.getManifest();\n await this.repository.writeManifest({\n version: PACKAGE_VERSION,\n createdAt: existing?.createdAt ?? new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n });\n\n // Worker starts automatically, but we can verify connection\n await this.queue.waitUntilReady();\n this.isRunning = true;\n this.setupCompleted = true;\n this.startTimeoutMonitor();\n logger.info('[setup] Dispatcher started (BullMQ)', { queue: this.queueName });\n }\n\n async teardown(): Promise<void> {\n this.isRunning = false;\n if (this.timeoutTimer) {\n clearInterval(this.timeoutTimer);\n this.timeoutTimer = null;\n }\n await this.queue.close();\n await this.worker.close();\n await this.repository.teardown();\n logger.info('[teardown] Dispatcher stopped');\n }\n\n on(\n event: CallbackEvent,\n callback: (message: Message, api: Blip, dispatcherId: string) => void\n ): this {\n this.callbacks[event] = callback;\n return this;\n }\n\n async getMetrics(): Promise<DispatcherMetrics> {\n const metrics: DispatcherMetrics = {\n total: 0,\n byState: {},\n byStatus: {},\n cumulative: {\n dispatched: await this.repository.getMetric('dispatched'),\n delivered: await this.repository.getMetric('delivered'),\n failed: await this.repository.getMetric('failed'),\n },\n };\n\n // Use Repository to get counts (Persistent View)\n const states = Object.values(MessageState);\n for (const state of states) {\n metrics.byState[state] = await this.repository.countMessages({ state });\n }\n\n const statuses = Object.values(MessageStatus);\n for (const status of statuses) {\n metrics.byStatus[status] = await this.repository.countMessages({ status });\n }\n\n metrics.total = Object.values(metrics.byState).reduce((a, b) => a + (b || 0), 0);\n return metrics;\n }\n\n private emit(event: CallbackEvent, message: Message): void {\n this.callbacks[event]?.(message, this.api, this.id);\n }\n\n // ----------------------------------------------------------------------\n // SEND LOGIC\n // ----------------------------------------------------------------------\n\n async send(\n descriptor: DispatcherDescriptor,\n contactId: string,\n payload: MessagePayload,\n options?: DispatchMessageOptions\n ): Promise<Message> {\n this.descriptors.set(descriptor.id, descriptor);\n\n const validatedContactId = descriptor.toContactId(contactId);\n const messageData = descriptor.transform(payload);\n const now = new Date().toISOString();\n\n const message: Message = {\n messageId: uuidv4(),\n contactId: validatedContactId,\n descriptorId: descriptor.id,\n payload: messageData,\n status: MessageStatus.INIT,\n state: MessageState.INIT,\n createdAt: now,\n attempts: 0,\n retries: this.maxRetries,\n };\n\n // Options\n const descriptorOptions = descriptor.messageOptions;\n const mergedOptions: DispatchMessageOptions = { ...descriptorOptions, ...options };\n const { schedule, ...messageOptions } = mergedOptions;\n message.options = messageOptions;\n\n // Events\n this.emit('dispatch', message);\n descriptor.emit('dispatch', message, this.api, this.id);\n\n // Scheduling\n const scheduledTo = this.calculateScheduledTime(schedule, mergedOptions.shifts);\n let delay = 0;\n\n if (scheduledTo) {\n message.scheduledTo = scheduledTo;\n message.state = MessageState.SCHEDULED;\n\n const scheduleTime = new Date(scheduledTo).getTime();\n delay = Math.max(0, scheduleTime - Date.now());\n\n this.emit('scheduled', message);\n descriptor.emit('scheduled', message, this.api, this.id);\n logger.info('[send] message scheduled', { messageId: message.messageId, scheduledTo, delay });\n } else {\n message.state = MessageState.QUEUED;\n message.status = MessageStatus.INIT;\n logger.info('[send] message queued', { messageId: message.messageId });\n }\n\n // message.expiresAt is now managed by Redis TTL (Retention), separate from state timeouts.\n // We don't need to manually set `message.expiresAt` field unless we want to track it for debugging/API.\n // Let's set it to indicate when it WILL expire from Redis.\n message.expiresAt = new Date(\n Date.now() +\n (message.state === MessageState.SCHEDULED ? delay + this.retention : this.retention)\n ).toISOString();\n\n // Save Initial State\n await this.stateMachine.transition(message, message.state, message.status);\n\n // Add to Queue\n await this.queue.add(\n 'send',\n { messageId: message.messageId },\n {\n jobId: message.messageId, // Use messageId match\n delay,\n priority: 1, // High priority for sends\n }\n );\n\n return message;\n }\n\n async cancel(messageId: string): Promise<boolean> {\n const message = await this.repository.getMessage(messageId);\n\n if (!message) {\n logger.warn('[cancel] message not found', { messageId });\n return false;\n }\n\n if (message.state === MessageState.FINAL) {\n logger.warn('[cancel] message already final', { messageId, status: message.status });\n return false;\n }\n\n // Try to remove from BullMQ\n const job = await this.queue.getJob(messageId);\n if (job) {\n await job.remove();\n logger.info('[cancel] removed job from queue', { messageId });\n }\n\n await this.stateMachine.transition(message, MessageState.FINAL, MessageStatus.CANCELED);\n\n logger.info('[cancel] message canceled', { messageId });\n return true;\n }\n\n // ----------------------------------------------------------------------\n // WORKER PROCESSOR\n // ----------------------------------------------------------------------\n\n private async processJob(job: Job): Promise<void> {\n const { messageId } = job.data;\n const message = await this.repository.getMessage(messageId);\n\n if (!message) {\n logger.warn(`[processJob] Message not found: ${messageId}`);\n return;\n }\n\n // Refresh context\n const descriptor = this.descriptors.get(message.descriptorId) || null;\n\n switch (job.name) {\n case 'send':\n await this.handleSendJob(message, descriptor);\n break;\n case 'check':\n await this.handleCheckJob(message, descriptor);\n break;\n default:\n logger.warn(`[processJob] Unknown job name: ${job.name}`);\n }\n }\n\n private async handleSendJob(\n message: Message,\n descriptor: DispatcherDescriptor | null\n ): Promise<void> {\n message.lastDispatchAttemptAt = new Date().toISOString();\n await this.stateMachine.transition(message, MessageState.DISPATCHED, MessageStatus.PENDING);\n\n try {\n await this.api.sendMessage(message.contactId, message.payload, message.messageId);\n\n // Handle Extras/Intent/State\n await this.handlePostSendOperations(message, message.options);\n\n // Update Status: PENDING -> Wait for Accept/Deliver\n message.sentAt = new Date().toISOString();\n await this.stateMachine.transition(message, MessageState.DISPATCHED, MessageStatus.PENDING); // Persist sentAt update\n\n logger.info('[handleSendJob] Message sent to API', { messageId: message.messageId });\n await this.repository.incrementMetric('dispatched');\n\n // Schedule status check\n await this.queue.add(\n 'check',\n { messageId: message.messageId },\n {\n delay: this.pollingIntervals.pending,\n priority: 5, // Lower priority than sends\n }\n );\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n await this.handleDispatchFailure(message, descriptor, err);\n }\n }\n\n private async handlePostSendOperations(\n message: Message,\n options: MessageOptions = {}\n ): Promise<void> {\n const contact: Contact = { ...(options.contact || {}) };\n\n if (options.intent) {\n if (typeof options.intent === 'string') {\n contact.intent = options.intent;\n } else {\n contact.intent = options.intent.intent;\n const { intent, ...rest } = options.intent;\n Object.entries(rest).forEach(([key, value]) => {\n if (value !== undefined && value !== null) {\n contact[key] = typeof value === 'object' ? JSON.stringify(value) : String(value);\n }\n });\n }\n }\n\n if (Object.keys(contact).length > 0) {\n await this.api.mergeContact(message.contactId, contact);\n }\n\n if (options.state) {\n await this.api.setState(message.contactId, options.state.botId, options.state.stateId);\n }\n }\n\n private async handleCheckJob(\n message: Message,\n descriptor: DispatcherDescriptor | null\n ): Promise<void> {\n if (message.state === MessageState.FINAL) return;\n\n // Timeout Check logic is now handled by startTimeoutMonitor, but we can do a quick check here too.\n if (this.checkAndHandleTimeout(message)) {\n await this.handleTimeout(message, descriptor);\n return;\n }\n\n try {\n const state = await this.api.getDispatchState(message.messageId, message.contactId);\n if (!state) {\n // No state found yet? Schedule check again\n await this.rescheduleCheck(message, this.pollingIntervals.pending);\n return;\n }\n\n let nextCheckDelay = this.pollingIntervals.pending;\n let updated = false;\n\n switch (state) {\n case DispatchState.ACCEPTED:\n if (message.status !== MessageStatus.SENDING) {\n await this.stateMachine.transition(message, message.state, MessageStatus.SENDING);\n updated = true;\n }\n // Use longer interval for subsequent checks\n nextCheckDelay = this.pollingIntervals.sending;\n break;\n\n case DispatchState.RECEIVED:\n case DispatchState.CONSUMED:\n // Check for implicit reply first\n // If we find a reply, it means the user read and replied.\n const reply = await this.api.getMessageAfter(message.contactId, message.messageId);\n if (reply) {\n const newStatus = MessageStatus.REPLIED; // Using REPLIED if available, or READ\n if (message.status !== newStatus && message.status !== MessageStatus.READ) {\n // Implicit Read and Replied\n // We need to transition to REPLIED.\n // Note: StateMachine sets readAt/repliedAt if missing.\n await this.repository.incrementMetric('delivered'); // Ensure delivered count\n // Force FINAL state as REPLIED is terminal\n await this.stateMachine.transition(\n message,\n MessageState.FINAL,\n MessageStatus.REPLIED\n );\n updated = true;\n }\n break;\n }\n\n const newStatus =\n state === DispatchState.CONSUMED ? MessageStatus.READ : MessageStatus.DELIVERED;\n if (message.status !== newStatus) {\n // Increment delivered only on first transition to delivered/read/replied\n await this.repository.incrementMetric('delivered');\n await this.stateMachine.transition(message, message.state, newStatus);\n updated = true;\n }\n\n // Check for Final\n const finalStatus = message.options?.finalStatus || 'DELIVERED';\n if (\n this.getStatusRank(message.status) >= this.getStatusRank(finalStatus as MessageStatus)\n ) {\n await this.stateMachine.transition(message, MessageState.FINAL, message.status);\n updated = true;\n } else {\n nextCheckDelay = this.pollingIntervals.delivered;\n }\n break;\n\n case DispatchState.FAILED:\n await this.handleDispatchFailure(\n message,\n descriptor,\n new Error('Dispatch failed from Gateway')\n );\n return;\n }\n\n if (updated) {\n }\n\n // If not final, reschedule check\n if ((message.state as MessageState) !== MessageState.FINAL) {\n await this.rescheduleCheck(message, nextCheckDelay);\n }\n } catch (error) {\n logger.error('[handleCheckJob] Error', error);\n // Retry check later\n await this.rescheduleCheck(message, this.pollingIntervals.pending);\n }\n }\n\n private checkAndHandleTimeout(message: Message): boolean {\n const now = new Date();\n\n // Check Status Timeouts\n if (message.status === MessageStatus.PENDING) {\n const startTime = message.lastDispatchAttemptAt || message.sentAt || message.createdAt;\n if (now.getTime() - new Date(startTime).getTime() > this.timeouts.pending) return true;\n }\n\n if (message.status === MessageStatus.SENDING && message.acceptedAt) {\n if (now.getTime() - new Date(message.acceptedAt).getTime() > this.timeouts.sending)\n return true;\n }\n\n return false;\n }\n\n private async handleTimeout(\n message: Message,\n descriptor: DispatcherDescriptor | null\n ): Promise<void> {\n await this.stateMachine.transition(message, MessageState.FINAL, MessageStatus.FAILED, {\n error: 'Timeout Exceeded',\n });\n logger.info('[handleTimeout] Message timed out', { messageId: message.messageId });\n }\n\n private startTimeoutMonitor(): void {\n if (this.timeoutTimer) return;\n\n // Scan periodically (e.g. every 10s)\n this.timeoutTimer = setInterval(async () => {\n try {\n const pending = await this.repository.getMessages({ status: MessageStatus.PENDING });\n const sending = await this.repository.getMessages({ status: MessageStatus.SENDING });\n const all = [...pending, ...sending];\n\n for (const message of all) {\n if (this.checkAndHandleTimeout(message)) {\n const descriptor = this.descriptors.get(message.descriptorId) || null;\n await this.handleTimeout(message, descriptor);\n }\n }\n\n // --- Active Cleanup Monitor ---\n const expiredIds = await this.repository.getRetentionMessages(100);\n if (expiredIds.length > 0) {\n logger.debug('[CleanupMonitor] Cleaning up expired messages', {\n count: expiredIds.length,\n });\n for (const id of expiredIds) {\n await this.repository.deleteMessage(id);\n }\n }\n } catch (error) {\n logger.error('[TimeoutMonitor] Error during scan', error);\n }\n }, 10 * 1000);\n }\n\n private async rescheduleCheck(message: Message, delay: number): Promise<void> {\n await this.queue.add('check', { messageId: message.messageId }, { delay, priority: 5 });\n }\n\n private async handleDispatchFailure(\n message: Message,\n descriptor: DispatcherDescriptor | null,\n error: Error\n ): Promise<void> {\n message.attempts = (message.attempts ?? 0) + 1;\n message.error = error.message;\n\n logger.error('[handleDispatchFailure]', {\n messageId: message.messageId,\n attempts: message.attempts,\n maxRetries: this.maxRetries,\n error: error.message,\n });\n\n if (message.attempts <= this.maxRetries) {\n message.retries = this.maxRetries - message.attempts;\n const retryDelay =\n this.retryIntervals[message.attempts - 1] ||\n this.retryIntervals[this.retryIntervals.length - 1];\n\n await this.stateMachine.transition(message, MessageState.SCHEDULED, message.status);\n\n this.emit('retry', message);\n descriptor?.emit('retry', message, this.api, this.id);\n\n await this.queue.add(\n 'send',\n { messageId: message.messageId },\n {\n delay: retryDelay,\n priority: 1,\n }\n );\n\n logger.info('[handleDispatchFailure] Rescheduled retry', {\n messageId: message.messageId,\n retryDelay,\n });\n } else {\n message.retries = 0;\n await this.stateMachine.transition(message, MessageState.FINAL, MessageStatus.FAILED);\n await this.repository.incrementMetric('failed');\n }\n }\n\n private calculateScheduledTime(schedule?: string, shifts?: Shift[]): string | undefined {\n if (schedule) {\n return schedule;\n }\n\n if (!shifts || shifts.length === 0) {\n return undefined;\n }\n\n const now = new Date();\n\n if (this.isWithinShifts(now, shifts)) {\n return undefined;\n }\n\n const nextShiftTime = this.findNextShiftTime(now, shifts);\n return nextShiftTime?.toISOString();\n }\n\n private isWithinShifts(date: Date, shifts: Shift[]): boolean {\n const dayOfWeek = date.getDay();\n const dayBit = dayOfWeek === 0 ? 64 : Math.pow(2, dayOfWeek - 1);\n\n for (const shift of shifts) {\n if ((shift.days & dayBit) === 0) {\n continue;\n }\n\n const gmt = shift.gmt || '-3';\n const offset = parseInt(gmt, 10);\n const shiftDate = new Date(date.getTime() - offset * 60 * 60 * 1000);\n\n const currentTime = shiftDate.getHours() * 60 + shiftDate.getMinutes();\n const [startHour, startMin] = shift.start.split(':').map(Number);\n const [endHour, endMin] = shift.end.split(':').map(Number);\n const startTime = startHour * 60 + startMin;\n const endTime = endHour * 60 + endMin;\n\n if (currentTime >= startTime && currentTime < endTime) {\n return true;\n }\n }\n\n return false;\n }\n\n private findNextShiftTime(date: Date, shifts: Shift[]): Date | undefined {\n const maxDaysAhead = 7;\n\n for (let daysAhead = 0; daysAhead <= maxDaysAhead; daysAhead++) {\n const checkDate = new Date(date);\n checkDate.setDate(checkDate.getDate() + daysAhead);\n\n const dayOfWeek = checkDate.getDay();\n const dayBit = dayOfWeek === 0 ? 64 : Math.pow(2, dayOfWeek - 1);\n\n const availableShifts = shifts.filter((shift) => (shift.days & dayBit) !== 0);\n\n if (availableShifts.length === 0) {\n continue;\n }\n\n availableShifts.sort((a, b) => {\n const [aHour, aMin] = a.start.split(':').map(Number);\n const [bHour, bMin] = b.start.split(':').map(Number);\n return aHour * 60 + aMin - (bHour * 60 + bMin);\n });\n\n for (const shift of availableShifts) {\n const gmt = shift.gmt || '-3';\n const offset = parseInt(gmt, 10);\n\n const [startHour, startMin] = shift.start.split(':').map(Number);\n\n const shiftStart = new Date(checkDate);\n shiftStart.setHours(startHour, startMin, 0, 0);\n const shiftStartUTC = new Date(shiftStart.getTime() + offset * 60 * 60 * 1000);\n\n if (daysAhead === 0) {\n if (shiftStartUTC > date) {\n return shiftStartUTC;\n }\n } else {\n return shiftStartUTC;\n }\n }\n }\n\n return undefined;\n }\n\n private getStatusRank(status: MessageStatus): number {\n const ranks: Record<MessageStatus, number> = {\n [MessageStatus.INIT]: 0,\n [MessageStatus.PENDING]: 1,\n [MessageStatus.SENDING]: 2,\n [MessageStatus.DELIVERED]: 3,\n [MessageStatus.READ]: 4,\n [MessageStatus.REPLIED]: 5,\n [MessageStatus.FAILED]: 6,\n [MessageStatus.CANCELED]: 6,\n };\n return ranks[status] || 0;\n }\n}\n"],"mappings":"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","names":["require_package","__commonJSMin","exports","module","uuidv4","Queue","Worker","PACKAGE_VERSION","logger","getLogger","Dispatcher","id","repository","connection","options","DispatcherStateMachine","event","message","Blip","DispatcherQuery","Queue","Worker","job","error","err","existing","callback","metrics","states","MessageState","state","statuses","MessageStatus","status","a","b","descriptor","contactId","payload","validatedContactId","messageData","now","uuidv4","mergedOptions","schedule","messageOptions","scheduledTo","delay","scheduleTime","messageId","contact","intent","rest","key","value","nextCheckDelay","updated","newStatus","finalStatus","startTime","pending","sending","all","expiredIds","retryDelay","shifts","date","dayOfWeek","dayBit","shift","gmt","offset","shiftDate","currentTime","startHour","startMin","endHour","endMin","endTime","daysAhead","checkDate","availableShifts","aHour","aMin","bHour","bMin","shiftStart","shiftStartUTC"]}
1
+ {"version":3,"sources":["../package.json","../src/core/Dispatcher.ts"],"sourcesContent":["{\n \"name\": \"@dawntech/dispatcher\",\n \"version\": \"0.2.6\",\n \"description\": \"A TypeScript Node.js package for sending push messages in conversational chatbots on the Blip platform.\",\n \"main\": \"dist/index.js\",\n \"module\": \"dist/index.mjs\",\n \"types\": \"dist/index.d.ts\",\n \"scripts\": {\n \"start\": \"node dist/index.js\",\n \"build\": \"tsup\",\n \"build:watch\": \"tsup --watch\",\n \"dev\": \"tsx watch src/server.ts\",\n \"test:basic\": \"tsx tests/integration/scenarios/1-basic-send.ts\",\n \"test:contact\": \"tsx tests/integration/scenarios/2-contact-update.ts\",\n \"test:schedule\": \"tsx tests/integration/scenarios/3-scheduling.ts\",\n \"test:status\": \"tsx tests/integration/scenarios/4-status-config.ts\",\n \"test:intent\": \"tsx tests/integration/scenarios/5-intent.ts\",\n \"test:rate-limit\": \"tsx tests/integration/scenarios/6-rate-limiting.ts\",\n \"test:high-load\": \"tsx tests/integration/scenarios/7-high-load.ts\",\n \"test:retries\": \"tsx tests/integration/scenarios/8-retries.ts\",\n \"test:expiry\": \"tsx tests/integration/scenarios/9-expiration.ts\",\n \"test:cluster\": \"tsx tests/integration/scenarios/10-cluster.ts\",\n \"test:monitor\": \"tsx tests/integration/scenarios/11-monitor.ts\",\n \"test\": \"jest\",\n \"test:watch\": \"jest --watch\",\n \"test:coverage\": \"jest --coverage\",\n \"test:blip-api\": \"tsx tests/blip-api.ts\",\n \"clean\": \"rm -rf dist\",\n \"setup\": \"bash scripts/setup.sh\",\n \"docker:up\": \"docker-compose up -d\",\n \"docker:down\": \"docker-compose down\",\n \"docker:logs\": \"docker-compose logs -f\",\n \"docker:redis-cli\": \"docker-compose exec redis redis-cli\",\n \"docker:clean\": \"docker-compose down -v\",\n \"docker:restart\": \"docker-compose restart\",\n \"docker:tools\": \"docker-compose --profile tools up -d\",\n \"format\": \"prettier --write \\\"src/**/*.ts\\\" \\\"tests/**/*.ts\\\"\",\n \"format:check\": \"prettier --check \\\"src/**/*.ts\\\" \\\"tests/**/*.ts\\\"\",\n \"prepublishOnly\": \"npm run build\"\n },\n \"packageManager\": \"npm@11.8.0\",\n \"devDependencies\": {\n \"@types/body-parser\": \"^1.19.6\",\n \"@types/cors\": \"^2.8.19\",\n \"@types/debug\": \"^4.1.12\",\n \"@types/express\": \"^5.0.6\",\n \"@types/ioredis\": \"^4.28.10\",\n \"@types/jest\": \"^29.5.14\",\n \"@types/lodash\": \"^4.17.20\",\n \"@types/node\": \"^20.19.24\",\n \"husky\": \"^9.1.7\",\n \"jest\": \"^29.7.0\",\n \"lint-staged\": \"^16.2.6\",\n \"prettier\": \"^3.6.2\",\n \"ts-jest\": \"^29.2.5\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.7.0\",\n \"typescript\": \"^5.3.0\"\n },\n \"keywords\": [],\n \"author\": \"\",\n \"license\": \"ISC\",\n \"engines\": {\n \"node\": \">=24.0.0\"\n },\n \"dependencies\": {\n \"axios\": \"^1.13.1\",\n \"body-parser\": \"^2.2.2\",\n \"bullmq\": \"^5.67.1\",\n \"cors\": \"^2.8.6\",\n \"debug\": \"^4.4.3\",\n \"dotenv\": \"^17.2.3\",\n \"express\": \"^5.2.1\",\n \"ioredis\": \"^5.9.2\",\n \"lodash\": \"^4.17.21\",\n \"rate-limiter-flexible\": \"^9.0.1\",\n \"redis\": \"^5.9.0\",\n \"uuid\": \"^13.0.0\"\n }\n}\n","import { v4 as uuidv4 } from 'uuid';\nimport { Queue, Worker, Job, ConnectionOptions } from 'bullmq';\nimport IORedis from 'ioredis';\nimport { DispatcherDescriptor, CallbackMap } from './DispatcherDescriptor.js';\nimport { DispatcherRepository } from './DispatcherRepository.js';\nimport { DispatcherStateMachine } from './DispatcherStateMachine.js';\nimport { Blip } from './Blip.js';\nimport {\n Contact,\n Message,\n MessagePayload,\n MessageState,\n MessageStatus,\n MessageOptions,\n DispatchMessageOptions,\n DispatcherOptions,\n ConnectionConfig,\n Shift,\n DispatchState,\n CallbackEvent,\n DispatcherMetrics,\n QueryFilter,\n} from '../types/index.js';\nimport { getLogger } from '../utils/logger.js';\nimport { DispatcherQuery } from './DispatcherQuery.js';\n\n// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst { version: PACKAGE_VERSION } = require('../../package.json');\nconst logger = getLogger('Dispatcher');\n\nexport class Dispatcher {\n public readonly id: string;\n private repository: DispatcherRepository;\n private stateMachine: DispatcherStateMachine;\n\n private api: Blip;\n private callbacks: CallbackMap = {};\n private descriptors: Map<string, DispatcherDescriptor> = new Map();\n\n private queue: Queue;\n private worker: Worker;\n private redis: IORedis;\n private queueName: string;\n\n private maxRetries: number;\n private retryIntervals: number[];\n\n private timeouts: {\n pending: number;\n sending: number;\n };\n private retention: number;\n\n private timeoutTimer: NodeJS.Timeout | null;\n\n private pollingIntervals: {\n scheduled: number;\n pending: number;\n sending: number;\n delivered: number;\n read: number;\n queue: number;\n };\n\n private isRunning = false;\n private setupCompleted = false;\n public readonly query: DispatcherQuery;\n\n constructor(\n id: string,\n repository: DispatcherRepository,\n connection: ConnectionConfig,\n options?: DispatcherOptions\n ) {\n this.id = id;\n\n this.repository = repository;\n this.redis = this.repository.redis;\n\n // Initialize State Machine with emit callback\n this.stateMachine = new DispatcherStateMachine(this.id, this.repository, (event, message) => {\n this.emit(event, message);\n const descriptor = this.descriptors.get(message.descriptorId);\n descriptor?.emit(event, message, this.api, this.id);\n });\n\n this.api = new Blip(connection.contract, connection.key);\n this.queueName = `dispatcher-${this.id.replace(/:/g, '-')}`;\n\n this.maxRetries = options?.maxRetries ?? 0;\n this.retryIntervals = options?.retryIntervals ?? [1 * 1000, 5 * 1000, 15 * 1000];\n\n this.timeouts = {\n pending: options?.timeouts?.pending ?? 2 * 60 * 1000, // 2 minutes\n sending: options?.timeouts?.sending ?? 2 * 60 * 1000, // 2 minute\n };\n this.retention = options?.retention ?? 2 * 24 * 60 * 60 * 1000; // 2 days\n\n // Polling intervals for status checks (re-queue delays)\n this.pollingIntervals = {\n scheduled: options?.pollingIntervals?.scheduled ?? 30 * 1000,\n pending: options?.pollingIntervals?.pending ?? 10 * 1000,\n sending: options?.pollingIntervals?.sending ?? 10 * 1000,\n delivered: options?.pollingIntervals?.delivered ?? 30 * 60 * 1000,\n read: options?.pollingIntervals?.read ?? 30 * 60 * 1000,\n queue: options?.pollingIntervals?.queue ?? 1 * 1000,\n };\n this.timeoutTimer = null;\n this.query = new DispatcherQuery(this.repository);\n\n // Queue Configuration\n this.queue = new Queue(this.queueName, {\n connection: this.redis as ConnectionOptions,\n defaultJobOptions: {\n removeOnComplete: true, // Keep Redis clean\n removeOnFail: true, // We handle failures manually\n },\n });\n\n // Worker Configuration\n this.worker = new Worker(\n this.queueName,\n async (job: Job) => {\n try {\n await this.processJob(job);\n } catch (error) {\n logger.error(`[Worker] Job ${job.name} failed`, error);\n throw error; // Let BullMQ mark as failed\n }\n },\n {\n // @ts-ignore\n connection: this.redis,\n concurrency: options?.batchSize || 50, // Concurrency matches batch capability\n limiter: options?.rateLimits?.global\n ? {\n max: options.rateLimits.global.points,\n duration: options.rateLimits.global.duration * 1000,\n }\n : undefined, // Worker limiter is alternative to Queue limiter\n }\n );\n\n this.worker.on('error', (err) => logger.error('[Worker] Error', err));\n this.worker.on('failed', (job, err) => logger.error(`[Worker] Job ${job?.id} failed`, err));\n }\n\n async setup(): Promise<void> {\n if (this.setupCompleted) return;\n await this.repository.setup();\n\n // Write/update manifest\n const existing = await this.repository.getManifest();\n await this.repository.writeManifest({\n version: PACKAGE_VERSION,\n createdAt: existing?.createdAt ?? new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n });\n\n // Worker starts automatically, but we can verify connection\n await this.queue.waitUntilReady();\n this.isRunning = true;\n this.setupCompleted = true;\n this.startTimeoutMonitor();\n logger.info('[setup] Dispatcher started (BullMQ)', { queue: this.queueName });\n }\n\n async teardown(): Promise<void> {\n this.isRunning = false;\n if (this.timeoutTimer) {\n clearInterval(this.timeoutTimer);\n this.timeoutTimer = null;\n }\n await this.queue.close();\n await this.worker.close();\n await this.repository.teardown();\n logger.info('[teardown] Dispatcher stopped');\n }\n\n on(\n event: CallbackEvent,\n callback: (message: Message, api: Blip, dispatcherId: string) => void\n ): this {\n this.callbacks[event] = callback;\n return this;\n }\n\n async getMetrics(): Promise<DispatcherMetrics> {\n const metrics: DispatcherMetrics = {\n total: 0,\n byState: {},\n byStatus: {},\n cumulative: {\n dispatched: await this.repository.getMetric('dispatched'),\n delivered: await this.repository.getMetric('delivered'),\n failed: await this.repository.getMetric('failed'),\n },\n };\n\n // Use Repository to get counts (Persistent View)\n const states = Object.values(MessageState);\n for (const state of states) {\n metrics.byState[state] = await this.repository.countMessages({ state });\n }\n\n const statuses = Object.values(MessageStatus);\n for (const status of statuses) {\n metrics.byStatus[status] = await this.repository.countMessages({ status });\n }\n\n metrics.total = Object.values(metrics.byState).reduce((a, b) => a + (b || 0), 0);\n return metrics;\n }\n\n private emit(event: CallbackEvent, message: Message): void {\n this.callbacks[event]?.(message, this.api, this.id);\n }\n\n // ----------------------------------------------------------------------\n // SEND LOGIC\n // ----------------------------------------------------------------------\n\n async send(\n descriptor: DispatcherDescriptor,\n contactId: string,\n payload: MessagePayload,\n options?: DispatchMessageOptions\n ): Promise<Message> {\n this.descriptors.set(descriptor.id, descriptor);\n\n const validatedContactId = descriptor.toContactId(contactId);\n const messageData = descriptor.transform(payload);\n const now = new Date().toISOString();\n\n const message: Message = {\n messageId: uuidv4(),\n contactId: validatedContactId,\n descriptorId: descriptor.id,\n payload: messageData,\n status: MessageStatus.INIT,\n state: MessageState.INIT,\n createdAt: now,\n attempts: 0,\n retries: this.maxRetries,\n };\n\n // Options\n const descriptorOptions = descriptor.messageOptions;\n const mergedOptions: DispatchMessageOptions = { ...descriptorOptions, ...options };\n const { schedule, ...messageOptions } = mergedOptions;\n message.options = messageOptions;\n\n // Events\n this.emit('dispatch', message);\n descriptor.emit('dispatch', message, this.api, this.id);\n\n // Scheduling\n const scheduledTo = this.calculateScheduledTime(schedule, mergedOptions.shifts);\n let delay = 0;\n\n if (scheduledTo) {\n message.scheduledTo = scheduledTo;\n message.state = MessageState.SCHEDULED;\n\n const scheduleTime = new Date(scheduledTo).getTime();\n delay = Math.max(0, scheduleTime - Date.now());\n\n this.emit('scheduled', message);\n descriptor.emit('scheduled', message, this.api, this.id);\n logger.info('[send] message scheduled', { messageId: message.messageId, scheduledTo, delay });\n } else {\n message.state = MessageState.QUEUED;\n message.status = MessageStatus.INIT;\n logger.info('[send] message queued', { messageId: message.messageId });\n }\n\n // message.expiresAt is now managed by Redis TTL (Retention), separate from state timeouts.\n // We don't need to manually set `message.expiresAt` field unless we want to track it for debugging/API.\n // Let's set it to indicate when it WILL expire from Redis.\n message.expiresAt = new Date(\n Date.now() +\n (message.state === MessageState.SCHEDULED ? delay + this.retention : this.retention)\n ).toISOString();\n\n // Save Initial State\n await this.stateMachine.transition(message, message.state, message.status);\n\n // Add to Queue\n await this.queue.add(\n 'send',\n { messageId: message.messageId },\n {\n jobId: message.messageId, // Use messageId match\n delay,\n priority: 1, // High priority for sends\n }\n );\n\n return message;\n }\n\n async cancel(messageId: string): Promise<boolean> {\n const message = await this.repository.getMessage(messageId);\n\n if (!message) {\n logger.warn('[cancel] message not found', { messageId });\n return false;\n }\n\n if (message.state === MessageState.FINAL) {\n logger.warn('[cancel] message already final', { messageId, status: message.status });\n return false;\n }\n\n // Try to remove from BullMQ\n const job = await this.queue.getJob(messageId);\n if (job) {\n await job.remove();\n logger.info('[cancel] removed job from queue', { messageId });\n }\n\n await this.stateMachine.transition(message, MessageState.FINAL, MessageStatus.CANCELED);\n\n logger.info('[cancel] message canceled', { messageId });\n return true;\n }\n\n // ----------------------------------------------------------------------\n // WORKER PROCESSOR\n // ----------------------------------------------------------------------\n\n private async processJob(job: Job): Promise<void> {\n const { messageId } = job.data;\n const message = await this.repository.getMessage(messageId);\n\n if (!message) {\n logger.warn(`[processJob] Message not found: ${messageId}`);\n return;\n }\n\n // Refresh context\n const descriptor = this.descriptors.get(message.descriptorId) || null;\n\n switch (job.name) {\n case 'send':\n await this.handleSendJob(message, descriptor);\n break;\n case 'check':\n await this.handleCheckJob(message, descriptor);\n break;\n default:\n logger.warn(`[processJob] Unknown job name: ${job.name}`);\n }\n }\n\n private async handleSendJob(\n message: Message,\n descriptor: DispatcherDescriptor | null\n ): Promise<void> {\n message.lastDispatchAttemptAt = new Date().toISOString();\n await this.stateMachine.transition(message, MessageState.DISPATCHED, MessageStatus.PENDING);\n\n try {\n await this.api.sendMessage(message.contactId, message.payload, message.messageId);\n\n // Handle Extras/Intent/State\n await this.handlePostSendOperations(message, message.options);\n\n // Update Status: PENDING -> Wait for Accept/Deliver\n message.sentAt = new Date().toISOString();\n await this.stateMachine.transition(message, MessageState.DISPATCHED, MessageStatus.PENDING); // Persist sentAt update\n\n logger.info('[handleSendJob] Message sent to API', { messageId: message.messageId });\n await this.repository.incrementMetric('dispatched');\n\n // Schedule status check\n await this.queue.add(\n 'check',\n { messageId: message.messageId },\n {\n delay: this.pollingIntervals.pending,\n priority: 5, // Lower priority than sends\n }\n );\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n await this.handleDispatchFailure(message, descriptor, err);\n }\n }\n\n private async handlePostSendOperations(\n message: Message,\n options: MessageOptions = {}\n ): Promise<void> {\n const contact: Contact = { ...(options.contact || {}) };\n\n if (options.intent) {\n if (typeof options.intent === 'string') {\n contact.intent = options.intent;\n } else {\n contact.intent = options.intent.intent;\n const { intent, ...rest } = options.intent;\n Object.entries(rest).forEach(([key, value]) => {\n if (value !== undefined && value !== null) {\n contact[key] = typeof value === 'object' ? JSON.stringify(value) : String(value);\n }\n });\n }\n }\n\n if (Object.keys(contact).length > 0) {\n await this.api.mergeContact(message.contactId, contact);\n }\n\n if (options.state) {\n await this.api.setState(message.contactId, options.state.botId, options.state.stateId);\n }\n }\n\n private async handleCheckJob(\n message: Message,\n descriptor: DispatcherDescriptor | null\n ): Promise<void> {\n if (message.state === MessageState.FINAL) return;\n\n // Timeout Check logic is now handled by startTimeoutMonitor, but we can do a quick check here too.\n if (this.checkAndHandleTimeout(message)) {\n await this.handleTimeout(message, descriptor);\n return;\n }\n\n try {\n const state = await this.api.getDispatchState(message.messageId, message.contactId);\n if (!state) {\n // No state found yet? Schedule check again\n await this.rescheduleCheck(message, this.pollingIntervals.pending);\n return;\n }\n\n let nextCheckDelay = this.pollingIntervals.pending;\n let updated = false;\n\n switch (state) {\n case DispatchState.ACCEPTED:\n if (message.status !== MessageStatus.SENDING) {\n await this.stateMachine.transition(message, message.state, MessageStatus.SENDING);\n updated = true;\n }\n // Use longer interval for subsequent checks\n nextCheckDelay = this.pollingIntervals.sending;\n break;\n\n case DispatchState.RECEIVED:\n case DispatchState.CONSUMED:\n // Check for implicit reply first\n // If we find a reply, it means the user read and replied.\n const reply = await this.api.getMessageAfter(message.contactId, message.messageId);\n if (reply) {\n const newStatus = MessageStatus.REPLIED; // Using REPLIED if available, or READ\n if (message.status !== newStatus && message.status !== MessageStatus.READ) {\n // Implicit Read and Replied\n // We need to transition to REPLIED.\n // Note: StateMachine sets readAt/repliedAt if missing.\n await this.repository.incrementMetric('delivered'); // Ensure delivered count\n // Force FINAL state as REPLIED is terminal\n await this.stateMachine.transition(\n message,\n MessageState.FINAL,\n MessageStatus.REPLIED\n );\n updated = true;\n }\n break;\n }\n\n const newStatus =\n state === DispatchState.CONSUMED ? MessageStatus.READ : MessageStatus.DELIVERED;\n if (message.status !== newStatus) {\n // Increment delivered only on first transition to delivered/read/replied\n await this.repository.incrementMetric('delivered');\n await this.stateMachine.transition(message, message.state, newStatus);\n updated = true;\n }\n\n // Check for Final\n const finalStatus = message.options?.finalStatus || 'DELIVERED';\n if (\n this.getStatusRank(message.status) >= this.getStatusRank(finalStatus as MessageStatus)\n ) {\n await this.stateMachine.transition(message, MessageState.FINAL, message.status);\n updated = true;\n } else {\n nextCheckDelay = this.pollingIntervals.delivered;\n }\n break;\n\n case DispatchState.FAILED:\n await this.handleDispatchFailure(\n message,\n descriptor,\n new Error('Dispatch failed from Gateway')\n );\n return;\n }\n\n if (updated) {\n }\n\n // If not final, reschedule check\n if ((message.state as MessageState) !== MessageState.FINAL) {\n await this.rescheduleCheck(message, nextCheckDelay);\n }\n } catch (error) {\n logger.error('[handleCheckJob] Error', error);\n // Retry check later\n await this.rescheduleCheck(message, this.pollingIntervals.pending);\n }\n }\n\n private checkAndHandleTimeout(message: Message): boolean {\n const now = new Date();\n\n // Check Status Timeouts\n if (message.status === MessageStatus.PENDING) {\n const startTime = message.lastDispatchAttemptAt || message.sentAt || message.createdAt;\n if (now.getTime() - new Date(startTime).getTime() > this.timeouts.pending) return true;\n }\n\n if (message.status === MessageStatus.SENDING && message.acceptedAt) {\n if (now.getTime() - new Date(message.acceptedAt).getTime() > this.timeouts.sending)\n return true;\n }\n\n return false;\n }\n\n private async handleTimeout(\n message: Message,\n descriptor: DispatcherDescriptor | null\n ): Promise<void> {\n await this.stateMachine.transition(message, MessageState.FINAL, MessageStatus.FAILED, {\n error: 'Timeout Exceeded',\n });\n logger.info('[handleTimeout] Message timed out', { messageId: message.messageId });\n }\n\n private startTimeoutMonitor(): void {\n if (this.timeoutTimer) return;\n\n // Scan periodically (e.g. every 10s)\n this.timeoutTimer = setInterval(async () => {\n try {\n const pending = await this.repository.getMessages({ status: MessageStatus.PENDING });\n const sending = await this.repository.getMessages({ status: MessageStatus.SENDING });\n const all = [...pending, ...sending];\n\n for (const message of all) {\n if (this.checkAndHandleTimeout(message)) {\n const descriptor = this.descriptors.get(message.descriptorId) || null;\n await this.handleTimeout(message, descriptor);\n }\n }\n\n // --- Active Cleanup Monitor ---\n const expiredIds = await this.repository.getRetentionMessages(100);\n if (expiredIds.length > 0) {\n logger.debug('[CleanupMonitor] Cleaning up expired messages', {\n count: expiredIds.length,\n });\n for (const id of expiredIds) {\n await this.repository.deleteMessage(id);\n }\n }\n } catch (error) {\n logger.error('[TimeoutMonitor] Error during scan', error);\n }\n }, 10 * 1000);\n }\n\n private async rescheduleCheck(message: Message, delay: number): Promise<void> {\n await this.queue.add('check', { messageId: message.messageId }, { delay, priority: 5 });\n }\n\n private async handleDispatchFailure(\n message: Message,\n descriptor: DispatcherDescriptor | null,\n error: Error\n ): Promise<void> {\n message.attempts = (message.attempts ?? 0) + 1;\n message.error = error.message;\n\n logger.error('[handleDispatchFailure]', {\n messageId: message.messageId,\n attempts: message.attempts,\n maxRetries: this.maxRetries,\n error: error.message,\n });\n\n if (message.attempts <= this.maxRetries) {\n message.retries = this.maxRetries - message.attempts;\n const retryDelay =\n this.retryIntervals[message.attempts - 1] ||\n this.retryIntervals[this.retryIntervals.length - 1];\n\n await this.stateMachine.transition(message, MessageState.SCHEDULED, message.status);\n\n this.emit('retry', message);\n descriptor?.emit('retry', message, this.api, this.id);\n\n await this.queue.add(\n 'send',\n { messageId: message.messageId },\n {\n delay: retryDelay,\n priority: 1,\n }\n );\n\n logger.info('[handleDispatchFailure] Rescheduled retry', {\n messageId: message.messageId,\n retryDelay,\n });\n } else {\n message.retries = 0;\n await this.stateMachine.transition(message, MessageState.FINAL, MessageStatus.FAILED);\n await this.repository.incrementMetric('failed');\n }\n }\n\n private calculateScheduledTime(schedule?: string, shifts?: Shift[]): string | undefined {\n if (schedule) {\n return schedule;\n }\n\n if (!shifts || shifts.length === 0) {\n return undefined;\n }\n\n const now = new Date();\n\n if (this.isWithinShifts(now, shifts)) {\n return undefined;\n }\n\n const nextShiftTime = this.findNextShiftTime(now, shifts);\n return nextShiftTime?.toISOString();\n }\n\n private isWithinShifts(date: Date, shifts: Shift[]): boolean {\n const dayOfWeek = date.getDay();\n const dayBit = dayOfWeek === 0 ? 64 : Math.pow(2, dayOfWeek - 1);\n\n for (const shift of shifts) {\n if ((shift.days & dayBit) === 0) {\n continue;\n }\n\n const gmt = shift.gmt || '-3';\n const offset = parseInt(gmt, 10);\n const shiftDate = new Date(date.getTime() - offset * 60 * 60 * 1000);\n\n const currentTime = shiftDate.getHours() * 60 + shiftDate.getMinutes();\n const [startHour, startMin] = shift.start.split(':').map(Number);\n const [endHour, endMin] = shift.end.split(':').map(Number);\n const startTime = startHour * 60 + startMin;\n const endTime = endHour * 60 + endMin;\n\n if (currentTime >= startTime && currentTime < endTime) {\n return true;\n }\n }\n\n return false;\n }\n\n private findNextShiftTime(date: Date, shifts: Shift[]): Date | undefined {\n const maxDaysAhead = 7;\n\n for (let daysAhead = 0; daysAhead <= maxDaysAhead; daysAhead++) {\n const checkDate = new Date(date);\n checkDate.setDate(checkDate.getDate() + daysAhead);\n\n const dayOfWeek = checkDate.getDay();\n const dayBit = dayOfWeek === 0 ? 64 : Math.pow(2, dayOfWeek - 1);\n\n const availableShifts = shifts.filter((shift) => (shift.days & dayBit) !== 0);\n\n if (availableShifts.length === 0) {\n continue;\n }\n\n availableShifts.sort((a, b) => {\n const [aHour, aMin] = a.start.split(':').map(Number);\n const [bHour, bMin] = b.start.split(':').map(Number);\n return aHour * 60 + aMin - (bHour * 60 + bMin);\n });\n\n for (const shift of availableShifts) {\n const gmt = shift.gmt || '-3';\n const offset = parseInt(gmt, 10);\n\n const [startHour, startMin] = shift.start.split(':').map(Number);\n\n const shiftStart = new Date(checkDate);\n shiftStart.setHours(startHour, startMin, 0, 0);\n const shiftStartUTC = new Date(shiftStart.getTime() + offset * 60 * 60 * 1000);\n\n if (daysAhead === 0) {\n if (shiftStartUTC > date) {\n return shiftStartUTC;\n }\n } else {\n return shiftStartUTC;\n }\n }\n }\n\n return undefined;\n }\n\n private getStatusRank(status: MessageStatus): number {\n const ranks: Record<MessageStatus, number> = {\n [MessageStatus.INIT]: 0,\n [MessageStatus.PENDING]: 1,\n [MessageStatus.SENDING]: 2,\n [MessageStatus.DELIVERED]: 3,\n [MessageStatus.READ]: 4,\n [MessageStatus.REPLIED]: 5,\n [MessageStatus.FAILED]: 6,\n [MessageStatus.CANCELED]: 6,\n };\n return ranks[status] || 0;\n }\n}\n"],"mappings":"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","names":["require_package","__commonJSMin","exports","module","uuidv4","Queue","Worker","PACKAGE_VERSION","logger","getLogger","Dispatcher","id","repository","connection","options","DispatcherStateMachine","event","message","Blip","DispatcherQuery","Queue","Worker","job","error","err","existing","callback","metrics","states","MessageState","state","statuses","MessageStatus","status","a","b","descriptor","contactId","payload","validatedContactId","messageData","now","uuidv4","mergedOptions","schedule","messageOptions","scheduledTo","delay","scheduleTime","messageId","contact","intent","rest","key","value","nextCheckDelay","updated","newStatus","finalStatus","startTime","pending","sending","all","expiredIds","retryDelay","shifts","date","dayOfWeek","dayBit","shift","gmt","offset","shiftDate","currentTime","startHour","startMin","endHour","endMin","endTime","daysAhead","checkDate","availableShifts","aHour","aMin","bHour","bMin","shiftStart","shiftStartUTC"]}
@@ -1,2 +1,2 @@
1
- "use strict";var V=Object.create;var v=Object.defineProperty;var G=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var j=Object.getPrototypeOf,J=Object.prototype.hasOwnProperty;var K=(a,t)=>()=>(t||a((t={exports:{}}).exports,t),t.exports),Q=(a,t)=>{for(var e in t)v(a,e,{get:t[e],enumerable:!0})},P=(a,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of H(t))!J.call(a,s)&&s!==e&&v(a,s,{get:()=>t[s],enumerable:!(i=G(t,s))||i.enumerable});return a};var O=(a,t,e)=>(e=a!=null?V(j(a)):{},P(t||!a||!a.__esModule?v(e,"default",{value:a,enumerable:!0}):e,a)),B=a=>P(v({},"__esModule",{value:!0}),a);var $=K((Et,Z)=>{Z.exports={name:"@dawntech/dispatcher",version:"0.2.2",description:"A TypeScript Node.js package for sending push messages in conversational chatbots on the Blip platform.",main:"dist/index.js",module:"dist/index.mjs",types:"dist/index.d.ts",scripts:{start:"node dist/index.js",build:"tsup","build:watch":"tsup --watch",dev:"tsx watch src/server.ts","test:basic":"tsx tests/integration/scenarios/1-basic-send.ts","test:contact":"tsx tests/integration/scenarios/2-contact-update.ts","test:schedule":"tsx tests/integration/scenarios/3-scheduling.ts","test:status":"tsx tests/integration/scenarios/4-status-config.ts","test:intent":"tsx tests/integration/scenarios/5-intent.ts","test:rate-limit":"tsx tests/integration/scenarios/6-rate-limiting.ts","test:high-load":"tsx tests/integration/scenarios/7-high-load.ts","test:retries":"tsx tests/integration/scenarios/8-retries.ts","test:expiry":"tsx tests/integration/scenarios/9-expiration.ts","test:cluster":"tsx tests/integration/scenarios/10-cluster.ts","test:monitor":"tsx tests/integration/scenarios/11-monitor.ts",test:"jest","test:watch":"jest --watch","test:coverage":"jest --coverage","test:blip-api":"tsx tests/blip-api.ts",clean:"rm -rf dist",setup:"bash scripts/setup.sh","docker:up":"docker-compose up -d","docker:down":"docker-compose down","docker:logs":"docker-compose logs -f","docker:redis-cli":"docker-compose exec redis redis-cli","docker:clean":"docker-compose down -v","docker:restart":"docker-compose restart","docker:tools":"docker-compose --profile tools up -d",format:'prettier --write "src/**/*.ts" "tests/**/*.ts"',"format:check":'prettier --check "src/**/*.ts" "tests/**/*.ts"',prepublishOnly:"npm run build"},packageManager:"npm@11.8.0",devDependencies:{"@types/body-parser":"^1.19.6","@types/cors":"^2.8.19","@types/debug":"^4.1.12","@types/express":"^5.0.6","@types/ioredis":"^4.28.10","@types/jest":"^29.5.14","@types/lodash":"^4.17.20","@types/node":"^20.19.24",husky:"^9.1.7",jest:"^29.7.0","lint-staged":"^16.2.6",prettier:"^3.6.2","ts-jest":"^29.2.5",tsup:"^8.5.1",tsx:"^4.7.0",typescript:"^5.3.0"},keywords:[],author:"",license:"ISC",engines:{node:">=24.0.0"},dependencies:{axios:"^1.13.1","body-parser":"^2.2.2",bullmq:"^5.67.1",cors:"^2.8.6",debug:"^4.4.3",dotenv:"^17.2.3",express:"^5.2.1",ioredis:"^5.9.2",lodash:"^4.17.21","rate-limiter-flexible":"^9.0.1",redis:"^5.9.0",uuid:"^13.0.0"}}});var W={};Q(W,{Dispatcher:()=>N});module.exports=B(W);var U=require("uuid"),R=require("bullmq");var M=(r=>(r.INIT="INIT",r.DISPATCHED="DISPATCHED",r.SCHEDULED="SCHEDULED",r.QUEUED="QUEUED",r.FINAL="FINAL",r))(M||{}),E=(n=>(n.INIT="INIT",n.PENDING="PENDING",n.SENDING="SENDING",n.DELIVERED="DELIVERED",n.READ="READ",n.REPLIED="REPLIED",n.FAILED="FAILED",n.CANCELED="CANCELED",n))(E||{});var w=O(require("debug")),A={debug:"debug",info:"info",warn:"warn",error:"error"},k=new Map;function z(a){if(a==null)return a;if(typeof a=="object")try{return JSON.stringify(a,null,2)}catch{return a}return a}function S(a){return(...t)=>{let e=t.map(z);a(...e)}}function I(a){if(!k.has(a)){let t=(0,w.default)(`${a}:${A.debug}`),e=(0,w.default)(`${a}:${A.info}`),i=(0,w.default)(`${a}:${A.warn}`),s=(0,w.default)(`${a}:${A.error}`);s.log=console.error.bind(console);let r={debug:S(t),info:S(e),warn:S(i),error:S(s)};k.set(a,r)}return k.get(a)}var q=I("StateMachine"),x=class{constructor(t,e,i){this.id=t;this.repository=e;this.emit=i}async transition(t,e,i,s){let r=t.state,o=t.status;r==="FINAL"&&e!=="FINAL"&&q.warn(`[transition] Attempting to move from FINAL back to ${e}`,{messageId:t.messageId}),t.state=e,t.status=i,s&&Object.assign(t,s);let c=new Date().toISOString();return i==="SENDING"&&!t.acceptedAt&&o!=="SENDING"&&(t.acceptedAt=c),i==="DELIVERED"&&!t.deliveredAt&&(t.deliveredAt=c),i==="READ"&&!t.readAt&&(t.readAt=c),i==="REPLIED"&&!t.repliedAt&&(t.repliedAt=c,t.readAt||(t.readAt=c)),i==="FAILED"&&!t.failedAt&&(t.failedAt=c),e==="DISPATCHED"&&!t.sentAt&&"PENDING",await this.repository.upsertMessage(t),o!==i&&(i==="REPLIED"&&o!=="READ"&&this.emit("read",t),this.emitStatusEvent(t,i)),e==="SCHEDULED"&&r!=="SCHEDULED"&&this.emit("scheduled",t),q.debug(`[transition] ${t.messageId} : ${r}/${o} -> ${e}/${i}`),t}emitStatusEvent(t,e){switch(e){case"SENDING":this.emit("sending",t);break;case"DELIVERED":this.emit("delivered",t);break;case"READ":this.emit("read",t);break;case"REPLIED":this.emit("replied",t);break;case"FAILED":this.emit("failed",t);break;case"CANCELED":this.emit("canceled",t);break}}};var F=O(require("axios")),T=require("uuid");var m;(t=>{let a;(s=>{let e;(u=>(u.GET="get",u.SET="set",u.DELETE="delete",u.OBSERVE="observe",u.SUBSCRIBE="subscribe",u.MERGE="merge"))(e=s.Method||(s.Method={}));let i;(c=>(c.SUCCESS="success",c.FAILURE="failure"))(i=s.Status||(s.Status={}))})(a=t.Lime||(t.Lime={}))})(m||(m={}));var p=I("Blip"),C=class{constructor(t,e,i=3e4){let s=`https://${t}.http.msging.net`;this.client=F.default.create({baseURL:s,timeout:i,headers:{"Content-Type":"application/json",Authorization:e}}),this.client.interceptors.response.use(r=>r,r=>{if(r.response){let o=r.response.data?.reason||{code:r.response.status,description:r.response.statusText||"Unknown error"};throw new D(o.description,o.code)}else throw r.request?new D("No response from server",0):new D(r.message,0)})}async postCommand(t){let e={...t,id:t.id||(0,T.v4)()};p.debug("[postCommand] payload",e);let s=(await this.client.post("/commands",e)).data;if(s.status!==m.Lime.Status.SUCCESS)throw p.error("[postCommand] failed",{method:e.method,uri:e.uri,status:s.status,reason:s.reason}),new D(s.reason?.description||"Command failed",s.reason?.code||0);return p.debug("[postCommand] succeeded",e.uri),s}async postMessage(t){let e={...t,id:t.id||(0,T.v4)()};return p.info("[postMessage] payload",e),(await this.client.post("/messages",e)).data}async mergeContact(t,e){p.info("[mergeContact] called with",{contactId:t,data:e});let i={...e,identity:t},s={method:m.Lime.Method.MERGE,uri:"/contacts",type:"application/vnd.lime.contact+json",resource:i};await this.postCommand(s)}async sendMessage(t,e,i){p.info("[sendMessage] called with",{contactId:t,message:e,id:i});let s=i||(0,T.v4)(),r={id:s,to:t,type:e.type,content:e.content};return await this.postMessage(r),p.info("[sendMessage] sent",{contactId:t,messageId:s}),s}async getDispatchState(t,e){p.info("[getDispatchState] called with",{messageId:t,contactId:e});let i={method:m.Lime.Method.GET,uri:`/notifications?id=${t}`,to:"postmaster@msging.net"};try{let s=await this.postCommand(i);if(!s.resource||!s.resource.items||s.resource.items.length===0)return p.debug("[getDispatchState] no notifications found",{messageId:t,contactId:e}),null;let r={failed:4,consumed:3,received:2,accepted:1,dispatched:0},o=null,c=-1;for(let n of s.resource.items){let h=n.event,d=r[h]??-1;d>c&&(c=d,o=h)}return p.info("[getDispatchState] state retrieved",{messageId:t,contactId:e,state:o,notificationsCount:s.resource.items.length}),o}catch(s){if(s instanceof D&&s.code===67)return p.debug("[getDispatchState] resource not found",{messageId:t,contactId:e}),null;throw p.error("[getDispatchState] failed",{messageId:t,contactId:e,error:s}),s}}async getMessageAfter(t,e){p.info("[getMessageAfter] called with",{contactId:t,messageId:e});let i=e,s=0,r=10;for(;s<r;){let o={method:m.Lime.Method.GET,uri:`/threads/${t}?$skip=0&$take=1&$order=asc&messageId=${i}`,to:"postmaster@msging.net"};try{let c=await this.postCommand(o);if(!c.resource||!c.resource.items||c.resource.items.length===0)return p.debug("[getMessageAfter] no message found after",{contactId:t,messageId:i}),null;let n=c.resource.items[0];if(n.direction==="received")return p.info("[getMessageAfter] found received message",{contactId:t,messageId:i,nextMessageId:n.id}),n;p.debug("[getMessageAfter] skipping sent message",{contactId:t,messageId:n.id}),i=n.id,s++}catch(c){if(c instanceof D&&c.code===67)return p.debug("[getMessageAfter] resource not found",{contactId:t,messageId:i}),null;throw p.error("[getMessageAfter] failed",{contactId:t,messageId:i,error:c}),c}}return p.warn("[getMessageAfter] max traversal attempts reached",{contactId:t,startMessageId:e}),null}async sendEvent(t,e,i,s){p.info("[sendEvent] called with",{contactId:t,category:e,action:i,extras:s});let r={to:"postmaster@analytics.msging.net",method:m.Lime.Method.SET,type:"application/vnd.iris.eventTrack+json",uri:"/event-track",resource:{category:e,action:i,contact:{identity:t},extras:s}};await this.postCommand(r)}async setState(t,e,i="onboarding"){p.info("[setState] called with",{contactId:t,botId:e,stateId:i});let s={uri:`/flow-id?shortName=${e}`,to:"postmaster@builder.msging.net",method:m.Lime.Method.GET},r=await this.postCommand(s);if(!r.resource)throw p.error("[setState] flow ID not found",{botId:e}),new D(`Flow ID not found for bot: ${e}`,404);let o=r.resource,c={method:m.Lime.Method.SET,uri:`/contexts/${t}/stateid@${o}`,resource:i,type:"text/plain"};await this.postCommand(c);let n={method:m.Lime.Method.SET,uri:`/contexts/${t}/master-state`,resource:`${e}@msging.net`,type:"text/plain"};await this.postCommand(n)}},D=class a extends Error{constructor(t,e){super(t),this.name="BlipError",this.code=e,Object.setPrototypeOf(this,a.prototype)}};var X=I("DispatcherQuery"),L=class{constructor(t){this.repository=t}get client(){return this.repository.redis}async query(t){let e=[],i=this.repository.keyPrefix;if(t.contactId&&e.push(this.repository.getContactKey(t.contactId)),t.descriptorId&&e.push(this.repository.getDescriptorKey(t.descriptorId)),t.status){let d=Array.isArray(t.status)?t.status:[t.status];d.length===1?e.push(this.repository.getStatusKey(d[0])):d.length>1&&e.push(this.repository.getStatusKey(d[0]))}if(t.state){let d=Array.isArray(t.state)?t.state:[t.state];d.length===1&&e.push(this.repository.getStateKey(d[0]))}let s=[];if(e.length>0)s=await this.client.sinter(e);else{let d=Object.values(E).map(u=>this.repository.getStatusKey(u));s=await this.client.sunion(d)}let r=t.skip??0,o=t.size??50,c=s.slice(r,r+o),n=[],h=[];for(let d of c){let u=await this.repository.getMessage(d);if(u){if(t.status&&!(Array.isArray(t.status)?t.status:[t.status]).includes(u.status)||t.state&&!(Array.isArray(t.state)?t.state:[t.state]).includes(u.state))continue;n.push(u)}else h.push(d)}return h.length>0&&this.cleanupIndices(h,t),n}async cleanupIndices(t,e){let i=this.client.pipeline(),s=this.repository.keyPrefix;e.contactId&&i.srem(this.repository.getContactKey(e.contactId),t),e.descriptorId&&i.srem(this.repository.getDescriptorKey(e.descriptorId),t),e.status&&(Array.isArray(e.status)?e.status:[e.status]).forEach(o=>{i.srem(this.repository.getStatusKey(o),t)}),e.state&&(Array.isArray(e.state)?e.state:[e.state]).forEach(o=>{i.srem(this.repository.getStateKey(o),t)}),await i.exec(),X.debug("[cleanupIndices] Removed expired IDs from checked indices",{count:t.length})}};var{version:_}=$(),l=I("Dispatcher"),N=class{constructor(t,e,i,s){this.callbacks={};this.descriptors=new Map;this.isRunning=!1;this.setupCompleted=!1;this.id=t,this.repository=e,this.redis=this.repository.redis,this.stateMachine=new x(this.id,this.repository,(r,o)=>{this.emit(r,o),this.descriptors.get(o.descriptorId)?.emit(r,o,this.api,this.id)}),this.api=new C(i.contract,i.key),this.queueName=`dispatcher-${this.id.replace(/:/g,"-")}`,this.maxRetries=s?.maxRetries??0,this.retryIntervals=s?.retryIntervals??[1*1e3,5*1e3,15*1e3],this.timeouts={pending:s?.timeouts?.pending??120*1e3,sending:s?.timeouts?.sending??120*1e3},this.retention=s?.retention??2880*60*1e3,this.pollingIntervals={scheduled:s?.pollingIntervals?.scheduled??30*1e3,pending:s?.pollingIntervals?.pending??10*1e3,sending:s?.pollingIntervals?.sending??10*1e3,delivered:s?.pollingIntervals?.delivered??1800*1e3,read:s?.pollingIntervals?.read??1800*1e3,queue:s?.pollingIntervals?.queue??1*1e3},this.timeoutTimer=null,this.query=new L(this.repository),this.queue=new R.Queue(this.queueName,{connection:this.redis,defaultJobOptions:{removeOnComplete:!0,removeOnFail:!0}}),this.worker=new R.Worker(this.queueName,async r=>{try{await this.processJob(r)}catch(o){throw l.error(`[Worker] Job ${r.name} failed`,o),o}},{connection:this.redis,concurrency:s?.batchSize||50,limiter:s?.rateLimits?.global?{max:s.rateLimits.global.points,duration:s.rateLimits.global.duration*1e3}:void 0}),this.worker.on("error",r=>l.error("[Worker] Error",r)),this.worker.on("failed",(r,o)=>l.error(`[Worker] Job ${r?.id} failed`,o))}async setup(){if(this.setupCompleted)return;await this.repository.setup();let t=await this.repository.getManifest();await this.repository.writeManifest({version:_,createdAt:t?.createdAt??new Date().toISOString(),updatedAt:new Date().toISOString()}),await this.queue.waitUntilReady(),this.isRunning=!0,this.setupCompleted=!0,this.startTimeoutMonitor(),l.info("[setup] Dispatcher started (BullMQ)",{queue:this.queueName})}async teardown(){this.isRunning=!1,this.timeoutTimer&&(clearInterval(this.timeoutTimer),this.timeoutTimer=null),await this.queue.close(),await this.worker.close(),await this.repository.teardown(),l.info("[teardown] Dispatcher stopped")}on(t,e){return this.callbacks[t]=e,this}async getMetrics(){let t={total:0,byState:{},byStatus:{},cumulative:{dispatched:await this.repository.getMetric("dispatched"),delivered:await this.repository.getMetric("delivered"),failed:await this.repository.getMetric("failed")}},e=Object.values(M);for(let s of e)t.byState[s]=await this.repository.countMessages({state:s});let i=Object.values(E);for(let s of i)t.byStatus[s]=await this.repository.countMessages({status:s});return t.total=Object.values(t.byState).reduce((s,r)=>s+(r||0),0),t}emit(t,e){this.callbacks[t]?.(e,this.api,this.id)}async send(t,e,i,s){this.descriptors.set(t.id,t);let r=t.toContactId(e),o=t.transform(i),c=new Date().toISOString(),n={messageId:(0,U.v4)(),contactId:r,descriptorId:t.id,payload:o,status:"INIT",state:"INIT",createdAt:c,attempts:0,retries:this.maxRetries},d={...t.messageOptions,...s},{schedule:u,...y}=d;n.options=y,this.emit("dispatch",n),t.emit("dispatch",n,this.api,this.id);let f=this.calculateScheduledTime(u,d.shifts),g=0;if(f){n.scheduledTo=f,n.state="SCHEDULED";let b=new Date(f).getTime();g=Math.max(0,b-Date.now()),this.emit("scheduled",n),t.emit("scheduled",n,this.api,this.id),l.info("[send] message scheduled",{messageId:n.messageId,scheduledTo:f,delay:g})}else n.state="QUEUED",n.status="INIT",l.info("[send] message queued",{messageId:n.messageId});return n.expiresAt=new Date(Date.now()+(n.state==="SCHEDULED"?g+this.retention:this.retention)).toISOString(),await this.stateMachine.transition(n,n.state,n.status),await this.queue.add("send",{messageId:n.messageId},{jobId:n.messageId,delay:g,priority:1}),n}async cancel(t){let e=await this.repository.getMessage(t);if(!e)return l.warn("[cancel] message not found",{messageId:t}),!1;if(e.state==="FINAL")return l.warn("[cancel] message already final",{messageId:t,status:e.status}),!1;let i=await this.queue.getJob(t);return i&&(await i.remove(),l.info("[cancel] removed job from queue",{messageId:t})),await this.stateMachine.transition(e,"FINAL","CANCELED"),l.info("[cancel] message canceled",{messageId:t}),!0}async processJob(t){let{messageId:e}=t.data,i=await this.repository.getMessage(e);if(!i){l.warn(`[processJob] Message not found: ${e}`);return}let s=this.descriptors.get(i.descriptorId)||null;switch(t.name){case"send":await this.handleSendJob(i,s);break;case"check":await this.handleCheckJob(i,s);break;default:l.warn(`[processJob] Unknown job name: ${t.name}`)}}async handleSendJob(t,e){t.lastDispatchAttemptAt=new Date().toISOString(),await this.stateMachine.transition(t,"DISPATCHED","PENDING");try{await this.api.sendMessage(t.contactId,t.payload,t.messageId),await this.handlePostSendOperations(t,t.options),t.sentAt=new Date().toISOString(),await this.stateMachine.transition(t,"DISPATCHED","PENDING"),l.info("[handleSendJob] Message sent to API",{messageId:t.messageId}),await this.repository.incrementMetric("dispatched"),await this.queue.add("check",{messageId:t.messageId},{delay:this.pollingIntervals.pending,priority:5})}catch(i){let s=i instanceof Error?i:new Error(String(i));await this.handleDispatchFailure(t,e,s)}}async handlePostSendOperations(t,e={}){let i={...e.contact||{}};if(e.intent)if(typeof e.intent=="string")i.intent=e.intent;else{i.intent=e.intent.intent;let{intent:s,...r}=e.intent;Object.entries(r).forEach(([o,c])=>{c!=null&&(i[o]=typeof c=="object"?JSON.stringify(c):String(c))})}Object.keys(i).length>0&&await this.api.mergeContact(t.contactId,i),e.state&&await this.api.setState(t.contactId,e.state.botId,e.state.stateId)}async handleCheckJob(t,e){if(t.state!=="FINAL"){if(this.checkAndHandleTimeout(t)){await this.handleTimeout(t,e);return}try{let i=await this.api.getDispatchState(t.messageId,t.contactId);if(!i){await this.rescheduleCheck(t,this.pollingIntervals.pending);return}let s=this.pollingIntervals.pending,r=!1;switch(i){case"accepted":t.status!=="SENDING"&&(await this.stateMachine.transition(t,t.state,"SENDING"),r=!0),s=this.pollingIntervals.sending;break;case"received":case"consumed":if(await this.api.getMessageAfter(t.contactId,t.messageId)){let h="REPLIED";t.status!==h&&t.status!=="READ"&&(await this.repository.incrementMetric("delivered"),await this.stateMachine.transition(t,"FINAL","REPLIED"),r=!0);break}let c=i==="consumed"?"READ":"DELIVERED";t.status!==c&&(await this.repository.incrementMetric("delivered"),await this.stateMachine.transition(t,t.state,c),r=!0);let n=t.options?.finalStatus||"DELIVERED";this.getStatusRank(t.status)>=this.getStatusRank(n)?(await this.stateMachine.transition(t,"FINAL",t.status),r=!0):s=this.pollingIntervals.delivered;break;case"failed":await this.handleDispatchFailure(t,e,new Error("Dispatch failed from Gateway"));return}t.state!=="FINAL"&&await this.rescheduleCheck(t,s)}catch(i){l.error("[handleCheckJob] Error",i),await this.rescheduleCheck(t,this.pollingIntervals.pending)}}}checkAndHandleTimeout(t){let e=new Date;if(t.status==="PENDING"){let i=t.lastDispatchAttemptAt||t.sentAt||t.createdAt;if(e.getTime()-new Date(i).getTime()>this.timeouts.pending)return!0}return!!(t.status==="SENDING"&&t.acceptedAt&&e.getTime()-new Date(t.acceptedAt).getTime()>this.timeouts.sending)}async handleTimeout(t,e){await this.stateMachine.transition(t,"FINAL","FAILED",{error:"Timeout Exceeded"}),l.info("[handleTimeout] Message timed out",{messageId:t.messageId})}startTimeoutMonitor(){this.timeoutTimer||(this.timeoutTimer=setInterval(async()=>{try{let t=await this.repository.getMessages({status:"PENDING"}),e=await this.repository.getMessages({status:"SENDING"}),i=[...t,...e];for(let r of i)if(this.checkAndHandleTimeout(r)){let o=this.descriptors.get(r.descriptorId)||null;await this.handleTimeout(r,o)}let s=await this.repository.getRetentionMessages(100);if(s.length>0){l.debug("[CleanupMonitor] Cleaning up expired messages",{count:s.length});for(let r of s)await this.repository.deleteMessage(r)}}catch(t){l.error("[TimeoutMonitor] Error during scan",t)}},10*1e3))}async rescheduleCheck(t,e){await this.queue.add("check",{messageId:t.messageId},{delay:e,priority:5})}async handleDispatchFailure(t,e,i){if(t.attempts=(t.attempts??0)+1,t.error=i.message,l.error("[handleDispatchFailure]",{messageId:t.messageId,attempts:t.attempts,maxRetries:this.maxRetries,error:i.message}),t.attempts<=this.maxRetries){t.retries=this.maxRetries-t.attempts;let s=this.retryIntervals[t.attempts-1]||this.retryIntervals[this.retryIntervals.length-1];await this.stateMachine.transition(t,"SCHEDULED",t.status),this.emit("retry",t),e?.emit("retry",t,this.api,this.id),await this.queue.add("send",{messageId:t.messageId},{delay:s,priority:1}),l.info("[handleDispatchFailure] Rescheduled retry",{messageId:t.messageId,retryDelay:s})}else t.retries=0,await this.stateMachine.transition(t,"FINAL","FAILED"),await this.repository.incrementMetric("failed")}calculateScheduledTime(t,e){if(t)return t;if(!e||e.length===0)return;let i=new Date;return this.isWithinShifts(i,e)?void 0:this.findNextShiftTime(i,e)?.toISOString()}isWithinShifts(t,e){let i=t.getDay(),s=i===0?64:Math.pow(2,i-1);for(let r of e){if((r.days&s)===0)continue;let o=r.gmt||"-3",c=parseInt(o,10),n=new Date(t.getTime()-c*60*60*1e3),h=n.getHours()*60+n.getMinutes(),[d,u]=r.start.split(":").map(Number),[y,f]=r.end.split(":").map(Number),g=d*60+u,b=y*60+f;if(h>=g&&h<b)return!0}return!1}findNextShiftTime(t,e){for(let s=0;s<=7;s++){let r=new Date(t);r.setDate(r.getDate()+s);let o=r.getDay(),c=o===0?64:Math.pow(2,o-1),n=e.filter(h=>(h.days&c)!==0);if(n.length!==0){n.sort((h,d)=>{let[u,y]=h.start.split(":").map(Number),[f,g]=d.start.split(":").map(Number);return u*60+y-(f*60+g)});for(let h of n){let d=h.gmt||"-3",u=parseInt(d,10),[y,f]=h.start.split(":").map(Number),g=new Date(r);g.setHours(y,f,0,0);let b=new Date(g.getTime()+u*60*60*1e3);if(s===0){if(b>t)return b}else return b}}}}getStatusRank(t){return{INIT:0,PENDING:1,SENDING:2,DELIVERED:3,READ:4,REPLIED:5,FAILED:6,CANCELED:6}[t]||0}};0&&(module.exports={Dispatcher});
1
+ "use strict";var V=Object.create;var v=Object.defineProperty;var G=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var j=Object.getPrototypeOf,J=Object.prototype.hasOwnProperty;var K=(a,t)=>()=>(t||a((t={exports:{}}).exports,t),t.exports),Q=(a,t)=>{for(var e in t)v(a,e,{get:t[e],enumerable:!0})},P=(a,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of H(t))!J.call(a,s)&&s!==e&&v(a,s,{get:()=>t[s],enumerable:!(i=G(t,s))||i.enumerable});return a};var O=(a,t,e)=>(e=a!=null?V(j(a)):{},P(t||!a||!a.__esModule?v(e,"default",{value:a,enumerable:!0}):e,a)),B=a=>P(v({},"__esModule",{value:!0}),a);var $=K((Et,Z)=>{Z.exports={name:"@dawntech/dispatcher",version:"0.2.6",description:"A TypeScript Node.js package for sending push messages in conversational chatbots on the Blip platform.",main:"dist/index.js",module:"dist/index.mjs",types:"dist/index.d.ts",scripts:{start:"node dist/index.js",build:"tsup","build:watch":"tsup --watch",dev:"tsx watch src/server.ts","test:basic":"tsx tests/integration/scenarios/1-basic-send.ts","test:contact":"tsx tests/integration/scenarios/2-contact-update.ts","test:schedule":"tsx tests/integration/scenarios/3-scheduling.ts","test:status":"tsx tests/integration/scenarios/4-status-config.ts","test:intent":"tsx tests/integration/scenarios/5-intent.ts","test:rate-limit":"tsx tests/integration/scenarios/6-rate-limiting.ts","test:high-load":"tsx tests/integration/scenarios/7-high-load.ts","test:retries":"tsx tests/integration/scenarios/8-retries.ts","test:expiry":"tsx tests/integration/scenarios/9-expiration.ts","test:cluster":"tsx tests/integration/scenarios/10-cluster.ts","test:monitor":"tsx tests/integration/scenarios/11-monitor.ts",test:"jest","test:watch":"jest --watch","test:coverage":"jest --coverage","test:blip-api":"tsx tests/blip-api.ts",clean:"rm -rf dist",setup:"bash scripts/setup.sh","docker:up":"docker-compose up -d","docker:down":"docker-compose down","docker:logs":"docker-compose logs -f","docker:redis-cli":"docker-compose exec redis redis-cli","docker:clean":"docker-compose down -v","docker:restart":"docker-compose restart","docker:tools":"docker-compose --profile tools up -d",format:'prettier --write "src/**/*.ts" "tests/**/*.ts"',"format:check":'prettier --check "src/**/*.ts" "tests/**/*.ts"',prepublishOnly:"npm run build"},packageManager:"npm@11.8.0",devDependencies:{"@types/body-parser":"^1.19.6","@types/cors":"^2.8.19","@types/debug":"^4.1.12","@types/express":"^5.0.6","@types/ioredis":"^4.28.10","@types/jest":"^29.5.14","@types/lodash":"^4.17.20","@types/node":"^20.19.24",husky:"^9.1.7",jest:"^29.7.0","lint-staged":"^16.2.6",prettier:"^3.6.2","ts-jest":"^29.2.5",tsup:"^8.5.1",tsx:"^4.7.0",typescript:"^5.3.0"},keywords:[],author:"",license:"ISC",engines:{node:">=24.0.0"},dependencies:{axios:"^1.13.1","body-parser":"^2.2.2",bullmq:"^5.67.1",cors:"^2.8.6",debug:"^4.4.3",dotenv:"^17.2.3",express:"^5.2.1",ioredis:"^5.9.2",lodash:"^4.17.21","rate-limiter-flexible":"^9.0.1",redis:"^5.9.0",uuid:"^13.0.0"}}});var W={};Q(W,{Dispatcher:()=>N});module.exports=B(W);var U=require("uuid"),R=require("bullmq");var M=(r=>(r.INIT="INIT",r.DISPATCHED="DISPATCHED",r.SCHEDULED="SCHEDULED",r.QUEUED="QUEUED",r.FINAL="FINAL",r))(M||{}),E=(n=>(n.INIT="INIT",n.PENDING="PENDING",n.SENDING="SENDING",n.DELIVERED="DELIVERED",n.READ="READ",n.REPLIED="REPLIED",n.FAILED="FAILED",n.CANCELED="CANCELED",n))(E||{});var w=O(require("debug")),A={debug:"debug",info:"info",warn:"warn",error:"error"},k=new Map;function z(a){if(a==null)return a;if(typeof a=="object")try{return JSON.stringify(a,null,2)}catch{return a}return a}function S(a){return(...t)=>{let e=t.map(z);a(...e)}}function I(a){if(!k.has(a)){let t=(0,w.default)(`${a}:${A.debug}`),e=(0,w.default)(`${a}:${A.info}`),i=(0,w.default)(`${a}:${A.warn}`),s=(0,w.default)(`${a}:${A.error}`);s.log=console.error.bind(console);let r={debug:S(t),info:S(e),warn:S(i),error:S(s)};k.set(a,r)}return k.get(a)}var q=I("StateMachine"),x=class{constructor(t,e,i){this.id=t;this.repository=e;this.emit=i}async transition(t,e,i,s){let r=t.state,o=t.status;r==="FINAL"&&e!=="FINAL"&&q.warn(`[transition] Attempting to move from FINAL back to ${e}`,{messageId:t.messageId}),t.state=e,t.status=i,s&&Object.assign(t,s);let c=new Date().toISOString();return i==="SENDING"&&!t.acceptedAt&&o!=="SENDING"&&(t.acceptedAt=c),i==="DELIVERED"&&!t.deliveredAt&&(t.deliveredAt=c),i==="READ"&&!t.readAt&&(t.readAt=c),i==="REPLIED"&&!t.repliedAt&&(t.repliedAt=c,t.readAt||(t.readAt=c)),i==="FAILED"&&!t.failedAt&&(t.failedAt=c),e==="DISPATCHED"&&!t.sentAt&&"PENDING",await this.repository.upsertMessage(t),o!==i&&(i==="REPLIED"&&o!=="READ"&&this.emit("read",t),this.emitStatusEvent(t,i)),e==="SCHEDULED"&&r!=="SCHEDULED"&&this.emit("scheduled",t),q.debug(`[transition] ${t.messageId} : ${r}/${o} -> ${e}/${i}`),t}emitStatusEvent(t,e){switch(e){case"SENDING":this.emit("sending",t);break;case"DELIVERED":this.emit("delivered",t);break;case"READ":this.emit("read",t);break;case"REPLIED":this.emit("replied",t);break;case"FAILED":this.emit("failed",t);break;case"CANCELED":this.emit("canceled",t);break}}};var F=O(require("axios")),T=require("uuid");var m;(t=>{let a;(s=>{let e;(u=>(u.GET="get",u.SET="set",u.DELETE="delete",u.OBSERVE="observe",u.SUBSCRIBE="subscribe",u.MERGE="merge"))(e=s.Method||(s.Method={}));let i;(c=>(c.SUCCESS="success",c.FAILURE="failure"))(i=s.Status||(s.Status={}))})(a=t.Lime||(t.Lime={}))})(m||(m={}));var p=I("Blip"),C=class{constructor(t,e,i=3e4){let s=`https://${t}.http.msging.net`;this.client=F.default.create({baseURL:s,timeout:i,headers:{"Content-Type":"application/json",Authorization:e}}),this.client.interceptors.response.use(r=>r,r=>{if(r.response){let o=r.response.data?.reason||{code:r.response.status,description:r.response.statusText||"Unknown error"};throw new D(o.description,o.code)}else throw r.request?new D("No response from server",0):new D(r.message,0)})}async postCommand(t){let e={...t,id:t.id||(0,T.v4)()};p.debug("[postCommand] payload",e);let s=(await this.client.post("/commands",e)).data;if(s.status!==m.Lime.Status.SUCCESS)throw p.error("[postCommand] failed",{method:e.method,uri:e.uri,status:s.status,reason:s.reason}),new D(s.reason?.description||"Command failed",s.reason?.code||0);return p.debug("[postCommand] succeeded",e.uri),s}async postMessage(t){let e={...t,id:t.id||(0,T.v4)()};return p.info("[postMessage] payload",e),(await this.client.post("/messages",e)).data}async mergeContact(t,e){p.info("[mergeContact] called with",{contactId:t,data:e});let i={...e,identity:t},s={method:m.Lime.Method.MERGE,uri:"/contacts",type:"application/vnd.lime.contact+json",resource:i};await this.postCommand(s)}async sendMessage(t,e,i){p.info("[sendMessage] called with",{contactId:t,message:e,id:i});let s=i||(0,T.v4)(),r={id:s,to:t,type:e.type,content:e.content};return await this.postMessage(r),p.info("[sendMessage] sent",{contactId:t,messageId:s}),s}async getDispatchState(t,e){p.info("[getDispatchState] called with",{messageId:t,contactId:e});let i={method:m.Lime.Method.GET,uri:`/notifications?id=${t}`,to:"postmaster@msging.net"};try{let s=await this.postCommand(i);if(!s.resource||!s.resource.items||s.resource.items.length===0)return p.debug("[getDispatchState] no notifications found",{messageId:t,contactId:e}),null;let r={failed:4,consumed:3,received:2,accepted:1,dispatched:0},o=null,c=-1;for(let n of s.resource.items){let h=n.event,d=r[h]??-1;d>c&&(c=d,o=h)}return p.info("[getDispatchState] state retrieved",{messageId:t,contactId:e,state:o,notificationsCount:s.resource.items.length}),o}catch(s){if(s instanceof D&&s.code===67)return p.debug("[getDispatchState] resource not found",{messageId:t,contactId:e}),null;throw p.error("[getDispatchState] failed",{messageId:t,contactId:e,error:s}),s}}async getMessageAfter(t,e){p.info("[getMessageAfter] called with",{contactId:t,messageId:e});let i=e,s=0,r=10;for(;s<r;){let o={method:m.Lime.Method.GET,uri:`/threads/${t}?$skip=0&$take=1&$order=asc&messageId=${i}`,to:"postmaster@msging.net"};try{let c=await this.postCommand(o);if(!c.resource||!c.resource.items||c.resource.items.length===0)return p.debug("[getMessageAfter] no message found after",{contactId:t,messageId:i}),null;let n=c.resource.items[0];if(n.direction==="received")return p.info("[getMessageAfter] found received message",{contactId:t,messageId:i,nextMessageId:n.id}),n;p.debug("[getMessageAfter] skipping sent message",{contactId:t,messageId:n.id}),i=n.id,s++}catch(c){if(c instanceof D&&c.code===67)return p.debug("[getMessageAfter] resource not found",{contactId:t,messageId:i}),null;throw p.error("[getMessageAfter] failed",{contactId:t,messageId:i,error:c}),c}}return p.warn("[getMessageAfter] max traversal attempts reached",{contactId:t,startMessageId:e}),null}async sendEvent(t,e,i,s){p.info("[sendEvent] called with",{contactId:t,category:e,action:i,extras:s});let r={to:"postmaster@analytics.msging.net",method:m.Lime.Method.SET,type:"application/vnd.iris.eventTrack+json",uri:"/event-track",resource:{category:e,action:i,contact:{identity:t},extras:s}};await this.postCommand(r)}async setState(t,e,i="onboarding"){p.info("[setState] called with",{contactId:t,botId:e,stateId:i});let s={uri:`/flow-id?shortName=${e}`,to:"postmaster@builder.msging.net",method:m.Lime.Method.GET},r=await this.postCommand(s);if(!r.resource)throw p.error("[setState] flow ID not found",{botId:e}),new D(`Flow ID not found for bot: ${e}`,404);let o=r.resource,c={method:m.Lime.Method.SET,uri:`/contexts/${t}/stateid@${o}`,resource:i,type:"text/plain"};await this.postCommand(c);let n={method:m.Lime.Method.SET,uri:`/contexts/${t}/master-state`,resource:`${e}@msging.net`,type:"text/plain"};await this.postCommand(n)}},D=class a extends Error{constructor(t,e){super(t),this.name="BlipError",this.code=e,Object.setPrototypeOf(this,a.prototype)}};var X=I("DispatcherQuery"),L=class{constructor(t){this.repository=t}get client(){return this.repository.redis}async query(t){let e=[],i=this.repository.keyPrefix;if(t.contactId&&e.push(this.repository.getContactKey(t.contactId)),t.descriptorId&&e.push(this.repository.getDescriptorKey(t.descriptorId)),t.status){let d=Array.isArray(t.status)?t.status:[t.status];d.length===1?e.push(this.repository.getStatusKey(d[0])):d.length>1&&e.push(this.repository.getStatusKey(d[0]))}if(t.state){let d=Array.isArray(t.state)?t.state:[t.state];d.length===1&&e.push(this.repository.getStateKey(d[0]))}let s=[];if(e.length>0)s=await this.client.sinter(e);else{let d=Object.values(E).map(u=>this.repository.getStatusKey(u));s=await this.client.sunion(d)}let r=t.skip??0,o=t.size??50,c=s.slice(r,r+o),n=[],h=[];for(let d of c){let u=await this.repository.getMessage(d);if(u){if(t.status&&!(Array.isArray(t.status)?t.status:[t.status]).includes(u.status)||t.state&&!(Array.isArray(t.state)?t.state:[t.state]).includes(u.state))continue;n.push(u)}else h.push(d)}return h.length>0&&this.cleanupIndices(h,t),n}async cleanupIndices(t,e){let i=this.client.pipeline(),s=this.repository.keyPrefix;e.contactId&&i.srem(this.repository.getContactKey(e.contactId),t),e.descriptorId&&i.srem(this.repository.getDescriptorKey(e.descriptorId),t),e.status&&(Array.isArray(e.status)?e.status:[e.status]).forEach(o=>{i.srem(this.repository.getStatusKey(o),t)}),e.state&&(Array.isArray(e.state)?e.state:[e.state]).forEach(o=>{i.srem(this.repository.getStateKey(o),t)}),await i.exec(),X.debug("[cleanupIndices] Removed expired IDs from checked indices",{count:t.length})}};var{version:_}=$(),l=I("Dispatcher"),N=class{constructor(t,e,i,s){this.callbacks={};this.descriptors=new Map;this.isRunning=!1;this.setupCompleted=!1;this.id=t,this.repository=e,this.redis=this.repository.redis,this.stateMachine=new x(this.id,this.repository,(r,o)=>{this.emit(r,o),this.descriptors.get(o.descriptorId)?.emit(r,o,this.api,this.id)}),this.api=new C(i.contract,i.key),this.queueName=`dispatcher-${this.id.replace(/:/g,"-")}`,this.maxRetries=s?.maxRetries??0,this.retryIntervals=s?.retryIntervals??[1*1e3,5*1e3,15*1e3],this.timeouts={pending:s?.timeouts?.pending??120*1e3,sending:s?.timeouts?.sending??120*1e3},this.retention=s?.retention??2880*60*1e3,this.pollingIntervals={scheduled:s?.pollingIntervals?.scheduled??30*1e3,pending:s?.pollingIntervals?.pending??10*1e3,sending:s?.pollingIntervals?.sending??10*1e3,delivered:s?.pollingIntervals?.delivered??1800*1e3,read:s?.pollingIntervals?.read??1800*1e3,queue:s?.pollingIntervals?.queue??1*1e3},this.timeoutTimer=null,this.query=new L(this.repository),this.queue=new R.Queue(this.queueName,{connection:this.redis,defaultJobOptions:{removeOnComplete:!0,removeOnFail:!0}}),this.worker=new R.Worker(this.queueName,async r=>{try{await this.processJob(r)}catch(o){throw l.error(`[Worker] Job ${r.name} failed`,o),o}},{connection:this.redis,concurrency:s?.batchSize||50,limiter:s?.rateLimits?.global?{max:s.rateLimits.global.points,duration:s.rateLimits.global.duration*1e3}:void 0}),this.worker.on("error",r=>l.error("[Worker] Error",r)),this.worker.on("failed",(r,o)=>l.error(`[Worker] Job ${r?.id} failed`,o))}async setup(){if(this.setupCompleted)return;await this.repository.setup();let t=await this.repository.getManifest();await this.repository.writeManifest({version:_,createdAt:t?.createdAt??new Date().toISOString(),updatedAt:new Date().toISOString()}),await this.queue.waitUntilReady(),this.isRunning=!0,this.setupCompleted=!0,this.startTimeoutMonitor(),l.info("[setup] Dispatcher started (BullMQ)",{queue:this.queueName})}async teardown(){this.isRunning=!1,this.timeoutTimer&&(clearInterval(this.timeoutTimer),this.timeoutTimer=null),await this.queue.close(),await this.worker.close(),await this.repository.teardown(),l.info("[teardown] Dispatcher stopped")}on(t,e){return this.callbacks[t]=e,this}async getMetrics(){let t={total:0,byState:{},byStatus:{},cumulative:{dispatched:await this.repository.getMetric("dispatched"),delivered:await this.repository.getMetric("delivered"),failed:await this.repository.getMetric("failed")}},e=Object.values(M);for(let s of e)t.byState[s]=await this.repository.countMessages({state:s});let i=Object.values(E);for(let s of i)t.byStatus[s]=await this.repository.countMessages({status:s});return t.total=Object.values(t.byState).reduce((s,r)=>s+(r||0),0),t}emit(t,e){this.callbacks[t]?.(e,this.api,this.id)}async send(t,e,i,s){this.descriptors.set(t.id,t);let r=t.toContactId(e),o=t.transform(i),c=new Date().toISOString(),n={messageId:(0,U.v4)(),contactId:r,descriptorId:t.id,payload:o,status:"INIT",state:"INIT",createdAt:c,attempts:0,retries:this.maxRetries},d={...t.messageOptions,...s},{schedule:u,...y}=d;n.options=y,this.emit("dispatch",n),t.emit("dispatch",n,this.api,this.id);let f=this.calculateScheduledTime(u,d.shifts),g=0;if(f){n.scheduledTo=f,n.state="SCHEDULED";let b=new Date(f).getTime();g=Math.max(0,b-Date.now()),this.emit("scheduled",n),t.emit("scheduled",n,this.api,this.id),l.info("[send] message scheduled",{messageId:n.messageId,scheduledTo:f,delay:g})}else n.state="QUEUED",n.status="INIT",l.info("[send] message queued",{messageId:n.messageId});return n.expiresAt=new Date(Date.now()+(n.state==="SCHEDULED"?g+this.retention:this.retention)).toISOString(),await this.stateMachine.transition(n,n.state,n.status),await this.queue.add("send",{messageId:n.messageId},{jobId:n.messageId,delay:g,priority:1}),n}async cancel(t){let e=await this.repository.getMessage(t);if(!e)return l.warn("[cancel] message not found",{messageId:t}),!1;if(e.state==="FINAL")return l.warn("[cancel] message already final",{messageId:t,status:e.status}),!1;let i=await this.queue.getJob(t);return i&&(await i.remove(),l.info("[cancel] removed job from queue",{messageId:t})),await this.stateMachine.transition(e,"FINAL","CANCELED"),l.info("[cancel] message canceled",{messageId:t}),!0}async processJob(t){let{messageId:e}=t.data,i=await this.repository.getMessage(e);if(!i){l.warn(`[processJob] Message not found: ${e}`);return}let s=this.descriptors.get(i.descriptorId)||null;switch(t.name){case"send":await this.handleSendJob(i,s);break;case"check":await this.handleCheckJob(i,s);break;default:l.warn(`[processJob] Unknown job name: ${t.name}`)}}async handleSendJob(t,e){t.lastDispatchAttemptAt=new Date().toISOString(),await this.stateMachine.transition(t,"DISPATCHED","PENDING");try{await this.api.sendMessage(t.contactId,t.payload,t.messageId),await this.handlePostSendOperations(t,t.options),t.sentAt=new Date().toISOString(),await this.stateMachine.transition(t,"DISPATCHED","PENDING"),l.info("[handleSendJob] Message sent to API",{messageId:t.messageId}),await this.repository.incrementMetric("dispatched"),await this.queue.add("check",{messageId:t.messageId},{delay:this.pollingIntervals.pending,priority:5})}catch(i){let s=i instanceof Error?i:new Error(String(i));await this.handleDispatchFailure(t,e,s)}}async handlePostSendOperations(t,e={}){let i={...e.contact||{}};if(e.intent)if(typeof e.intent=="string")i.intent=e.intent;else{i.intent=e.intent.intent;let{intent:s,...r}=e.intent;Object.entries(r).forEach(([o,c])=>{c!=null&&(i[o]=typeof c=="object"?JSON.stringify(c):String(c))})}Object.keys(i).length>0&&await this.api.mergeContact(t.contactId,i),e.state&&await this.api.setState(t.contactId,e.state.botId,e.state.stateId)}async handleCheckJob(t,e){if(t.state!=="FINAL"){if(this.checkAndHandleTimeout(t)){await this.handleTimeout(t,e);return}try{let i=await this.api.getDispatchState(t.messageId,t.contactId);if(!i){await this.rescheduleCheck(t,this.pollingIntervals.pending);return}let s=this.pollingIntervals.pending,r=!1;switch(i){case"accepted":t.status!=="SENDING"&&(await this.stateMachine.transition(t,t.state,"SENDING"),r=!0),s=this.pollingIntervals.sending;break;case"received":case"consumed":if(await this.api.getMessageAfter(t.contactId,t.messageId)){let h="REPLIED";t.status!==h&&t.status!=="READ"&&(await this.repository.incrementMetric("delivered"),await this.stateMachine.transition(t,"FINAL","REPLIED"),r=!0);break}let c=i==="consumed"?"READ":"DELIVERED";t.status!==c&&(await this.repository.incrementMetric("delivered"),await this.stateMachine.transition(t,t.state,c),r=!0);let n=t.options?.finalStatus||"DELIVERED";this.getStatusRank(t.status)>=this.getStatusRank(n)?(await this.stateMachine.transition(t,"FINAL",t.status),r=!0):s=this.pollingIntervals.delivered;break;case"failed":await this.handleDispatchFailure(t,e,new Error("Dispatch failed from Gateway"));return}t.state!=="FINAL"&&await this.rescheduleCheck(t,s)}catch(i){l.error("[handleCheckJob] Error",i),await this.rescheduleCheck(t,this.pollingIntervals.pending)}}}checkAndHandleTimeout(t){let e=new Date;if(t.status==="PENDING"){let i=t.lastDispatchAttemptAt||t.sentAt||t.createdAt;if(e.getTime()-new Date(i).getTime()>this.timeouts.pending)return!0}return!!(t.status==="SENDING"&&t.acceptedAt&&e.getTime()-new Date(t.acceptedAt).getTime()>this.timeouts.sending)}async handleTimeout(t,e){await this.stateMachine.transition(t,"FINAL","FAILED",{error:"Timeout Exceeded"}),l.info("[handleTimeout] Message timed out",{messageId:t.messageId})}startTimeoutMonitor(){this.timeoutTimer||(this.timeoutTimer=setInterval(async()=>{try{let t=await this.repository.getMessages({status:"PENDING"}),e=await this.repository.getMessages({status:"SENDING"}),i=[...t,...e];for(let r of i)if(this.checkAndHandleTimeout(r)){let o=this.descriptors.get(r.descriptorId)||null;await this.handleTimeout(r,o)}let s=await this.repository.getRetentionMessages(100);if(s.length>0){l.debug("[CleanupMonitor] Cleaning up expired messages",{count:s.length});for(let r of s)await this.repository.deleteMessage(r)}}catch(t){l.error("[TimeoutMonitor] Error during scan",t)}},10*1e3))}async rescheduleCheck(t,e){await this.queue.add("check",{messageId:t.messageId},{delay:e,priority:5})}async handleDispatchFailure(t,e,i){if(t.attempts=(t.attempts??0)+1,t.error=i.message,l.error("[handleDispatchFailure]",{messageId:t.messageId,attempts:t.attempts,maxRetries:this.maxRetries,error:i.message}),t.attempts<=this.maxRetries){t.retries=this.maxRetries-t.attempts;let s=this.retryIntervals[t.attempts-1]||this.retryIntervals[this.retryIntervals.length-1];await this.stateMachine.transition(t,"SCHEDULED",t.status),this.emit("retry",t),e?.emit("retry",t,this.api,this.id),await this.queue.add("send",{messageId:t.messageId},{delay:s,priority:1}),l.info("[handleDispatchFailure] Rescheduled retry",{messageId:t.messageId,retryDelay:s})}else t.retries=0,await this.stateMachine.transition(t,"FINAL","FAILED"),await this.repository.incrementMetric("failed")}calculateScheduledTime(t,e){if(t)return t;if(!e||e.length===0)return;let i=new Date;return this.isWithinShifts(i,e)?void 0:this.findNextShiftTime(i,e)?.toISOString()}isWithinShifts(t,e){let i=t.getDay(),s=i===0?64:Math.pow(2,i-1);for(let r of e){if((r.days&s)===0)continue;let o=r.gmt||"-3",c=parseInt(o,10),n=new Date(t.getTime()-c*60*60*1e3),h=n.getHours()*60+n.getMinutes(),[d,u]=r.start.split(":").map(Number),[y,f]=r.end.split(":").map(Number),g=d*60+u,b=y*60+f;if(h>=g&&h<b)return!0}return!1}findNextShiftTime(t,e){for(let s=0;s<=7;s++){let r=new Date(t);r.setDate(r.getDate()+s);let o=r.getDay(),c=o===0?64:Math.pow(2,o-1),n=e.filter(h=>(h.days&c)!==0);if(n.length!==0){n.sort((h,d)=>{let[u,y]=h.start.split(":").map(Number),[f,g]=d.start.split(":").map(Number);return u*60+y-(f*60+g)});for(let h of n){let d=h.gmt||"-3",u=parseInt(d,10),[y,f]=h.start.split(":").map(Number),g=new Date(r);g.setHours(y,f,0,0);let b=new Date(g.getTime()+u*60*60*1e3);if(s===0){if(b>t)return b}else return b}}}}getStatusRank(t){return{INIT:0,PENDING:1,SENDING:2,DELIVERED:3,READ:4,REPLIED:5,FAILED:6,CANCELED:6}[t]||0}};0&&(module.exports={Dispatcher});
2
2
  //# sourceMappingURL=Dispatcher.js.map