@dawntech/dispatcher 0.0.1 → 0.2.2

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.
Files changed (57) hide show
  1. package/README.md +321 -2
  2. package/dist/{chunk-6IAFH374.mjs → chunk-4LYB64T2.mjs} +1 -1
  3. package/dist/{chunk-6IAFH374.mjs.map → chunk-4LYB64T2.mjs.map} +1 -1
  4. package/dist/chunk-B7IDVU6T.mjs +2 -0
  5. package/dist/chunk-B7IDVU6T.mjs.map +1 -0
  6. package/dist/{chunk-XLRB2LAD.mjs → chunk-CVPGUSFU.mjs} +2 -2
  7. package/dist/chunk-JD53PFR4.mjs +2 -0
  8. package/dist/chunk-JD53PFR4.mjs.map +1 -0
  9. package/dist/chunk-LKT4N6L5.mjs +2 -0
  10. package/dist/chunk-LKT4N6L5.mjs.map +1 -0
  11. package/dist/{chunk-YDWZJWCU.mjs → chunk-VTJMLZE3.mjs} +2 -2
  12. package/dist/core/Blip.js +1 -1
  13. package/dist/core/Blip.mjs +1 -1
  14. package/dist/core/Dispatcher.js +1 -1
  15. package/dist/core/Dispatcher.js.map +1 -1
  16. package/dist/core/Dispatcher.mjs +1 -1
  17. package/dist/core/DispatcherDescriptor.mjs +1 -1
  18. package/dist/core/DispatcherMonitor.js.map +1 -1
  19. package/dist/core/DispatcherMonitor.mjs +1 -1
  20. package/dist/core/DispatcherQuery.js +1 -1
  21. package/dist/core/DispatcherQuery.js.map +1 -1
  22. package/dist/core/DispatcherQuery.mjs +1 -1
  23. package/dist/core/DispatcherRepository.d.mts +4 -1
  24. package/dist/core/DispatcherRepository.js +1 -1
  25. package/dist/core/DispatcherRepository.js.map +1 -1
  26. package/dist/core/DispatcherRepository.mjs +1 -1
  27. package/dist/core/DispatcherStateMachine.js +1 -1
  28. package/dist/core/DispatcherStateMachine.mjs +1 -1
  29. package/dist/errors/index.mjs +1 -1
  30. package/dist/errors/index.mjs.map +1 -1
  31. package/dist/index.d.mts +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/index.mjs +1 -1
  35. package/dist/server.js +1 -1
  36. package/dist/server.js.map +1 -1
  37. package/dist/server.mjs +1 -1
  38. package/dist/server.mjs.map +1 -1
  39. package/dist/types/blip.mjs +1 -1
  40. package/dist/types/index.d.mts +6 -1
  41. package/dist/types/index.js +1 -1
  42. package/dist/types/index.js.map +1 -1
  43. package/dist/types/index.mjs +1 -1
  44. package/dist/utils/logger.mjs +1 -1
  45. package/package.json +36 -37
  46. package/.prettierignore +0 -4
  47. package/.prettierrc +0 -7
  48. package/dist/chunk-OR34YAY3.mjs +0 -2
  49. package/dist/chunk-OR34YAY3.mjs.map +0 -1
  50. package/dist/chunk-SLZLYBPB.mjs +0 -2
  51. package/dist/chunk-SLZLYBPB.mjs.map +0 -1
  52. package/jest.config.js +0 -48
  53. package/tsconfig.json +0 -22
  54. package/tsconfig.test.json +0 -10
  55. package/tsup.config.js +0 -10
  56. /package/dist/{chunk-XLRB2LAD.mjs.map → chunk-CVPGUSFU.mjs.map} +0 -0
  57. /package/dist/{chunk-YDWZJWCU.mjs.map → chunk-VTJMLZE3.mjs.map} +0 -0
package/README.md CHANGED
@@ -1,3 +1,322 @@
1
- # @dawntech/dwn-dispatcher
1
+ # dwn-dispatcher
2
2
 
3
- Pending readme.
3
+ [![npm](https://img.shields.io/npm/v/@dawntech/dispatcher)](https://www.npmjs.com/package/@dawntech/dispatcher)
4
+
5
+ A TypeScript Node.js package for sending push messages in conversational chatbots on the Blip platform. Provides a robust, multi-instance compliant system with automatic retries, scheduling, and shift-based time windows.
6
+
7
+ ## Features
8
+
9
+ - Channel-agnostic message dispatching (WhatsApp, Google Business Messages, etc.)
10
+ - Asynchronous message delivery with status monitoring
11
+ - Automatic retries with exponential backoff
12
+ - Multi-instance deployment support with distributed locking
13
+ - Shift-aware scheduling with timezone support
14
+ - Template-based messaging with payload filling
15
+ - Event publishing for monitoring and analytics
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ # npm
21
+ npm install @dawntech/dispatcher
22
+
23
+ # yarn
24
+ yarn add @dawntech/dispatcher
25
+
26
+ # pnpm
27
+ pnpm add @dawntech/dispatcher
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### Basic Example
33
+
34
+ ```typescript
35
+ import { Dispatcher, Descriptor } from '@dawntech/dispatcher';
36
+
37
+ // 1. Setup Infrastructure
38
+ const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
39
+ const connection = {
40
+ contract: 'my-blip-contract',
41
+ key: 'my-blip-api-key',
42
+ };
43
+
44
+ const dispatcher = new Dispatcher('my-dispatcher-1', redisUrl, connection);
45
+ await dispatcher.setup();
46
+ ```
47
+
48
+ ### Dispatcher ID Convention
49
+
50
+ The dispatcher ID supports the `:` character as a **namespace separator**. Redis tools (such as Redis Commander and Redis Insight) interpret `:` as a folder delimiter, creating a navigable tree structure for keys.
51
+
52
+ Use this convention to organize dispatchers hierarchically:
53
+
54
+ ```typescript
55
+ // Flat ID
56
+ const dispatcher = new Dispatcher('main', redisUrl, connection);
57
+ // Keys: dwn-dispatcher:main:message:..., dwn-dispatcher:main:queue:..., etc.
58
+
59
+ // Namespaced ID
60
+ const dispatcher = new Dispatcher('acme:onboarding', redisUrl, connection);
61
+ // Keys: dwn-dispatcher:acme:onboarding:message:..., dwn-dispatcher:acme:onboarding:queue:..., etc.
62
+ ```
63
+
64
+ This results in the following folder structure in Redis GUI tools:
65
+
66
+ ```text
67
+ dwn-dispatcher:
68
+ ├── acme:
69
+ │ └── onboarding:
70
+ │ ├── manifest ← created on setup
71
+ │ ├── message:...
72
+ │ ├── index:...
73
+ │ ├── queue:...
74
+ │ └── metrics:...
75
+ └── main:
76
+ ├── manifest
77
+ ├── message:...
78
+ ├── index:...
79
+ ├── queue:...
80
+ └── metrics:...
81
+ ```
82
+
83
+ Every dispatcher creates a `manifest` key (Redis hash) on `setup()` containing metadata such as the package version and timestamps. This key is used to validate that a dispatcher actually exists.
84
+
85
+ ```typescript
86
+ // 2. Define a Descriptor (Message content + Event listeners)
87
+ const welcomeDescriptor = new Descriptor('welcome_message', (payload) => ({
88
+ type: 'text/plain',
89
+ content: `Welcome ${payload.name}!`,
90
+ }));
91
+
92
+ // Optional: listen to events for this specific message
93
+ welcomeDescriptor.on('delivered', (msg) => {
94
+ console.log(`Message ${msg.messageId} was delivered!`);
95
+ });
96
+
97
+ // 3. Send message
98
+ await dispatcher.send(welcomeDescriptor, 'contact@phone.number', {
99
+ name: 'John Doe',
100
+ });
101
+ ```
102
+
103
+ ### With Scheduling
104
+
105
+ ```typescript
106
+ await dispatcher.send('contact@phone.number', welcomeDescriptor, payload, {
107
+ schedule: '2024-01-15T14:00:00Z',
108
+ });
109
+ ```
110
+
111
+ ### With Shift Restrictions
112
+
113
+ ```typescript
114
+ await dispatcher.send('contact@phone.number', welcomeDescriptor, payload, {
115
+ shifts: [
116
+ {
117
+ days: 31, // Mon-Fri (bitmask)
118
+ start: '09:00',
119
+ end: '18:00',
120
+ gmt: '-3',
121
+ },
122
+ ],
123
+ });
124
+ ```
125
+
126
+ ### Monitoring
127
+
128
+ You can monitor the Dispatcher metrics and receive alerts for failures or high load using `DispatcherMonitor`.
129
+
130
+ ```typescript
131
+ import { Dispatcher, DispatcherMonitor } from '@dawntech/dispatcher';
132
+
133
+ // 1. Create Dispatcher
134
+ const dispatcher = new Dispatcher('my-dispatcher', redisUrl, connection);
135
+ await dispatcher.setup();
136
+
137
+ // 2. Create Monitor attached to Dispatcher
138
+ const monitor = new DispatcherMonitor(dispatcher, {
139
+ interval: 60000, // Check every minute
140
+ rules: [
141
+ {
142
+ type: 'failure_rate',
143
+ threshold: 0.05, // 5% failure rate
144
+ window: 60000 * 60, // 1 hour window
145
+ debounce: 30 * 60000, // Alert at most every 30 mins
146
+ },
147
+ {
148
+ type: 'queue_size',
149
+ threshold: 1000, // Alert if > 1000 messages pending/scheduled
150
+ debounce: 10 * 60000, // Alert at most every 10 mins
151
+ },
152
+ ],
153
+ });
154
+
155
+ // 3. Listen for Alerts
156
+ monitor.on('alert', (alert) => {
157
+ console.error(`[ALERT] ${alert.type}: ${alert.message}`, alert.details);
158
+ // Send to external monitoring (e.g., Slack, PagerDuty, Datadog)
159
+ });
160
+
161
+ monitor.on('resolved', (alert) => {
162
+ console.log(`[RESOLVED] ${alert.type}: ${alert.message}`);
163
+ });
164
+
165
+ // 4. Start Monitoring
166
+ monitor.start();
167
+
168
+ // ... application runs ...
169
+
170
+ // 5. Cleanup
171
+ monitor.stop();
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)
@@ -1,2 +1,2 @@
1
1
  var r=(e=>(e.INIT="INIT",e.DISPATCHED="DISPATCHED",e.SCHEDULED="SCHEDULED",e.QUEUED="QUEUED",e.FINAL="FINAL",e))(r||{}),i=(n=>(n.INIT="INIT",n.PENDING="PENDING",n.SENDING="SENDING",n.DELIVERED="DELIVERED",n.READ="READ",n.REPLIED="REPLIED",n.FAILED="FAILED",n.CANCELED="CANCELED",n))(i||{}),s=(e=>(e.ACCEPTED="accepted",e.DISPATCHED="dispatched",e.RECEIVED="received",e.CONSUMED="consumed",e.FAILED="failed",e))(s||{}),o=(t=>(t[t.MONDAY=1]="MONDAY",t[t.TUESDAY=2]="TUESDAY",t[t.WEDNESDAY=4]="WEDNESDAY",t[t.THURSDAY=8]="THURSDAY",t[t.FRIDAY=16]="FRIDAY",t[t.SATURDAY=32]="SATURDAY",t[t.SUNDAY=64]="SUNDAY",t))(o||{});export{r as a,i as b,s as c,o as d};
2
- //# sourceMappingURL=chunk-6IAFH374.mjs.map
2
+ //# sourceMappingURL=chunk-4LYB64T2.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types/index.ts"],"sourcesContent":["import { Vnd } from './blip';\n\nexport type Contact = Omit<Vnd.Iris.Contact, 'identity'>;\n\nexport type MessageData = {\n type: string;\n content: Record<string, any> | string;\n};\n\nexport type MessagePayload = Record<string, any> | Array<any> | string;\n\nexport enum MessageState {\n INIT = 'INIT',\n DISPATCHED = 'DISPATCHED',\n SCHEDULED = 'SCHEDULED',\n QUEUED = 'QUEUED',\n FINAL = 'FINAL',\n}\n\nexport enum MessageStatus {\n INIT = 'INIT',\n PENDING = 'PENDING',\n SENDING = 'SENDING',\n DELIVERED = 'DELIVERED',\n READ = 'READ',\n REPLIED = 'REPLIED',\n FAILED = 'FAILED',\n CANCELED = 'CANCELED',\n}\n\nexport enum DispatchState {\n ACCEPTED = 'accepted',\n DISPATCHED = 'dispatched',\n RECEIVED = 'received',\n CONSUMED = 'consumed',\n FAILED = 'failed',\n}\n\nexport interface Message {\n messageId: string;\n contactId: string;\n descriptorId: string;\n\n payload: MessageData;\n status: MessageStatus;\n state: MessageState;\n\n attempts?: number;\n retries?: number;\n\n options?: MessageOptions; // Persisted options for scheduled/retried messages\n\n expiresAt?: string; // ISO 8601 datetime\n\n scheduledTo?: string; // ISO 8601 datetime\n sentAt?: string; // ISO 8601 datetime\n acceptedAt?: string; // ISO 8601 datetime - When message was accepted by gateway (SENDING status)\n lastDispatchAttemptAt?: string; // ISO 8601 datetime - When last dispatch attempt started\n createdAt: string; // ISO 8601 datetime\n deliveredAt?: string; // ISO 8601 datetime\n readAt?: string; // ISO 8601 datetime\n repliedAt?: string; // ISO 8601 datetime\n failedAt?: string; // ISO 8601 datetime\n\n error?: string;\n}\n\nexport interface Shift {\n days: number; // Bitmask: use Weekdays enum (e.g., Weekdays.MONDAY | Weekdays.FRIDAY) or manual (31=Mon-Fri)\n start: string; // HH:mm format (e.g., \"09:00\")\n end: string; // HH:mm format (e.g., \"18:00\")\n gmt?: string; // Timezone offset (default: \"-3\" for BRT)\n}\n\nexport enum Weekdays {\n MONDAY = 1,\n TUESDAY = 2,\n WEDNESDAY = 4,\n THURSDAY = 8,\n FRIDAY = 16,\n SATURDAY = 32,\n SUNDAY = 64,\n}\n\nexport interface Intent {\n intent: string;\n dueDate?: string;\n event?: string;\n payload?: any | null;\n expired?: {\n event?: string;\n intent?: string;\n };\n}\n\nexport type CallbackEvent =\n | 'dispatch' // Before message send process begins\n | 'sending' // MessageStatus.SENDING - message accepted by gateway\n | 'delivered' // MessageStatus.DELIVERED - message successfully delivered\n | 'failed' // MessageStatus.FAILED - message failed permanently\n | 'read' // MessageStatus.READ - message read by recipient\n | 'replied' // MessageStatus.REPLIED - recipient replied\n | 'scheduled' // MessageState.SCHEDULED - message scheduled\n | 'retry' // Retry attempt scheduled\n | 'evicted' // Message evicted from state machine due to TTL\n | 'canceled'; // Message canceled before send\n\nexport type ConnectionConfig = {\n contract: string; // Contract ID (without https:// prefix)\n key: string; // Bot authorization key\n};\n\nexport interface MessageOptions {\n shifts?: Array<Shift>; // Business hours constraints\n contact?: Contact;\n state?: {\n // Bot state to set after send\n stateId: string;\n botId: string;\n botIds?: Array<string>; // Bot IDs to reset before setting state\n };\n intent?: string | Intent; // Intent to trigger after send\n finalStatus?: 'DELIVERED' | 'READ' | 'REPLIED'; // Default: 'DELIVERED'\n}\n\nexport interface DispatchMessageOptions extends MessageOptions {\n schedule?: string; // ISO 8601 datetime to schedule send\n}\n\nexport interface DispatcherOptions {\n maxRetries?: number; // Default: 0\n retryIntervals?: number[]; // Default: [1000, 5000, 15000] (exponential backoff in ms)\n\n // Storage Duration (Redis TTL)\n retention?: number; // Default: 3600000 ms (1 hour) - Time to keep message in storage after creation/dispatch\n\n // State Constraints (Force transition to FAILED)\n timeouts?: {\n pending?: number; // Default: 10000 ms (10 seconds)\n sending?: number; // Default: 60000 ms (60 seconds)\n };\n\n batchSize?: number; // Default: 100 messages per polling cycle\n pollingIntervals?: {\n scheduled?: number; // Default: 5000 ms\n pending?: number; // Default: 5000 ms\n sending?: number; // Default: 10000 ms\n delivered?: number; // Default: 1800000 ms (30 minutes)\n read?: number; // Default: 3600000 ms (60 minutes)\n queue?: number; // Default: 100 ms\n };\n maxQueueSize?: number; // Maximum number of pending/scheduled messages. Oldest evicted if exceeded.\n rateLimits?: {\n global?: { points: number; duration: number }; // Global rate limit (requests per duration)\n message?: { points: number; duration: number }; // Message sending rate limit (requests per duration)\n };\n}\n\nexport interface QueryFilter {\n contactId?: string;\n descriptorId?: string;\n status?: MessageStatus | MessageStatus[];\n state?: MessageState | MessageState[];\n size?: number;\n skip?: number;\n}\n\nexport interface DispatcherMetrics {\n total: number;\n byState: {\n [key in MessageState]?: number;\n };\n byStatus: {\n [key in MessageStatus]?: number;\n };\n cumulative: {\n dispatched: number;\n delivered: number;\n failed: number;\n };\n}\n\n/**\n * @deprecated Use DispatcherOptions instead\n */\nexport type MessageDispatcherOptions = DispatcherOptions;\n"],"mappings":"AAWO,IAAKA,OACVA,EAAA,KAAO,OACPA,EAAA,WAAa,aACbA,EAAA,UAAY,YACZA,EAAA,OAAS,SACTA,EAAA,MAAQ,QALEA,OAAA,IAQAC,OACVA,EAAA,KAAO,OACPA,EAAA,QAAU,UACVA,EAAA,QAAU,UACVA,EAAA,UAAY,YACZA,EAAA,KAAO,OACPA,EAAA,QAAU,UACVA,EAAA,OAAS,SACTA,EAAA,SAAW,WARDA,OAAA,IAWAC,OACVA,EAAA,SAAW,WACXA,EAAA,WAAa,aACbA,EAAA,SAAW,WACXA,EAAA,SAAW,WACXA,EAAA,OAAS,SALCA,OAAA,IA4CAC,OACVA,IAAA,OAAS,GAAT,SACAA,IAAA,QAAU,GAAV,UACAA,IAAA,UAAY,GAAZ,YACAA,IAAA,SAAW,GAAX,WACAA,IAAA,OAAS,IAAT,SACAA,IAAA,SAAW,IAAX,WACAA,IAAA,OAAS,IAAT,SAPUA,OAAA","names":["MessageState","MessageStatus","DispatchState","Weekdays"]}
1
+ {"version":3,"sources":["../src/types/index.ts"],"sourcesContent":["import { Vnd } from './blip';\n\nexport type Contact = Omit<Vnd.Iris.Contact, 'identity'>;\n\nexport type MessageData = {\n type: string;\n content: Record<string, any> | string;\n};\n\nexport type MessagePayload = Record<string, any> | Array<any> | string;\n\nexport enum MessageState {\n INIT = 'INIT',\n DISPATCHED = 'DISPATCHED',\n SCHEDULED = 'SCHEDULED',\n QUEUED = 'QUEUED',\n FINAL = 'FINAL',\n}\n\nexport enum MessageStatus {\n INIT = 'INIT',\n PENDING = 'PENDING',\n SENDING = 'SENDING',\n DELIVERED = 'DELIVERED',\n READ = 'READ',\n REPLIED = 'REPLIED',\n FAILED = 'FAILED',\n CANCELED = 'CANCELED',\n}\n\nexport enum DispatchState {\n ACCEPTED = 'accepted',\n DISPATCHED = 'dispatched',\n RECEIVED = 'received',\n CONSUMED = 'consumed',\n FAILED = 'failed',\n}\n\nexport interface Message {\n messageId: string;\n contactId: string;\n descriptorId: string;\n\n payload: MessageData;\n status: MessageStatus;\n state: MessageState;\n\n attempts?: number;\n retries?: number;\n\n options?: MessageOptions; // Persisted options for scheduled/retried messages\n\n expiresAt?: string; // ISO 8601 datetime\n\n scheduledTo?: string; // ISO 8601 datetime\n sentAt?: string; // ISO 8601 datetime\n acceptedAt?: string; // ISO 8601 datetime - When message was accepted by gateway (SENDING status)\n lastDispatchAttemptAt?: string; // ISO 8601 datetime - When last dispatch attempt started\n createdAt: string; // ISO 8601 datetime\n deliveredAt?: string; // ISO 8601 datetime\n readAt?: string; // ISO 8601 datetime\n repliedAt?: string; // ISO 8601 datetime\n failedAt?: string; // ISO 8601 datetime\n\n error?: string;\n}\n\nexport interface Shift {\n days: number; // Bitmask: use Weekdays enum (e.g., Weekdays.MONDAY | Weekdays.FRIDAY) or manual (31=Mon-Fri)\n start: string; // HH:mm format (e.g., \"09:00\")\n end: string; // HH:mm format (e.g., \"18:00\")\n gmt?: string; // Timezone offset (default: \"-3\" for BRT)\n}\n\nexport enum Weekdays {\n MONDAY = 1,\n TUESDAY = 2,\n WEDNESDAY = 4,\n THURSDAY = 8,\n FRIDAY = 16,\n SATURDAY = 32,\n SUNDAY = 64,\n}\n\nexport interface Intent {\n intent: string;\n dueDate?: string;\n event?: string;\n payload?: any | null;\n expired?: {\n event?: string;\n intent?: string;\n };\n}\n\nexport type CallbackEvent =\n | 'dispatch' // Before message send process begins\n | 'sending' // MessageStatus.SENDING - message accepted by gateway\n | 'delivered' // MessageStatus.DELIVERED - message successfully delivered\n | 'failed' // MessageStatus.FAILED - message failed permanently\n | 'read' // MessageStatus.READ - message read by recipient\n | 'replied' // MessageStatus.REPLIED - recipient replied\n | 'scheduled' // MessageState.SCHEDULED - message scheduled\n | 'retry' // Retry attempt scheduled\n | 'evicted' // Message evicted from state machine due to TTL\n | 'canceled'; // Message canceled before send\n\nexport type ConnectionConfig = {\n contract: string; // Contract ID (without https:// prefix)\n key: string; // Bot authorization key\n};\n\nexport interface MessageOptions {\n shifts?: Array<Shift>; // Business hours constraints\n contact?: Contact;\n state?: {\n // Bot state to set after send\n stateId: string;\n botId: string;\n botIds?: Array<string>; // Bot IDs to reset before setting state\n };\n intent?: string | Intent; // Intent to trigger after send\n finalStatus?: 'DELIVERED' | 'READ' | 'REPLIED'; // Default: 'DELIVERED'\n}\n\nexport interface DispatchMessageOptions extends MessageOptions {\n schedule?: string; // ISO 8601 datetime to schedule send\n}\n\nexport interface DispatcherOptions {\n maxRetries?: number; // Default: 0\n retryIntervals?: number[]; // Default: [1000, 5000, 15000] (exponential backoff in ms)\n\n // Storage Duration (Redis TTL)\n retention?: number; // Default: 3600000 ms (1 hour) - Time to keep message in storage after creation/dispatch\n\n // State Constraints (Force transition to FAILED)\n timeouts?: {\n pending?: number; // Default: 10000 ms (10 seconds)\n sending?: number; // Default: 60000 ms (60 seconds)\n };\n\n batchSize?: number; // Default: 100 messages per polling cycle\n pollingIntervals?: {\n scheduled?: number; // Default: 5000 ms\n pending?: number; // Default: 5000 ms\n sending?: number; // Default: 10000 ms\n delivered?: number; // Default: 1800000 ms (30 minutes)\n read?: number; // Default: 3600000 ms (60 minutes)\n queue?: number; // Default: 100 ms\n };\n maxQueueSize?: number; // Maximum number of pending/scheduled messages. Oldest evicted if exceeded.\n rateLimits?: {\n global?: { points: number; duration: number }; // Global rate limit (requests per duration)\n message?: { points: number; duration: number }; // Message sending rate limit (requests per duration)\n };\n}\n\nexport interface QueryFilter {\n contactId?: string;\n descriptorId?: string;\n status?: MessageStatus | MessageStatus[];\n state?: MessageState | MessageState[];\n size?: number;\n skip?: number;\n}\n\nexport interface DispatcherMetrics {\n total: number;\n byState: {\n [key in MessageState]?: number;\n };\n byStatus: {\n [key in MessageStatus]?: number;\n };\n cumulative: {\n dispatched: number;\n delivered: number;\n failed: number;\n };\n}\n\nexport interface DispatcherManifest {\n version: string;\n createdAt: string;\n updatedAt: string;\n}\n\n/**\n * @deprecated Use DispatcherOptions instead\n */\nexport type MessageDispatcherOptions = DispatcherOptions;\n"],"mappings":"AAWO,IAAKA,OACVA,EAAA,KAAO,OACPA,EAAA,WAAa,aACbA,EAAA,UAAY,YACZA,EAAA,OAAS,SACTA,EAAA,MAAQ,QALEA,OAAA,IAQAC,OACVA,EAAA,KAAO,OACPA,EAAA,QAAU,UACVA,EAAA,QAAU,UACVA,EAAA,UAAY,YACZA,EAAA,KAAO,OACPA,EAAA,QAAU,UACVA,EAAA,OAAS,SACTA,EAAA,SAAW,WARDA,OAAA,IAWAC,OACVA,EAAA,SAAW,WACXA,EAAA,WAAa,aACbA,EAAA,SAAW,WACXA,EAAA,SAAW,WACXA,EAAA,OAAS,SALCA,OAAA,IA4CAC,OACVA,IAAA,OAAS,GAAT,SACAA,IAAA,QAAU,GAAV,UACAA,IAAA,UAAY,GAAZ,YACAA,IAAA,SAAW,GAAX,WACAA,IAAA,OAAS,IAAT,SACAA,IAAA,SAAW,IAAX,WACAA,IAAA,OAAS,IAAT,SAPUA,OAAA","names":["MessageState","MessageStatus","DispatchState","Weekdays"]}
@@ -0,0 +1,2 @@
1
+ import{b as d}from"./chunk-OXXLVJVC.mjs";import g from"ioredis";var u=d("Repository"),c=class c{constructor(e,t){this.client=new g(t,{maxRetriesPerRequest:null}),this.keyPrefix=`dwn-dispatcher:${e}`,this.client.on("error",i=>{u.error("[client] Redis error",i)})}async setup(){if(this.client.status==="ready"){u.debug("[setup] Redis already connected, skipping");return}this.client.status==="wait"&&await this.client.connect(),u.info("[setup] Repository connected",{status:this.client.status})}async teardown(){this.client.status!=="end"&&(await this.client.quit(),u.info("[teardown] Repository disconnected"))}get redis(){return this.client}getManifestKey(){return`${this.keyPrefix}:manifest`}getKey(e){return`${this.keyPrefix}:message:${e}`}getStateKey(e){return`${this.keyPrefix}:index:state:${e.toLowerCase()}`}getStatusKey(e){return`${this.keyPrefix}:index:status:${e.toLowerCase()}`}getContactKey(e){return`${this.keyPrefix}:index:contact:${e}`}getDescriptorKey(e){return`${this.keyPrefix}:index:descriptor:${e}`}getQueueKey(e){return`${this.keyPrefix}:queue:${e.toLowerCase()}`}async upsertMessage(e,t){let i=this.getKey(e.messageId),n=JSON.stringify(e),s=this.client.pipeline();s.set(i,n),e.contactId&&s.sadd(this.getContactKey(e.contactId),e.messageId),e.descriptorId&&s.sadd(this.getDescriptorKey(e.descriptorId),e.messageId);for(let r of c.INDEXED_STATUSES){let o=this.getStatusKey(r);e.status===r?s.sadd(o,e.messageId):s.srem(o,e.messageId)}for(let r of c.INDEXED_STATES){let o=this.getStateKey(r);e.state===r?s.sadd(o,e.messageId):s.srem(o,e.messageId)}let a=Date.now()+(t||36e5*24*2);if(e.state==="SCHEDULED"&&e.scheduledTo&&(a=new Date(e.scheduledTo).getTime()+(t||0)),s.zadd(this.getQueueKey("retention"),a,e.messageId),e.state==="SCHEDULED"&&e.scheduledTo){let r=new Date(e.scheduledTo).getTime();s.zadd(this.getQueueKey("scheduled"),r,e.messageId)}else s.zrem(this.getQueueKey("scheduled"),e.messageId);if(e.state==="QUEUED"){let r=new Date(e.createdAt||Date.now()).getTime();s.zadd(this.getQueueKey("queued"),r,e.messageId)}else s.zrem(this.getQueueKey("queued"),e.messageId);if(e.state==="DISPATCHED"){let r=new Date(e.createdAt||Date.now()).getTime();s.zadd(this.getQueueKey("dispatched"),r,e.messageId)}else s.zrem(this.getQueueKey("dispatched"),e.messageId);await s.exec(),u.debug("[upsertMessage]",{messageId:e.messageId,status:e.status,state:e.state})}async getMessage(e){let t=this.getKey(e),i=await this.client.get(t);return i?JSON.parse(i):null}async getMessages(e){let t=[];if(e.state==="SCHEDULED"){let n=Date.now(),s=e.skip??0,a=e.size??0;a>0?t=await this.client.zrangebyscore(this.getQueueKey("scheduled"),0,n,"LIMIT",s,a):t=await this.client.zrangebyscore(this.getQueueKey("scheduled"),0,n)}else if(e.state==="QUEUED")t=await this.client.zrange(this.getQueueKey("queued"),e.skip??0,(e.skip??0)+(e.size?e.size-1:-1));else if(e.state==="DISPATCHED")t=await this.client.zrange(this.getQueueKey("dispatched"),e.skip??0,(e.skip??0)+(e.size?e.size-1:-1));else if(e.status){let n=await this.client.smembers(this.getStatusKey(e.status)),s=e.skip??0,a=e.size;t=a?n.slice(s,s+a):n.slice(s)}else if(e.state)try{let n=await this.client.smembers(this.getStateKey(e.state)),s=e.skip??0,a=e.size;t=a?n.slice(s,s+a):n.slice(s)}catch{return[]}else return u.warn("[getMessages] no filter provided"),[];let i=[];for(let n of t){let s=await this.getMessage(n);if(s){if(e.status&&s.status!==e.status||e.state&&s.state!==e.state)continue;i.push(s)}}return u.debug("[getMessages]",{count:i.length,filter:e}),i}async getQueueSize(){return await this.client.zcard(this.getQueueKey("dispatched"))}async evictOldest(e){if(e<=0)return 0;let t=await this.client.zpopmin(this.getQueueKey("dispatched"),e),i=0;for(let n=0;n<t.length;n+=2){let s=t[n];await this.deleteMessage(s),i++}return i}async getExpiredMessages(e=50){let t=Date.now();return await this.client.zrangebyscore(this.getQueueKey("expiration"),0,t,"LIMIT",0,e)}async getRetentionMessages(e=50){let t=Date.now();return await this.client.zrangebyscore(this.getQueueKey("retention"),0,t,"LIMIT",0,e)}async incrementMetric(e,t=1){let i=`${this.keyPrefix}:metrics:${e}`;return await this.client.incrby(i,t)}async getMetric(e){let t=`${this.keyPrefix}:metrics:${e}`,i=await this.client.get(t);return i?parseInt(i,10):0}async deleteMessage(e){let t=await this.getMessage(e);t&&await this.deleteMessageData(e,t)}async deleteMessageData(e,t){let i=this.client.pipeline();i.del(this.getKey(e)),i.zrem(this.getQueueKey("scheduled"),e),i.zrem(this.getQueueKey("queued"),e),i.zrem(this.getQueueKey("dispatched"),e),i.zrem(this.getQueueKey("expiration"),e),i.zrem(this.getQueueKey("retention"),e);for(let n of c.INDEXED_STATUSES)i.srem(this.getStatusKey(n),e);for(let n of c.INDEXED_STATES)i.srem(this.getStateKey(n),e);t.contactId&&i.srem(this.getContactKey(t.contactId),e),t.descriptorId&&i.srem(this.getDescriptorKey(t.descriptorId),e),await i.exec()}async countMessages(e){if(e.state==="SCHEDULED")return await this.client.zcard(this.getQueueKey("scheduled"));if(e.state==="QUEUED")return await this.client.zcard(this.getQueueKey("queued"));if(e.state==="DISPATCHED")return await this.client.zcard(this.getQueueKey("dispatched"));if(e.status)return await this.client.scard(this.getStatusKey(e.status));if(e.state)try{return await this.client.scard(this.getStateKey(e.state))}catch{return 0}return 0}async getMetrics(e){let t={cumulative:{dispatched:0,delivered:0,failed:0},queues:{queued:0,scheduled:0,dispatched:0},status:{}},i=async a=>{if(e){let r=this.getDescriptorKey(e);return(await this.client.sinter(a,r)).length}return await this.client.scard(a)};for(let a of c.INDEXED_STATUSES){let r=await i(this.getStatusKey(a));t.status[a]=r,a==="DELIVERED"&&(t.cumulative.delivered=r),a==="FAILED"&&(t.cumulative.failed=r)}let n=this.getStateKey("DISPATCHED"),s=await i(n);return t.cumulative.dispatched=s+t.cumulative.delivered+t.cumulative.failed,e?(t.queues.queued=await i(this.getStateKey("QUEUED")),t.queues.scheduled=await i(this.getStateKey("SCHEDULED")),t.queues.dispatched=s):(t.queues.queued=await this.client.zcard(this.getQueueKey("queued")),t.queues.scheduled=await this.client.zcard(this.getQueueKey("scheduled")),t.queues.dispatched=await this.client.zcard(this.getQueueKey("dispatched"))),t}async getDescriptors(){let e=this.getDescriptorKey("*"),t=`${this.keyPrefix}:index:descriptor:`,i=await this.client.keys(e),n=[];for(let s of i){let a=s.slice(t.length);if(a){let r=await this.client.scard(s);n.push({id:a,count:r})}}return n.sort((s,a)=>a.count-s.count),n}async writeManifest(e){let t=this.getManifestKey();await this.client.hset(t,{version:e.version,createdAt:e.createdAt,updatedAt:e.updatedAt}),u.info("[writeManifest] Manifest written",{key:t})}async getManifest(){let e=this.getManifestKey(),t=await this.client.hgetall(e);return!t||Object.keys(t).length===0?null:t}};c.INDEXED_STATUSES=["INIT","PENDING","SENDING","DELIVERED","READ","REPLIED","FAILED","CANCELED"],c.INDEXED_STATES=["INIT","DISPATCHED","SCHEDULED","QUEUED","FINAL"];var h=c;export{h as a};
2
+ //# sourceMappingURL=chunk-B7IDVU6T.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/DispatcherRepository.ts"],"sourcesContent":["import { DispatcherManifest, Message, MessageState, MessageStatus } from '../types/index.js';\nimport { getLogger } from '../utils/logger.js';\nimport IORedis from 'ioredis';\n\nconst logger = getLogger('Repository');\n\nexport class DispatcherRepository {\n private static readonly INDEXED_STATUSES = [\n MessageStatus.INIT,\n MessageStatus.PENDING,\n MessageStatus.SENDING,\n MessageStatus.DELIVERED,\n MessageStatus.READ,\n MessageStatus.REPLIED,\n MessageStatus.FAILED,\n MessageStatus.CANCELED,\n ];\n\n private static readonly INDEXED_STATES = [\n MessageState.INIT,\n MessageState.DISPATCHED,\n MessageState.SCHEDULED,\n MessageState.QUEUED,\n MessageState.FINAL,\n ];\n\n private client: IORedis;\n private keyPrefix: string;\n\n constructor(dispatcherId: string, redisUrl: string) {\n this.client = new IORedis(redisUrl, { maxRetriesPerRequest: null });\n this.keyPrefix = `dwn-dispatcher:${dispatcherId}`;\n\n this.client.on('error', (err) => {\n logger.error('[client] Redis error', err);\n });\n }\n\n async setup(): Promise<void> {\n if (this.client.status === 'ready') {\n logger.debug('[setup] Redis already connected, skipping');\n return;\n }\n // IORedis connects automatically, but we can wait for ready\n if (this.client.status === 'wait') {\n await this.client.connect();\n }\n logger.info('[setup] Repository connected', { status: this.client.status });\n }\n\n async teardown(): Promise<void> {\n if (this.client.status === 'end') {\n return;\n }\n await this.client.quit();\n logger.info('[teardown] Repository disconnected');\n }\n\n public get redis(): IORedis {\n return this.client;\n }\n\n public getManifestKey(): string {\n return `${this.keyPrefix}:manifest`;\n }\n\n public getKey(messageId: string): string {\n return `${this.keyPrefix}:message:${messageId}`;\n }\n\n public getStateKey(state: MessageState): string {\n return `${this.keyPrefix}:index:state:${state.toLowerCase()}`;\n }\n\n public getStatusKey(status: MessageStatus): string {\n return `${this.keyPrefix}:index:status:${status.toLowerCase()}`;\n }\n\n public getContactKey(contactId: string): string {\n return `${this.keyPrefix}:index:contact:${contactId}`;\n }\n\n public getDescriptorKey(descriptorId: string): string {\n return `${this.keyPrefix}:index:descriptor:${descriptorId}`;\n }\n\n public getQueueKey(\n name: 'scheduled' | 'queued' | 'dispatched' | 'expiration' | 'retention'\n ): string {\n return `${this.keyPrefix}:queue:${name.toLowerCase()}`;\n }\n\n async upsertMessage(message: Message, ttl?: number): Promise<void> {\n const key = this.getKey(message.messageId);\n const data = JSON.stringify(message);\n\n const pipeline = this.client.pipeline();\n\n pipeline.set(key, data);\n\n // --- INDICES (Sets for Querying) ---\n // Prefix: index:contact, index:descriptor, index:status, index:state\n\n // 1. Contact Index\n if (message.contactId) {\n pipeline.sadd(this.getContactKey(message.contactId), message.messageId);\n }\n // 2. Descriptor Index\n if (message.descriptorId) {\n pipeline.sadd(this.getDescriptorKey(message.descriptorId), message.messageId);\n }\n\n // 3. Status Index (ALL statuses are indexed)\n for (const status of DispatcherRepository.INDEXED_STATUSES) {\n const indexKey = this.getStatusKey(status);\n if (message.status === status) {\n pipeline.sadd(indexKey, message.messageId);\n } else {\n pipeline.srem(indexKey, message.messageId);\n }\n }\n\n // 4. State Index (ALL states are indexed)\n for (const state of DispatcherRepository.INDEXED_STATES) {\n const indexKey = this.getStateKey(state);\n if (message.state === state) {\n pipeline.sadd(indexKey, message.messageId);\n } else {\n pipeline.srem(indexKey, message.messageId);\n }\n }\n\n // --- QUEUES (ZSets for Processing) ---\n // Prefix: queue:scheduled, queue:queued, queue:dispatched, queue:retention, queue:expiration\n\n // 0. Retention Queue (Data Cleanup)\n // We ALWAYS enforce retention.\n let retentionTime = Date.now() + (ttl || 3600000 * 24 * 2); // Default to provided TTL or fallbacks (should be provided by Dispatcher)\n\n if (message.state === MessageState.SCHEDULED && message.scheduledTo) {\n // Retention starts after scheduled time\n retentionTime = new Date(message.scheduledTo).getTime() + (ttl || 0);\n }\n pipeline.zadd(this.getQueueKey('retention'), retentionTime, message.messageId);\n\n // 0.5 Expiration Queue (State Timeouts - PENDING/SENDING)\n // If message is in transient state, we might want to track it in queue:expiration for TimeoutMonitor\n // The Dispatcher currently handles TimeoutMonitor by scanning INDEXED PENDING/SENDING.\n // We can keep that logic or move to ZSET.\n // For now, let's strictly follow the plan: queue:retention for cleanup.\n // \"queue:expiration is reserved for state timeouts\" - Implementation of that would be in Dispatcher/StateMachine logic\n // explicitly adding to it, OR we can infer here.\n // Let's stick to existing TimeoutMonitor scanning Indices for now as it works,\n // unless we want to optimize. The user just wanted to separate concerns.\n // I will NOT add implicit queue:expiration logic here to avoid conflict with existing Monitor.\n // But I MUST remove usage of queue:expiration for *retention* purposes if it was there.\n // The previous code used queue:expiration for cleanup. I will rename that to queue:expiration -> queue:retention in effect.\n\n // 1. Scheduled Queue\n if (message.state === MessageState.SCHEDULED && message.scheduledTo) {\n const scheduledTime = new Date(message.scheduledTo).getTime();\n pipeline.zadd(this.getQueueKey('scheduled'), scheduledTime, message.messageId);\n } else {\n pipeline.zrem(this.getQueueKey('scheduled'), message.messageId);\n }\n\n // 2. Queued Queue (FIFO)\n if (message.state === MessageState.QUEUED) {\n const score = new Date(message.createdAt || Date.now()).getTime();\n pipeline.zadd(this.getQueueKey('queued'), score, message.messageId);\n } else {\n pipeline.zrem(this.getQueueKey('queued'), message.messageId);\n }\n\n // 3. Dispatched Queue (LRU/Tracking)\n if (message.state === MessageState.DISPATCHED) {\n const score = new Date(message.createdAt || Date.now()).getTime();\n pipeline.zadd(this.getQueueKey('dispatched'), score, message.messageId);\n } else {\n pipeline.zrem(this.getQueueKey('dispatched'), message.messageId);\n }\n\n await pipeline.exec();\n\n logger.debug('[upsertMessage]', {\n messageId: message.messageId,\n status: message.status,\n state: message.state,\n });\n }\n\n async getMessage(messageId: string): Promise<Message | null> {\n const key = this.getKey(messageId);\n const data = await this.client.get(key);\n\n if (!data) {\n return null;\n }\n\n return JSON.parse(data) as Message;\n }\n\n async getMessages(filter: {\n status?: MessageStatus;\n state?: MessageState;\n size?: number;\n skip?: number;\n }): Promise<Message[]> {\n let messageIds: string[] = [];\n\n // Note: DispatcherQuery is now the preferred way to query with complex filters.\n // This method is kept for backward compatibility and simple state/queue polling.\n\n if (filter.state === MessageState.SCHEDULED) {\n const now = Date.now();\n const skip = filter.skip ?? 0;\n const size = filter.size ?? 0;\n\n // Use the QUEUE key for scheduled checking\n // IORedis: zrangebyscore(key, min, max, 'LIMIT', offset, count)\n if (size > 0) {\n messageIds = await this.client.zrangebyscore(\n this.getQueueKey('scheduled'),\n 0,\n now,\n 'LIMIT',\n skip,\n size\n );\n } else {\n messageIds = await this.client.zrangebyscore(this.getQueueKey('scheduled'), 0, now);\n }\n } else if (filter.state === MessageState.QUEUED) {\n // Use the QUEUE key for FIFO processing\n messageIds = await this.client.zrange(\n this.getQueueKey('queued'),\n filter.skip ?? 0,\n (filter.skip ?? 0) + (filter.size ? filter.size - 1 : -1)\n );\n } else if (filter.state === MessageState.DISPATCHED) {\n // Use the QUEUE key for Tracking\n messageIds = await this.client.zrange(\n this.getQueueKey('dispatched'),\n filter.skip ?? 0,\n (filter.skip ?? 0) + (filter.size ? filter.size - 1 : -1)\n );\n } else if (filter.status) {\n // Use the INDEX key for status\n const allIds = await this.client.smembers(this.getStatusKey(filter.status));\n const skip = filter.skip ?? 0;\n const size = filter.size;\n messageIds = size ? allIds.slice(skip, skip + size) : allIds.slice(skip);\n } else if (filter.state) {\n // Use the INDEX key for generic state querying\n try {\n const allIds = await this.client.smembers(this.getStateKey(filter.state));\n const skip = filter.skip ?? 0;\n const size = filter.size;\n messageIds = size ? allIds.slice(skip, skip + size) : allIds.slice(skip);\n } catch (error) {\n return [];\n }\n } else {\n logger.warn('[getMessages] no filter provided');\n return [];\n }\n\n const messages: Message[] = [];\n for (const messageId of messageIds) {\n const message = await this.getMessage(messageId);\n if (message) {\n if (filter.status && message.status !== filter.status) {\n continue;\n }\n if (filter.state && message.state !== filter.state) {\n continue;\n }\n messages.push(message);\n }\n }\n\n logger.debug('[getMessages]', { count: messages.length, filter });\n return messages;\n }\n\n async getQueueSize(): Promise<number> {\n return await this.client.zcard(this.getQueueKey('dispatched'));\n }\n\n async evictOldest(count: number): Promise<number> {\n if (count <= 0) return 0;\n\n // Evict from DISPATCHED ZSet (Oldest first -> min score)\n // IORedis: zpopmin(key, count)\n const popped = await this.client.zpopmin(this.getQueueKey('dispatched'), count);\n // popped is [value1, score1, value2, score2, ...]\n\n let evicted = 0;\n for (let i = 0; i < popped.length; i += 2) {\n const messageId = popped[i];\n await this.deleteMessage(messageId);\n evicted++;\n }\n\n return evicted;\n }\n\n async getExpiredMessages(count: number = 50): Promise<string[]> {\n const now = Date.now();\n return await this.client.zrangebyscore(\n this.getQueueKey('expiration'),\n 0,\n now,\n 'LIMIT',\n 0,\n count\n );\n }\n\n async getRetentionMessages(count: number = 50): Promise<string[]> {\n const now = Date.now();\n return await this.client.zrangebyscore(\n this.getQueueKey('retention'),\n 0,\n now,\n 'LIMIT',\n 0,\n count\n );\n }\n\n async incrementMetric(key: string, value: number = 1): Promise<number> {\n const metricKey = `${this.keyPrefix}:metrics:${key}`;\n return await this.client.incrby(metricKey, value);\n }\n\n async getMetric(key: string): Promise<number> {\n const metricKey = `${this.keyPrefix}:metrics:${key}`;\n const value = await this.client.get(metricKey);\n return value ? parseInt(value, 10) : 0;\n }\n\n async deleteMessage(messageId: string): Promise<void> {\n const msg = await this.getMessage(messageId);\n if (!msg) return;\n await this.deleteMessageData(messageId, msg);\n }\n\n private async deleteMessageData(messageId: string, message: Message): Promise<void> {\n const pipeline = this.client.pipeline();\n pipeline.del(this.getKey(messageId));\n\n // Remove from Queues\n pipeline.zrem(this.getQueueKey('scheduled'), messageId);\n pipeline.zrem(this.getQueueKey('queued'), messageId);\n pipeline.zrem(this.getQueueKey('dispatched'), messageId);\n pipeline.zrem(this.getQueueKey('expiration'), messageId);\n pipeline.zrem(this.getQueueKey('retention'), messageId);\n\n // Remove from Indices\n for (const status of DispatcherRepository.INDEXED_STATUSES) {\n pipeline.srem(this.getStatusKey(status), messageId);\n }\n\n for (const state of DispatcherRepository.INDEXED_STATES) {\n pipeline.srem(this.getStateKey(state), messageId);\n }\n\n if (message.contactId) {\n pipeline.srem(this.getContactKey(message.contactId), messageId);\n }\n\n if (message.descriptorId) {\n pipeline.srem(this.getDescriptorKey(message.descriptorId), messageId);\n }\n\n await pipeline.exec();\n }\n\n async countMessages(filter: { status?: MessageStatus; state?: MessageState }): Promise<number> {\n if (filter.state === MessageState.SCHEDULED) {\n return await this.client.zcard(this.getQueueKey('scheduled'));\n }\n\n if (filter.state === MessageState.QUEUED) {\n return await this.client.zcard(this.getQueueKey('queued'));\n }\n\n if (filter.state === MessageState.DISPATCHED) {\n return await this.client.zcard(this.getQueueKey('dispatched'));\n }\n\n if (filter.status) {\n return await this.client.scard(this.getStatusKey(filter.status));\n }\n\n if (filter.state) {\n try {\n return await this.client.scard(this.getStateKey(filter.state));\n } catch (error) {\n return 0;\n }\n }\n\n return 0;\n }\n\n async getMetrics(descriptorId?: string): Promise<any> {\n const metrics: any = {\n cumulative: { dispatched: 0, delivered: 0, failed: 0 },\n queues: { queued: 0, scheduled: 0, dispatched: 0 },\n status: {},\n };\n\n // Helper to get count\n const getCount = async (setKey: string) => {\n if (descriptorId) {\n const descriptorKey = this.getDescriptorKey(descriptorId);\n const intersection = await this.client.sinter(setKey, descriptorKey);\n return intersection.length;\n }\n return await this.client.scard(setKey);\n };\n\n // Status Counts\n for (const status of DispatcherRepository.INDEXED_STATUSES) {\n const count = await getCount(this.getStatusKey(status));\n metrics.status[status] = count;\n\n if (status === MessageStatus.DELIVERED) metrics.cumulative.delivered = count;\n if (status === MessageStatus.FAILED) metrics.cumulative.failed = count;\n }\n\n // Dispatched Total (approximate)\n const dispatchedKey = this.getStateKey(MessageState.DISPATCHED);\n const dispatchedCount = await getCount(dispatchedKey);\n metrics.cumulative.dispatched =\n dispatchedCount + metrics.cumulative.delivered + metrics.cumulative.failed;\n\n // Queues\n if (descriptorId) {\n // Filtered queues via indices (approximate intersection using state sets)\n metrics.queues.queued = await getCount(this.getStateKey(MessageState.QUEUED));\n metrics.queues.scheduled = await getCount(this.getStateKey(MessageState.SCHEDULED));\n metrics.queues.dispatched = dispatchedCount;\n } else {\n // Direct queue counts\n metrics.queues.queued = await this.client.zcard(this.getQueueKey('queued'));\n metrics.queues.scheduled = await this.client.zcard(this.getQueueKey('scheduled'));\n metrics.queues.dispatched = await this.client.zcard(this.getQueueKey('dispatched'));\n }\n\n return metrics;\n }\n\n async getDescriptors(): Promise<{ id: string; count: number }[]> {\n const pattern = this.getDescriptorKey('*');\n const descriptorPrefix = `${this.keyPrefix}:index:descriptor:`;\n const keys = await this.client.keys(pattern);\n\n const descriptors = [];\n for (const key of keys) {\n const id = key.slice(descriptorPrefix.length);\n if (id) {\n const count = await this.client.scard(key);\n descriptors.push({ id, count });\n }\n }\n\n descriptors.sort((a, b) => b.count - a.count);\n return descriptors;\n }\n\n async writeManifest(manifest: DispatcherManifest): Promise<void> {\n const key = this.getManifestKey();\n await this.client.hset(key, {\n version: manifest.version,\n createdAt: manifest.createdAt,\n updatedAt: manifest.updatedAt,\n });\n logger.info('[writeManifest] Manifest written', { key });\n }\n\n async getManifest(): Promise<DispatcherManifest | null> {\n const key = this.getManifestKey();\n const data = await this.client.hgetall(key);\n if (!data || Object.keys(data).length === 0) {\n return null;\n }\n return data as unknown as DispatcherManifest;\n }\n}\n"],"mappings":"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","names":["IORedis","logger","getLogger","_DispatcherRepository","dispatcherId","redisUrl","err","messageId","state","status","contactId","descriptorId","name","message","ttl","key","data","pipeline","indexKey","retentionTime","scheduledTime","score","filter","messageIds","now","skip","size","allIds","messages","count","popped","evicted","i","value","metricKey","msg","metrics","getCount","setKey","descriptorKey","dispatchedKey","dispatchedCount","pattern","descriptorPrefix","keys","descriptors","id","a","b","manifest","DispatcherRepository"]}
@@ -1,2 +1,2 @@
1
- import{b as y}from"./chunk-6IAFH374.mjs";import{b as u}from"./chunk-OXXLVJVC.mjs";var m=u("DispatcherQuery"),g=class{constructor(s){this.repository=s}get client(){return this.repository.redis}async query(s){let t=[],i=this.repository.keyPrefix;if(s.contactId&&t.push(this.repository.getContactKey(s.contactId)),s.descriptorId&&t.push(this.repository.getDescriptorKey(s.descriptorId)),s.status){let e=Array.isArray(s.status)?s.status:[s.status];e.length===1?t.push(this.repository.getStatusKey(e[0])):e.length>1&&t.push(this.repository.getStatusKey(e[0]))}if(s.state){let e=Array.isArray(s.state)?s.state:[s.state];e.length===1&&t.push(this.repository.getStateKey(e[0]))}let o=[];if(t.length>0)o=await this.client.sinter(t);else{let e=Object.values(y).map(r=>this.repository.getStatusKey(r));o=await this.client.sunion(e)}let p=s.skip??0,a=s.size??50,h=o.slice(p,p+a),n=[],c=[];for(let e of h){let r=await this.repository.getMessage(e);if(r){if(s.status&&!(Array.isArray(s.status)?s.status:[s.status]).includes(r.status)||s.state&&!(Array.isArray(s.state)?s.state:[s.state]).includes(r.state))continue;n.push(r)}else c.push(e)}return c.length>0&&this.cleanupIndices(c,s),n}async cleanupIndices(s,t){let i=this.client.pipeline(),o=this.repository.keyPrefix;t.contactId&&i.srem(this.repository.getContactKey(t.contactId),s),t.descriptorId&&i.srem(this.repository.getDescriptorKey(t.descriptorId),s),t.status&&(Array.isArray(t.status)?t.status:[t.status]).forEach(a=>{i.srem(this.repository.getStatusKey(a),s)}),t.state&&(Array.isArray(t.state)?t.state:[t.state]).forEach(a=>{i.srem(this.repository.getStateKey(a),s)}),await i.exec(),m.debug("[cleanupIndices] Removed expired IDs from checked indices",{count:s.length})}};export{g as a};
2
- //# sourceMappingURL=chunk-XLRB2LAD.mjs.map
1
+ import{b as y}from"./chunk-4LYB64T2.mjs";import{b as u}from"./chunk-OXXLVJVC.mjs";var m=u("DispatcherQuery"),g=class{constructor(s){this.repository=s}get client(){return this.repository.redis}async query(s){let t=[],i=this.repository.keyPrefix;if(s.contactId&&t.push(this.repository.getContactKey(s.contactId)),s.descriptorId&&t.push(this.repository.getDescriptorKey(s.descriptorId)),s.status){let e=Array.isArray(s.status)?s.status:[s.status];e.length===1?t.push(this.repository.getStatusKey(e[0])):e.length>1&&t.push(this.repository.getStatusKey(e[0]))}if(s.state){let e=Array.isArray(s.state)?s.state:[s.state];e.length===1&&t.push(this.repository.getStateKey(e[0]))}let o=[];if(t.length>0)o=await this.client.sinter(t);else{let e=Object.values(y).map(r=>this.repository.getStatusKey(r));o=await this.client.sunion(e)}let p=s.skip??0,a=s.size??50,h=o.slice(p,p+a),n=[],c=[];for(let e of h){let r=await this.repository.getMessage(e);if(r){if(s.status&&!(Array.isArray(s.status)?s.status:[s.status]).includes(r.status)||s.state&&!(Array.isArray(s.state)?s.state:[s.state]).includes(r.state))continue;n.push(r)}else c.push(e)}return c.length>0&&this.cleanupIndices(c,s),n}async cleanupIndices(s,t){let i=this.client.pipeline(),o=this.repository.keyPrefix;t.contactId&&i.srem(this.repository.getContactKey(t.contactId),s),t.descriptorId&&i.srem(this.repository.getDescriptorKey(t.descriptorId),s),t.status&&(Array.isArray(t.status)?t.status:[t.status]).forEach(a=>{i.srem(this.repository.getStatusKey(a),s)}),t.state&&(Array.isArray(t.state)?t.state:[t.state]).forEach(a=>{i.srem(this.repository.getStateKey(a),s)}),await i.exec(),m.debug("[cleanupIndices] Removed expired IDs from checked indices",{count:s.length})}};export{g as a};
2
+ //# sourceMappingURL=chunk-CVPGUSFU.mjs.map
@@ -0,0 +1,2 @@
1
+ var c=(b,a)=>()=>(a||b((a={exports:{}}).exports,a),a.exports);export{c as a};
2
+ //# sourceMappingURL=chunk-JD53PFR4.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,2 @@
1
+ import{a as D}from"./chunk-UFUGEIFL.mjs";import{a as I}from"./chunk-QA6PFVGP.mjs";import{a as v}from"./chunk-CVPGUSFU.mjs";import{a as f,b as y}from"./chunk-4LYB64T2.mjs";import{b as w}from"./chunk-OXXLVJVC.mjs";import{a as S}from"./chunk-JD53PFR4.mjs";var b=S((A,k)=>{k.exports={name:"@dawntech/dispatcher",version:"0.2.2",description:"A TypeScript Node.js package for sending push messages in conversational chatbots on the Blip platform.",main:"dist/index.js",module:"dist/index.mjs",types:"dist/index.d.ts",scripts:{start:"node dist/index.js",build:"tsup","build:watch":"tsup --watch",dev:"tsx watch src/server.ts","test:basic":"tsx tests/integration/scenarios/1-basic-send.ts","test:contact":"tsx tests/integration/scenarios/2-contact-update.ts","test:schedule":"tsx tests/integration/scenarios/3-scheduling.ts","test:status":"tsx tests/integration/scenarios/4-status-config.ts","test:intent":"tsx tests/integration/scenarios/5-intent.ts","test:rate-limit":"tsx tests/integration/scenarios/6-rate-limiting.ts","test:high-load":"tsx tests/integration/scenarios/7-high-load.ts","test:retries":"tsx tests/integration/scenarios/8-retries.ts","test:expiry":"tsx tests/integration/scenarios/9-expiration.ts","test:cluster":"tsx tests/integration/scenarios/10-cluster.ts","test:monitor":"tsx tests/integration/scenarios/11-monitor.ts",test:"jest","test:watch":"jest --watch","test:coverage":"jest --coverage","test:blip-api":"tsx tests/blip-api.ts",clean:"rm -rf dist",setup:"bash scripts/setup.sh","docker:up":"docker-compose up -d","docker:down":"docker-compose down","docker:logs":"docker-compose logs -f","docker:redis-cli":"docker-compose exec redis redis-cli","docker:clean":"docker-compose down -v","docker:restart":"docker-compose restart","docker:tools":"docker-compose --profile tools up -d",format:'prettier --write "src/**/*.ts" "tests/**/*.ts"',"format:check":'prettier --check "src/**/*.ts" "tests/**/*.ts"',prepublishOnly:"npm run build"},packageManager:"npm@11.8.0",devDependencies:{"@types/body-parser":"^1.19.6","@types/cors":"^2.8.19","@types/debug":"^4.1.12","@types/express":"^5.0.6","@types/ioredis":"^4.28.10","@types/jest":"^29.5.14","@types/lodash":"^4.17.20","@types/node":"^20.19.24",husky:"^9.1.7",jest:"^29.7.0","lint-staged":"^16.2.6",prettier:"^3.6.2","ts-jest":"^29.2.5",tsup:"^8.5.1",tsx:"^4.7.0",typescript:"^5.3.0"},keywords:[],author:"",license:"ISC",engines:{node:">=24.0.0"},dependencies:{axios:"^1.13.1","body-parser":"^2.2.2",bullmq:"^5.67.1",cors:"^2.8.6",debug:"^4.4.3",dotenv:"^17.2.3",express:"^5.2.1",ioredis:"^5.9.2",lodash:"^4.17.21","rate-limiter-flexible":"^9.0.1",redis:"^5.9.0",uuid:"^13.0.0"}}});import{v4 as E}from"uuid";import{Queue as N,Worker as x}from"bullmq";var{version:T}=b(),o=w("Dispatcher"),M=class{constructor(t,e,i,s){this.callbacks={};this.descriptors=new Map;this.isRunning=!1;this.setupCompleted=!1;this.id=t,this.repository=e,this.redis=this.repository.redis,this.stateMachine=new D(this.id,this.repository,(r,n)=>{this.emit(r,n),this.descriptors.get(n.descriptorId)?.emit(r,n,this.api,this.id)}),this.api=new I(i.contract,i.key),this.queueName=`dispatcher-${this.id.replace(/:/g,"-")}`,this.maxRetries=s?.maxRetries??0,this.retryIntervals=s?.retryIntervals??[1*1e3,5*1e3,15*1e3],this.timeouts={pending:s?.timeouts?.pending??120*1e3,sending:s?.timeouts?.sending??120*1e3},this.retention=s?.retention??2880*60*1e3,this.pollingIntervals={scheduled:s?.pollingIntervals?.scheduled??30*1e3,pending:s?.pollingIntervals?.pending??10*1e3,sending:s?.pollingIntervals?.sending??10*1e3,delivered:s?.pollingIntervals?.delivered??1800*1e3,read:s?.pollingIntervals?.read??1800*1e3,queue:s?.pollingIntervals?.queue??1*1e3},this.timeoutTimer=null,this.query=new v(this.repository),this.queue=new N(this.queueName,{connection:this.redis,defaultJobOptions:{removeOnComplete:!0,removeOnFail:!0}}),this.worker=new x(this.queueName,async r=>{try{await this.processJob(r)}catch(n){throw o.error(`[Worker] Job ${r.name} failed`,n),n}},{connection:this.redis,concurrency:s?.batchSize||50,limiter:s?.rateLimits?.global?{max:s.rateLimits.global.points,duration:s.rateLimits.global.duration*1e3}:void 0}),this.worker.on("error",r=>o.error("[Worker] Error",r)),this.worker.on("failed",(r,n)=>o.error(`[Worker] Job ${r?.id} failed`,n))}async setup(){if(this.setupCompleted)return;await this.repository.setup();let t=await this.repository.getManifest();await this.repository.writeManifest({version:T,createdAt:t?.createdAt??new Date().toISOString(),updatedAt:new Date().toISOString()}),await this.queue.waitUntilReady(),this.isRunning=!0,this.setupCompleted=!0,this.startTimeoutMonitor(),o.info("[setup] Dispatcher started (BullMQ)",{queue:this.queueName})}async teardown(){this.isRunning=!1,this.timeoutTimer&&(clearInterval(this.timeoutTimer),this.timeoutTimer=null),await this.queue.close(),await this.worker.close(),await this.repository.teardown(),o.info("[teardown] Dispatcher stopped")}on(t,e){return this.callbacks[t]=e,this}async getMetrics(){let t={total:0,byState:{},byStatus:{},cumulative:{dispatched:await this.repository.getMetric("dispatched"),delivered:await this.repository.getMetric("delivered"),failed:await this.repository.getMetric("failed")}},e=Object.values(f);for(let s of e)t.byState[s]=await this.repository.countMessages({state:s});let i=Object.values(y);for(let s of i)t.byStatus[s]=await this.repository.countMessages({status:s});return t.total=Object.values(t.byState).reduce((s,r)=>s+(r||0),0),t}emit(t,e){this.callbacks[t]?.(e,this.api,this.id)}async send(t,e,i,s){this.descriptors.set(t.id,t);let r=t.toContactId(e),n=t.transform(i),c=new Date().toISOString(),a={messageId:E(),contactId:r,descriptorId:t.id,payload:n,status:"INIT",state:"INIT",createdAt:c,attempts:0,retries:this.maxRetries},l={...t.messageOptions,...s},{schedule:u,...m}=l;a.options=m,this.emit("dispatch",a),t.emit("dispatch",a,this.api,this.id);let p=this.calculateScheduledTime(u,l.shifts),h=0;if(p){a.scheduledTo=p,a.state="SCHEDULED";let g=new Date(p).getTime();h=Math.max(0,g-Date.now()),this.emit("scheduled",a),t.emit("scheduled",a,this.api,this.id),o.info("[send] message scheduled",{messageId:a.messageId,scheduledTo:p,delay:h})}else a.state="QUEUED",a.status="INIT",o.info("[send] message queued",{messageId:a.messageId});return a.expiresAt=new Date(Date.now()+(a.state==="SCHEDULED"?h+this.retention:this.retention)).toISOString(),await this.stateMachine.transition(a,a.state,a.status),await this.queue.add("send",{messageId:a.messageId},{jobId:a.messageId,delay:h,priority:1}),a}async cancel(t){let e=await this.repository.getMessage(t);if(!e)return o.warn("[cancel] message not found",{messageId:t}),!1;if(e.state==="FINAL")return o.warn("[cancel] message already final",{messageId:t,status:e.status}),!1;let i=await this.queue.getJob(t);return i&&(await i.remove(),o.info("[cancel] removed job from queue",{messageId:t})),await this.stateMachine.transition(e,"FINAL","CANCELED"),o.info("[cancel] message canceled",{messageId:t}),!0}async processJob(t){let{messageId:e}=t.data,i=await this.repository.getMessage(e);if(!i){o.warn(`[processJob] Message not found: ${e}`);return}let s=this.descriptors.get(i.descriptorId)||null;switch(t.name){case"send":await this.handleSendJob(i,s);break;case"check":await this.handleCheckJob(i,s);break;default:o.warn(`[processJob] Unknown job name: ${t.name}`)}}async handleSendJob(t,e){t.lastDispatchAttemptAt=new Date().toISOString(),await this.stateMachine.transition(t,"DISPATCHED","PENDING");try{await this.api.sendMessage(t.contactId,t.payload,t.messageId),await this.handlePostSendOperations(t,t.options),t.sentAt=new Date().toISOString(),await this.stateMachine.transition(t,"DISPATCHED","PENDING"),o.info("[handleSendJob] Message sent to API",{messageId:t.messageId}),await this.repository.incrementMetric("dispatched"),await this.queue.add("check",{messageId:t.messageId},{delay:this.pollingIntervals.pending,priority:5})}catch(i){let s=i instanceof Error?i:new Error(String(i));await this.handleDispatchFailure(t,e,s)}}async handlePostSendOperations(t,e={}){let i={...e.contact||{}};if(e.intent)if(typeof e.intent=="string")i.intent=e.intent;else{i.intent=e.intent.intent;let{intent:s,...r}=e.intent;Object.entries(r).forEach(([n,c])=>{c!=null&&(i[n]=typeof c=="object"?JSON.stringify(c):String(c))})}Object.keys(i).length>0&&await this.api.mergeContact(t.contactId,i),e.state&&await this.api.setState(t.contactId,e.state.botId,e.state.stateId)}async handleCheckJob(t,e){if(t.state!=="FINAL"){if(this.checkAndHandleTimeout(t)){await this.handleTimeout(t,e);return}try{let i=await this.api.getDispatchState(t.messageId,t.contactId);if(!i){await this.rescheduleCheck(t,this.pollingIntervals.pending);return}let s=this.pollingIntervals.pending,r=!1;switch(i){case"accepted":t.status!=="SENDING"&&(await this.stateMachine.transition(t,t.state,"SENDING"),r=!0),s=this.pollingIntervals.sending;break;case"received":case"consumed":if(await this.api.getMessageAfter(t.contactId,t.messageId)){let d="REPLIED";t.status!==d&&t.status!=="READ"&&(await this.repository.incrementMetric("delivered"),await this.stateMachine.transition(t,"FINAL","REPLIED"),r=!0);break}let c=i==="consumed"?"READ":"DELIVERED";t.status!==c&&(await this.repository.incrementMetric("delivered"),await this.stateMachine.transition(t,t.state,c),r=!0);let a=t.options?.finalStatus||"DELIVERED";this.getStatusRank(t.status)>=this.getStatusRank(a)?(await this.stateMachine.transition(t,"FINAL",t.status),r=!0):s=this.pollingIntervals.delivered;break;case"failed":await this.handleDispatchFailure(t,e,new Error("Dispatch failed from Gateway"));return}t.state!=="FINAL"&&await this.rescheduleCheck(t,s)}catch(i){o.error("[handleCheckJob] Error",i),await this.rescheduleCheck(t,this.pollingIntervals.pending)}}}checkAndHandleTimeout(t){let e=new Date;if(t.status==="PENDING"){let i=t.lastDispatchAttemptAt||t.sentAt||t.createdAt;if(e.getTime()-new Date(i).getTime()>this.timeouts.pending)return!0}return!!(t.status==="SENDING"&&t.acceptedAt&&e.getTime()-new Date(t.acceptedAt).getTime()>this.timeouts.sending)}async handleTimeout(t,e){await this.stateMachine.transition(t,"FINAL","FAILED",{error:"Timeout Exceeded"}),o.info("[handleTimeout] Message timed out",{messageId:t.messageId})}startTimeoutMonitor(){this.timeoutTimer||(this.timeoutTimer=setInterval(async()=>{try{let t=await this.repository.getMessages({status:"PENDING"}),e=await this.repository.getMessages({status:"SENDING"}),i=[...t,...e];for(let r of i)if(this.checkAndHandleTimeout(r)){let n=this.descriptors.get(r.descriptorId)||null;await this.handleTimeout(r,n)}let s=await this.repository.getRetentionMessages(100);if(s.length>0){o.debug("[CleanupMonitor] Cleaning up expired messages",{count:s.length});for(let r of s)await this.repository.deleteMessage(r)}}catch(t){o.error("[TimeoutMonitor] Error during scan",t)}},10*1e3))}async rescheduleCheck(t,e){await this.queue.add("check",{messageId:t.messageId},{delay:e,priority:5})}async handleDispatchFailure(t,e,i){if(t.attempts=(t.attempts??0)+1,t.error=i.message,o.error("[handleDispatchFailure]",{messageId:t.messageId,attempts:t.attempts,maxRetries:this.maxRetries,error:i.message}),t.attempts<=this.maxRetries){t.retries=this.maxRetries-t.attempts;let s=this.retryIntervals[t.attempts-1]||this.retryIntervals[this.retryIntervals.length-1];await this.stateMachine.transition(t,"SCHEDULED",t.status),this.emit("retry",t),e?.emit("retry",t,this.api,this.id),await this.queue.add("send",{messageId:t.messageId},{delay:s,priority:1}),o.info("[handleDispatchFailure] Rescheduled retry",{messageId:t.messageId,retryDelay:s})}else t.retries=0,await this.stateMachine.transition(t,"FINAL","FAILED"),await this.repository.incrementMetric("failed")}calculateScheduledTime(t,e){if(t)return t;if(!e||e.length===0)return;let i=new Date;return this.isWithinShifts(i,e)?void 0:this.findNextShiftTime(i,e)?.toISOString()}isWithinShifts(t,e){let i=t.getDay(),s=i===0?64:Math.pow(2,i-1);for(let r of e){if((r.days&s)===0)continue;let n=r.gmt||"-3",c=parseInt(n,10),a=new Date(t.getTime()-c*60*60*1e3),d=a.getHours()*60+a.getMinutes(),[l,u]=r.start.split(":").map(Number),[m,p]=r.end.split(":").map(Number),h=l*60+u,g=m*60+p;if(d>=h&&d<g)return!0}return!1}findNextShiftTime(t,e){for(let s=0;s<=7;s++){let r=new Date(t);r.setDate(r.getDate()+s);let n=r.getDay(),c=n===0?64:Math.pow(2,n-1),a=e.filter(d=>(d.days&c)!==0);if(a.length!==0){a.sort((d,l)=>{let[u,m]=d.start.split(":").map(Number),[p,h]=l.start.split(":").map(Number);return u*60+m-(p*60+h)});for(let d of a){let l=d.gmt||"-3",u=parseInt(l,10),[m,p]=d.start.split(":").map(Number),h=new Date(r);h.setHours(m,p,0,0);let g=new Date(h.getTime()+u*60*60*1e3);if(s===0){if(g>t)return g}else return g}}}}getStatusRank(t){return{INIT:0,PENDING:1,SENDING:2,DELIVERED:3,READ:4,REPLIED:5,FAILED:6,CANCELED:6}[t]||0}};export{M as a};
2
+ //# sourceMappingURL=chunk-LKT4N6L5.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../package.json","../src/core/Dispatcher.ts"],"sourcesContent":["{\n \"name\": \"@dawntech/dispatcher\",\n \"version\": \"0.2.2\",\n \"description\": \"A TypeScript Node.js package for sending push messages in conversational chatbots on the Blip platform.\",\n \"main\": \"dist/index.js\",\n \"module\": \"dist/index.mjs\",\n \"types\": \"dist/index.d.ts\",\n \"scripts\": {\n \"start\": \"node dist/index.js\",\n \"build\": \"tsup\",\n \"build:watch\": \"tsup --watch\",\n \"dev\": \"tsx watch src/server.ts\",\n \"test:basic\": \"tsx tests/integration/scenarios/1-basic-send.ts\",\n \"test:contact\": \"tsx tests/integration/scenarios/2-contact-update.ts\",\n \"test:schedule\": \"tsx tests/integration/scenarios/3-scheduling.ts\",\n \"test:status\": \"tsx tests/integration/scenarios/4-status-config.ts\",\n \"test:intent\": \"tsx tests/integration/scenarios/5-intent.ts\",\n \"test:rate-limit\": \"tsx tests/integration/scenarios/6-rate-limiting.ts\",\n \"test:high-load\": \"tsx tests/integration/scenarios/7-high-load.ts\",\n \"test:retries\": \"tsx tests/integration/scenarios/8-retries.ts\",\n \"test:expiry\": \"tsx tests/integration/scenarios/9-expiration.ts\",\n \"test:cluster\": \"tsx tests/integration/scenarios/10-cluster.ts\",\n \"test:monitor\": \"tsx tests/integration/scenarios/11-monitor.ts\",\n \"test\": \"jest\",\n \"test:watch\": \"jest --watch\",\n \"test:coverage\": \"jest --coverage\",\n \"test:blip-api\": \"tsx tests/blip-api.ts\",\n \"clean\": \"rm -rf dist\",\n \"setup\": \"bash scripts/setup.sh\",\n \"docker:up\": \"docker-compose up -d\",\n \"docker:down\": \"docker-compose down\",\n \"docker:logs\": \"docker-compose logs -f\",\n \"docker:redis-cli\": \"docker-compose exec redis redis-cli\",\n \"docker:clean\": \"docker-compose down -v\",\n \"docker:restart\": \"docker-compose restart\",\n \"docker:tools\": \"docker-compose --profile tools up -d\",\n \"format\": \"prettier --write \\\"src/**/*.ts\\\" \\\"tests/**/*.ts\\\"\",\n \"format:check\": \"prettier --check \\\"src/**/*.ts\\\" \\\"tests/**/*.ts\\\"\",\n \"prepublishOnly\": \"npm run build\"\n },\n \"packageManager\": \"npm@11.8.0\",\n \"devDependencies\": {\n \"@types/body-parser\": \"^1.19.6\",\n \"@types/cors\": \"^2.8.19\",\n \"@types/debug\": \"^4.1.12\",\n \"@types/express\": \"^5.0.6\",\n \"@types/ioredis\": \"^4.28.10\",\n \"@types/jest\": \"^29.5.14\",\n \"@types/lodash\": \"^4.17.20\",\n \"@types/node\": \"^20.19.24\",\n \"husky\": \"^9.1.7\",\n \"jest\": \"^29.7.0\",\n \"lint-staged\": \"^16.2.6\",\n \"prettier\": \"^3.6.2\",\n \"ts-jest\": \"^29.2.5\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.7.0\",\n \"typescript\": \"^5.3.0\"\n },\n \"keywords\": [],\n \"author\": \"\",\n \"license\": \"ISC\",\n \"engines\": {\n \"node\": \">=24.0.0\"\n },\n \"dependencies\": {\n \"axios\": \"^1.13.1\",\n \"body-parser\": \"^2.2.2\",\n \"bullmq\": \"^5.67.1\",\n \"cors\": \"^2.8.6\",\n \"debug\": \"^4.4.3\",\n \"dotenv\": \"^17.2.3\",\n \"express\": \"^5.2.1\",\n \"ioredis\": \"^5.9.2\",\n \"lodash\": \"^4.17.21\",\n \"rate-limiter-flexible\": \"^9.0.1\",\n \"redis\": \"^5.9.0\",\n \"uuid\": \"^13.0.0\"\n }\n}\n","import { v4 as uuidv4 } from 'uuid';\nimport { Queue, Worker, Job, ConnectionOptions } from 'bullmq';\nimport IORedis from 'ioredis';\nimport { DispatcherDescriptor, CallbackMap } from './DispatcherDescriptor.js';\nimport { DispatcherRepository } from './DispatcherRepository.js';\nimport { DispatcherStateMachine } from './DispatcherStateMachine.js';\nimport { Blip } from './Blip.js';\nimport {\n Contact,\n Message,\n MessagePayload,\n MessageState,\n MessageStatus,\n MessageOptions,\n DispatchMessageOptions,\n DispatcherOptions,\n ConnectionConfig,\n Shift,\n DispatchState,\n CallbackEvent,\n DispatcherMetrics,\n QueryFilter,\n} from '../types/index.js';\nimport { getLogger } from '../utils/logger.js';\nimport { DispatcherQuery } from './DispatcherQuery.js';\n\n// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst { version: PACKAGE_VERSION } = require('../../package.json');\nconst logger = getLogger('Dispatcher');\n\nexport class Dispatcher {\n public readonly id: string;\n private repository: DispatcherRepository;\n private stateMachine: DispatcherStateMachine;\n\n private api: Blip;\n private callbacks: CallbackMap = {};\n private descriptors: Map<string, DispatcherDescriptor> = new Map();\n\n private queue: Queue;\n private worker: Worker;\n private redis: IORedis;\n private queueName: string;\n\n private maxRetries: number;\n private retryIntervals: number[];\n\n private timeouts: {\n pending: number;\n sending: number;\n };\n private retention: number;\n\n private timeoutTimer: NodeJS.Timeout | null;\n\n private pollingIntervals: {\n scheduled: number;\n pending: number;\n sending: number;\n delivered: number;\n read: number;\n queue: number;\n };\n\n private isRunning = false;\n private setupCompleted = false;\n public readonly query: DispatcherQuery;\n\n constructor(\n id: string,\n repository: DispatcherRepository,\n connection: ConnectionConfig,\n options?: DispatcherOptions\n ) {\n this.id = id;\n\n this.repository = repository;\n this.redis = this.repository.redis;\n\n // Initialize State Machine with emit callback\n this.stateMachine = new DispatcherStateMachine(this.id, this.repository, (event, message) => {\n this.emit(event, message);\n const descriptor = this.descriptors.get(message.descriptorId);\n descriptor?.emit(event, message, this.api, this.id);\n });\n\n this.api = new Blip(connection.contract, connection.key);\n this.queueName = `dispatcher-${this.id.replace(/:/g, '-')}`;\n\n this.maxRetries = options?.maxRetries ?? 0;\n this.retryIntervals = options?.retryIntervals ?? [1 * 1000, 5 * 1000, 15 * 1000];\n\n this.timeouts = {\n pending: options?.timeouts?.pending ?? 2 * 60 * 1000, // 2 minutes\n sending: options?.timeouts?.sending ?? 2 * 60 * 1000, // 2 minute\n };\n this.retention = options?.retention ?? 2 * 24 * 60 * 60 * 1000; // 2 days\n\n // Polling intervals for status checks (re-queue delays)\n this.pollingIntervals = {\n scheduled: options?.pollingIntervals?.scheduled ?? 30 * 1000,\n pending: options?.pollingIntervals?.pending ?? 10 * 1000,\n sending: options?.pollingIntervals?.sending ?? 10 * 1000,\n delivered: options?.pollingIntervals?.delivered ?? 30 * 60 * 1000,\n read: options?.pollingIntervals?.read ?? 30 * 60 * 1000,\n queue: options?.pollingIntervals?.queue ?? 1 * 1000,\n };\n this.timeoutTimer = null;\n this.query = new DispatcherQuery(this.repository);\n\n // Queue Configuration\n this.queue = new Queue(this.queueName, {\n connection: this.redis as ConnectionOptions,\n defaultJobOptions: {\n removeOnComplete: true, // Keep Redis clean\n removeOnFail: true, // We handle failures manually\n },\n });\n\n // Worker Configuration\n this.worker = new Worker(\n this.queueName,\n async (job: Job) => {\n try {\n await this.processJob(job);\n } catch (error) {\n logger.error(`[Worker] Job ${job.name} failed`, error);\n throw error; // Let BullMQ mark as failed\n }\n },\n {\n // @ts-ignore\n connection: this.redis,\n concurrency: options?.batchSize || 50, // Concurrency matches batch capability\n limiter: options?.rateLimits?.global\n ? {\n max: options.rateLimits.global.points,\n duration: options.rateLimits.global.duration * 1000,\n }\n : undefined, // Worker limiter is alternative to Queue limiter\n }\n );\n\n this.worker.on('error', (err) => logger.error('[Worker] Error', err));\n this.worker.on('failed', (job, err) => logger.error(`[Worker] Job ${job?.id} failed`, err));\n }\n\n async setup(): Promise<void> {\n if (this.setupCompleted) return;\n await this.repository.setup();\n\n // Write/update manifest\n const existing = await this.repository.getManifest();\n await this.repository.writeManifest({\n version: PACKAGE_VERSION,\n createdAt: existing?.createdAt ?? new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n });\n\n // Worker starts automatically, but we can verify connection\n await this.queue.waitUntilReady();\n this.isRunning = true;\n this.setupCompleted = true;\n this.startTimeoutMonitor();\n logger.info('[setup] Dispatcher started (BullMQ)', { queue: this.queueName });\n }\n\n async teardown(): Promise<void> {\n this.isRunning = false;\n if (this.timeoutTimer) {\n clearInterval(this.timeoutTimer);\n this.timeoutTimer = null;\n }\n await this.queue.close();\n await this.worker.close();\n await this.repository.teardown();\n logger.info('[teardown] Dispatcher stopped');\n }\n\n on(\n event: CallbackEvent,\n callback: (message: Message, api: Blip, dispatcherId: string) => void\n ): this {\n this.callbacks[event] = callback;\n return this;\n }\n\n async getMetrics(): Promise<DispatcherMetrics> {\n const metrics: DispatcherMetrics = {\n total: 0,\n byState: {},\n byStatus: {},\n cumulative: {\n dispatched: await this.repository.getMetric('dispatched'),\n delivered: await this.repository.getMetric('delivered'),\n failed: await this.repository.getMetric('failed'),\n },\n };\n\n // Use Repository to get counts (Persistent View)\n const states = Object.values(MessageState);\n for (const state of states) {\n metrics.byState[state] = await this.repository.countMessages({ state });\n }\n\n const statuses = Object.values(MessageStatus);\n for (const status of statuses) {\n metrics.byStatus[status] = await this.repository.countMessages({ status });\n }\n\n metrics.total = Object.values(metrics.byState).reduce((a, b) => a + (b || 0), 0);\n return metrics;\n }\n\n private emit(event: CallbackEvent, message: Message): void {\n this.callbacks[event]?.(message, this.api, this.id);\n }\n\n // ----------------------------------------------------------------------\n // SEND LOGIC\n // ----------------------------------------------------------------------\n\n async send(\n descriptor: DispatcherDescriptor,\n contactId: string,\n payload: MessagePayload,\n options?: DispatchMessageOptions\n ): Promise<Message> {\n this.descriptors.set(descriptor.id, descriptor);\n\n const validatedContactId = descriptor.toContactId(contactId);\n const messageData = descriptor.transform(payload);\n const now = new Date().toISOString();\n\n const message: Message = {\n messageId: uuidv4(),\n contactId: validatedContactId,\n descriptorId: descriptor.id,\n payload: messageData,\n status: MessageStatus.INIT,\n state: MessageState.INIT,\n createdAt: now,\n attempts: 0,\n retries: this.maxRetries,\n };\n\n // Options\n const descriptorOptions = descriptor.messageOptions;\n const mergedOptions: DispatchMessageOptions = { ...descriptorOptions, ...options };\n const { schedule, ...messageOptions } = mergedOptions;\n message.options = messageOptions;\n\n // Events\n this.emit('dispatch', message);\n descriptor.emit('dispatch', message, this.api, this.id);\n\n // Scheduling\n const scheduledTo = this.calculateScheduledTime(schedule, mergedOptions.shifts);\n let delay = 0;\n\n if (scheduledTo) {\n message.scheduledTo = scheduledTo;\n message.state = MessageState.SCHEDULED;\n\n const scheduleTime = new Date(scheduledTo).getTime();\n delay = Math.max(0, scheduleTime - Date.now());\n\n this.emit('scheduled', message);\n descriptor.emit('scheduled', message, this.api, this.id);\n logger.info('[send] message scheduled', { messageId: message.messageId, scheduledTo, delay });\n } else {\n message.state = MessageState.QUEUED;\n message.status = MessageStatus.INIT;\n logger.info('[send] message queued', { messageId: message.messageId });\n }\n\n // message.expiresAt is now managed by Redis TTL (Retention), separate from state timeouts.\n // We don't need to manually set `message.expiresAt` field unless we want to track it for debugging/API.\n // Let's set it to indicate when it WILL expire from Redis.\n message.expiresAt = new Date(\n Date.now() +\n (message.state === MessageState.SCHEDULED ? delay + this.retention : this.retention)\n ).toISOString();\n\n // Save Initial State\n await this.stateMachine.transition(message, message.state, message.status);\n\n // Add to Queue\n await this.queue.add(\n 'send',\n { messageId: message.messageId },\n {\n jobId: message.messageId, // Use messageId match\n delay,\n priority: 1, // High priority for sends\n }\n );\n\n return message;\n }\n\n async cancel(messageId: string): Promise<boolean> {\n const message = await this.repository.getMessage(messageId);\n\n if (!message) {\n logger.warn('[cancel] message not found', { messageId });\n return false;\n }\n\n if (message.state === MessageState.FINAL) {\n logger.warn('[cancel] message already final', { messageId, status: message.status });\n return false;\n }\n\n // Try to remove from BullMQ\n const job = await this.queue.getJob(messageId);\n if (job) {\n await job.remove();\n logger.info('[cancel] removed job from queue', { messageId });\n }\n\n await this.stateMachine.transition(message, MessageState.FINAL, MessageStatus.CANCELED);\n\n logger.info('[cancel] message canceled', { messageId });\n return true;\n }\n\n // ----------------------------------------------------------------------\n // WORKER PROCESSOR\n // ----------------------------------------------------------------------\n\n private async processJob(job: Job): Promise<void> {\n const { messageId } = job.data;\n const message = await this.repository.getMessage(messageId);\n\n if (!message) {\n logger.warn(`[processJob] Message not found: ${messageId}`);\n return;\n }\n\n // Refresh context\n const descriptor = this.descriptors.get(message.descriptorId) || null;\n\n switch (job.name) {\n case 'send':\n await this.handleSendJob(message, descriptor);\n break;\n case 'check':\n await this.handleCheckJob(message, descriptor);\n break;\n default:\n logger.warn(`[processJob] Unknown job name: ${job.name}`);\n }\n }\n\n private async handleSendJob(\n message: Message,\n descriptor: DispatcherDescriptor | null\n ): Promise<void> {\n message.lastDispatchAttemptAt = new Date().toISOString();\n await this.stateMachine.transition(message, MessageState.DISPATCHED, MessageStatus.PENDING);\n\n try {\n await this.api.sendMessage(message.contactId, message.payload, message.messageId);\n\n // Handle Extras/Intent/State\n await this.handlePostSendOperations(message, message.options);\n\n // Update Status: PENDING -> Wait for Accept/Deliver\n message.sentAt = new Date().toISOString();\n await this.stateMachine.transition(message, MessageState.DISPATCHED, MessageStatus.PENDING); // Persist sentAt update\n\n logger.info('[handleSendJob] Message sent to API', { messageId: message.messageId });\n await this.repository.incrementMetric('dispatched');\n\n // Schedule status check\n await this.queue.add(\n 'check',\n { messageId: message.messageId },\n {\n delay: this.pollingIntervals.pending,\n priority: 5, // Lower priority than sends\n }\n );\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n await this.handleDispatchFailure(message, descriptor, err);\n }\n }\n\n private async handlePostSendOperations(\n message: Message,\n options: MessageOptions = {}\n ): Promise<void> {\n const contact: Contact = { ...(options.contact || {}) };\n\n if (options.intent) {\n if (typeof options.intent === 'string') {\n contact.intent = options.intent;\n } else {\n contact.intent = options.intent.intent;\n const { intent, ...rest } = options.intent;\n Object.entries(rest).forEach(([key, value]) => {\n if (value !== undefined && value !== null) {\n contact[key] = typeof value === 'object' ? JSON.stringify(value) : String(value);\n }\n });\n }\n }\n\n if (Object.keys(contact).length > 0) {\n await this.api.mergeContact(message.contactId, contact);\n }\n\n if (options.state) {\n await this.api.setState(message.contactId, options.state.botId, options.state.stateId);\n }\n }\n\n private async handleCheckJob(\n message: Message,\n descriptor: DispatcherDescriptor | null\n ): Promise<void> {\n if (message.state === MessageState.FINAL) return;\n\n // Timeout Check logic is now handled by startTimeoutMonitor, but we can do a quick check here too.\n if (this.checkAndHandleTimeout(message)) {\n await this.handleTimeout(message, descriptor);\n return;\n }\n\n try {\n const state = await this.api.getDispatchState(message.messageId, message.contactId);\n if (!state) {\n // No state found yet? Schedule check again\n await this.rescheduleCheck(message, this.pollingIntervals.pending);\n return;\n }\n\n let nextCheckDelay = this.pollingIntervals.pending;\n let updated = false;\n\n switch (state) {\n case DispatchState.ACCEPTED:\n if (message.status !== MessageStatus.SENDING) {\n await this.stateMachine.transition(message, message.state, MessageStatus.SENDING);\n updated = true;\n }\n // Use longer interval for subsequent checks\n nextCheckDelay = this.pollingIntervals.sending;\n break;\n\n case DispatchState.RECEIVED:\n case DispatchState.CONSUMED:\n // Check for implicit reply first\n // If we find a reply, it means the user read and replied.\n const reply = await this.api.getMessageAfter(message.contactId, message.messageId);\n if (reply) {\n const newStatus = MessageStatus.REPLIED; // Using REPLIED if available, or READ\n if (message.status !== newStatus && message.status !== MessageStatus.READ) {\n // Implicit Read and Replied\n // We need to transition to REPLIED.\n // Note: StateMachine sets readAt/repliedAt if missing.\n await this.repository.incrementMetric('delivered'); // Ensure delivered count\n // Force FINAL state as REPLIED is terminal\n await this.stateMachine.transition(\n message,\n MessageState.FINAL,\n MessageStatus.REPLIED\n );\n updated = true;\n }\n break;\n }\n\n const newStatus =\n state === DispatchState.CONSUMED ? MessageStatus.READ : MessageStatus.DELIVERED;\n if (message.status !== newStatus) {\n // Increment delivered only on first transition to delivered/read/replied\n await this.repository.incrementMetric('delivered');\n await this.stateMachine.transition(message, message.state, newStatus);\n updated = true;\n }\n\n // Check for Final\n const finalStatus = message.options?.finalStatus || 'DELIVERED';\n if (\n this.getStatusRank(message.status) >= this.getStatusRank(finalStatus as MessageStatus)\n ) {\n await this.stateMachine.transition(message, MessageState.FINAL, message.status);\n updated = true;\n } else {\n nextCheckDelay = this.pollingIntervals.delivered;\n }\n break;\n\n case DispatchState.FAILED:\n await this.handleDispatchFailure(\n message,\n descriptor,\n new Error('Dispatch failed from Gateway')\n );\n return;\n }\n\n if (updated) {\n }\n\n // If not final, reschedule check\n if ((message.state as MessageState) !== MessageState.FINAL) {\n await this.rescheduleCheck(message, nextCheckDelay);\n }\n } catch (error) {\n logger.error('[handleCheckJob] Error', error);\n // Retry check later\n await this.rescheduleCheck(message, this.pollingIntervals.pending);\n }\n }\n\n private checkAndHandleTimeout(message: Message): boolean {\n const now = new Date();\n\n // Check Status Timeouts\n if (message.status === MessageStatus.PENDING) {\n const startTime = message.lastDispatchAttemptAt || message.sentAt || message.createdAt;\n if (now.getTime() - new Date(startTime).getTime() > this.timeouts.pending) return true;\n }\n\n if (message.status === MessageStatus.SENDING && message.acceptedAt) {\n if (now.getTime() - new Date(message.acceptedAt).getTime() > this.timeouts.sending)\n return true;\n }\n\n return false;\n }\n\n private async handleTimeout(\n message: Message,\n descriptor: DispatcherDescriptor | null\n ): Promise<void> {\n await this.stateMachine.transition(message, MessageState.FINAL, MessageStatus.FAILED, {\n error: 'Timeout Exceeded',\n });\n logger.info('[handleTimeout] Message timed out', { messageId: message.messageId });\n }\n\n private startTimeoutMonitor(): void {\n if (this.timeoutTimer) return;\n\n // Scan periodically (e.g. every 10s)\n this.timeoutTimer = setInterval(async () => {\n try {\n const pending = await this.repository.getMessages({ status: MessageStatus.PENDING });\n const sending = await this.repository.getMessages({ status: MessageStatus.SENDING });\n const all = [...pending, ...sending];\n\n for (const message of all) {\n if (this.checkAndHandleTimeout(message)) {\n const descriptor = this.descriptors.get(message.descriptorId) || null;\n await this.handleTimeout(message, descriptor);\n }\n }\n\n // --- Active Cleanup Monitor ---\n const expiredIds = await this.repository.getRetentionMessages(100);\n if (expiredIds.length > 0) {\n logger.debug('[CleanupMonitor] Cleaning up expired messages', {\n count: expiredIds.length,\n });\n for (const id of expiredIds) {\n await this.repository.deleteMessage(id);\n }\n }\n } catch (error) {\n logger.error('[TimeoutMonitor] Error during scan', error);\n }\n }, 10 * 1000);\n }\n\n private async rescheduleCheck(message: Message, delay: number): Promise<void> {\n await this.queue.add('check', { messageId: message.messageId }, { delay, priority: 5 });\n }\n\n private async handleDispatchFailure(\n message: Message,\n descriptor: DispatcherDescriptor | null,\n error: Error\n ): Promise<void> {\n message.attempts = (message.attempts ?? 0) + 1;\n message.error = error.message;\n\n logger.error('[handleDispatchFailure]', {\n messageId: message.messageId,\n attempts: message.attempts,\n maxRetries: this.maxRetries,\n error: error.message,\n });\n\n if (message.attempts <= this.maxRetries) {\n message.retries = this.maxRetries - message.attempts;\n const retryDelay =\n this.retryIntervals[message.attempts - 1] ||\n this.retryIntervals[this.retryIntervals.length - 1];\n\n await this.stateMachine.transition(message, MessageState.SCHEDULED, message.status);\n\n this.emit('retry', message);\n descriptor?.emit('retry', message, this.api, this.id);\n\n await this.queue.add(\n 'send',\n { messageId: message.messageId },\n {\n delay: retryDelay,\n priority: 1,\n }\n );\n\n logger.info('[handleDispatchFailure] Rescheduled retry', {\n messageId: message.messageId,\n retryDelay,\n });\n } else {\n message.retries = 0;\n await this.stateMachine.transition(message, MessageState.FINAL, MessageStatus.FAILED);\n await this.repository.incrementMetric('failed');\n }\n }\n\n private calculateScheduledTime(schedule?: string, shifts?: Shift[]): string | undefined {\n if (schedule) {\n return schedule;\n }\n\n if (!shifts || shifts.length === 0) {\n return undefined;\n }\n\n const now = new Date();\n\n if (this.isWithinShifts(now, shifts)) {\n return undefined;\n }\n\n const nextShiftTime = this.findNextShiftTime(now, shifts);\n return nextShiftTime?.toISOString();\n }\n\n private isWithinShifts(date: Date, shifts: Shift[]): boolean {\n const dayOfWeek = date.getDay();\n const dayBit = dayOfWeek === 0 ? 64 : Math.pow(2, dayOfWeek - 1);\n\n for (const shift of shifts) {\n if ((shift.days & dayBit) === 0) {\n continue;\n }\n\n const gmt = shift.gmt || '-3';\n const offset = parseInt(gmt, 10);\n const shiftDate = new Date(date.getTime() - offset * 60 * 60 * 1000);\n\n const currentTime = shiftDate.getHours() * 60 + shiftDate.getMinutes();\n const [startHour, startMin] = shift.start.split(':').map(Number);\n const [endHour, endMin] = shift.end.split(':').map(Number);\n const startTime = startHour * 60 + startMin;\n const endTime = endHour * 60 + endMin;\n\n if (currentTime >= startTime && currentTime < endTime) {\n return true;\n }\n }\n\n return false;\n }\n\n private findNextShiftTime(date: Date, shifts: Shift[]): Date | undefined {\n const maxDaysAhead = 7;\n\n for (let daysAhead = 0; daysAhead <= maxDaysAhead; daysAhead++) {\n const checkDate = new Date(date);\n checkDate.setDate(checkDate.getDate() + daysAhead);\n\n const dayOfWeek = checkDate.getDay();\n const dayBit = dayOfWeek === 0 ? 64 : Math.pow(2, dayOfWeek - 1);\n\n const availableShifts = shifts.filter((shift) => (shift.days & dayBit) !== 0);\n\n if (availableShifts.length === 0) {\n continue;\n }\n\n availableShifts.sort((a, b) => {\n const [aHour, aMin] = a.start.split(':').map(Number);\n const [bHour, bMin] = b.start.split(':').map(Number);\n return aHour * 60 + aMin - (bHour * 60 + bMin);\n });\n\n for (const shift of availableShifts) {\n const gmt = shift.gmt || '-3';\n const offset = parseInt(gmt, 10);\n\n const [startHour, startMin] = shift.start.split(':').map(Number);\n\n const shiftStart = new Date(checkDate);\n shiftStart.setHours(startHour, startMin, 0, 0);\n const shiftStartUTC = new Date(shiftStart.getTime() + offset * 60 * 60 * 1000);\n\n if (daysAhead === 0) {\n if (shiftStartUTC > date) {\n return shiftStartUTC;\n }\n } else {\n return shiftStartUTC;\n }\n }\n }\n\n return undefined;\n }\n\n private getStatusRank(status: MessageStatus): number {\n const ranks: Record<MessageStatus, number> = {\n [MessageStatus.INIT]: 0,\n [MessageStatus.PENDING]: 1,\n [MessageStatus.SENDING]: 2,\n [MessageStatus.DELIVERED]: 3,\n [MessageStatus.READ]: 4,\n [MessageStatus.REPLIED]: 5,\n [MessageStatus.FAILED]: 6,\n [MessageStatus.CANCELED]: 6,\n };\n return ranks[status] || 0;\n }\n}\n"],"mappings":"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","names":["require_package","__commonJSMin","exports","module","uuidv4","Queue","Worker","PACKAGE_VERSION","logger","getLogger","Dispatcher","id","repository","connection","options","DispatcherStateMachine","event","message","Blip","DispatcherQuery","Queue","Worker","job","error","err","existing","callback","metrics","states","MessageState","state","statuses","MessageStatus","status","a","b","descriptor","contactId","payload","validatedContactId","messageData","now","uuidv4","mergedOptions","schedule","messageOptions","scheduledTo","delay","scheduleTime","messageId","contact","intent","rest","key","value","nextCheckDelay","updated","newStatus","finalStatus","startTime","pending","sending","all","expiredIds","retryDelay","shifts","date","dayOfWeek","dayBit","shift","gmt","offset","shiftDate","currentTime","startHour","startMin","endHour","endMin","endTime","daysAhead","checkDate","availableShifts","aHour","aMin","bHour","bMin","shiftStart","shiftStartUTC"]}
@@ -1,2 +1,2 @@
1
- import{a as d,b as m}from"./chunk-6IAFH374.mjs";import{b as v}from"./chunk-OXXLVJVC.mjs";import{EventEmitter as b}from"events";import{Queue as M,Worker as S}from"bullmq";var n=v("DispatcherMonitor"),y=class extends b{constructor(t,e,s){super();this.history=[];this.lastAlerts={};this.activeAlerts=new Set;this.isRunning=!1;this.id=t,this.repository=e,this.options={interval:6e4,historySize:1e3,...s},this.queueName=`monitor-${this.id}`,this.queue=new M(this.queueName,{connection:e.redis,defaultJobOptions:{removeOnComplete:!0,removeOnFail:!0}}),this.worker=new S(this.queueName,async i=>{i.name==="check"&&await this.check()},{connection:e.redis}),this.worker.on("error",i=>n.error("[MonitorWorker] Error",i)),this.worker.on("failed",(i,a)=>n.error(`[MonitorWorker] Job ${i?.id} failed`,a))}async start(){this.isRunning||(n.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(),n.info("[Monitor] Stopped")}async collectMetrics(){let t=this.repository,e={total:0,byState:{},byStatus:{},cumulative:{dispatched:0,delivered:0,failed:0}},i=Object.values(d).map(async r=>{e.byState[r]=await t.countMessages({state:r})}),o=Object.values(m).map(async r=>{e.byStatus[r]=await t.countMessages({status:r})}),l=["dispatched","delivered","failed"].map(async r=>{e.cumulative[r]=await t.getMetric(r)});return await Promise.all([...i,...o,...l]),e.total=Object.values(e.byState).reduce((r,h)=>r+(h||0),0),e}async check(){try{let t=await this.collectMetrics(),e=Date.now();this.history.push({timestamp:e,metrics:t}),this.cleanHistory();for(let s of this.options.rules)await this.evaluateRule(s,t,e)}catch(t){n.error("[Monitor] Error during check",t)}}cleanHistory(){let t=this.options.historySize;this.history.length>t&&(this.history=this.history.slice(this.history.length-t));let e=Math.max(...this.options.rules.map(i=>i.window||0)),s=Date.now()-e-6e4;if(this.history.length>0&&this.history[0].timestamp<s){let i=this.history.findIndex(a=>a.timestamp>=s);i>0&&(this.history=this.history.slice(i))}}async evaluateRule(t,e,s){let i=`${t.type}`,a=!1,o=0,c={};switch(t.type){case"queue_size":let l=e.byState.FINAL||0;o=e.total-l,a=o>t.threshold,c={current:o,threshold:t.threshold};break;case"failure_rate":if(!t.window){n.warn("[Monitor] failure_rate rule missing window");return}let r=this.findSnapshotAt(s-t.window);if(!r)return;let h=e.cumulative.failed,f=r.metrics.cumulative.failed,p=h-f,w=e.cumulative.dispatched,g=r.metrics.cumulative.dispatched,u=w-g;u===0?o=0:o=p/u,a=o>t.threshold,c={rate:(o*100).toFixed(2)+"%",threshold:(t.threshold*100).toFixed(2)+"%",failed:p,dispatched:u,window:t.window};break}a?this.activeAlerts.has(i)?t.debounce&&!this.isDebounced(i,t.debounce)&&this.emitAlert(i,t,o,c):(this.emitAlert(i,t,o,c),this.activeAlerts.add(i)):this.activeAlerts.has(i)&&(this.resolveAlert(i,t),this.activeAlerts.delete(i))}isDebounced(t,e){if(!e)return!1;let s=this.lastAlerts[t];return s?Date.now()-s<e:!1}emitAlert(t,e,s,i){n.warn(`[Monitor] Alert triggered: ${e.type}`,i),this.lastAlerts[t]=Date.now();let a={type:e.type,message:`${e.type} exceeded threshold`,level:"warning",details:i,timestamp:new Date().toISOString()};this.emit("alert",a)}resolveAlert(t,e){n.info(`[Monitor] Alert resolved: ${e.type}`);let s={type:e.type,message:`${e.type} resolved`,level:"warning",details:{},timestamp:new Date().toISOString()};this.emit("resolved",s)}findSnapshotAt(t){if(this.history.length===0)return null;for(let e of this.history)if(e.timestamp>=t)return e;return this.history[0]}};export{y as a};
2
- //# sourceMappingURL=chunk-YDWZJWCU.mjs.map
1
+ import{a as d,b as m}from"./chunk-4LYB64T2.mjs";import{b as v}from"./chunk-OXXLVJVC.mjs";import{EventEmitter as b}from"events";import{Queue as M,Worker as S}from"bullmq";var n=v("DispatcherMonitor"),y=class extends b{constructor(t,e,s){super();this.history=[];this.lastAlerts={};this.activeAlerts=new Set;this.isRunning=!1;this.id=t,this.repository=e,this.options={interval:6e4,historySize:1e3,...s},this.queueName=`monitor-${this.id}`,this.queue=new M(this.queueName,{connection:e.redis,defaultJobOptions:{removeOnComplete:!0,removeOnFail:!0}}),this.worker=new S(this.queueName,async i=>{i.name==="check"&&await this.check()},{connection:e.redis}),this.worker.on("error",i=>n.error("[MonitorWorker] Error",i)),this.worker.on("failed",(i,a)=>n.error(`[MonitorWorker] Job ${i?.id} failed`,a))}async start(){this.isRunning||(n.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(),n.info("[Monitor] Stopped")}async collectMetrics(){let t=this.repository,e={total:0,byState:{},byStatus:{},cumulative:{dispatched:0,delivered:0,failed:0}},i=Object.values(d).map(async r=>{e.byState[r]=await t.countMessages({state:r})}),o=Object.values(m).map(async r=>{e.byStatus[r]=await t.countMessages({status:r})}),l=["dispatched","delivered","failed"].map(async r=>{e.cumulative[r]=await t.getMetric(r)});return await Promise.all([...i,...o,...l]),e.total=Object.values(e.byState).reduce((r,h)=>r+(h||0),0),e}async check(){try{let t=await this.collectMetrics(),e=Date.now();this.history.push({timestamp:e,metrics:t}),this.cleanHistory();for(let s of this.options.rules)await this.evaluateRule(s,t,e)}catch(t){n.error("[Monitor] Error during check",t)}}cleanHistory(){let t=this.options.historySize;this.history.length>t&&(this.history=this.history.slice(this.history.length-t));let e=Math.max(...this.options.rules.map(i=>i.window||0)),s=Date.now()-e-6e4;if(this.history.length>0&&this.history[0].timestamp<s){let i=this.history.findIndex(a=>a.timestamp>=s);i>0&&(this.history=this.history.slice(i))}}async evaluateRule(t,e,s){let i=`${t.type}`,a=!1,o=0,c={};switch(t.type){case"queue_size":let l=e.byState.FINAL||0;o=e.total-l,a=o>t.threshold,c={current:o,threshold:t.threshold};break;case"failure_rate":if(!t.window){n.warn("[Monitor] failure_rate rule missing window");return}let r=this.findSnapshotAt(s-t.window);if(!r)return;let h=e.cumulative.failed,f=r.metrics.cumulative.failed,p=h-f,w=e.cumulative.dispatched,g=r.metrics.cumulative.dispatched,u=w-g;u===0?o=0:o=p/u,a=o>t.threshold,c={rate:(o*100).toFixed(2)+"%",threshold:(t.threshold*100).toFixed(2)+"%",failed:p,dispatched:u,window:t.window};break}a?this.activeAlerts.has(i)?t.debounce&&!this.isDebounced(i,t.debounce)&&this.emitAlert(i,t,o,c):(this.emitAlert(i,t,o,c),this.activeAlerts.add(i)):this.activeAlerts.has(i)&&(this.resolveAlert(i,t),this.activeAlerts.delete(i))}isDebounced(t,e){if(!e)return!1;let s=this.lastAlerts[t];return s?Date.now()-s<e:!1}emitAlert(t,e,s,i){n.warn(`[Monitor] Alert triggered: ${e.type}`,i),this.lastAlerts[t]=Date.now();let a={type:e.type,message:`${e.type} exceeded threshold`,level:"warning",details:i,timestamp:new Date().toISOString()};this.emit("alert",a)}resolveAlert(t,e){n.info(`[Monitor] Alert resolved: ${e.type}`);let s={type:e.type,message:`${e.type} resolved`,level:"warning",details:{},timestamp:new Date().toISOString()};this.emit("resolved",s)}findSnapshotAt(t){if(this.history.length===0)return null;for(let e of this.history)if(e.timestamp>=t)return e;return this.history[0]}};export{y as a};
2
+ //# sourceMappingURL=chunk-VTJMLZE3.mjs.map