@dawntech/dispatcher 0.2.10 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +8 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -65,6 +65,10 @@ declare namespace Vnd {
|
|
|
65
65
|
content: unknown;
|
|
66
66
|
date: string;
|
|
67
67
|
status?: string;
|
|
68
|
+
reason?: {
|
|
69
|
+
code: number;
|
|
70
|
+
description: string;
|
|
71
|
+
};
|
|
68
72
|
[key: string]: unknown;
|
|
69
73
|
}
|
|
70
74
|
interface Contact {
|
|
@@ -405,7 +409,10 @@ declare class Blip {
|
|
|
405
409
|
* @param contactId - Contact identifier to filter notifications
|
|
406
410
|
* @returns DispatchState or null if not found
|
|
407
411
|
*/
|
|
408
|
-
getDispatchState(messageId: string, contactId: string): Promise<
|
|
412
|
+
getDispatchState(messageId: string, contactId: string): Promise<{
|
|
413
|
+
state: DispatchState;
|
|
414
|
+
reason?: string;
|
|
415
|
+
} | null>;
|
|
409
416
|
/**
|
|
410
417
|
* Get the first message received after a specific message ID
|
|
411
418
|
* @param contactId - Contact identifier
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var tt=Object.create;var x=Object.defineProperty;var et=Object.getOwnPropertyDescriptor;var st=Object.getOwnPropertyNames;var it=Object.getPrototypeOf,rt=Object.prototype.hasOwnProperty;var nt=(c,t)=>()=>(t||c((t={exports:{}}).exports,t),t.exports),at=(c,t)=>{for(var e in t)x(c,e,{get:t[e],enumerable:!0})},_=(c,t,e,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of st(t))!rt.call(c,i)&&i!==e&&x(c,i,{get:()=>t[i],enumerable:!(s=et(t,i))||s.enumerable});return c};var z=(c,t,e)=>(e=c!=null?tt(it(c)):{},_(t||!c||!c.__esModule?x(e,"default",{value:c,enumerable:!0}):e,c)),ot=c=>_(x({},"__esModule",{value:!0}),c);var W=nt((qt,ut)=>{ut.exports={name:"@dawntech/dispatcher",version:"0.2.10",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",exports:{".":{import:{types:"./dist/index.d.mts",default:"./dist/index.mjs"},require:{types:"./dist/index.d.ts",default:"./dist/index.js"}}},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:lint":'eslint "src/**/*.ts" "tests/**/*.ts"',"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",eslint:"^9.39.3",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","typescript-eslint":"^8.56.1"},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 ht={};at(ht,{Blip:()=>k,BlipError:()=>D,Channel:()=>H,DispatchState:()=>G,Dispatcher:()=>K,DispatcherDescriptor:()=>R,DispatcherMonitor:()=>$,DispatcherRepository:()=>T,MessageState:()=>A,MessageStatus:()=>b,Vnd:()=>y,Weekdays:()=>V,enableLogger:()=>j,getLogger:()=>m});module.exports=ot(ht);var R=class{constructor(t,e,s){this.callbacks={};this.id=t,this.transformFn=e,this.contactIdTransform=s?.toContactId||(i=>i),this.options=s}transform(t){return this.transformFn(t)}toContactId(t){return this.contactIdTransform(t)}get messageOptions(){return this.options}on(t,e){let s=this.options?.finalStatus||"DELIVERED";if(t==="read"&&s!=="READ"&&s!=="REPLIED")throw new Error(`Cannot listen to 'read' event when finalStatus is '${s}'. Set finalStatus to 'READ' or 'REPLIED'.`);if(t==="replied"&&s!=="REPLIED")throw new Error(`Cannot listen to 'replied' event when finalStatus is '${s}'. Set finalStatus to 'REPLIED'.`);return this.callbacks[t]=e,this}emit(t,e,s,i){this.callbacks[t]?.(e,s,i)}};var X=require("uuid"),q=require("bullmq");var A=(r=>(r.INIT="INIT",r.DISPATCHED="DISPATCHED",r.SCHEDULED="SCHEDULED",r.QUEUED="QUEUED",r.FINAL="FINAL",r))(A||{}),b=(o=>(o.INIT="INIT",o.PENDING="PENDING",o.SENDING="SENDING",o.DELIVERED="DELIVERED",o.READ="READ",o.REPLIED="REPLIED",o.FAILED="FAILED",o.CANCELED="CANCELED",o))(b||{}),G=(r=>(r.ACCEPTED="accepted",r.DISPATCHED="dispatched",r.RECEIVED="received",r.CONSUMED="consumed",r.FAILED="failed",r))(G||{}),H=(d=>(d.BLIP_CHAT="BLIP_CHAT",d.EMAIL="EMAIL",d.MESSENGER="MESSENGER",d.SKYPE="SKYPE",d.SMS_TAKE="SMS_TAKE",d.SMS_TANGRAM="SMS_TANGRAM",d.TELEGRAM="TELEGRAM",d.WHATSAPP="WHATSAPP",d.INSTAGRAM="INSTAGRAM",d.GOOGLE_RCS="GOOGLE_RCS",d.MICROSOFT_TEAMS="MICROSOFT_TEAMS",d.APPLE_BUSINESS_CHAT="APPLE_BUSINESS_CHAT",d.WORKPLACE="WORKPLACE",d))(H||{}),V=(n=>(n[n.MONDAY=1]="MONDAY",n[n.TUESDAY=2]="TUESDAY",n[n.WEDNESDAY=4]="WEDNESDAY",n[n.THURSDAY=8]="THURSDAY",n[n.FRIDAY=16]="FRIDAY",n[n.SATURDAY=32]="SATURDAY",n[n.SUNDAY=64]="SUNDAY",n))(V||{});var v=z(require("debug")),P={debug:"debug",info:"info",warn:"warn",error:"error"},Q=new Map;function ct(c){if(c==null)return c;if(typeof c=="object")try{return JSON.stringify(c,null,2)}catch{return c}return c}function L(c){return(...t)=>{let e=t.map(ct);c(...e)}}function m(c){if(!Q.has(c)){let t=(0,v.default)(`${c}:${P.debug}`),e=(0,v.default)(`${c}:${P.info}`),s=(0,v.default)(`${c}:${P.warn}`),i=(0,v.default)(`${c}:${P.error}`);i.log=console.error.bind(console);let r={debug:L(t),info:L(e),warn:L(s),error:L(i)};Q.set(c,r)}return Q.get(c)}function j(c){let t=v.default.disable();v.default.enable(`${t},${c}`)}var J=z(require("ioredis")),M=m("Repository"),I=class I{constructor(t,e){this.client=new J.default(e,{maxRetriesPerRequest:null}),this.keyPrefix=`dwn-dispatcher:${t}`,this.client.on("error",s=>{M.error("[client] Redis error",s)})}async setup(){if(this.client.status==="ready"){M.debug("[setup] Redis already connected, skipping");return}this.client.status==="wait"&&await this.client.connect(),M.info("[setup] Repository connected",{status:this.client.status})}async teardown(){this.client.status!=="end"&&(await this.client.quit(),M.info("[teardown] Repository disconnected"))}get redis(){return this.client}getManifestKey(){return`${this.keyPrefix}:manifest`}getKey(t){return`${this.keyPrefix}:message:${t}`}getStateKey(t){return`${this.keyPrefix}:index:state:${t.toLowerCase()}`}getStatusKey(t){return`${this.keyPrefix}:index:status:${t.toLowerCase()}`}getContactKey(t){return`${this.keyPrefix}:index:contact:${t}`}getDescriptorKey(t){return`${this.keyPrefix}:index:descriptor:${t}`}getQueueKey(t){return`${this.keyPrefix}:queue:${t.toLowerCase()}`}async upsertMessage(t,e){let s=this.getKey(t.messageId),i=JSON.stringify(t),r=this.client.pipeline();r.set(s,i),t.contactId&&r.sadd(this.getContactKey(t.contactId),t.messageId),t.descriptorId&&r.sadd(this.getDescriptorKey(t.descriptorId),t.messageId);for(let n of I.INDEXED_STATUSES){let o=this.getStatusKey(n);t.status===n?r.sadd(o,t.messageId):r.srem(o,t.messageId)}for(let n of I.INDEXED_STATES){let o=this.getStateKey(n);t.state===n?r.sadd(o,t.messageId):r.srem(o,t.messageId)}let a=Date.now()+(e||36e5*24*2);if(t.state==="SCHEDULED"&&t.scheduledTo&&(a=new Date(t.scheduledTo).getTime()+(e||0)),r.zadd(this.getQueueKey("retention"),a,t.messageId),t.state==="SCHEDULED"&&t.scheduledTo){let n=new Date(t.scheduledTo).getTime();r.zadd(this.getQueueKey("scheduled"),n,t.messageId)}else r.zrem(this.getQueueKey("scheduled"),t.messageId);if(t.state==="QUEUED"){let n=new Date(t.createdAt||Date.now()).getTime();r.zadd(this.getQueueKey("queued"),n,t.messageId)}else r.zrem(this.getQueueKey("queued"),t.messageId);if(t.state==="DISPATCHED"){let n=new Date(t.createdAt||Date.now()).getTime();r.zadd(this.getQueueKey("dispatched"),n,t.messageId)}else r.zrem(this.getQueueKey("dispatched"),t.messageId);await r.exec(),M.debug("[upsertMessage]",{messageId:t.messageId,status:t.status,state:t.state})}async getMessage(t){let e=this.getKey(t),s=await this.client.get(e);return s?JSON.parse(s):null}async getMessages(t){let e=[];if(t.state==="SCHEDULED"){let i=Date.now(),r=t.skip??0,a=t.size??0;a>0?e=await this.client.zrangebyscore(this.getQueueKey("scheduled"),0,i,"LIMIT",r,a):e=await this.client.zrangebyscore(this.getQueueKey("scheduled"),0,i)}else if(t.state==="QUEUED")e=await this.client.zrange(this.getQueueKey("queued"),t.skip??0,(t.skip??0)+(t.size?t.size-1:-1));else if(t.state==="DISPATCHED")e=await this.client.zrange(this.getQueueKey("dispatched"),t.skip??0,(t.skip??0)+(t.size?t.size-1:-1));else if(t.status){let i=await this.client.smembers(this.getStatusKey(t.status)),r=t.skip??0,a=t.size;e=a?i.slice(r,r+a):i.slice(r)}else if(t.state)try{let i=await this.client.smembers(this.getStateKey(t.state)),r=t.skip??0,a=t.size;e=a?i.slice(r,r+a):i.slice(r)}catch{return[]}else return M.warn("[getMessages] no filter provided"),[];let s=[];for(let i of e){let r=await this.getMessage(i);if(r){if(t.status&&r.status!==t.status||t.state&&r.state!==t.state)continue;s.push(r)}}return M.debug("[getMessages]",{count:s.length,filter:t}),s}async getQueueSize(){return await this.client.zcard(this.getQueueKey("dispatched"))}async evictOldest(t){if(t<=0)return 0;let e=await this.client.zpopmin(this.getQueueKey("dispatched"),t),s=0;for(let i=0;i<e.length;i+=2){let r=e[i];await this.deleteMessage(r),s++}return s}async getExpiredMessages(t=50){let e=Date.now();return await this.client.zrangebyscore(this.getQueueKey("expiration"),0,e,"LIMIT",0,t)}async getRetentionMessages(t=50){let e=Date.now();return await this.client.zrangebyscore(this.getQueueKey("retention"),0,e,"LIMIT",0,t)}async incrementMetric(t,e=1){let s=`${this.keyPrefix}:metrics:${t}`;return await this.client.incrby(s,e)}async getMetric(t){let e=`${this.keyPrefix}:metrics:${t}`,s=await this.client.get(e);return s?parseInt(s,10):0}async deleteMessage(t){let e=await this.getMessage(t);e&&await this.deleteMessageData(t,e)}async deleteMessageData(t,e){let s=this.client.pipeline();s.del(this.getKey(t)),s.zrem(this.getQueueKey("scheduled"),t),s.zrem(this.getQueueKey("queued"),t),s.zrem(this.getQueueKey("dispatched"),t),s.zrem(this.getQueueKey("expiration"),t),s.zrem(this.getQueueKey("retention"),t);for(let i of I.INDEXED_STATUSES)s.srem(this.getStatusKey(i),t);for(let i of I.INDEXED_STATES)s.srem(this.getStateKey(i),t);e.contactId&&s.srem(this.getContactKey(e.contactId),t),e.descriptorId&&s.srem(this.getDescriptorKey(e.descriptorId),t),await s.exec()}async countMessages(t){if(t.state==="SCHEDULED")return await this.client.zcard(this.getQueueKey("scheduled"));if(t.state==="QUEUED")return await this.client.zcard(this.getQueueKey("queued"));if(t.state==="DISPATCHED")return await this.client.zcard(this.getQueueKey("dispatched"));if(t.status)return await this.client.scard(this.getStatusKey(t.status));if(t.state)try{return await this.client.scard(this.getStateKey(t.state))}catch{return 0}return 0}async getMetrics(t){let e={cumulative:{dispatched:0,delivered:0,failed:0},queues:{queued:0,scheduled:0,dispatched:0},status:{}},s=async a=>{if(t){let n=this.getDescriptorKey(t);return(await this.client.sinter(a,n)).length}return await this.client.scard(a)};for(let a of I.INDEXED_STATUSES){let n=await s(this.getStatusKey(a));e.status[a]=n,a==="DELIVERED"&&(e.cumulative.delivered=n),a==="FAILED"&&(e.cumulative.failed=n)}let i=this.getStateKey("DISPATCHED"),r=await s(i);return e.cumulative.dispatched=r+e.cumulative.delivered+e.cumulative.failed,t?(e.queues.queued=await s(this.getStateKey("QUEUED")),e.queues.scheduled=await s(this.getStateKey("SCHEDULED")),e.queues.dispatched=r):(e.queues.queued=await this.client.zcard(this.getQueueKey("queued")),e.queues.scheduled=await this.client.zcard(this.getQueueKey("scheduled")),e.queues.dispatched=await this.client.zcard(this.getQueueKey("dispatched"))),e}async getDescriptors(){let t=this.getDescriptorKey("*"),e=`${this.keyPrefix}:index:descriptor:`,s=await this.client.keys(t),i=[];for(let r of s){let a=r.slice(e.length);if(a){let n=await this.client.scard(r);i.push({id:a,count:n})}}return i.sort((r,a)=>a.count-r.count),i}async writeManifest(t){let e=this.getManifestKey();await this.client.hset(e,{version:t.version,createdAt:t.createdAt,updatedAt:t.updatedAt}),M.info("[writeManifest] Manifest written",{key:e})}async getManifest(){let t=this.getManifestKey(),e=await this.client.hgetall(t);return!e||Object.keys(e).length===0?null:e}};I.INDEXED_STATUSES=["INIT","PENDING","SENDING","DELIVERED","READ","REPLIED","FAILED","CANCELED"],I.INDEXED_STATES=["INIT","DISPATCHED","SCHEDULED","QUEUED","FINAL"];var T=I;var B=m("StateMachine"),C=class{constructor(t,e,s){this.id=t;this.repository=e;this.emit=s}async transition(t,e,s,i){let r=t.state,a=t.status;r==="FINAL"&&e!=="FINAL"&&B.warn(`[transition] Attempting to move from FINAL back to ${e}`,{messageId:t.messageId}),t.state=e,t.status=s,i&&Object.assign(t,i);let n=new Date().toISOString();return s==="SENDING"&&!t.acceptedAt&&a!=="SENDING"&&(t.acceptedAt=n),s==="DELIVERED"&&!t.deliveredAt&&(t.deliveredAt=n),s==="READ"&&!t.readAt&&(t.readAt=n),s==="REPLIED"&&!t.repliedAt&&(t.repliedAt=n,t.readAt||(t.readAt=n)),s==="FAILED"&&!t.failedAt&&(t.failedAt=n),e==="DISPATCHED"&&!t.sentAt&&"PENDING",await this.repository.upsertMessage(t),a!==s&&(s==="REPLIED"&&a!=="READ"&&this.emit("read",t),this.emitStatusEvent(t,s)),e==="SCHEDULED"&&r!=="SCHEDULED"&&this.emit("scheduled",t),B.debug(`[transition] ${t.messageId} : ${r}/${a} -> ${e}/${s}`),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 Y=z(require("axios")),N=require("uuid");var y;(t=>{let c;(i=>{let e;(g=>(g.GET="get",g.SET="set",g.DELETE="delete",g.OBSERVE="observe",g.SUBSCRIBE="subscribe",g.MERGE="merge"))(e=i.Method||(i.Method={}));let s;(n=>(n.SUCCESS="success",n.FAILURE="failure"))(s=i.Status||(i.Status={}))})(c=t.Lime||(t.Lime={}))})(y||(y={}));var p=m("Blip"),k=class{constructor(t,e,s=3e4){let i=`https://${t}.http.msging.net`;this.client=Y.default.create({baseURL:i,timeout:s,headers:{"Content-Type":"application/json",Authorization:e}}),this.client.interceptors.response.use(r=>r,r=>{if(r.response){let a=r.response.data?.reason||{code:r.response.status,description:r.response.statusText||"Unknown error"};throw new D(a.description,a.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,N.v4)()};p.debug("[postCommand] payload",e);let i=(await this.client.post("/commands",e)).data;if(i.status!==y.Lime.Status.SUCCESS)throw p.error("[postCommand] failed",{method:e.method,uri:e.uri,status:i.status,reason:i.reason}),new D(i.reason?.description||"Command failed",i.reason?.code||0);return p.debug("[postCommand] succeeded",e.uri),i}async postMessage(t){let e={...t,id:t.id||(0,N.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 s={...e,identity:t},i={method:y.Lime.Method.MERGE,uri:"/contacts",type:"application/vnd.lime.contact+json",resource:s};await this.postCommand(i)}async sendMessage(t,e,s){p.info("[sendMessage] called with",{contactId:t,message:e,id:s});let i=s||(0,N.v4)(),r={id:i,to:t,type:e.type,content:e.content};return await this.postMessage(r),p.info("[sendMessage] sent",{contactId:t,messageId:i}),i}async getDispatchState(t,e){p.info("[getDispatchState] called with",{messageId:t,contactId:e});let s={method:y.Lime.Method.GET,uri:`/threads/${e}?$take=100`,to:"postmaster@msging.net"};try{let i=await this.postCommand(s);if(!i.resource||!i.resource.items||i.resource.items.length===0)return p.debug("[getDispatchState] no messages found in thread",{messageId:t,contactId:e}),null;let r=i.resource.items.find(n=>n.id===t);if(!r)return p.debug("[getDispatchState] message not found in recent thread",{messageId:t,contactId:e}),null;let a=r.status;return p.info("[getDispatchState] state retrieved",{messageId:t,contactId:e,state:a}),a}catch(i){if(i instanceof D&&i.code===67)return p.debug("[getDispatchState] resource not found",{messageId:t,contactId:e}),null;throw p.error("[getDispatchState] failed",{messageId:t,contactId:e,error:i}),i}}async getMessageAfter(t,e){p.info("[getMessageAfter] called with",{contactId:t,messageId:e});let s=e,i=0,r=10;for(;i<r;){let a={method:y.Lime.Method.GET,uri:`/threads/${t}?$skip=0&$take=1&$order=asc&messageId=${s}`,to:"postmaster@msging.net"};try{let n=await this.postCommand(a);if(!n.resource||!n.resource.items||n.resource.items.length===0)return p.debug("[getMessageAfter] no message found after",{contactId:t,messageId:s}),null;let o=n.resource.items[0];if(o.direction==="received")return p.info("[getMessageAfter] found received message",{contactId:t,messageId:s,nextMessageId:o.id}),o;p.debug("[getMessageAfter] skipping sent message",{contactId:t,messageId:o.id}),s=o.id,i++}catch(n){if(n instanceof D&&n.code===67)return p.debug("[getMessageAfter] resource not found",{contactId:t,messageId:s}),null;throw p.error("[getMessageAfter] failed",{contactId:t,messageId:s,error:n}),n}}return p.warn("[getMessageAfter] max traversal attempts reached",{contactId:t,startMessageId:e}),null}async sendEvent(t,e,s,i){p.info("[sendEvent] called with",{contactId:t,category:e,action:s,extras:i});let r={to:"postmaster@analytics.msging.net",method:y.Lime.Method.SET,type:"application/vnd.iris.eventTrack+json",uri:"/event-track",resource:{category:e,action:s,contact:{identity:t},extras:i}};await this.postCommand(r)}async setState(t,e,s="onboarding"){p.info("[setState] called with",{contactId:t,botId:e,stateId:s});let i={uri:`/flow-id?shortName=${e}`,to:"postmaster@builder.msging.net",method:y.Lime.Method.GET},r=await this.postCommand(i);if(!r.resource)throw p.error("[setState] flow ID not found",{botId:e}),new D(`Flow ID not found for bot: ${e}`,404);let a=r.resource,n={method:y.Lime.Method.SET,uri:`/contexts/${t}/stateid@${a}`,resource:s,type:"text/plain"};await this.postCommand(n);let o={method:y.Lime.Method.SET,uri:`/contexts/${t}/master-state`,resource:`${e}@msging.net`,type:"text/plain"};await this.postCommand(o)}},D=class c extends Error{constructor(t,e){super(t),this.name="BlipError",this.code=e,Object.setPrototypeOf(this,c.prototype)}};var dt=m("DispatcherQuery"),O=class{constructor(t){this.repository=t}get client(){return this.repository.redis}async query(t){let e=[];if(t.contactId&&e.push(this.repository.getContactKey(t.contactId)),t.descriptorId&&e.push(this.repository.getDescriptorKey(t.descriptorId)),t.status){let u=Array.isArray(t.status)?t.status:[t.status];u.length===1?e.push(this.repository.getStatusKey(u[0])):u.length>1&&e.push(this.repository.getStatusKey(u[0]))}if(t.state){let u=Array.isArray(t.state)?t.state:[t.state];u.length===1&&e.push(this.repository.getStateKey(u[0]))}let s=[];if(e.length>0)s=await this.client.sinter(e);else{let u=Object.values(b).map(l=>this.repository.getStatusKey(l));s=await this.client.sunion(u)}let i=t.skip??0,r=t.size??50,a=s.slice(i,i+r),n=[],o=[];for(let u of a){let l=await this.repository.getMessage(u);if(l){if(t.status&&!(Array.isArray(t.status)?t.status:[t.status]).includes(l.status)||t.state&&!(Array.isArray(t.state)?t.state:[t.state]).includes(l.state))continue;n.push(l)}else o.push(u)}return o.length>0&&this.cleanupIndices(o,t),n}async cleanupIndices(t,e){let s=this.client.pipeline();e.contactId&&s.srem(this.repository.getContactKey(e.contactId),t),e.descriptorId&&s.srem(this.repository.getDescriptorKey(e.descriptorId),t),e.status&&(Array.isArray(e.status)?e.status:[e.status]).forEach(r=>{s.srem(this.repository.getStatusKey(r),t)}),e.state&&(Array.isArray(e.state)?e.state:[e.state]).forEach(r=>{s.srem(this.repository.getStateKey(r),t)}),await s.exec(),dt.debug("[cleanupIndices] Removed expired IDs from checked indices",{count:t.length})}};var{version:pt}=W(),h=m("Dispatcher"),K=class{constructor(t,e,s,i){this.callbacks={};this.descriptors=new Map;this.isRunning=!1;this.setupCompleted=!1;this.setupPromise=null;this.timeoutMonitorRunning=!1;this.timeoutTimer=null;this.id=t,this.repository=i?.repository??new T(t,e),this.stateMachine=new C(this.id,this.repository,(r,a)=>{this.emit(r,a),this.descriptors.get(a.descriptorId)?.emit(r,a,this.api,this.id)}),this.api=new k(s.contract,s.key),this.queueName=`dispatcher-${this.id.replace(/:/g,"-")}`,this.maxRetries=i?.maxRetries??0,this.retryIntervals=i?.retryIntervals??[1*1e3,5*1e3,15*1e3],this.timeouts={pending:i?.timeouts?.pending??120*1e3,sending:i?.timeouts?.sending??120*1e3},this.retention=i?.retention??2880*60*1e3,this.pollingIntervals={scheduled:i?.pollingIntervals?.scheduled??30*1e3,pending:i?.pollingIntervals?.pending??10*1e3,sending:i?.pollingIntervals?.sending??10*1e3,delivered:i?.pollingIntervals?.delivered??1800*1e3,read:i?.pollingIntervals?.read??1800*1e3,queue:i?.pollingIntervals?.queue??1*1e3},this.query=new O(this.repository),this.queue=new q.Queue(this.queueName,{connection:this.repository.redis,defaultJobOptions:{removeOnComplete:!0,removeOnFail:!0}}),this.worker=new q.Worker(this.queueName,async r=>{try{await this.processJob(r)}catch(a){throw h.error(`[Worker] Job ${r.name} failed`,a),a}},{connection:this.repository.redis,concurrency:i?.batchSize||50,limiter:i?.rateLimits?.global?{max:i.rateLimits.global.points,duration:i.rateLimits.global.duration*1e3}:void 0}),this.worker.on("error",r=>h.error("[Worker] Error",r)),this.worker.on("failed",(r,a)=>h.error(`[Worker] Job ${r?.id} failed`,a))}get redis(){return this.repository.redis}async setup(){return this.setupPromise?this.setupPromise:(this.setupPromise=this._doSetup(),this.setupPromise)}async _doSetup(){await this.repository.setup();let t=await this.repository.getManifest();await this.repository.writeManifest({version:pt,createdAt:t?.createdAt??new Date().toISOString(),updatedAt:new Date().toISOString()}),await this.queue.waitUntilReady(),this.isRunning=!0,this.setupCompleted=!0,this.startTimeoutMonitor(),h.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(),h.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(A);for(let i of e)t.byState[i]=await this.repository.countMessages({state:i});let s=Object.values(b);for(let i of s)t.byStatus[i]=await this.repository.countMessages({status:i});return t.total=Object.values(t.byState).reduce((i,r)=>i+(r||0),0),t}emit(t,e){this.callbacks[t]?.(e,this.api,this.id)}async send(t,e,s,i){this.descriptors.set(t.id,t);let r=t.toContactId(e),a=t.transform(s),n=new Date().toISOString(),o={messageId:(0,X.v4)(),contactId:r,descriptorId:t.id,payload:a,status:"INIT",state:"INIT",createdAt:n,attempts:0,retries:this.maxRetries},l={...t.messageOptions,...i},{schedule:g,...w}=l;o.options=w,this.emit("dispatch",o),t.emit("dispatch",o,this.api,this.id);let d=this.calculateScheduledTime(g,l.shifts),f=0;if(d){o.scheduledTo=d,o.state="SCHEDULED";let E=new Date(d).getTime();f=Math.max(0,E-Date.now()),this.emit("scheduled",o),t.emit("scheduled",o,this.api,this.id),h.info("[send] message scheduled",{messageId:o.messageId,scheduledTo:d,delay:f})}else o.state="QUEUED",o.status="INIT",h.info("[send] message queued",{messageId:o.messageId});return o.expiresAt=new Date(Date.now()+(o.state==="SCHEDULED"?f+this.retention:this.retention)).toISOString(),await this.stateMachine.transition(o,o.state,o.status),await this.queue.add("send",{messageId:o.messageId},{jobId:o.messageId,delay:f,priority:1}),o}async cancel(t){let e=await this.repository.getMessage(t);if(!e)return h.warn("[cancel] message not found",{messageId:t}),!1;if(e.state==="FINAL")return h.warn("[cancel] message already final",{messageId:t,status:e.status}),!1;let s=await this.queue.getJob(t);return s&&(await s.remove(),h.info("[cancel] removed job from queue",{messageId:t})),await this.stateMachine.transition(e,"FINAL","CANCELED"),h.info("[cancel] message canceled",{messageId:t}),!0}async processJob(t){let{messageId:e}=t.data,s=await this.repository.getMessage(e);if(!s){h.warn(`[processJob] Message not found: ${e}`);return}let i=this.descriptors.get(s.descriptorId)||null;switch(t.name){case"send":await this.handleSendJob(s,i);break;case"check":await this.handleCheckJob(s,i);break;default:h.warn(`[processJob] Unknown job name: ${t.name}`)}}async handleSendJob(t,e){if(t.state==="FINAL"){h.warn("[handleSendJob] Message already final, skipping",{messageId:t.messageId});return}if(t.state==="DISPATCHED"){h.warn("[handleSendJob] Message already dispatched, scheduling check instead",{messageId:t.messageId}),await this.rescheduleCheck(t,0);return}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"),h.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(s){let i=s instanceof Error?s:new Error(String(s));await this.handleDispatchFailure(t,e,i)}}async handlePostSendOperations(t,e={}){let s={...e.contact||{}};if(e.intent)if(typeof e.intent=="string")s.intent=e.intent;else{s.intent=e.intent.intent;let{intent:i,...r}=e.intent;Object.entries(r).forEach(([a,n])=>{n!=null&&(s[a]=typeof n=="object"?JSON.stringify(n):String(n))})}Object.keys(s).length>0&&await this.api.mergeContact(t.contactId,s),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 s=await this.api.getDispatchState(t.messageId,t.contactId);if(!s){await this.rescheduleCheck(t,this.pollingIntervals.pending);return}let i=this.pollingIntervals.pending;switch(s){case"accepted":t.status!=="SENDING"&&await this.stateMachine.transition(t,t.state,"SENDING"),i=this.pollingIntervals.sending;break;case"received":case"consumed":i=await this.handleDeliveredState(t,s,i);break;case"failed":await this.handleDispatchFailure(t,e,new Error("Dispatch failed from Gateway"));return}t.state!=="FINAL"&&await this.rescheduleCheck(t,i)}catch(s){h.error("[handleCheckJob] Error",s),await this.rescheduleCheck(t,this.pollingIntervals.pending)}}}checkAndHandleTimeout(t){let e=new Date;if(t.status==="PENDING"){let s=t.lastDispatchAttemptAt||t.sentAt||t.createdAt;if(e.getTime()-new Date(s).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"}),h.info("[handleTimeout] Message timed out",{messageId:t.messageId})}startTimeoutMonitor(){this.timeoutTimer||(this.timeoutTimer=setInterval(()=>{this.timeoutMonitorRunning||(this.timeoutMonitorRunning=!0,this._runTimeoutMonitorCycle().finally(()=>{this.timeoutMonitorRunning=!1}))},1e4))}async _runTimeoutMonitorCycle(){try{let t=await this.repository.getMessages({status:"PENDING"}),e=await this.repository.getMessages({status:"SENDING"});for(let i of[...t,...e])if(this.checkAndHandleTimeout(i)){let r=await this.repository.getMessage(i.messageId);if(r&&r.state!=="FINAL"&&(r.status==="PENDING"||r.status==="SENDING")){let a=this.descriptors.get(r.descriptorId)||null;await this.handleTimeout(r,a)}}let s=await this.repository.getRetentionMessages(100);if(s.length>0){h.debug("[CleanupMonitor] Cleaning up expired messages",{count:s.length});for(let i of s)await this.repository.deleteMessage(i)}}catch(t){h.error("[TimeoutMonitor] Error during scan",t)}}async rescheduleCheck(t,e){await this.queue.add("check",{messageId:t.messageId},{delay:Math.max(0,e),priority:5})}async handleDeliveredState(t,e,s){if(await this.api.getMessageAfter(t.contactId,t.messageId))return t.status!=="REPLIED"&&t.status!=="READ"&&(await this.repository.incrementMetric("delivered"),await this.stateMachine.transition(t,"FINAL","REPLIED")),s;let r=e==="consumed"?"READ":"DELIVERED";t.status!==r&&(await this.repository.incrementMetric("delivered"),await this.stateMachine.transition(t,t.state,r));let a=t.options?.finalStatus||"DELIVERED";if(this.getStatusRank(t.status)>=this.getStatusRank(a))await this.stateMachine.transition(t,"FINAL",t.status);else return this.pollingIntervals.delivered;return s}async handleDispatchFailure(t,e,s){if(t.attempts=(t.attempts??0)+1,t.error=s.message,h.error("[handleDispatchFailure]",{messageId:t.messageId,attempts:t.attempts,maxRetries:this.maxRetries,error:s.message}),t.attempts<=this.maxRetries){t.retries=this.maxRetries-t.attempts;let i=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},{jobId:t.messageId,delay:i,priority:1}),h.info("[handleDispatchFailure] Rescheduled retry",{messageId:t.messageId,retryDelay:i})}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 s=new Date;return this.isWithinShifts(s,e)?void 0:this.findNextShiftTime(s,e)?.toISOString()}isWithinShifts(t,e){let s=t.getDay(),i=s===0?64:Math.pow(2,s-1);for(let r of e){if((r.days&i)===0)continue;let a=r.gmt||"-3",n=parseInt(a,10),o=new Date(t.getTime()-n*60*60*1e3),u=o.getHours()*60+o.getMinutes(),[l,g]=r.start.split(":").map(Number),[w,d]=r.end.split(":").map(Number),f=l*60+g,E=w*60+d;if(u>=f&&u<E)return!0}return!1}findNextShiftTime(t,e){for(let i=0;i<=7;i++){let r=new Date(t);r.setDate(r.getDate()+i);let a=r.getDay(),n=a===0?64:Math.pow(2,a-1),o=e.filter(u=>(u.days&n)!==0);if(o.length!==0){o.sort((u,l)=>{let[g,w]=u.start.split(":").map(Number),[d,f]=l.start.split(":").map(Number);return g*60+w-(d*60+f)});for(let u of o){let l=u.gmt||"-3",g=parseInt(l,10),[w,d]=u.start.split(":").map(Number),f=new Date(r);f.setHours(w,d,0,0);let E=new Date(f.getTime()+g*60*60*1e3);if(i===0){if(E>t)return E}else return E}}}}getStatusRank(t){return{INIT:0,PENDING:1,SENDING:2,DELIVERED:3,READ:4,REPLIED:5,FAILED:6,CANCELED:6}[t]||0}static sanitizeContactId(t,e){if(t.includes("@"))return t;let s={BLIP_CHAT:"@0mn.io",EMAIL:"@mailgun.gw.msging.net",MESSENGER:"@messenger.gw.msging.net",SKYPE:"@skype.gw.msging.net",SMS_TAKE:"@take.io",SMS_TANGRAM:"@tangram.com.br",TELEGRAM:"@telegram.gw.msging.net",WHATSAPP:"@wa.gw.msging.net",INSTAGRAM:"@instagram.gw.msging.net",GOOGLE_RCS:"@googlercs.gw.msging.net",MICROSOFT_TEAMS:"@abs.gw.msging.net",APPLE_BUSINESS_CHAT:"@businesschat.gw.msging.net",WORKPLACE:"@workplace.gw.msging.net"};if(!s[e])throw new Error(`Unknown channel: ${e}`);return e==="WHATSAPP"&&t.startsWith("+")&&(t=t.slice(1)),`${t}${s[e]}`}};var Z=require("events"),F=require("bullmq");var S=m("DispatcherMonitor"),$=class extends Z.EventEmitter{constructor(e,s){super();this.history=[];this.lastAlerts={};this.activeAlerts=new Set;this.isRunning=!1;this.id=e.id,this.dispatcher=e,this.options={interval:6e4,historySize:1e3,...s},this.queueName=`monitor-${this.id}`,this.queue=new F.Queue(this.queueName,{connection:e.redis,defaultJobOptions:{removeOnComplete:!0,removeOnFail:!0}}),this.worker=new F.Worker(this.queueName,async i=>{i.name==="check"&&await this.check()},{connection:e.redis}),this.worker.on("error",i=>S.error("[MonitorWorker] Error",i)),this.worker.on("failed",(i,r)=>S.error(`[MonitorWorker] Job ${i?.id} failed`,r))}async start(){this.isRunning||(S.info("[Monitor] Started"),await this.queue.obliterate({force:!0}),await this.queue.add("check",{},{repeat:{every:this.options.interval,immediately:!0}}),this.isRunning=!0)}async stop(){this.isRunning=!1,await this.queue.close(),await this.worker.close(),S.info("[Monitor] Stopped")}async check(){try{let e=await this.dispatcher.getMetrics(),s=Date.now();this.history.push({timestamp:s,metrics:e}),this.cleanHistory();for(let i of this.options.rules)await this.evaluateRule(i,e,s)}catch(e){S.error("[Monitor] Error during check",e)}}cleanHistory(){let e=this.options.historySize;this.history.length>e&&(this.history=this.history.slice(this.history.length-e));let s=Math.max(...this.options.rules.map(r=>r.window||0)),i=Date.now()-s-6e4;if(this.history.length>0&&this.history[0].timestamp<i){let r=this.history.findIndex(a=>a.timestamp>=i);r>0&&(this.history=this.history.slice(r))}}async evaluateRule(e,s,i){let r=`${e.type}`,a=!1,n=0,o={};switch(e.type){case"queue_size":let u=s.byState.FINAL||0;n=s.total-u,a=n>e.threshold,o={current:n,threshold:e.threshold};break;case"failure_rate":if(!e.window){S.warn("[Monitor] failure_rate rule missing window");return}let l=this.findSnapshotAt(i-e.window);if(!l)return;let g=s.cumulative.failed,w=l.metrics.cumulative.failed,d=g-w,f=s.cumulative.dispatched,E=l.metrics.cumulative.dispatched,U=f-E;U===0?n=0:n=d/U,a=n>e.threshold,o={rate:(n*100).toFixed(2)+"%",threshold:(e.threshold*100).toFixed(2)+"%",failed:d,dispatched:U,window:e.window};break}a?this.activeAlerts.has(r)?e.debounce&&!this.isDebounced(r,e.debounce)&&this.emitAlert(r,e,n,o):(this.emitAlert(r,e,n,o),this.activeAlerts.add(r)):this.activeAlerts.has(r)&&(this.resolveAlert(r,e),this.activeAlerts.delete(r))}isDebounced(e,s){if(!s)return!1;let i=this.lastAlerts[e];return i?Date.now()-i<s:!1}emitAlert(e,s,i,r){S.warn(`[Monitor] Alert triggered: ${s.type}`,r),this.lastAlerts[e]=Date.now();let a={type:s.type,message:`${s.type} exceeded threshold`,level:"warning",details:r,timestamp:new Date().toISOString()};this.emit("alert",a)}resolveAlert(e,s){S.info(`[Monitor] Alert resolved: ${s.type}`);let i={type:s.type,message:`${s.type} resolved`,level:"warning",details:{},timestamp:new Date().toISOString()};this.emit("resolved",i)}findSnapshotAt(e){if(this.history.length===0)return null;for(let s of this.history)if(s.timestamp>=e)return s;return this.history[0]}};0&&(module.exports={Blip,BlipError,Channel,DispatchState,Dispatcher,DispatcherDescriptor,DispatcherMonitor,DispatcherRepository,MessageState,MessageStatus,Vnd,Weekdays,enableLogger,getLogger});
|
|
1
|
+
"use strict";var tt=Object.create;var x=Object.defineProperty;var et=Object.getOwnPropertyDescriptor;var st=Object.getOwnPropertyNames;var it=Object.getPrototypeOf,rt=Object.prototype.hasOwnProperty;var nt=(c,t)=>()=>(t||c((t={exports:{}}).exports,t),t.exports),at=(c,t)=>{for(var e in t)x(c,e,{get:t[e],enumerable:!0})},_=(c,t,e,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of st(t))!rt.call(c,i)&&i!==e&&x(c,i,{get:()=>t[i],enumerable:!(s=et(t,i))||s.enumerable});return c};var z=(c,t,e)=>(e=c!=null?tt(it(c)):{},_(t||!c||!c.__esModule?x(e,"default",{value:c,enumerable:!0}):e,c)),ot=c=>_(x({},"__esModule",{value:!0}),c);var W=nt((qt,ut)=>{ut.exports={name:"@dawntech/dispatcher",version:"0.2.11",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",exports:{".":{import:{types:"./dist/index.d.mts",default:"./dist/index.mjs"},require:{types:"./dist/index.d.ts",default:"./dist/index.js"}}},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:lint":'eslint "src/**/*.ts" "tests/**/*.ts"',"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",eslint:"^9.39.3",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","typescript-eslint":"^8.56.1"},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 ht={};at(ht,{Blip:()=>k,BlipError:()=>D,Channel:()=>H,DispatchState:()=>G,Dispatcher:()=>K,DispatcherDescriptor:()=>R,DispatcherMonitor:()=>$,DispatcherRepository:()=>T,MessageState:()=>A,MessageStatus:()=>b,Vnd:()=>y,Weekdays:()=>V,enableLogger:()=>j,getLogger:()=>m});module.exports=ot(ht);var R=class{constructor(t,e,s){this.callbacks={};this.id=t,this.transformFn=e,this.contactIdTransform=s?.toContactId||(i=>i),this.options=s}transform(t){return this.transformFn(t)}toContactId(t){return this.contactIdTransform(t)}get messageOptions(){return this.options}on(t,e){let s=this.options?.finalStatus||"DELIVERED";if(t==="read"&&s!=="READ"&&s!=="REPLIED")throw new Error(`Cannot listen to 'read' event when finalStatus is '${s}'. Set finalStatus to 'READ' or 'REPLIED'.`);if(t==="replied"&&s!=="REPLIED")throw new Error(`Cannot listen to 'replied' event when finalStatus is '${s}'. Set finalStatus to 'REPLIED'.`);return this.callbacks[t]=e,this}emit(t,e,s,i){this.callbacks[t]?.(e,s,i)}};var X=require("uuid"),q=require("bullmq");var A=(r=>(r.INIT="INIT",r.DISPATCHED="DISPATCHED",r.SCHEDULED="SCHEDULED",r.QUEUED="QUEUED",r.FINAL="FINAL",r))(A||{}),b=(o=>(o.INIT="INIT",o.PENDING="PENDING",o.SENDING="SENDING",o.DELIVERED="DELIVERED",o.READ="READ",o.REPLIED="REPLIED",o.FAILED="FAILED",o.CANCELED="CANCELED",o))(b||{}),G=(r=>(r.ACCEPTED="accepted",r.DISPATCHED="dispatched",r.RECEIVED="received",r.CONSUMED="consumed",r.FAILED="failed",r))(G||{}),H=(d=>(d.BLIP_CHAT="BLIP_CHAT",d.EMAIL="EMAIL",d.MESSENGER="MESSENGER",d.SKYPE="SKYPE",d.SMS_TAKE="SMS_TAKE",d.SMS_TANGRAM="SMS_TANGRAM",d.TELEGRAM="TELEGRAM",d.WHATSAPP="WHATSAPP",d.INSTAGRAM="INSTAGRAM",d.GOOGLE_RCS="GOOGLE_RCS",d.MICROSOFT_TEAMS="MICROSOFT_TEAMS",d.APPLE_BUSINESS_CHAT="APPLE_BUSINESS_CHAT",d.WORKPLACE="WORKPLACE",d))(H||{}),V=(n=>(n[n.MONDAY=1]="MONDAY",n[n.TUESDAY=2]="TUESDAY",n[n.WEDNESDAY=4]="WEDNESDAY",n[n.THURSDAY=8]="THURSDAY",n[n.FRIDAY=16]="FRIDAY",n[n.SATURDAY=32]="SATURDAY",n[n.SUNDAY=64]="SUNDAY",n))(V||{});var v=z(require("debug")),P={debug:"debug",info:"info",warn:"warn",error:"error"},Q=new Map;function ct(c){if(c==null)return c;if(typeof c=="object")try{return JSON.stringify(c,null,2)}catch{return c}return c}function L(c){return(...t)=>{let e=t.map(ct);c(...e)}}function m(c){if(!Q.has(c)){let t=(0,v.default)(`${c}:${P.debug}`),e=(0,v.default)(`${c}:${P.info}`),s=(0,v.default)(`${c}:${P.warn}`),i=(0,v.default)(`${c}:${P.error}`);i.log=console.error.bind(console);let r={debug:L(t),info:L(e),warn:L(s),error:L(i)};Q.set(c,r)}return Q.get(c)}function j(c){let t=v.default.disable();v.default.enable(`${t},${c}`)}var J=z(require("ioredis")),M=m("Repository"),I=class I{constructor(t,e){this.client=new J.default(e,{maxRetriesPerRequest:null}),this.keyPrefix=`dwn-dispatcher:${t}`,this.client.on("error",s=>{M.error("[client] Redis error",s)})}async setup(){if(this.client.status==="ready"){M.debug("[setup] Redis already connected, skipping");return}this.client.status==="wait"&&await this.client.connect(),M.info("[setup] Repository connected",{status:this.client.status})}async teardown(){this.client.status!=="end"&&(await this.client.quit(),M.info("[teardown] Repository disconnected"))}get redis(){return this.client}getManifestKey(){return`${this.keyPrefix}:manifest`}getKey(t){return`${this.keyPrefix}:message:${t}`}getStateKey(t){return`${this.keyPrefix}:index:state:${t.toLowerCase()}`}getStatusKey(t){return`${this.keyPrefix}:index:status:${t.toLowerCase()}`}getContactKey(t){return`${this.keyPrefix}:index:contact:${t}`}getDescriptorKey(t){return`${this.keyPrefix}:index:descriptor:${t}`}getQueueKey(t){return`${this.keyPrefix}:queue:${t.toLowerCase()}`}async upsertMessage(t,e){let s=this.getKey(t.messageId),i=JSON.stringify(t),r=this.client.pipeline();r.set(s,i),t.contactId&&r.sadd(this.getContactKey(t.contactId),t.messageId),t.descriptorId&&r.sadd(this.getDescriptorKey(t.descriptorId),t.messageId);for(let n of I.INDEXED_STATUSES){let o=this.getStatusKey(n);t.status===n?r.sadd(o,t.messageId):r.srem(o,t.messageId)}for(let n of I.INDEXED_STATES){let o=this.getStateKey(n);t.state===n?r.sadd(o,t.messageId):r.srem(o,t.messageId)}let a=Date.now()+(e||36e5*24*2);if(t.state==="SCHEDULED"&&t.scheduledTo&&(a=new Date(t.scheduledTo).getTime()+(e||0)),r.zadd(this.getQueueKey("retention"),a,t.messageId),t.state==="SCHEDULED"&&t.scheduledTo){let n=new Date(t.scheduledTo).getTime();r.zadd(this.getQueueKey("scheduled"),n,t.messageId)}else r.zrem(this.getQueueKey("scheduled"),t.messageId);if(t.state==="QUEUED"){let n=new Date(t.createdAt||Date.now()).getTime();r.zadd(this.getQueueKey("queued"),n,t.messageId)}else r.zrem(this.getQueueKey("queued"),t.messageId);if(t.state==="DISPATCHED"){let n=new Date(t.createdAt||Date.now()).getTime();r.zadd(this.getQueueKey("dispatched"),n,t.messageId)}else r.zrem(this.getQueueKey("dispatched"),t.messageId);await r.exec(),M.debug("[upsertMessage]",{messageId:t.messageId,status:t.status,state:t.state})}async getMessage(t){let e=this.getKey(t),s=await this.client.get(e);return s?JSON.parse(s):null}async getMessages(t){let e=[];if(t.state==="SCHEDULED"){let i=Date.now(),r=t.skip??0,a=t.size??0;a>0?e=await this.client.zrangebyscore(this.getQueueKey("scheduled"),0,i,"LIMIT",r,a):e=await this.client.zrangebyscore(this.getQueueKey("scheduled"),0,i)}else if(t.state==="QUEUED")e=await this.client.zrange(this.getQueueKey("queued"),t.skip??0,(t.skip??0)+(t.size?t.size-1:-1));else if(t.state==="DISPATCHED")e=await this.client.zrange(this.getQueueKey("dispatched"),t.skip??0,(t.skip??0)+(t.size?t.size-1:-1));else if(t.status){let i=await this.client.smembers(this.getStatusKey(t.status)),r=t.skip??0,a=t.size;e=a?i.slice(r,r+a):i.slice(r)}else if(t.state)try{let i=await this.client.smembers(this.getStateKey(t.state)),r=t.skip??0,a=t.size;e=a?i.slice(r,r+a):i.slice(r)}catch{return[]}else return M.warn("[getMessages] no filter provided"),[];let s=[];for(let i of e){let r=await this.getMessage(i);if(r){if(t.status&&r.status!==t.status||t.state&&r.state!==t.state)continue;s.push(r)}}return M.debug("[getMessages]",{count:s.length,filter:t}),s}async getQueueSize(){return await this.client.zcard(this.getQueueKey("dispatched"))}async evictOldest(t){if(t<=0)return 0;let e=await this.client.zpopmin(this.getQueueKey("dispatched"),t),s=0;for(let i=0;i<e.length;i+=2){let r=e[i];await this.deleteMessage(r),s++}return s}async getExpiredMessages(t=50){let e=Date.now();return await this.client.zrangebyscore(this.getQueueKey("expiration"),0,e,"LIMIT",0,t)}async getRetentionMessages(t=50){let e=Date.now();return await this.client.zrangebyscore(this.getQueueKey("retention"),0,e,"LIMIT",0,t)}async incrementMetric(t,e=1){let s=`${this.keyPrefix}:metrics:${t}`;return await this.client.incrby(s,e)}async getMetric(t){let e=`${this.keyPrefix}:metrics:${t}`,s=await this.client.get(e);return s?parseInt(s,10):0}async deleteMessage(t){let e=await this.getMessage(t);e&&await this.deleteMessageData(t,e)}async deleteMessageData(t,e){let s=this.client.pipeline();s.del(this.getKey(t)),s.zrem(this.getQueueKey("scheduled"),t),s.zrem(this.getQueueKey("queued"),t),s.zrem(this.getQueueKey("dispatched"),t),s.zrem(this.getQueueKey("expiration"),t),s.zrem(this.getQueueKey("retention"),t);for(let i of I.INDEXED_STATUSES)s.srem(this.getStatusKey(i),t);for(let i of I.INDEXED_STATES)s.srem(this.getStateKey(i),t);e.contactId&&s.srem(this.getContactKey(e.contactId),t),e.descriptorId&&s.srem(this.getDescriptorKey(e.descriptorId),t),await s.exec()}async countMessages(t){if(t.state==="SCHEDULED")return await this.client.zcard(this.getQueueKey("scheduled"));if(t.state==="QUEUED")return await this.client.zcard(this.getQueueKey("queued"));if(t.state==="DISPATCHED")return await this.client.zcard(this.getQueueKey("dispatched"));if(t.status)return await this.client.scard(this.getStatusKey(t.status));if(t.state)try{return await this.client.scard(this.getStateKey(t.state))}catch{return 0}return 0}async getMetrics(t){let e={cumulative:{dispatched:0,delivered:0,failed:0},queues:{queued:0,scheduled:0,dispatched:0},status:{}},s=async a=>{if(t){let n=this.getDescriptorKey(t);return(await this.client.sinter(a,n)).length}return await this.client.scard(a)};for(let a of I.INDEXED_STATUSES){let n=await s(this.getStatusKey(a));e.status[a]=n,a==="DELIVERED"&&(e.cumulative.delivered=n),a==="FAILED"&&(e.cumulative.failed=n)}let i=this.getStateKey("DISPATCHED"),r=await s(i);return e.cumulative.dispatched=r+e.cumulative.delivered+e.cumulative.failed,t?(e.queues.queued=await s(this.getStateKey("QUEUED")),e.queues.scheduled=await s(this.getStateKey("SCHEDULED")),e.queues.dispatched=r):(e.queues.queued=await this.client.zcard(this.getQueueKey("queued")),e.queues.scheduled=await this.client.zcard(this.getQueueKey("scheduled")),e.queues.dispatched=await this.client.zcard(this.getQueueKey("dispatched"))),e}async getDescriptors(){let t=this.getDescriptorKey("*"),e=`${this.keyPrefix}:index:descriptor:`,s=await this.client.keys(t),i=[];for(let r of s){let a=r.slice(e.length);if(a){let n=await this.client.scard(r);i.push({id:a,count:n})}}return i.sort((r,a)=>a.count-r.count),i}async writeManifest(t){let e=this.getManifestKey();await this.client.hset(e,{version:t.version,createdAt:t.createdAt,updatedAt:t.updatedAt}),M.info("[writeManifest] Manifest written",{key:e})}async getManifest(){let t=this.getManifestKey(),e=await this.client.hgetall(t);return!e||Object.keys(e).length===0?null:e}};I.INDEXED_STATUSES=["INIT","PENDING","SENDING","DELIVERED","READ","REPLIED","FAILED","CANCELED"],I.INDEXED_STATES=["INIT","DISPATCHED","SCHEDULED","QUEUED","FINAL"];var T=I;var B=m("StateMachine"),C=class{constructor(t,e,s){this.id=t;this.repository=e;this.emit=s}async transition(t,e,s,i){let r=t.state,a=t.status;r==="FINAL"&&e!=="FINAL"&&B.warn(`[transition] Attempting to move from FINAL back to ${e}`,{messageId:t.messageId}),t.state=e,t.status=s,i&&Object.assign(t,i);let n=new Date().toISOString();return s==="SENDING"&&!t.acceptedAt&&a!=="SENDING"&&(t.acceptedAt=n),s==="DELIVERED"&&!t.deliveredAt&&(t.deliveredAt=n),s==="READ"&&!t.readAt&&(t.readAt=n),s==="REPLIED"&&!t.repliedAt&&(t.repliedAt=n,t.readAt||(t.readAt=n)),s==="FAILED"&&!t.failedAt&&(t.failedAt=n),e==="DISPATCHED"&&!t.sentAt&&"PENDING",await this.repository.upsertMessage(t),a!==s&&(s==="REPLIED"&&a!=="READ"&&this.emit("read",t),this.emitStatusEvent(t,s)),e==="SCHEDULED"&&r!=="SCHEDULED"&&this.emit("scheduled",t),B.debug(`[transition] ${t.messageId} : ${r}/${a} -> ${e}/${s}`),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 Y=z(require("axios")),N=require("uuid");var y;(t=>{let c;(i=>{let e;(g=>(g.GET="get",g.SET="set",g.DELETE="delete",g.OBSERVE="observe",g.SUBSCRIBE="subscribe",g.MERGE="merge"))(e=i.Method||(i.Method={}));let s;(n=>(n.SUCCESS="success",n.FAILURE="failure"))(s=i.Status||(i.Status={}))})(c=t.Lime||(t.Lime={}))})(y||(y={}));var p=m("Blip"),k=class{constructor(t,e,s=3e4){let i=`https://${t}.http.msging.net`;this.client=Y.default.create({baseURL:i,timeout:s,headers:{"Content-Type":"application/json",Authorization:e}}),this.client.interceptors.response.use(r=>r,r=>{if(r.response){let a=r.response.data?.reason||{code:r.response.status,description:r.response.statusText||"Unknown error"};throw new D(a.description,a.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,N.v4)()};p.debug("[postCommand] payload",e);let i=(await this.client.post("/commands",e)).data;if(i.status!==y.Lime.Status.SUCCESS)throw p.error("[postCommand] failed",{method:e.method,uri:e.uri,status:i.status,reason:i.reason}),new D(i.reason?.description||"Command failed",i.reason?.code||0);return p.debug("[postCommand] succeeded",e.uri),i}async postMessage(t){let e={...t,id:t.id||(0,N.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 s={...e,identity:t},i={method:y.Lime.Method.MERGE,uri:"/contacts",type:"application/vnd.lime.contact+json",resource:s};await this.postCommand(i)}async sendMessage(t,e,s){p.info("[sendMessage] called with",{contactId:t,message:e,id:s});let i=s||(0,N.v4)(),r={id:i,to:t,type:e.type,content:e.content};return await this.postMessage(r),p.info("[sendMessage] sent",{contactId:t,messageId:i}),i}async getDispatchState(t,e){p.info("[getDispatchState] called with",{messageId:t,contactId:e});let s={method:y.Lime.Method.GET,uri:`/threads/${e}?$take=100`,to:"postmaster@msging.net"};try{let i=await this.postCommand(s);if(!i.resource||!i.resource.items||i.resource.items.length===0)return p.debug("[getDispatchState] no messages found in thread",{messageId:t,contactId:e}),null;let r=i.resource.items.find(n=>n.id===t);if(!r)return p.debug("[getDispatchState] message not found in recent thread",{messageId:t,contactId:e}),null;let a=r.status;return p.info("[getDispatchState] state retrieved",{messageId:t,contactId:e,state:a,reason:r.reason?.description}),{state:a,reason:r.reason?.description}}catch(i){if(i instanceof D&&i.code===67)return p.debug("[getDispatchState] resource not found",{messageId:t,contactId:e}),null;throw p.error("[getDispatchState] failed",{messageId:t,contactId:e,error:i}),i}}async getMessageAfter(t,e){p.info("[getMessageAfter] called with",{contactId:t,messageId:e});let s=e,i=0,r=10;for(;i<r;){let a={method:y.Lime.Method.GET,uri:`/threads/${t}?$skip=0&$take=1&$order=asc&messageId=${s}`,to:"postmaster@msging.net"};try{let n=await this.postCommand(a);if(!n.resource||!n.resource.items||n.resource.items.length===0)return p.debug("[getMessageAfter] no message found after",{contactId:t,messageId:s}),null;let o=n.resource.items[0];if(o.direction==="received")return p.info("[getMessageAfter] found received message",{contactId:t,messageId:s,nextMessageId:o.id}),o;p.debug("[getMessageAfter] skipping sent message",{contactId:t,messageId:o.id}),s=o.id,i++}catch(n){if(n instanceof D&&n.code===67)return p.debug("[getMessageAfter] resource not found",{contactId:t,messageId:s}),null;throw p.error("[getMessageAfter] failed",{contactId:t,messageId:s,error:n}),n}}return p.warn("[getMessageAfter] max traversal attempts reached",{contactId:t,startMessageId:e}),null}async sendEvent(t,e,s,i){p.info("[sendEvent] called with",{contactId:t,category:e,action:s,extras:i});let r={to:"postmaster@analytics.msging.net",method:y.Lime.Method.SET,type:"application/vnd.iris.eventTrack+json",uri:"/event-track",resource:{category:e,action:s,contact:{identity:t},extras:i}};await this.postCommand(r)}async setState(t,e,s="onboarding"){p.info("[setState] called with",{contactId:t,botId:e,stateId:s});let i={uri:`/flow-id?shortName=${e}`,to:"postmaster@builder.msging.net",method:y.Lime.Method.GET},r=await this.postCommand(i);if(!r.resource)throw p.error("[setState] flow ID not found",{botId:e}),new D(`Flow ID not found for bot: ${e}`,404);let a=r.resource,n={method:y.Lime.Method.SET,uri:`/contexts/${t}/stateid@${a}`,resource:s,type:"text/plain"};await this.postCommand(n);let o={method:y.Lime.Method.SET,uri:`/contexts/${t}/master-state`,resource:`${e}@msging.net`,type:"text/plain"};await this.postCommand(o)}},D=class c extends Error{constructor(t,e){super(t),this.name="BlipError",this.code=e,Object.setPrototypeOf(this,c.prototype)}};var dt=m("DispatcherQuery"),O=class{constructor(t){this.repository=t}get client(){return this.repository.redis}async query(t){let e=[];if(t.contactId&&e.push(this.repository.getContactKey(t.contactId)),t.descriptorId&&e.push(this.repository.getDescriptorKey(t.descriptorId)),t.status){let u=Array.isArray(t.status)?t.status:[t.status];u.length===1?e.push(this.repository.getStatusKey(u[0])):u.length>1&&e.push(this.repository.getStatusKey(u[0]))}if(t.state){let u=Array.isArray(t.state)?t.state:[t.state];u.length===1&&e.push(this.repository.getStateKey(u[0]))}let s=[];if(e.length>0)s=await this.client.sinter(e);else{let u=Object.values(b).map(l=>this.repository.getStatusKey(l));s=await this.client.sunion(u)}let i=t.skip??0,r=t.size??50,a=s.slice(i,i+r),n=[],o=[];for(let u of a){let l=await this.repository.getMessage(u);if(l){if(t.status&&!(Array.isArray(t.status)?t.status:[t.status]).includes(l.status)||t.state&&!(Array.isArray(t.state)?t.state:[t.state]).includes(l.state))continue;n.push(l)}else o.push(u)}return o.length>0&&this.cleanupIndices(o,t),n}async cleanupIndices(t,e){let s=this.client.pipeline();e.contactId&&s.srem(this.repository.getContactKey(e.contactId),t),e.descriptorId&&s.srem(this.repository.getDescriptorKey(e.descriptorId),t),e.status&&(Array.isArray(e.status)?e.status:[e.status]).forEach(r=>{s.srem(this.repository.getStatusKey(r),t)}),e.state&&(Array.isArray(e.state)?e.state:[e.state]).forEach(r=>{s.srem(this.repository.getStateKey(r),t)}),await s.exec(),dt.debug("[cleanupIndices] Removed expired IDs from checked indices",{count:t.length})}};var{version:pt}=W(),h=m("Dispatcher"),K=class{constructor(t,e,s,i){this.callbacks={};this.descriptors=new Map;this.isRunning=!1;this.setupCompleted=!1;this.setupPromise=null;this.timeoutMonitorRunning=!1;this.timeoutTimer=null;this.id=t,this.repository=i?.repository??new T(t,e),this.stateMachine=new C(this.id,this.repository,(r,a)=>{this.emit(r,a),this.descriptors.get(a.descriptorId)?.emit(r,a,this.api,this.id)}),this.api=new k(s.contract,s.key),this.queueName=`dispatcher-${this.id.replace(/:/g,"-")}`,this.maxRetries=i?.maxRetries??0,this.retryIntervals=i?.retryIntervals??[1*1e3,5*1e3,15*1e3],this.timeouts={pending:i?.timeouts?.pending??120*1e3,sending:i?.timeouts?.sending??120*1e3},this.retention=i?.retention??2880*60*1e3,this.pollingIntervals={scheduled:i?.pollingIntervals?.scheduled??30*1e3,pending:i?.pollingIntervals?.pending??10*1e3,sending:i?.pollingIntervals?.sending??10*1e3,delivered:i?.pollingIntervals?.delivered??1800*1e3,read:i?.pollingIntervals?.read??1800*1e3,queue:i?.pollingIntervals?.queue??1*1e3},this.query=new O(this.repository),this.queue=new q.Queue(this.queueName,{connection:this.repository.redis,defaultJobOptions:{removeOnComplete:!0,removeOnFail:!0}}),this.worker=new q.Worker(this.queueName,async r=>{try{await this.processJob(r)}catch(a){throw h.error(`[Worker] Job ${r.name} failed`,a),a}},{connection:this.repository.redis,concurrency:i?.batchSize||50,limiter:i?.rateLimits?.global?{max:i.rateLimits.global.points,duration:i.rateLimits.global.duration*1e3}:void 0}),this.worker.on("error",r=>h.error("[Worker] Error",r)),this.worker.on("failed",(r,a)=>h.error(`[Worker] Job ${r?.id} failed`,a))}get redis(){return this.repository.redis}async setup(){return this.setupPromise?this.setupPromise:(this.setupPromise=this._doSetup(),this.setupPromise)}async _doSetup(){await this.repository.setup();let t=await this.repository.getManifest();await this.repository.writeManifest({version:pt,createdAt:t?.createdAt??new Date().toISOString(),updatedAt:new Date().toISOString()}),await this.queue.waitUntilReady(),this.isRunning=!0,this.setupCompleted=!0,this.startTimeoutMonitor(),h.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(),h.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(A);for(let i of e)t.byState[i]=await this.repository.countMessages({state:i});let s=Object.values(b);for(let i of s)t.byStatus[i]=await this.repository.countMessages({status:i});return t.total=Object.values(t.byState).reduce((i,r)=>i+(r||0),0),t}emit(t,e){this.callbacks[t]?.(e,this.api,this.id)}async send(t,e,s,i){this.descriptors.set(t.id,t);let r=t.toContactId(e),a=t.transform(s),n=new Date().toISOString(),o={messageId:(0,X.v4)(),contactId:r,descriptorId:t.id,payload:a,status:"INIT",state:"INIT",createdAt:n,attempts:0,retries:this.maxRetries},l={...t.messageOptions,...i},{schedule:g,...w}=l;o.options=w,this.emit("dispatch",o),t.emit("dispatch",o,this.api,this.id);let d=this.calculateScheduledTime(g,l.shifts),f=0;if(d){o.scheduledTo=d,o.state="SCHEDULED";let E=new Date(d).getTime();f=Math.max(0,E-Date.now()),this.emit("scheduled",o),t.emit("scheduled",o,this.api,this.id),h.info("[send] message scheduled",{messageId:o.messageId,scheduledTo:d,delay:f})}else o.state="QUEUED",o.status="INIT",h.info("[send] message queued",{messageId:o.messageId});return o.expiresAt=new Date(Date.now()+(o.state==="SCHEDULED"?f+this.retention:this.retention)).toISOString(),await this.stateMachine.transition(o,o.state,o.status),await this.queue.add("send",{messageId:o.messageId},{jobId:o.messageId,delay:f,priority:1}),o}async cancel(t){let e=await this.repository.getMessage(t);if(!e)return h.warn("[cancel] message not found",{messageId:t}),!1;if(e.state==="FINAL")return h.warn("[cancel] message already final",{messageId:t,status:e.status}),!1;let s=await this.queue.getJob(t);return s&&(await s.remove(),h.info("[cancel] removed job from queue",{messageId:t})),await this.stateMachine.transition(e,"FINAL","CANCELED"),h.info("[cancel] message canceled",{messageId:t}),!0}async processJob(t){let{messageId:e}=t.data,s=await this.repository.getMessage(e);if(!s){h.warn(`[processJob] Message not found: ${e}`);return}let i=this.descriptors.get(s.descriptorId)||null;switch(t.name){case"send":await this.handleSendJob(s,i);break;case"check":await this.handleCheckJob(s,i);break;default:h.warn(`[processJob] Unknown job name: ${t.name}`)}}async handleSendJob(t,e){if(t.state==="FINAL"){h.warn("[handleSendJob] Message already final, skipping",{messageId:t.messageId});return}if(t.state==="DISPATCHED"){h.warn("[handleSendJob] Message already dispatched, scheduling check instead",{messageId:t.messageId}),await this.rescheduleCheck(t,0);return}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"),h.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(s){let i=s instanceof Error?s:new Error(String(s));await this.handleDispatchFailure(t,e,i)}}async handlePostSendOperations(t,e={}){let s={...e.contact||{}};if(e.intent)if(typeof e.intent=="string")s.intent=e.intent;else{s.intent=e.intent.intent;let{intent:i,...r}=e.intent;Object.entries(r).forEach(([a,n])=>{n!=null&&(s[a]=typeof n=="object"?JSON.stringify(n):String(n))})}Object.keys(s).length>0&&await this.api.mergeContact(t.contactId,s),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 s=await this.api.getDispatchState(t.messageId,t.contactId);if(!s){await this.rescheduleCheck(t,this.pollingIntervals.pending);return}let i=this.pollingIntervals.pending;switch(s.state){case"accepted":t.status!=="SENDING"&&await this.stateMachine.transition(t,t.state,"SENDING"),i=this.pollingIntervals.sending;break;case"received":case"consumed":i=await this.handleDeliveredState(t,s.state,i);break;case"failed":await this.handleDispatchFailure(t,e,new Error(s.reason??"Dispatch failed from Gateway"));return}t.state!=="FINAL"&&await this.rescheduleCheck(t,i)}catch(s){h.error("[handleCheckJob] Error",s),await this.rescheduleCheck(t,this.pollingIntervals.pending)}}}checkAndHandleTimeout(t){let e=new Date;if(t.status==="PENDING"){let s=t.lastDispatchAttemptAt||t.sentAt||t.createdAt;if(e.getTime()-new Date(s).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"}),h.info("[handleTimeout] Message timed out",{messageId:t.messageId})}startTimeoutMonitor(){this.timeoutTimer||(this.timeoutTimer=setInterval(()=>{this.timeoutMonitorRunning||(this.timeoutMonitorRunning=!0,this._runTimeoutMonitorCycle().finally(()=>{this.timeoutMonitorRunning=!1}))},1e4))}async _runTimeoutMonitorCycle(){try{let t=await this.repository.getMessages({status:"PENDING"}),e=await this.repository.getMessages({status:"SENDING"});for(let i of[...t,...e])if(this.checkAndHandleTimeout(i)){let r=await this.repository.getMessage(i.messageId);if(r&&r.state!=="FINAL"&&(r.status==="PENDING"||r.status==="SENDING")){let a=this.descriptors.get(r.descriptorId)||null;await this.handleTimeout(r,a)}}let s=await this.repository.getRetentionMessages(100);if(s.length>0){h.debug("[CleanupMonitor] Cleaning up expired messages",{count:s.length});for(let i of s)await this.repository.deleteMessage(i)}}catch(t){h.error("[TimeoutMonitor] Error during scan",t)}}async rescheduleCheck(t,e){await this.queue.add("check",{messageId:t.messageId},{delay:Math.max(0,e),priority:5})}async handleDeliveredState(t,e,s){if(await this.api.getMessageAfter(t.contactId,t.messageId))return t.status!=="REPLIED"&&t.status!=="READ"&&(await this.repository.incrementMetric("delivered"),await this.stateMachine.transition(t,"FINAL","REPLIED")),s;let r=e==="consumed"?"READ":"DELIVERED";t.status!==r&&(await this.repository.incrementMetric("delivered"),await this.stateMachine.transition(t,t.state,r));let a=t.options?.finalStatus||"DELIVERED";if(this.getStatusRank(t.status)>=this.getStatusRank(a))await this.stateMachine.transition(t,"FINAL",t.status);else return this.pollingIntervals.delivered;return s}async handleDispatchFailure(t,e,s){if(t.attempts=(t.attempts??0)+1,t.error=s.message,h.error("[handleDispatchFailure]",{messageId:t.messageId,attempts:t.attempts,maxRetries:this.maxRetries,error:s.message}),t.attempts<=this.maxRetries){t.retries=this.maxRetries-t.attempts;let i=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},{jobId:t.messageId,delay:i,priority:1}),h.info("[handleDispatchFailure] Rescheduled retry",{messageId:t.messageId,retryDelay:i})}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 s=new Date;return this.isWithinShifts(s,e)?void 0:this.findNextShiftTime(s,e)?.toISOString()}isWithinShifts(t,e){let s=t.getDay(),i=s===0?64:Math.pow(2,s-1);for(let r of e){if((r.days&i)===0)continue;let a=r.gmt||"-3",n=parseInt(a,10),o=new Date(t.getTime()-n*60*60*1e3),u=o.getHours()*60+o.getMinutes(),[l,g]=r.start.split(":").map(Number),[w,d]=r.end.split(":").map(Number),f=l*60+g,E=w*60+d;if(u>=f&&u<E)return!0}return!1}findNextShiftTime(t,e){for(let i=0;i<=7;i++){let r=new Date(t);r.setDate(r.getDate()+i);let a=r.getDay(),n=a===0?64:Math.pow(2,a-1),o=e.filter(u=>(u.days&n)!==0);if(o.length!==0){o.sort((u,l)=>{let[g,w]=u.start.split(":").map(Number),[d,f]=l.start.split(":").map(Number);return g*60+w-(d*60+f)});for(let u of o){let l=u.gmt||"-3",g=parseInt(l,10),[w,d]=u.start.split(":").map(Number),f=new Date(r);f.setHours(w,d,0,0);let E=new Date(f.getTime()+g*60*60*1e3);if(i===0){if(E>t)return E}else return E}}}}getStatusRank(t){return{INIT:0,PENDING:1,SENDING:2,DELIVERED:3,READ:4,REPLIED:5,FAILED:6,CANCELED:6}[t]||0}static sanitizeContactId(t,e){if(t.includes("@"))return t;let s={BLIP_CHAT:"@0mn.io",EMAIL:"@mailgun.gw.msging.net",MESSENGER:"@messenger.gw.msging.net",SKYPE:"@skype.gw.msging.net",SMS_TAKE:"@take.io",SMS_TANGRAM:"@tangram.com.br",TELEGRAM:"@telegram.gw.msging.net",WHATSAPP:"@wa.gw.msging.net",INSTAGRAM:"@instagram.gw.msging.net",GOOGLE_RCS:"@googlercs.gw.msging.net",MICROSOFT_TEAMS:"@abs.gw.msging.net",APPLE_BUSINESS_CHAT:"@businesschat.gw.msging.net",WORKPLACE:"@workplace.gw.msging.net"};if(!s[e])throw new Error(`Unknown channel: ${e}`);return e==="WHATSAPP"&&t.startsWith("+")&&(t=t.slice(1)),`${t}${s[e]}`}};var Z=require("events"),F=require("bullmq");var S=m("DispatcherMonitor"),$=class extends Z.EventEmitter{constructor(e,s){super();this.history=[];this.lastAlerts={};this.activeAlerts=new Set;this.isRunning=!1;this.id=e.id,this.dispatcher=e,this.options={interval:6e4,historySize:1e3,...s},this.queueName=`monitor-${this.id}`,this.queue=new F.Queue(this.queueName,{connection:e.redis,defaultJobOptions:{removeOnComplete:!0,removeOnFail:!0}}),this.worker=new F.Worker(this.queueName,async i=>{i.name==="check"&&await this.check()},{connection:e.redis}),this.worker.on("error",i=>S.error("[MonitorWorker] Error",i)),this.worker.on("failed",(i,r)=>S.error(`[MonitorWorker] Job ${i?.id} failed`,r))}async start(){this.isRunning||(S.info("[Monitor] Started"),await this.queue.obliterate({force:!0}),await this.queue.add("check",{},{repeat:{every:this.options.interval,immediately:!0}}),this.isRunning=!0)}async stop(){this.isRunning=!1,await this.queue.close(),await this.worker.close(),S.info("[Monitor] Stopped")}async check(){try{let e=await this.dispatcher.getMetrics(),s=Date.now();this.history.push({timestamp:s,metrics:e}),this.cleanHistory();for(let i of this.options.rules)await this.evaluateRule(i,e,s)}catch(e){S.error("[Monitor] Error during check",e)}}cleanHistory(){let e=this.options.historySize;this.history.length>e&&(this.history=this.history.slice(this.history.length-e));let s=Math.max(...this.options.rules.map(r=>r.window||0)),i=Date.now()-s-6e4;if(this.history.length>0&&this.history[0].timestamp<i){let r=this.history.findIndex(a=>a.timestamp>=i);r>0&&(this.history=this.history.slice(r))}}async evaluateRule(e,s,i){let r=`${e.type}`,a=!1,n=0,o={};switch(e.type){case"queue_size":let u=s.byState.FINAL||0;n=s.total-u,a=n>e.threshold,o={current:n,threshold:e.threshold};break;case"failure_rate":if(!e.window){S.warn("[Monitor] failure_rate rule missing window");return}let l=this.findSnapshotAt(i-e.window);if(!l)return;let g=s.cumulative.failed,w=l.metrics.cumulative.failed,d=g-w,f=s.cumulative.dispatched,E=l.metrics.cumulative.dispatched,U=f-E;U===0?n=0:n=d/U,a=n>e.threshold,o={rate:(n*100).toFixed(2)+"%",threshold:(e.threshold*100).toFixed(2)+"%",failed:d,dispatched:U,window:e.window};break}a?this.activeAlerts.has(r)?e.debounce&&!this.isDebounced(r,e.debounce)&&this.emitAlert(r,e,n,o):(this.emitAlert(r,e,n,o),this.activeAlerts.add(r)):this.activeAlerts.has(r)&&(this.resolveAlert(r,e),this.activeAlerts.delete(r))}isDebounced(e,s){if(!s)return!1;let i=this.lastAlerts[e];return i?Date.now()-i<s:!1}emitAlert(e,s,i,r){S.warn(`[Monitor] Alert triggered: ${s.type}`,r),this.lastAlerts[e]=Date.now();let a={type:s.type,message:`${s.type} exceeded threshold`,level:"warning",details:r,timestamp:new Date().toISOString()};this.emit("alert",a)}resolveAlert(e,s){S.info(`[Monitor] Alert resolved: ${s.type}`);let i={type:s.type,message:`${s.type} resolved`,level:"warning",details:{},timestamp:new Date().toISOString()};this.emit("resolved",i)}findSnapshotAt(e){if(this.history.length===0)return null;for(let s of this.history)if(s.timestamp>=e)return s;return this.history[0]}};0&&(module.exports={Blip,BlipError,Channel,DispatchState,Dispatcher,DispatcherDescriptor,DispatcherMonitor,DispatcherRepository,MessageState,MessageStatus,Vnd,Weekdays,enableLogger,getLogger});
|
|
2
2
|
//# sourceMappingURL=index.js.map
|