@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 +50 -17
- 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/eslint.config.mjs +33 -0
- package/package.json +5 -2
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
65
|
+
content: unknown;
|
|
66
66
|
date: string;
|
|
67
67
|
status?: string;
|
|
68
|
-
[key: string]:
|
|
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]:
|
|
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?:
|
|
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]:
|
|
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,
|
|
133
|
+
content: Record<string, unknown> | string;
|
|
134
134
|
};
|
|
135
|
-
type MessagePayload = Record<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?:
|
|
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<
|
|
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:
|
|
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:
|
|
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
|