@dawntech/dispatcher 0.2.8 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -150
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -170,153 +170,3 @@ monitor.start();
|
|
|
170
170
|
// 5. Cleanup
|
|
171
171
|
monitor.stop();
|
|
172
172
|
```
|
|
173
|
-
|
|
174
|
-
## Development Quick Start
|
|
175
|
-
|
|
176
|
-
### Prerequisites
|
|
177
|
-
|
|
178
|
-
- Node.js >= 24.0.0
|
|
179
|
-
- npm >= 10.0.0
|
|
180
|
-
- Docker and Docker Compose (for local development)
|
|
181
|
-
- Blip Platform account with API credentials
|
|
182
|
-
|
|
183
|
-
### Setup Local Environment
|
|
184
|
-
|
|
185
|
-
Run the setup script to install dependencies and start infrastructure:
|
|
186
|
-
|
|
187
|
-
```bash
|
|
188
|
-
npm run setup
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
This script will:
|
|
192
|
-
|
|
193
|
-
- Check prerequisites (Docker, Node.js)
|
|
194
|
-
- Create `.env` file from `.env.example`
|
|
195
|
-
- Install npm dependencies
|
|
196
|
-
- Start Redis container
|
|
197
|
-
- Display available commands
|
|
198
|
-
|
|
199
|
-
### Manual Setup
|
|
200
|
-
|
|
201
|
-
If you prefer to set up manually:
|
|
202
|
-
|
|
203
|
-
1. **Copy environment file**
|
|
204
|
-
|
|
205
|
-
```bash
|
|
206
|
-
cp .env.example .env
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
1. **Update `.env` with your Blip credentials**
|
|
210
|
-
|
|
211
|
-
```bash
|
|
212
|
-
BLIP_CONTRACT=your-contract-id
|
|
213
|
-
BLIP_API_KEY=your-api-key
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
1. **Install dependencies**
|
|
217
|
-
|
|
218
|
-
```bash
|
|
219
|
-
npm install
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
1. **Start Docker services**
|
|
223
|
-
|
|
224
|
-
```bash
|
|
225
|
-
npm run docker:up
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
### Available Commands
|
|
229
|
-
|
|
230
|
-
#### Development
|
|
231
|
-
|
|
232
|
-
- `npm run dev` - Start development with auto-reload
|
|
233
|
-
- `npm run build` - Build TypeScript to JavaScript
|
|
234
|
-
- `npm run setup` - Initial project setup (prerequisites check, install, docker up)
|
|
235
|
-
- `npm run clean` - Remove build artifacts
|
|
236
|
-
|
|
237
|
-
#### Testing & Examples
|
|
238
|
-
|
|
239
|
-
- `npm run test` - Run unit tests (Jest)
|
|
240
|
-
- `npm run test:basic` - Integration test: Basic message sending
|
|
241
|
-
- `npm run test:contact` - Integration test: Contact metadata update
|
|
242
|
-
- `npm run test:schedule` - Integration test: Message scheduling
|
|
243
|
-
- `npm run test:status-config` - Integration test: Custom final status (READ)
|
|
244
|
-
- `npm run test:intent` - Integration test: Message intent with extra fields
|
|
245
|
-
|
|
246
|
-
#### Docker
|
|
247
|
-
|
|
248
|
-
- `npm run docker:up` - Start Redis container
|
|
249
|
-
- `npm run docker:down` - Stop all containers
|
|
250
|
-
- `npm run docker:logs` - View container logs
|
|
251
|
-
- `npm run docker:redis-cli` - Open Redis CLI
|
|
252
|
-
- `npm run docker:clean` - Stop containers and remove volumes
|
|
253
|
-
- `npm run docker:restart` - Restart all containers
|
|
254
|
-
- `npm run docker:tools` - Start Redis GUI tools (Commander & Insight)
|
|
255
|
-
|
|
256
|
-
### Infrastructure
|
|
257
|
-
|
|
258
|
-
#### Redis
|
|
259
|
-
|
|
260
|
-
- **URL**: `redis://localhost:6379`
|
|
261
|
-
- **Purpose**: Primary storage for dispatcher repository and event transport
|
|
262
|
-
- **Persistence**: Volume-backed with AOF enabled
|
|
263
|
-
|
|
264
|
-
#### Optional GUI Tools
|
|
265
|
-
|
|
266
|
-
Start Redis management tools:
|
|
267
|
-
|
|
268
|
-
```bash
|
|
269
|
-
npm run docker:tools
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
- **Redis Commander**: <http://localhost:8081> - Web-based Redis management
|
|
273
|
-
- **Redis Insight**: <http://localhost:8001> - Advanced Redis GUI
|
|
274
|
-
|
|
275
|
-
### Project Structure
|
|
276
|
-
|
|
277
|
-
```text
|
|
278
|
-
src/
|
|
279
|
-
├── core/
|
|
280
|
-
│ ├── BlipAPI.ts # Blip API client
|
|
281
|
-
│ ├── Dispatcher.ts # Main orchestrator (lifecycle & scheduling)
|
|
282
|
-
│ ├── Descriptor.ts # Message content & event handling
|
|
283
|
-
│ └── MessageTemplate.ts # Template base class (legacy/base)
|
|
284
|
-
├── repositories/
|
|
285
|
-
│ ├── Repository.ts # Storage layer base interface
|
|
286
|
-
│ ├── RedisRepository.ts # Shared storage (Redis)
|
|
287
|
-
│ └── LocalRepository.ts # In-memory storage (Local)
|
|
288
|
-
├── types/
|
|
289
|
-
│ └── index.ts # TypeScript types
|
|
290
|
-
└── index.ts # Main entry point
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
## Build
|
|
294
|
-
|
|
295
|
-
Build the TypeScript code:
|
|
296
|
-
|
|
297
|
-
```bash
|
|
298
|
-
npm run build
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
Output will be in the `dist/` directory with:
|
|
302
|
-
|
|
303
|
-
- Compiled JavaScript files
|
|
304
|
-
- TypeScript declaration files (`.d.ts`)
|
|
305
|
-
- Source maps
|
|
306
|
-
|
|
307
|
-
## Testing
|
|
308
|
-
|
|
309
|
-
```bash
|
|
310
|
-
npm test
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
## Environment Variables
|
|
314
|
-
|
|
315
|
-
See `.env.example` for all available configuration options:
|
|
316
|
-
|
|
317
|
-
- **REDIS_URL**: Redis connection URL
|
|
318
|
-
- **BLIP_CONTRACT**: Blip contract identifier
|
|
319
|
-
- **BLIP_API_KEY**: Blip API key
|
|
320
|
-
- **MAX_RETRIES**: Maximum retry attempts (default: 0)
|
|
321
|
-
- **LOCK_TTL**: Distributed lock timeout in ms (default: 30000)
|
|
322
|
-
- **POLLING_INTERVAL**: Background polling interval in ms (default: 5000)
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
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});
|
|
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.9",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
|