@dawntech/dispatcher 0.2.7 → 0.2.8

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 CHANGED
@@ -21,7 +21,7 @@ declare namespace Vnd {
21
21
  }
22
22
  namespace Transport {
23
23
  namespace Payload {
24
- interface Request<T = any> {
24
+ interface Request<T = unknown> {
25
25
  id?: string;
26
26
  to?: string;
27
27
  method: Method;
@@ -29,7 +29,7 @@ declare namespace Vnd {
29
29
  type?: string;
30
30
  resource?: T;
31
31
  }
32
- interface Response<T = any> {
32
+ interface Response<T = unknown> {
33
33
  id?: string;
34
34
  from?: string;
35
35
  to?: string;
@@ -44,7 +44,7 @@ declare namespace Vnd {
44
44
  }
45
45
  }
46
46
  }
47
- interface Message<T = any> {
47
+ interface Message<T = unknown> {
48
48
  id?: string;
49
49
  to: string;
50
50
  from?: string;
@@ -52,7 +52,7 @@ declare namespace Vnd {
52
52
  content: T;
53
53
  }
54
54
  namespace Command {
55
- interface Resource<T = any> {
55
+ interface Resource<T = unknown> {
56
56
  [key: string]: T;
57
57
  }
58
58
  }
@@ -62,10 +62,10 @@ declare namespace Vnd {
62
62
  id: string;
63
63
  direction: 'sent' | 'received';
64
64
  type: string;
65
- content: any;
65
+ content: unknown;
66
66
  date: string;
67
67
  status?: string;
68
- [key: string]: any;
68
+ [key: string]: unknown;
69
69
  }
70
70
  interface Contact {
71
71
  identity: string;
@@ -76,7 +76,7 @@ declare namespace Vnd {
76
76
  gender?: string;
77
77
  city?: string;
78
78
  extras?: Record<string, string>;
79
- [key: string]: any;
79
+ [key: string]: unknown;
80
80
  }
81
81
  interface EventTrack {
82
82
  category: string;
@@ -84,7 +84,7 @@ declare namespace Vnd {
84
84
  contact?: {
85
85
  identity: string;
86
86
  };
87
- extras?: any;
87
+ extras?: unknown;
88
88
  count?: number;
89
89
  }
90
90
  interface Ticket {
@@ -101,7 +101,7 @@ declare namespace Vnd {
101
101
  agentIdentity?: string;
102
102
  closedDate?: string;
103
103
  transfers?: number;
104
- [key: string]: any;
104
+ [key: string]: unknown;
105
105
  }
106
106
  namespace ActiveCampaign {
107
107
  interface AudienceSummary {
@@ -130,9 +130,9 @@ type PartiallyRequiredStrict<T, K extends keyof T> = T & Required<Pick<T, K>>;
130
130
  type Contact = Omit<Vnd.Iris.Contact, 'identity'>;
131
131
  type MessageData = {
132
132
  type: string;
133
- content: Record<string, any> | string;
133
+ content: Record<string, unknown> | string;
134
134
  };
135
- type MessagePayload = Record<string, any> | Array<any> | string;
135
+ type MessagePayload = Record<string, unknown> | Array<unknown> | string;
136
136
  declare enum MessageState {
137
137
  INIT = "INIT",
138
138
  DISPATCHED = "DISPATCHED",
@@ -185,6 +185,21 @@ interface Shift {
185
185
  end: string;
186
186
  gmt?: string;
187
187
  }
188
+ declare enum Channel {
189
+ BLIP_CHAT = "BLIP_CHAT",
190
+ EMAIL = "EMAIL",
191
+ MESSENGER = "MESSENGER",
192
+ SKYPE = "SKYPE",
193
+ SMS_TAKE = "SMS_TAKE",
194
+ SMS_TANGRAM = "SMS_TANGRAM",
195
+ TELEGRAM = "TELEGRAM",
196
+ WHATSAPP = "WHATSAPP",
197
+ INSTAGRAM = "INSTAGRAM",
198
+ GOOGLE_RCS = "GOOGLE_RCS",
199
+ MICROSOFT_TEAMS = "MICROSOFT_TEAMS",
200
+ APPLE_BUSINESS_CHAT = "APPLE_BUSINESS_CHAT",
201
+ WORKPLACE = "WORKPLACE"
202
+ }
188
203
  declare enum Weekdays {
189
204
  MONDAY = 1,
190
205
  TUESDAY = 2,
@@ -198,7 +213,7 @@ interface Intent {
198
213
  intent: string;
199
214
  dueDate?: string;
200
215
  event?: string;
201
- payload?: any | null;
216
+ payload?: unknown;
202
217
  expired?: {
203
218
  event?: string;
204
219
  intent?: string;
@@ -418,7 +433,19 @@ declare class DispatcherRepository {
418
433
  status?: MessageStatus;
419
434
  state?: MessageState;
420
435
  }): Promise<number>;
421
- getMetrics(descriptorId?: string): Promise<any>;
436
+ getMetrics(descriptorId?: string): Promise<{
437
+ cumulative: {
438
+ dispatched: number;
439
+ delivered: number;
440
+ failed: number;
441
+ };
442
+ queues: {
443
+ queued: number;
444
+ scheduled: number;
445
+ dispatched: number;
446
+ };
447
+ status: Record<string, number>;
448
+ }>;
422
449
  getDescriptors(): Promise<{
423
450
  id: string;
424
451
  count: number;
@@ -450,13 +477,16 @@ declare class Dispatcher {
450
477
  private retryIntervals;
451
478
  private timeouts;
452
479
  private retention;
453
- private timeoutTimer;
454
480
  private pollingIntervals;
455
481
  private isRunning;
456
482
  private setupCompleted;
483
+ private setupPromise;
484
+ private timeoutMonitorRunning;
485
+ private timeoutTimer;
457
486
  readonly query: DispatcherQuery;
458
487
  constructor(id: string, repository: DispatcherRepository, connection: ConnectionConfig, options?: DispatcherOptions);
459
488
  setup(): Promise<void>;
489
+ private _doSetup;
460
490
  teardown(): Promise<void>;
461
491
  on(event: CallbackEvent, callback: (message: Message, api: Blip, dispatcherId: string) => void): this;
462
492
  getMetrics(): Promise<DispatcherMetrics>;
@@ -470,12 +500,15 @@ declare class Dispatcher {
470
500
  private checkAndHandleTimeout;
471
501
  private handleTimeout;
472
502
  private startTimeoutMonitor;
503
+ private _runTimeoutMonitorCycle;
473
504
  private rescheduleCheck;
505
+ private handleDeliveredState;
474
506
  private handleDispatchFailure;
475
507
  private calculateScheduledTime;
476
508
  private isWithinShifts;
477
509
  private findNextShiftTime;
478
510
  private getStatusRank;
511
+ static sanitizeContactId(id: string, source: Channel): string;
479
512
  }
480
513
 
481
514
  interface MonitorRule {
@@ -493,7 +526,7 @@ type MonitorAlert = {
493
526
  type: string;
494
527
  message: string;
495
528
  level: 'warning' | 'critical';
496
- details: any;
529
+ details: Record<string, unknown>;
497
530
  timestamp: string;
498
531
  };
499
532
  declare class DispatcherMonitor extends EventEmitter {
@@ -523,7 +556,7 @@ declare class DispatcherMonitor extends EventEmitter {
523
556
  private findSnapshotAt;
524
557
  }
525
558
 
526
- type LogFunction = (...args: any[]) => void;
559
+ type LogFunction = (...args: unknown[]) => void;
527
560
  interface Logger {
528
561
  debug: LogFunction;
529
562
  info: LogFunction;
@@ -547,4 +580,4 @@ declare function getLogger(scope: string): Logger;
547
580
  */
548
581
  declare function enableLogger(namespaces: string): void;
549
582
 
550
- export { Blip, type BlipConfig, BlipError, type CallbackEvent, type CallbackMap, type ConnectionConfig, type Contact, type DateInterval, type DispatchMessageOptions, DispatchState, Dispatcher, DispatcherDescriptor, type DispatcherDescriptorOptions, type DispatcherManifest, type DispatcherMetrics, DispatcherMonitor, type DispatcherOptions, DispatcherRepository, type Intent, type Message, type MessageData, type MessageDispatcherOptions, type MessageOptions, type MessagePayload, MessageState, MessageStatus, type MonitorAlert, type MonitorOptions, type MonitorRule, type PartiallyOptional, type PartiallyRequiredStrict, type QueryFilter, type Shift, Vnd, Weekdays, enableLogger, getLogger };
583
+ export { Blip, type BlipConfig, BlipError, type CallbackEvent, type CallbackMap, Channel, type ConnectionConfig, type Contact, type DateInterval, type DispatchMessageOptions, DispatchState, Dispatcher, DispatcherDescriptor, type DispatcherDescriptorOptions, type DispatcherManifest, type DispatcherMetrics, DispatcherMonitor, type DispatcherOptions, DispatcherRepository, type Intent, type Message, type MessageData, type MessageDispatcherOptions, type MessageOptions, type MessagePayload, MessageState, MessageStatus, type MonitorAlert, type MonitorOptions, type MonitorRule, type PartiallyOptional, type PartiallyRequiredStrict, type QueryFilter, type Shift, Vnd, Weekdays, enableLogger, getLogger };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";var Z=Object.create;var T=Object.defineProperty;var tt=Object.getOwnPropertyDescriptor;var et=Object.getOwnPropertyNames;var st=Object.getPrototypeOf,it=Object.prototype.hasOwnProperty;var rt=(c,t)=>()=>(t||c((t={exports:{}}).exports,t),t.exports),nt=(c,t)=>{for(var e in t)T(c,e,{get:t[e],enumerable:!0})},V=(c,t,e,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of et(t))!it.call(c,i)&&i!==e&&T(c,i,{get:()=>t[i],enumerable:!(s=tt(t,i))||s.enumerable});return c};var Q=(c,t,e)=>(e=c!=null?Z(st(c)):{},V(t||!c||!c.__esModule?T(e,"default",{value:c,enumerable:!0}):e,c)),at=c=>V(T({},"__esModule",{value:!0}),c);var X=rt((Nt,dt)=>{dt.exports={name:"@dawntech/dispatcher",version:"0.2.7",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: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 pt={};nt(pt,{Blip:()=>x,BlipError:()=>w,DispatchState:()=>C,Dispatcher:()=>K,DispatcherDescriptor:()=>k,DispatcherMonitor:()=>$,DispatcherRepository:()=>z,MessageState:()=>v,MessageStatus:()=>b,Vnd:()=>f,Weekdays:()=>j,enableLogger:()=>G,getLogger:()=>g});module.exports=at(pt);var k=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 Y=require("uuid"),q=require("bullmq");var v=(r=>(r.INIT="INIT",r.DISPATCHED="DISPATCHED",r.SCHEDULED="SCHEDULED",r.QUEUED="QUEUED",r.FINAL="FINAL",r))(v||{}),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||{}),C=(r=>(r.ACCEPTED="accepted",r.DISPATCHED="dispatched",r.RECEIVED="received",r.CONSUMED="consumed",r.FAILED="failed",r))(C||{}),j=(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))(j||{});var A=Q(require("debug")),P={debug:"debug",info:"info",warn:"warn",error:"error"},H=new Map;function ot(c){if(c==null)return c;if(typeof c=="object")try{return JSON.stringify(c,null,2)}catch{return c}return c}function R(c){return(...t)=>{let e=t.map(ot);c(...e)}}function g(c){if(!H.has(c)){let t=(0,A.default)(`${c}:${P.debug}`),e=(0,A.default)(`${c}:${P.info}`),s=(0,A.default)(`${c}:${P.warn}`),i=(0,A.default)(`${c}:${P.error}`);i.log=console.error.bind(console);let r={debug:R(t),info:R(e),warn:R(s),error:R(i)};H.set(c,r)}return H.get(c)}function G(c){let t=A.default.disable();A.default.enable(`${t},${c}`)}var J=g("StateMachine"),L=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"&&J.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),J.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 B=Q(require("axios")),N=require("uuid");var f;(t=>{let c;(i=>{let e;(u=>(u.GET="get",u.SET="set",u.DELETE="delete",u.OBSERVE="observe",u.SUBSCRIBE="subscribe",u.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={}))})(f||(f={}));var l=g("Blip"),x=class{constructor(t,e,s=3e4){let i=`https://${t}.http.msging.net`;this.client=B.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 w(a.description,a.code)}else throw r.request?new w("No response from server",0):new w(r.message,0)})}async postCommand(t){let e={...t,id:t.id||(0,N.v4)()};l.debug("[postCommand] payload",e);let i=(await this.client.post("/commands",e)).data;if(i.status!==f.Lime.Status.SUCCESS)throw l.error("[postCommand] failed",{method:e.method,uri:e.uri,status:i.status,reason:i.reason}),new w(i.reason?.description||"Command failed",i.reason?.code||0);return l.debug("[postCommand] succeeded",e.uri),i}async postMessage(t){let e={...t,id:t.id||(0,N.v4)()};return l.info("[postMessage] payload",e),(await this.client.post("/messages",e)).data}async mergeContact(t,e){l.info("[mergeContact] called with",{contactId:t,data:e});let s={...e,identity:t},i={method:f.Lime.Method.MERGE,uri:"/contacts",type:"application/vnd.lime.contact+json",resource:s};await this.postCommand(i)}async sendMessage(t,e,s){l.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),l.info("[sendMessage] sent",{contactId:t,messageId:i}),i}async getDispatchState(t,e){l.info("[getDispatchState] called with",{messageId:t,contactId:e});let s={method:f.Lime.Method.GET,uri:`/notifications?id=${t}`,to:"postmaster@msging.net"};try{let i=await this.postCommand(s);if(!i.resource||!i.resource.items||i.resource.items.length===0)return l.debug("[getDispatchState] no notifications found",{messageId:t,contactId:e}),null;let r={failed:4,consumed:3,received:2,accepted:1,dispatched:0},a=null,n=-1;for(let o of i.resource.items){let p=o.event,d=r[p]??-1;d>n&&(n=d,a=p)}return l.info("[getDispatchState] state retrieved",{messageId:t,contactId:e,state:a,notificationsCount:i.resource.items.length}),a}catch(i){if(i instanceof w&&i.code===67)return l.debug("[getDispatchState] resource not found",{messageId:t,contactId:e}),null;throw l.error("[getDispatchState] failed",{messageId:t,contactId:e,error:i}),i}}async getMessageAfter(t,e){l.info("[getMessageAfter] called with",{contactId:t,messageId:e});let s=e,i=0,r=10;for(;i<r;){let a={method:f.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 l.debug("[getMessageAfter] no message found after",{contactId:t,messageId:s}),null;let o=n.resource.items[0];if(o.direction==="received")return l.info("[getMessageAfter] found received message",{contactId:t,messageId:s,nextMessageId:o.id}),o;l.debug("[getMessageAfter] skipping sent message",{contactId:t,messageId:o.id}),s=o.id,i++}catch(n){if(n instanceof w&&n.code===67)return l.debug("[getMessageAfter] resource not found",{contactId:t,messageId:s}),null;throw l.error("[getMessageAfter] failed",{contactId:t,messageId:s,error:n}),n}}return l.warn("[getMessageAfter] max traversal attempts reached",{contactId:t,startMessageId:e}),null}async sendEvent(t,e,s,i){l.info("[sendEvent] called with",{contactId:t,category:e,action:s,extras:i});let r={to:"postmaster@analytics.msging.net",method:f.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"){l.info("[setState] called with",{contactId:t,botId:e,stateId:s});let i={uri:`/flow-id?shortName=${e}`,to:"postmaster@builder.msging.net",method:f.Lime.Method.GET},r=await this.postCommand(i);if(!r.resource)throw l.error("[setState] flow ID not found",{botId:e}),new w(`Flow ID not found for bot: ${e}`,404);let a=r.resource,n={method:f.Lime.Method.SET,uri:`/contexts/${t}/stateid@${a}`,resource:s,type:"text/plain"};await this.postCommand(n);let o={method:f.Lime.Method.SET,uri:`/contexts/${t}/master-state`,resource:`${e}@msging.net`,type:"text/plain"};await this.postCommand(o)}},w=class c extends Error{constructor(t,e){super(t),this.name="BlipError",this.code=e,Object.setPrototypeOf(this,c.prototype)}};var ct=g("DispatcherQuery"),O=class{constructor(t){this.repository=t}get client(){return this.repository.redis}async query(t){let e=[],s=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 i=[];if(e.length>0)i=await this.client.sinter(e);else{let d=Object.values(b).map(u=>this.repository.getStatusKey(u));i=await this.client.sunion(d)}let r=t.skip??0,a=t.size??50,n=i.slice(r,r+a),o=[],p=[];for(let d of n){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;o.push(u)}else p.push(d)}return p.length>0&&this.cleanupIndices(p,t),o}async cleanupIndices(t,e){let s=this.client.pipeline(),i=this.repository.keyPrefix;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(a=>{s.srem(this.repository.getStatusKey(a),t)}),e.state&&(Array.isArray(e.state)?e.state:[e.state]).forEach(a=>{s.srem(this.repository.getStateKey(a),t)}),await s.exec(),ct.debug("[cleanupIndices] Removed expired IDs from checked indices",{count:t.length})}};var{version:ut}=X(),h=g("Dispatcher"),K=class{constructor(t,e,s,i){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 L(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 x(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.timeoutTimer=null,this.query=new O(this.repository),this.queue=new q.Queue(this.queueName,{connection:this.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.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))}async setup(){if(this.setupCompleted)return;await this.repository.setup();let t=await this.repository.getManifest();await this.repository.writeManifest({version:ut,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(v);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,Y.v4)(),contactId:r,descriptorId:t.id,payload:a,status:"INIT",state:"INIT",createdAt:n,attempts:0,retries:this.maxRetries},d={...t.messageOptions,...i},{schedule:u,...D}=d;o.options=D,this.emit("dispatch",o),t.emit("dispatch",o,this.api,this.id);let m=this.calculateScheduledTime(u,d.shifts),y=0;if(m){o.scheduledTo=m,o.state="SCHEDULED";let E=new Date(m).getTime();y=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:m,delay:y})}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"?y+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:y,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){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,r=!1;switch(s){case"accepted":t.status!=="SENDING"&&(await this.stateMachine.transition(t,t.state,"SENDING"),r=!0),i=this.pollingIntervals.sending;break;case"received":case"consumed":if(await this.api.getMessageAfter(t.contactId,t.messageId)){let p="REPLIED";t.status!==p&&t.status!=="READ"&&(await this.repository.incrementMetric("delivered"),await this.stateMachine.transition(t,"FINAL","REPLIED"),r=!0);break}let n=s==="consumed"?"READ":"DELIVERED";t.status!==n&&(await this.repository.incrementMetric("delivered"),await this.stateMachine.transition(t,t.state,n),r=!0);let o=t.options?.finalStatus||"DELIVERED";this.getStatusRank(t.status)>=this.getStatusRank(o)?(await this.stateMachine.transition(t,"FINAL",t.status),r=!0):i=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,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(async()=>{try{let t=await this.repository.getMessages({status:"PENDING"}),e=await this.repository.getMessages({status:"SENDING"}),s=[...t,...e];for(let r of s)if(this.checkAndHandleTimeout(r)){let a=this.descriptors.get(r.descriptorId)||null;await this.handleTimeout(r,a)}let i=await this.repository.getRetentionMessages(100);if(i.length>0){h.debug("[CleanupMonitor] Cleaning up expired messages",{count:i.length});for(let r of i)await this.repository.deleteMessage(r)}}catch(t){h.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,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},{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),p=o.getHours()*60+o.getMinutes(),[d,u]=r.start.split(":").map(Number),[D,m]=r.end.split(":").map(Number),y=d*60+u,E=D*60+m;if(p>=y&&p<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(p=>(p.days&n)!==0);if(o.length!==0){o.sort((p,d)=>{let[u,D]=p.start.split(":").map(Number),[m,y]=d.start.split(":").map(Number);return u*60+D-(m*60+y)});for(let p of o){let d=p.gmt||"-3",u=parseInt(d,10),[D,m]=p.start.split(":").map(Number),y=new Date(r);y.setHours(D,m,0,0);let E=new Date(y.getTime()+u*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}};var _=require("events"),F=require("bullmq");var M=g("DispatcherMonitor"),$=class extends _.EventEmitter{constructor(e,s,i){super();this.history=[];this.lastAlerts={};this.activeAlerts=new Set;this.isRunning=!1;this.id=e,this.repository=s,this.options={interval:6e4,historySize:1e3,...i},this.queueName=`monitor-${this.id}`,this.queue=new F.Queue(this.queueName,{connection:s.redis,defaultJobOptions:{removeOnComplete:!0,removeOnFail:!0}}),this.worker=new F.Worker(this.queueName,async r=>{r.name==="check"&&await this.check()},{connection:s.redis}),this.worker.on("error",r=>M.error("[MonitorWorker] Error",r)),this.worker.on("failed",(r,a)=>M.error(`[MonitorWorker] Job ${r?.id} failed`,a))}async start(){this.isRunning||(M.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(),M.info("[Monitor] Stopped")}async collectMetrics(){let e=this.repository,s={total:0,byState:{},byStatus:{},cumulative:{dispatched:0,delivered:0,failed:0}},r=Object.values(v).map(async d=>{s.byState[d]=await e.countMessages({state:d})}),n=Object.values(b).map(async d=>{s.byStatus[d]=await e.countMessages({status:d})}),p=["dispatched","delivered","failed"].map(async d=>{s.cumulative[d]=await e.getMetric(d)});return await Promise.all([...r,...n,...p]),s.total=Object.values(s.byState).reduce((d,u)=>d+(u||0),0),s}async check(){try{let e=await this.collectMetrics(),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){M.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 p=s.byState.FINAL||0;n=s.total-p,a=n>e.threshold,o={current:n,threshold:e.threshold};break;case"failure_rate":if(!e.window){M.warn("[Monitor] failure_rate rule missing window");return}let d=this.findSnapshotAt(i-e.window);if(!d)return;let u=s.cumulative.failed,D=d.metrics.cumulative.failed,m=u-D,y=s.cumulative.dispatched,E=d.metrics.cumulative.dispatched,U=y-E;U===0?n=0:n=m/U,a=n>e.threshold,o={rate:(n*100).toFixed(2)+"%",threshold:(e.threshold*100).toFixed(2)+"%",failed:m,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){M.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){M.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]}};var W=Q(require("ioredis")),S=g("Repository"),I=class I{constructor(t,e){this.client=new W.default(e,{maxRetriesPerRequest:null}),this.keyPrefix=`dwn-dispatcher:${t}`,this.client.on("error",s=>{S.error("[client] Redis error",s)})}async setup(){if(this.client.status==="ready"){S.debug("[setup] Redis already connected, skipping");return}this.client.status==="wait"&&await this.client.connect(),S.info("[setup] Repository connected",{status:this.client.status})}async teardown(){this.client.status!=="end"&&(await this.client.quit(),S.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(),S.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 S.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 S.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}),S.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 z=I;0&&(module.exports={Blip,BlipError,DispatchState,Dispatcher,DispatcherDescriptor,DispatcherMonitor,DispatcherRepository,MessageState,MessageStatus,Vnd,Weekdays,enableLogger,getLogger});
1
+ "use strict";var tt=Object.create;var k=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)k(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&&k(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?k(e,"default",{value:c,enumerable:!0}):e,c)),ot=c=>_(k({},"__esModule",{value:!0}),c);var Y=nt((Lt,ut)=>{ut.exports={name:"@dawntech/dispatcher",version:"0.2.8",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 lt={};at(lt,{Blip:()=>T,BlipError:()=>D,Channel:()=>H,DispatchState:()=>G,Dispatcher:()=>O,DispatcherDescriptor:()=>x,DispatcherMonitor:()=>q,DispatcherRepository:()=>F,MessageState:()=>M,MessageStatus:()=>w,Vnd:()=>f,Weekdays:()=>V,enableLogger:()=>j,getLogger:()=>m});module.exports=ot(lt);var x=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 W=require("uuid"),K=require("bullmq");var M=(r=>(r.INIT="INIT",r.DISPATCHED="DISPATCHED",r.SCHEDULED="SCHEDULED",r.QUEUED="QUEUED",r.FINAL="FINAL",r))(M||{}),w=(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))(w||{}),G=(r=>(r.ACCEPTED="accepted",r.DISPATCHED="dispatched",r.RECEIVED="received",r.CONSUMED="consumed",r.FAILED="failed",r))(G||{}),H=(p=>(p.BLIP_CHAT="BLIP_CHAT",p.EMAIL="EMAIL",p.MESSENGER="MESSENGER",p.SKYPE="SKYPE",p.SMS_TAKE="SMS_TAKE",p.SMS_TANGRAM="SMS_TANGRAM",p.TELEGRAM="TELEGRAM",p.WHATSAPP="WHATSAPP",p.INSTAGRAM="INSTAGRAM",p.GOOGLE_RCS="GOOGLE_RCS",p.MICROSOFT_TEAMS="MICROSOFT_TEAMS",p.APPLE_BUSINESS_CHAT="APPLE_BUSINESS_CHAT",p.WORKPLACE="WORKPLACE",p))(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 A=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 R(c){return(...t)=>{let e=t.map(ct);c(...e)}}function m(c){if(!Q.has(c)){let t=(0,A.default)(`${c}:${P.debug}`),e=(0,A.default)(`${c}:${P.info}`),s=(0,A.default)(`${c}:${P.warn}`),i=(0,A.default)(`${c}:${P.error}`);i.log=console.error.bind(console);let r={debug:R(t),info:R(e),warn:R(s),error:R(i)};Q.set(c,r)}return Q.get(c)}function j(c){let t=A.default.disable();A.default.enable(`${t},${c}`)}var J=m("StateMachine"),L=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"&&J.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),J.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 B=z(require("axios")),C=require("uuid");var f;(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={}))})(f||(f={}));var l=m("Blip"),T=class{constructor(t,e,s=3e4){let i=`https://${t}.http.msging.net`;this.client=B.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,C.v4)()};l.debug("[postCommand] payload",e);let i=(await this.client.post("/commands",e)).data;if(i.status!==f.Lime.Status.SUCCESS)throw l.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 l.debug("[postCommand] succeeded",e.uri),i}async postMessage(t){let e={...t,id:t.id||(0,C.v4)()};return l.info("[postMessage] payload",e),(await this.client.post("/messages",e)).data}async mergeContact(t,e){l.info("[mergeContact] called with",{contactId:t,data:e});let s={...e,identity:t},i={method:f.Lime.Method.MERGE,uri:"/contacts",type:"application/vnd.lime.contact+json",resource:s};await this.postCommand(i)}async sendMessage(t,e,s){l.info("[sendMessage] called with",{contactId:t,message:e,id:s});let i=s||(0,C.v4)(),r={id:i,to:t,type:e.type,content:e.content};return await this.postMessage(r),l.info("[sendMessage] sent",{contactId:t,messageId:i}),i}async getDispatchState(t,e){l.info("[getDispatchState] called with",{messageId:t,contactId:e});let s={method:f.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 l.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 l.debug("[getDispatchState] message not found in recent thread",{messageId:t,contactId:e}),null;let a=r.status;return l.info("[getDispatchState] state retrieved",{messageId:t,contactId:e,state:a}),a}catch(i){if(i instanceof D&&i.code===67)return l.debug("[getDispatchState] resource not found",{messageId:t,contactId:e}),null;throw l.error("[getDispatchState] failed",{messageId:t,contactId:e,error:i}),i}}async getMessageAfter(t,e){l.info("[getMessageAfter] called with",{contactId:t,messageId:e});let s=e,i=0,r=10;for(;i<r;){let a={method:f.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 l.debug("[getMessageAfter] no message found after",{contactId:t,messageId:s}),null;let o=n.resource.items[0];if(o.direction==="received")return l.info("[getMessageAfter] found received message",{contactId:t,messageId:s,nextMessageId:o.id}),o;l.debug("[getMessageAfter] skipping sent message",{contactId:t,messageId:o.id}),s=o.id,i++}catch(n){if(n instanceof D&&n.code===67)return l.debug("[getMessageAfter] resource not found",{contactId:t,messageId:s}),null;throw l.error("[getMessageAfter] failed",{contactId:t,messageId:s,error:n}),n}}return l.warn("[getMessageAfter] max traversal attempts reached",{contactId:t,startMessageId:e}),null}async sendEvent(t,e,s,i){l.info("[sendEvent] called with",{contactId:t,category:e,action:s,extras:i});let r={to:"postmaster@analytics.msging.net",method:f.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"){l.info("[setState] called with",{contactId:t,botId:e,stateId:s});let i={uri:`/flow-id?shortName=${e}`,to:"postmaster@builder.msging.net",method:f.Lime.Method.GET},r=await this.postCommand(i);if(!r.resource)throw l.error("[setState] flow ID not found",{botId:e}),new D(`Flow ID not found for bot: ${e}`,404);let a=r.resource,n={method:f.Lime.Method.SET,uri:`/contexts/${t}/stateid@${a}`,resource:s,type:"text/plain"};await this.postCommand(n);let o={method:f.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"),N=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(w).map(d=>this.repository.getStatusKey(d));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 d=await this.repository.getMessage(u);if(d){if(t.status&&!(Array.isArray(t.status)?t.status:[t.status]).includes(d.status)||t.state&&!(Array.isArray(t.state)?t.state:[t.state]).includes(d.state))continue;n.push(d)}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}=Y(),h=m("Dispatcher"),O=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=e,this.redis=this.repository.redis,this.stateMachine=new L(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 T(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 N(this.repository),this.queue=new K.Queue(this.queueName,{connection:this.redis,defaultJobOptions:{removeOnComplete:!0,removeOnFail:!0}}),this.worker=new K.Worker(this.queueName,async r=>{try{await this.processJob(r)}catch(a){throw h.error(`[Worker] Job ${r.name} failed`,a),a}},{connection:this.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))}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(M);for(let i of e)t.byState[i]=await this.repository.countMessages({state:i});let s=Object.values(w);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,W.v4)(),contactId:r,descriptorId:t.id,payload:a,status:"INIT",state:"INIT",createdAt:n,attempts:0,retries:this.maxRetries},d={...t.messageOptions,...i},{schedule:g,...E}=d;o.options=E,this.emit("dispatch",o),t.emit("dispatch",o,this.api,this.id);let p=this.calculateScheduledTime(g,d.shifts),y=0;if(p){o.scheduledTo=p,o.state="SCHEDULED";let I=new Date(p).getTime();y=Math.max(0,I-Date.now()),this.emit("scheduled",o),t.emit("scheduled",o,this.api,this.id),h.info("[send] message scheduled",{messageId:o.messageId,scheduledTo:p,delay:y})}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"?y+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:y,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(),[d,g]=r.start.split(":").map(Number),[E,p]=r.end.split(":").map(Number),y=d*60+g,I=E*60+p;if(u>=y&&u<I)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,d)=>{let[g,E]=u.start.split(":").map(Number),[p,y]=d.start.split(":").map(Number);return g*60+E-(p*60+y)});for(let u of o){let d=u.gmt||"-3",g=parseInt(d,10),[E,p]=u.start.split(":").map(Number),y=new Date(r);y.setHours(E,p,0,0);let I=new Date(y.getTime()+g*60*60*1e3);if(i===0){if(I>t)return I}else return I}}}}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 X=require("events"),$=require("bullmq");var v=m("DispatcherMonitor"),q=class extends X.EventEmitter{constructor(e,s,i){super();this.history=[];this.lastAlerts={};this.activeAlerts=new Set;this.isRunning=!1;this.id=e,this.repository=s,this.options={interval:6e4,historySize:1e3,...i},this.queueName=`monitor-${this.id}`,this.queue=new $.Queue(this.queueName,{connection:s.redis,defaultJobOptions:{removeOnComplete:!0,removeOnFail:!0}}),this.worker=new $.Worker(this.queueName,async r=>{r.name==="check"&&await this.check()},{connection:s.redis}),this.worker.on("error",r=>v.error("[MonitorWorker] Error",r)),this.worker.on("failed",(r,a)=>v.error(`[MonitorWorker] Job ${r?.id} failed`,a))}async start(){this.isRunning||(v.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(),v.info("[Monitor] Stopped")}async collectMetrics(){let e=this.repository,s={total:0,byState:{},byStatus:{},cumulative:{dispatched:0,delivered:0,failed:0}},r=Object.values(M).map(async d=>{s.byState[d]=await e.countMessages({state:d})}),n=Object.values(w).map(async d=>{s.byStatus[d]=await e.countMessages({status:d})}),u=["dispatched","delivered","failed"].map(async d=>{s.cumulative[d]=await e.getMetric(d)});return await Promise.all([...r,...n,...u]),s.total=Object.values(s.byState).reduce((d,g)=>d+(g||0),0),s}async check(){try{let e=await this.collectMetrics(),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){v.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){v.warn("[Monitor] failure_rate rule missing window");return}let d=this.findSnapshotAt(i-e.window);if(!d)return;let g=s.cumulative.failed,E=d.metrics.cumulative.failed,p=g-E,y=s.cumulative.dispatched,I=d.metrics.cumulative.dispatched,U=y-I;U===0?n=0:n=p/U,a=n>e.threshold,o={rate:(n*100).toFixed(2)+"%",threshold:(e.threshold*100).toFixed(2)+"%",failed:p,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){v.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){v.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]}};var Z=z(require("ioredis")),S=m("Repository"),b=class b{constructor(t,e){this.client=new Z.default(e,{maxRetriesPerRequest:null}),this.keyPrefix=`dwn-dispatcher:${t}`,this.client.on("error",s=>{S.error("[client] Redis error",s)})}async setup(){if(this.client.status==="ready"){S.debug("[setup] Redis already connected, skipping");return}this.client.status==="wait"&&await this.client.connect(),S.info("[setup] Repository connected",{status:this.client.status})}async teardown(){this.client.status!=="end"&&(await this.client.quit(),S.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 b.INDEXED_STATUSES){let o=this.getStatusKey(n);t.status===n?r.sadd(o,t.messageId):r.srem(o,t.messageId)}for(let n of b.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(),S.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 S.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 S.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 b.INDEXED_STATUSES)s.srem(this.getStatusKey(i),t);for(let i of b.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 b.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}),S.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}};b.INDEXED_STATUSES=["INIT","PENDING","SENDING","DELIVERED","READ","REPLIED","FAILED","CANCELED"],b.INDEXED_STATES=["INIT","DISPATCHED","SCHEDULED","QUEUED","FINAL"];var F=b;0&&(module.exports={Blip,BlipError,Channel,DispatchState,Dispatcher,DispatcherDescriptor,DispatcherMonitor,DispatcherRepository,MessageState,MessageStatus,Vnd,Weekdays,enableLogger,getLogger});
2
2
  //# sourceMappingURL=index.js.map