@elqnt/chat 2.0.8 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +386 -0
- package/dist/api/index.d.mts +250 -8
- package/dist/api/index.d.ts +250 -8
- package/dist/api/index.js +115 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/index.mjs +109 -0
- package/dist/api/index.mjs.map +1 -1
- package/dist/hooks/index.d.mts +78 -0
- package/dist/hooks/index.d.ts +78 -0
- package/dist/hooks/index.js +709 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/index.mjs +683 -0
- package/dist/hooks/index.mjs.map +1 -0
- package/dist/index.d.mts +4 -109
- package/dist/index.d.ts +4 -109
- package/dist/index.js +699 -2039
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +690 -2016
- package/dist/index.mjs.map +1 -1
- package/dist/models/index.d.mts +76 -6
- package/dist/models/index.d.ts +76 -6
- package/dist/models/index.js +21 -0
- package/dist/models/index.js.map +1 -1
- package/dist/models/index.mjs +14 -0
- package/dist/models/index.mjs.map +1 -1
- package/dist/transport/index.d.mts +243 -0
- package/dist/transport/index.d.ts +243 -0
- package/dist/transport/index.js +875 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/index.mjs +843 -0
- package/dist/transport/index.mjs.map +1 -0
- package/dist/types-BB5nRdZs.d.mts +222 -0
- package/dist/types-CNvuxtcv.d.ts +222 -0
- package/package.json +20 -38
- package/dist/hooks/use-websocket-chat-admin.d.mts +0 -17
- package/dist/hooks/use-websocket-chat-admin.d.ts +0 -17
- package/dist/hooks/use-websocket-chat-admin.js +0 -1196
- package/dist/hooks/use-websocket-chat-admin.js.map +0 -1
- package/dist/hooks/use-websocket-chat-admin.mjs +0 -1172
- package/dist/hooks/use-websocket-chat-admin.mjs.map +0 -1
- package/dist/hooks/use-websocket-chat-base.d.mts +0 -81
- package/dist/hooks/use-websocket-chat-base.d.ts +0 -81
- package/dist/hooks/use-websocket-chat-base.js +0 -1025
- package/dist/hooks/use-websocket-chat-base.js.map +0 -1
- package/dist/hooks/use-websocket-chat-base.mjs +0 -1001
- package/dist/hooks/use-websocket-chat-base.mjs.map +0 -1
- package/dist/hooks/use-websocket-chat-customer.d.mts +0 -24
- package/dist/hooks/use-websocket-chat-customer.d.ts +0 -24
- package/dist/hooks/use-websocket-chat-customer.js +0 -1092
- package/dist/hooks/use-websocket-chat-customer.js.map +0 -1
- package/dist/hooks/use-websocket-chat-customer.mjs +0 -1068
- package/dist/hooks/use-websocket-chat-customer.mjs.map +0 -1
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// transport/types.ts
|
|
4
|
+
function createLogger(debug = false) {
|
|
5
|
+
return {
|
|
6
|
+
debug: debug ? console.log.bind(console, "[chat]") : () => {
|
|
7
|
+
},
|
|
8
|
+
info: console.info.bind(console, "[chat]"),
|
|
9
|
+
warn: console.warn.bind(console, "[chat]"),
|
|
10
|
+
error: console.error.bind(console, "[chat]")
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
14
|
+
maxRetries: 10,
|
|
15
|
+
intervals: [1e3, 2e3, 5e3],
|
|
16
|
+
backoffMultiplier: 1.5,
|
|
17
|
+
maxBackoffTime: 3e4
|
|
18
|
+
};
|
|
19
|
+
function calculateRetryInterval(retryCount, config = DEFAULT_RETRY_CONFIG) {
|
|
20
|
+
const {
|
|
21
|
+
intervals = DEFAULT_RETRY_CONFIG.intervals,
|
|
22
|
+
backoffMultiplier = DEFAULT_RETRY_CONFIG.backoffMultiplier,
|
|
23
|
+
maxBackoffTime = DEFAULT_RETRY_CONFIG.maxBackoffTime
|
|
24
|
+
} = config;
|
|
25
|
+
if (retryCount < intervals.length) {
|
|
26
|
+
return intervals[retryCount];
|
|
27
|
+
}
|
|
28
|
+
const baseInterval = intervals[intervals.length - 1] || 5e3;
|
|
29
|
+
const backoffTime = baseInterval * Math.pow(backoffMultiplier, retryCount - intervals.length + 1);
|
|
30
|
+
return Math.min(backoffTime, maxBackoffTime);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// transport/sse.ts
|
|
34
|
+
function createSSETransport(options = {}) {
|
|
35
|
+
const {
|
|
36
|
+
retryConfig = DEFAULT_RETRY_CONFIG,
|
|
37
|
+
debug = false,
|
|
38
|
+
logger = createLogger(debug)
|
|
39
|
+
} = options;
|
|
40
|
+
let eventSource;
|
|
41
|
+
let config;
|
|
42
|
+
let state = "disconnected";
|
|
43
|
+
let error;
|
|
44
|
+
let retryCount = 0;
|
|
45
|
+
let reconnectTimeout;
|
|
46
|
+
let intentionalDisconnect = false;
|
|
47
|
+
const metrics = {
|
|
48
|
+
latency: 0,
|
|
49
|
+
messagesSent: 0,
|
|
50
|
+
messagesReceived: 0,
|
|
51
|
+
messagesQueued: 0,
|
|
52
|
+
reconnectCount: 0,
|
|
53
|
+
transportType: "sse"
|
|
54
|
+
};
|
|
55
|
+
const globalHandlers = /* @__PURE__ */ new Set();
|
|
56
|
+
const typeHandlers = /* @__PURE__ */ new Map();
|
|
57
|
+
function emit(event) {
|
|
58
|
+
metrics.messagesReceived++;
|
|
59
|
+
metrics.lastMessageAt = Date.now();
|
|
60
|
+
globalHandlers.forEach((handler) => {
|
|
61
|
+
try {
|
|
62
|
+
handler(event);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
logger.error("Error in message handler:", err);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const handlers = typeHandlers.get(event.type);
|
|
68
|
+
if (handlers) {
|
|
69
|
+
handlers.forEach((handler) => {
|
|
70
|
+
try {
|
|
71
|
+
handler(event);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logger.error(`Error in ${event.type} handler:`, err);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function sendRest(endpoint, body) {
|
|
79
|
+
if (!config) {
|
|
80
|
+
throw new Error("Transport not connected");
|
|
81
|
+
}
|
|
82
|
+
const url = `${config.baseUrl}/${endpoint}`;
|
|
83
|
+
logger.debug(`POST ${endpoint}`, body);
|
|
84
|
+
const response = await fetch(url, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: JSON.stringify(body)
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
const errorText = await response.text();
|
|
91
|
+
throw new Error(`API error: ${response.status} - ${errorText}`);
|
|
92
|
+
}
|
|
93
|
+
return response.json();
|
|
94
|
+
}
|
|
95
|
+
function handleMessage(event) {
|
|
96
|
+
if (!event.data || event.data === "") return;
|
|
97
|
+
try {
|
|
98
|
+
const data = JSON.parse(event.data);
|
|
99
|
+
logger.debug("Received:", data.type);
|
|
100
|
+
emit(data);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
logger.error("Failed to parse SSE message:", err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function setupEventListeners(es) {
|
|
106
|
+
es.addEventListener("message", handleMessage);
|
|
107
|
+
const eventTypes = [
|
|
108
|
+
"reconnected",
|
|
109
|
+
"typing",
|
|
110
|
+
"stopped_typing",
|
|
111
|
+
"waiting",
|
|
112
|
+
"waiting_for_agent",
|
|
113
|
+
"human_agent_joined",
|
|
114
|
+
"human_agent_left",
|
|
115
|
+
"chat_ended",
|
|
116
|
+
"chat_updated",
|
|
117
|
+
"load_chat_response",
|
|
118
|
+
"new_chat_created",
|
|
119
|
+
"show_csat_survey",
|
|
120
|
+
"csat_response",
|
|
121
|
+
"user_suggested_actions",
|
|
122
|
+
"agent_execution_started",
|
|
123
|
+
"agent_execution_ended",
|
|
124
|
+
"agent_context_update",
|
|
125
|
+
"plan_pending_approval",
|
|
126
|
+
"step_started",
|
|
127
|
+
"step_completed",
|
|
128
|
+
"step_failed",
|
|
129
|
+
"plan_completed",
|
|
130
|
+
"skills_changed",
|
|
131
|
+
"summary_update"
|
|
132
|
+
];
|
|
133
|
+
eventTypes.forEach((type) => {
|
|
134
|
+
es.addEventListener(type, handleMessage);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function scheduleReconnect() {
|
|
138
|
+
if (intentionalDisconnect || !config) return;
|
|
139
|
+
const maxRetries = retryConfig.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries;
|
|
140
|
+
if (retryCount >= maxRetries) {
|
|
141
|
+
logger.error(`Max retries (${maxRetries}) exceeded`);
|
|
142
|
+
error = {
|
|
143
|
+
code: "CONNECTION_FAILED",
|
|
144
|
+
message: `Max retries (${maxRetries}) exceeded`,
|
|
145
|
+
retryable: false,
|
|
146
|
+
timestamp: Date.now()
|
|
147
|
+
};
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const interval = calculateRetryInterval(retryCount, retryConfig);
|
|
151
|
+
retryCount++;
|
|
152
|
+
metrics.reconnectCount++;
|
|
153
|
+
logger.info(`Reconnecting in ${interval}ms (attempt ${retryCount})`);
|
|
154
|
+
state = "reconnecting";
|
|
155
|
+
reconnectTimeout = setTimeout(() => {
|
|
156
|
+
if (config) {
|
|
157
|
+
transport.connect(config).catch((err) => {
|
|
158
|
+
logger.error("Reconnect failed:", err);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}, interval);
|
|
162
|
+
}
|
|
163
|
+
const transport = {
|
|
164
|
+
async connect(cfg) {
|
|
165
|
+
config = cfg;
|
|
166
|
+
intentionalDisconnect = false;
|
|
167
|
+
if (eventSource) {
|
|
168
|
+
eventSource.close();
|
|
169
|
+
eventSource = void 0;
|
|
170
|
+
}
|
|
171
|
+
if (reconnectTimeout) {
|
|
172
|
+
clearTimeout(reconnectTimeout);
|
|
173
|
+
reconnectTimeout = void 0;
|
|
174
|
+
}
|
|
175
|
+
state = retryCount > 0 ? "reconnecting" : "connecting";
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
const connectionStart = Date.now();
|
|
178
|
+
const url = `${cfg.baseUrl}/stream?orgId=${cfg.orgId}&userId=${cfg.userId}&clientType=${cfg.clientType}${cfg.chatKey ? `&chatId=${cfg.chatKey}` : ""}`;
|
|
179
|
+
logger.debug("Connecting to:", url);
|
|
180
|
+
const es = new EventSource(url);
|
|
181
|
+
es.onopen = () => {
|
|
182
|
+
const connectionTime = Date.now() - connectionStart;
|
|
183
|
+
logger.info(`Connected in ${connectionTime}ms`);
|
|
184
|
+
state = "connected";
|
|
185
|
+
error = void 0;
|
|
186
|
+
retryCount = 0;
|
|
187
|
+
metrics.connectedAt = Date.now();
|
|
188
|
+
metrics.latency = connectionTime;
|
|
189
|
+
setupEventListeners(es);
|
|
190
|
+
resolve();
|
|
191
|
+
};
|
|
192
|
+
es.onerror = () => {
|
|
193
|
+
if (es.readyState === EventSource.CLOSED) {
|
|
194
|
+
const sseError = {
|
|
195
|
+
code: "CONNECTION_FAILED",
|
|
196
|
+
message: "SSE connection failed",
|
|
197
|
+
retryable: true,
|
|
198
|
+
timestamp: Date.now()
|
|
199
|
+
};
|
|
200
|
+
error = sseError;
|
|
201
|
+
metrics.lastError = sseError;
|
|
202
|
+
state = "disconnected";
|
|
203
|
+
if (!intentionalDisconnect) {
|
|
204
|
+
scheduleReconnect();
|
|
205
|
+
}
|
|
206
|
+
if (retryCount === 0) {
|
|
207
|
+
reject(sseError);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
eventSource = es;
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
disconnect(intentional = true) {
|
|
215
|
+
logger.info("Disconnecting", { intentional });
|
|
216
|
+
intentionalDisconnect = intentional;
|
|
217
|
+
if (reconnectTimeout) {
|
|
218
|
+
clearTimeout(reconnectTimeout);
|
|
219
|
+
reconnectTimeout = void 0;
|
|
220
|
+
}
|
|
221
|
+
if (eventSource) {
|
|
222
|
+
eventSource.close();
|
|
223
|
+
eventSource = void 0;
|
|
224
|
+
}
|
|
225
|
+
state = "disconnected";
|
|
226
|
+
retryCount = 0;
|
|
227
|
+
},
|
|
228
|
+
async send(event) {
|
|
229
|
+
if (!config) {
|
|
230
|
+
throw new Error("Transport not connected");
|
|
231
|
+
}
|
|
232
|
+
switch (event.type) {
|
|
233
|
+
case "message":
|
|
234
|
+
await sendRest("send", {
|
|
235
|
+
orgId: event.orgId,
|
|
236
|
+
chatKey: event.chatKey,
|
|
237
|
+
userId: event.userId,
|
|
238
|
+
message: event.message
|
|
239
|
+
});
|
|
240
|
+
break;
|
|
241
|
+
case "typing":
|
|
242
|
+
await sendRest("typing", {
|
|
243
|
+
orgId: event.orgId,
|
|
244
|
+
chatKey: event.chatKey,
|
|
245
|
+
userId: event.userId,
|
|
246
|
+
typing: true
|
|
247
|
+
});
|
|
248
|
+
break;
|
|
249
|
+
case "stopped_typing":
|
|
250
|
+
await sendRest("typing", {
|
|
251
|
+
orgId: event.orgId,
|
|
252
|
+
chatKey: event.chatKey,
|
|
253
|
+
userId: event.userId,
|
|
254
|
+
typing: false
|
|
255
|
+
});
|
|
256
|
+
break;
|
|
257
|
+
case "load_chat":
|
|
258
|
+
await sendRest("load", {
|
|
259
|
+
orgId: event.orgId,
|
|
260
|
+
chatKey: event.chatKey,
|
|
261
|
+
userId: event.userId
|
|
262
|
+
});
|
|
263
|
+
break;
|
|
264
|
+
case "new_chat":
|
|
265
|
+
await sendRest("create", {
|
|
266
|
+
orgId: event.orgId,
|
|
267
|
+
userId: event.userId,
|
|
268
|
+
metadata: event.data
|
|
269
|
+
});
|
|
270
|
+
break;
|
|
271
|
+
case "end_chat":
|
|
272
|
+
await sendRest("end", {
|
|
273
|
+
orgId: event.orgId,
|
|
274
|
+
chatKey: event.chatKey,
|
|
275
|
+
userId: event.userId,
|
|
276
|
+
data: event.data
|
|
277
|
+
});
|
|
278
|
+
break;
|
|
279
|
+
default:
|
|
280
|
+
await sendRest("event", {
|
|
281
|
+
type: event.type,
|
|
282
|
+
orgId: event.orgId,
|
|
283
|
+
chatKey: event.chatKey,
|
|
284
|
+
userId: event.userId,
|
|
285
|
+
data: event.data
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
metrics.messagesSent++;
|
|
289
|
+
},
|
|
290
|
+
async sendMessage(message) {
|
|
291
|
+
if (!config) {
|
|
292
|
+
throw new Error("Transport not connected");
|
|
293
|
+
}
|
|
294
|
+
await sendRest("send", {
|
|
295
|
+
orgId: config.orgId,
|
|
296
|
+
chatKey: config.chatKey,
|
|
297
|
+
userId: config.userId,
|
|
298
|
+
message
|
|
299
|
+
});
|
|
300
|
+
metrics.messagesSent++;
|
|
301
|
+
},
|
|
302
|
+
onMessage(handler) {
|
|
303
|
+
globalHandlers.add(handler);
|
|
304
|
+
return () => globalHandlers.delete(handler);
|
|
305
|
+
},
|
|
306
|
+
on(eventType, handler) {
|
|
307
|
+
if (!typeHandlers.has(eventType)) {
|
|
308
|
+
typeHandlers.set(eventType, /* @__PURE__ */ new Set());
|
|
309
|
+
}
|
|
310
|
+
typeHandlers.get(eventType).add(handler);
|
|
311
|
+
return () => {
|
|
312
|
+
const handlers = typeHandlers.get(eventType);
|
|
313
|
+
if (handlers) {
|
|
314
|
+
handlers.delete(handler);
|
|
315
|
+
if (handlers.size === 0) {
|
|
316
|
+
typeHandlers.delete(eventType);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
getState() {
|
|
322
|
+
return state;
|
|
323
|
+
},
|
|
324
|
+
getMetrics() {
|
|
325
|
+
return { ...metrics };
|
|
326
|
+
},
|
|
327
|
+
getError() {
|
|
328
|
+
return error;
|
|
329
|
+
},
|
|
330
|
+
clearError() {
|
|
331
|
+
error = void 0;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
return transport;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// transport/sse-fetch.ts
|
|
338
|
+
function createFetchSSETransport(options = {}) {
|
|
339
|
+
const {
|
|
340
|
+
retryConfig = DEFAULT_RETRY_CONFIG,
|
|
341
|
+
debug = false,
|
|
342
|
+
logger = createLogger(debug),
|
|
343
|
+
customFetch = fetch
|
|
344
|
+
} = options;
|
|
345
|
+
let abortController;
|
|
346
|
+
let config;
|
|
347
|
+
let state = "disconnected";
|
|
348
|
+
let error;
|
|
349
|
+
let retryCount = 0;
|
|
350
|
+
let reconnectTimeout;
|
|
351
|
+
let intentionalDisconnect = false;
|
|
352
|
+
const metrics = {
|
|
353
|
+
latency: 0,
|
|
354
|
+
messagesSent: 0,
|
|
355
|
+
messagesReceived: 0,
|
|
356
|
+
messagesQueued: 0,
|
|
357
|
+
reconnectCount: 0,
|
|
358
|
+
transportType: "sse-fetch"
|
|
359
|
+
};
|
|
360
|
+
const globalHandlers = /* @__PURE__ */ new Set();
|
|
361
|
+
const typeHandlers = /* @__PURE__ */ new Map();
|
|
362
|
+
function emit(event) {
|
|
363
|
+
metrics.messagesReceived++;
|
|
364
|
+
metrics.lastMessageAt = Date.now();
|
|
365
|
+
globalHandlers.forEach((handler) => {
|
|
366
|
+
try {
|
|
367
|
+
handler(event);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
logger.error("Error in message handler:", err);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
const handlers = typeHandlers.get(event.type);
|
|
373
|
+
if (handlers) {
|
|
374
|
+
handlers.forEach((handler) => {
|
|
375
|
+
try {
|
|
376
|
+
handler(event);
|
|
377
|
+
} catch (err) {
|
|
378
|
+
logger.error(`Error in ${event.type} handler:`, err);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async function sendRest(endpoint, body) {
|
|
384
|
+
if (!config) {
|
|
385
|
+
throw new Error("Transport not connected");
|
|
386
|
+
}
|
|
387
|
+
const url = `${config.baseUrl}/${endpoint}`;
|
|
388
|
+
logger.debug(`POST ${endpoint}`, body);
|
|
389
|
+
const response = await customFetch(url, {
|
|
390
|
+
method: "POST",
|
|
391
|
+
headers: { "Content-Type": "application/json" },
|
|
392
|
+
body: JSON.stringify(body)
|
|
393
|
+
});
|
|
394
|
+
if (!response.ok) {
|
|
395
|
+
const errorText = await response.text();
|
|
396
|
+
throw new Error(`API error: ${response.status} - ${errorText}`);
|
|
397
|
+
}
|
|
398
|
+
return response.json();
|
|
399
|
+
}
|
|
400
|
+
function parseSSEChunk(chunk) {
|
|
401
|
+
const events = [];
|
|
402
|
+
const lines = chunk.split("\n");
|
|
403
|
+
let eventType = "message";
|
|
404
|
+
let data = "";
|
|
405
|
+
for (const line of lines) {
|
|
406
|
+
if (line.startsWith("event:")) {
|
|
407
|
+
eventType = line.slice(6).trim();
|
|
408
|
+
} else if (line.startsWith("data:")) {
|
|
409
|
+
data = line.slice(5).trim();
|
|
410
|
+
} else if (line === "" && data) {
|
|
411
|
+
try {
|
|
412
|
+
const parsed = JSON.parse(data);
|
|
413
|
+
events.push(parsed);
|
|
414
|
+
} catch {
|
|
415
|
+
logger.warn("Failed to parse SSE data:", data);
|
|
416
|
+
}
|
|
417
|
+
eventType = "message";
|
|
418
|
+
data = "";
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return events;
|
|
422
|
+
}
|
|
423
|
+
async function startStream(cfg) {
|
|
424
|
+
const url = `${cfg.baseUrl}/stream?orgId=${cfg.orgId}&userId=${cfg.userId}&clientType=${cfg.clientType}${cfg.chatKey ? `&chatId=${cfg.chatKey}` : ""}`;
|
|
425
|
+
logger.debug("Connecting to:", url);
|
|
426
|
+
abortController = new AbortController();
|
|
427
|
+
const response = await customFetch(url, {
|
|
428
|
+
method: "GET",
|
|
429
|
+
headers: {
|
|
430
|
+
Accept: "text/event-stream",
|
|
431
|
+
"Cache-Control": "no-cache"
|
|
432
|
+
},
|
|
433
|
+
signal: abortController.signal
|
|
434
|
+
});
|
|
435
|
+
if (!response.ok) {
|
|
436
|
+
throw new Error(`Stream connection failed: ${response.status}`);
|
|
437
|
+
}
|
|
438
|
+
if (!response.body) {
|
|
439
|
+
throw new Error("Response body is null - ReadableStream not supported");
|
|
440
|
+
}
|
|
441
|
+
const reader = response.body.getReader();
|
|
442
|
+
const decoder = new TextDecoder();
|
|
443
|
+
let buffer = "";
|
|
444
|
+
const readStream = async () => {
|
|
445
|
+
try {
|
|
446
|
+
while (true) {
|
|
447
|
+
const { done, value } = await reader.read();
|
|
448
|
+
if (done) {
|
|
449
|
+
logger.info("Stream ended");
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
buffer += decoder.decode(value, { stream: true });
|
|
453
|
+
const lastNewline = buffer.lastIndexOf("\n\n");
|
|
454
|
+
if (lastNewline !== -1) {
|
|
455
|
+
const complete = buffer.slice(0, lastNewline + 2);
|
|
456
|
+
buffer = buffer.slice(lastNewline + 2);
|
|
457
|
+
const events = parseSSEChunk(complete);
|
|
458
|
+
events.forEach(emit);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
} catch (err) {
|
|
462
|
+
if (err.name === "AbortError") {
|
|
463
|
+
logger.debug("Stream aborted");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
logger.error("Stream error:", err);
|
|
467
|
+
throw err;
|
|
468
|
+
}
|
|
469
|
+
if (!intentionalDisconnect) {
|
|
470
|
+
state = "disconnected";
|
|
471
|
+
scheduleReconnect();
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
readStream().catch((err) => {
|
|
475
|
+
if (!intentionalDisconnect) {
|
|
476
|
+
error = {
|
|
477
|
+
code: "CONNECTION_FAILED",
|
|
478
|
+
message: err.message,
|
|
479
|
+
retryable: true,
|
|
480
|
+
timestamp: Date.now(),
|
|
481
|
+
originalError: err
|
|
482
|
+
};
|
|
483
|
+
metrics.lastError = error;
|
|
484
|
+
state = "disconnected";
|
|
485
|
+
scheduleReconnect();
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
function scheduleReconnect() {
|
|
490
|
+
if (intentionalDisconnect || !config) return;
|
|
491
|
+
const maxRetries = retryConfig.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries;
|
|
492
|
+
if (retryCount >= maxRetries) {
|
|
493
|
+
logger.error(`Max retries (${maxRetries}) exceeded`);
|
|
494
|
+
error = {
|
|
495
|
+
code: "CONNECTION_FAILED",
|
|
496
|
+
message: `Max retries (${maxRetries}) exceeded`,
|
|
497
|
+
retryable: false,
|
|
498
|
+
timestamp: Date.now()
|
|
499
|
+
};
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const interval = calculateRetryInterval(retryCount, retryConfig);
|
|
503
|
+
retryCount++;
|
|
504
|
+
metrics.reconnectCount++;
|
|
505
|
+
logger.info(`Reconnecting in ${interval}ms (attempt ${retryCount})`);
|
|
506
|
+
state = "reconnecting";
|
|
507
|
+
reconnectTimeout = setTimeout(() => {
|
|
508
|
+
if (config) {
|
|
509
|
+
transport.connect(config).catch((err) => {
|
|
510
|
+
logger.error("Reconnect failed:", err);
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}, interval);
|
|
514
|
+
}
|
|
515
|
+
const transport = {
|
|
516
|
+
async connect(cfg) {
|
|
517
|
+
config = cfg;
|
|
518
|
+
intentionalDisconnect = false;
|
|
519
|
+
if (abortController) {
|
|
520
|
+
abortController.abort();
|
|
521
|
+
abortController = void 0;
|
|
522
|
+
}
|
|
523
|
+
if (reconnectTimeout) {
|
|
524
|
+
clearTimeout(reconnectTimeout);
|
|
525
|
+
reconnectTimeout = void 0;
|
|
526
|
+
}
|
|
527
|
+
state = retryCount > 0 ? "reconnecting" : "connecting";
|
|
528
|
+
const connectionStart = Date.now();
|
|
529
|
+
try {
|
|
530
|
+
await startStream(cfg);
|
|
531
|
+
const connectionTime = Date.now() - connectionStart;
|
|
532
|
+
logger.info(`Connected in ${connectionTime}ms`);
|
|
533
|
+
state = "connected";
|
|
534
|
+
error = void 0;
|
|
535
|
+
retryCount = 0;
|
|
536
|
+
metrics.connectedAt = Date.now();
|
|
537
|
+
metrics.latency = connectionTime;
|
|
538
|
+
} catch (err) {
|
|
539
|
+
const connectError = {
|
|
540
|
+
code: "CONNECTION_FAILED",
|
|
541
|
+
message: err.message,
|
|
542
|
+
retryable: true,
|
|
543
|
+
timestamp: Date.now(),
|
|
544
|
+
originalError: err
|
|
545
|
+
};
|
|
546
|
+
error = connectError;
|
|
547
|
+
metrics.lastError = connectError;
|
|
548
|
+
state = "disconnected";
|
|
549
|
+
if (!intentionalDisconnect) {
|
|
550
|
+
scheduleReconnect();
|
|
551
|
+
}
|
|
552
|
+
throw connectError;
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
disconnect(intentional = true) {
|
|
556
|
+
logger.info("Disconnecting", { intentional });
|
|
557
|
+
intentionalDisconnect = intentional;
|
|
558
|
+
if (reconnectTimeout) {
|
|
559
|
+
clearTimeout(reconnectTimeout);
|
|
560
|
+
reconnectTimeout = void 0;
|
|
561
|
+
}
|
|
562
|
+
if (abortController) {
|
|
563
|
+
abortController.abort();
|
|
564
|
+
abortController = void 0;
|
|
565
|
+
}
|
|
566
|
+
state = "disconnected";
|
|
567
|
+
retryCount = 0;
|
|
568
|
+
},
|
|
569
|
+
async send(event) {
|
|
570
|
+
if (!config) {
|
|
571
|
+
throw new Error("Transport not connected");
|
|
572
|
+
}
|
|
573
|
+
switch (event.type) {
|
|
574
|
+
case "message":
|
|
575
|
+
await sendRest("send", {
|
|
576
|
+
orgId: event.orgId,
|
|
577
|
+
chatKey: event.chatKey,
|
|
578
|
+
userId: event.userId,
|
|
579
|
+
message: event.message
|
|
580
|
+
});
|
|
581
|
+
break;
|
|
582
|
+
case "typing":
|
|
583
|
+
await sendRest("typing", {
|
|
584
|
+
orgId: event.orgId,
|
|
585
|
+
chatKey: event.chatKey,
|
|
586
|
+
userId: event.userId,
|
|
587
|
+
typing: true
|
|
588
|
+
});
|
|
589
|
+
break;
|
|
590
|
+
case "stopped_typing":
|
|
591
|
+
await sendRest("typing", {
|
|
592
|
+
orgId: event.orgId,
|
|
593
|
+
chatKey: event.chatKey,
|
|
594
|
+
userId: event.userId,
|
|
595
|
+
typing: false
|
|
596
|
+
});
|
|
597
|
+
break;
|
|
598
|
+
case "load_chat":
|
|
599
|
+
await sendRest("load", {
|
|
600
|
+
orgId: event.orgId,
|
|
601
|
+
chatKey: event.chatKey,
|
|
602
|
+
userId: event.userId
|
|
603
|
+
});
|
|
604
|
+
break;
|
|
605
|
+
case "new_chat":
|
|
606
|
+
await sendRest("create", {
|
|
607
|
+
orgId: event.orgId,
|
|
608
|
+
userId: event.userId,
|
|
609
|
+
metadata: event.data
|
|
610
|
+
});
|
|
611
|
+
break;
|
|
612
|
+
case "end_chat":
|
|
613
|
+
await sendRest("end", {
|
|
614
|
+
orgId: event.orgId,
|
|
615
|
+
chatKey: event.chatKey,
|
|
616
|
+
userId: event.userId,
|
|
617
|
+
data: event.data
|
|
618
|
+
});
|
|
619
|
+
break;
|
|
620
|
+
default:
|
|
621
|
+
await sendRest("event", {
|
|
622
|
+
type: event.type,
|
|
623
|
+
orgId: event.orgId,
|
|
624
|
+
chatKey: event.chatKey,
|
|
625
|
+
userId: event.userId,
|
|
626
|
+
data: event.data
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
metrics.messagesSent++;
|
|
630
|
+
},
|
|
631
|
+
async sendMessage(message) {
|
|
632
|
+
if (!config) {
|
|
633
|
+
throw new Error("Transport not connected");
|
|
634
|
+
}
|
|
635
|
+
await sendRest("send", {
|
|
636
|
+
orgId: config.orgId,
|
|
637
|
+
chatKey: config.chatKey,
|
|
638
|
+
userId: config.userId,
|
|
639
|
+
message
|
|
640
|
+
});
|
|
641
|
+
metrics.messagesSent++;
|
|
642
|
+
},
|
|
643
|
+
onMessage(handler) {
|
|
644
|
+
globalHandlers.add(handler);
|
|
645
|
+
return () => globalHandlers.delete(handler);
|
|
646
|
+
},
|
|
647
|
+
on(eventType, handler) {
|
|
648
|
+
if (!typeHandlers.has(eventType)) {
|
|
649
|
+
typeHandlers.set(eventType, /* @__PURE__ */ new Set());
|
|
650
|
+
}
|
|
651
|
+
typeHandlers.get(eventType).add(handler);
|
|
652
|
+
return () => {
|
|
653
|
+
const handlers = typeHandlers.get(eventType);
|
|
654
|
+
if (handlers) {
|
|
655
|
+
handlers.delete(handler);
|
|
656
|
+
if (handlers.size === 0) {
|
|
657
|
+
typeHandlers.delete(eventType);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
},
|
|
662
|
+
getState() {
|
|
663
|
+
return state;
|
|
664
|
+
},
|
|
665
|
+
getMetrics() {
|
|
666
|
+
return { ...metrics };
|
|
667
|
+
},
|
|
668
|
+
getError() {
|
|
669
|
+
return error;
|
|
670
|
+
},
|
|
671
|
+
clearError() {
|
|
672
|
+
error = void 0;
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
return transport;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// transport/whatsapp.ts
|
|
679
|
+
function createWhatsAppTransport(options = {}) {
|
|
680
|
+
const { debug = false, logger = createLogger(debug) } = options;
|
|
681
|
+
let state = "disconnected";
|
|
682
|
+
let error;
|
|
683
|
+
let config;
|
|
684
|
+
const metrics = {
|
|
685
|
+
latency: 0,
|
|
686
|
+
messagesSent: 0,
|
|
687
|
+
messagesReceived: 0,
|
|
688
|
+
messagesQueued: 0,
|
|
689
|
+
reconnectCount: 0,
|
|
690
|
+
transportType: "whatsapp"
|
|
691
|
+
};
|
|
692
|
+
const globalHandlers = /* @__PURE__ */ new Set();
|
|
693
|
+
const typeHandlers = /* @__PURE__ */ new Map();
|
|
694
|
+
const transport = {
|
|
695
|
+
async connect(cfg) {
|
|
696
|
+
logger.warn(
|
|
697
|
+
"WhatsApp transport is a stub. WhatsApp integration happens via backend webhooks."
|
|
698
|
+
);
|
|
699
|
+
logger.info(
|
|
700
|
+
"See transport/whatsapp.ts for integration documentation."
|
|
701
|
+
);
|
|
702
|
+
config = cfg;
|
|
703
|
+
state = "connected";
|
|
704
|
+
metrics.connectedAt = Date.now();
|
|
705
|
+
},
|
|
706
|
+
disconnect() {
|
|
707
|
+
state = "disconnected";
|
|
708
|
+
},
|
|
709
|
+
async send(event) {
|
|
710
|
+
if (!config) {
|
|
711
|
+
throw new Error("Transport not connected");
|
|
712
|
+
}
|
|
713
|
+
logger.warn("WhatsApp send is a stub. Implement backend webhook relay.");
|
|
714
|
+
metrics.messagesSent++;
|
|
715
|
+
},
|
|
716
|
+
async sendMessage(message) {
|
|
717
|
+
if (!config) {
|
|
718
|
+
throw new Error("Transport not connected");
|
|
719
|
+
}
|
|
720
|
+
logger.warn(
|
|
721
|
+
"WhatsApp sendMessage is a stub. Implement backend webhook relay."
|
|
722
|
+
);
|
|
723
|
+
metrics.messagesSent++;
|
|
724
|
+
},
|
|
725
|
+
onMessage(handler) {
|
|
726
|
+
globalHandlers.add(handler);
|
|
727
|
+
return () => globalHandlers.delete(handler);
|
|
728
|
+
},
|
|
729
|
+
on(eventType, handler) {
|
|
730
|
+
if (!typeHandlers.has(eventType)) {
|
|
731
|
+
typeHandlers.set(eventType, /* @__PURE__ */ new Set());
|
|
732
|
+
}
|
|
733
|
+
typeHandlers.get(eventType).add(handler);
|
|
734
|
+
return () => {
|
|
735
|
+
const handlers = typeHandlers.get(eventType);
|
|
736
|
+
if (handlers) {
|
|
737
|
+
handlers.delete(handler);
|
|
738
|
+
if (handlers.size === 0) {
|
|
739
|
+
typeHandlers.delete(eventType);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
},
|
|
744
|
+
getState() {
|
|
745
|
+
return state;
|
|
746
|
+
},
|
|
747
|
+
getMetrics() {
|
|
748
|
+
return { ...metrics };
|
|
749
|
+
},
|
|
750
|
+
getError() {
|
|
751
|
+
return error;
|
|
752
|
+
},
|
|
753
|
+
clearError() {
|
|
754
|
+
error = void 0;
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
return transport;
|
|
758
|
+
}
|
|
759
|
+
function mapWhatsAppToChatEvent(payload, orgId) {
|
|
760
|
+
const events = [];
|
|
761
|
+
for (const entry of payload.entry) {
|
|
762
|
+
for (const change of entry.changes) {
|
|
763
|
+
const value = change.value;
|
|
764
|
+
if (value.messages) {
|
|
765
|
+
for (const msg of value.messages) {
|
|
766
|
+
const contact = value.contacts?.find((c) => c.wa_id === msg.from);
|
|
767
|
+
events.push({
|
|
768
|
+
type: "message",
|
|
769
|
+
orgId,
|
|
770
|
+
chatKey: `wa_${msg.from}`,
|
|
771
|
+
// Use phone number as chat key
|
|
772
|
+
userId: msg.from,
|
|
773
|
+
timestamp: parseInt(msg.timestamp) * 1e3,
|
|
774
|
+
message: {
|
|
775
|
+
id: msg.id,
|
|
776
|
+
role: "user",
|
|
777
|
+
content: msg.text?.body || "",
|
|
778
|
+
time: parseInt(msg.timestamp) * 1e3,
|
|
779
|
+
status: "delivered",
|
|
780
|
+
senderId: msg.from,
|
|
781
|
+
senderName: contact?.profile.name,
|
|
782
|
+
createdAt: parseInt(msg.timestamp) * 1e3,
|
|
783
|
+
// Map attachments if present
|
|
784
|
+
attachments: mapWhatsAppAttachments(msg)
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (value.statuses) {
|
|
790
|
+
for (const status of value.statuses) {
|
|
791
|
+
events.push({
|
|
792
|
+
type: "message_status_update",
|
|
793
|
+
orgId,
|
|
794
|
+
chatKey: `wa_${status.recipient_id}`,
|
|
795
|
+
userId: status.recipient_id,
|
|
796
|
+
timestamp: parseInt(status.timestamp) * 1e3,
|
|
797
|
+
data: {
|
|
798
|
+
messageId: status.id,
|
|
799
|
+
status: status.status
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return events;
|
|
807
|
+
}
|
|
808
|
+
function mapWhatsAppAttachments(msg) {
|
|
809
|
+
switch (msg.type) {
|
|
810
|
+
case "image":
|
|
811
|
+
return msg.image ? [{ type: "image", url: `whatsapp://media/${msg.image.id}` }] : void 0;
|
|
812
|
+
case "document":
|
|
813
|
+
return msg.document ? [
|
|
814
|
+
{
|
|
815
|
+
type: "document",
|
|
816
|
+
url: `whatsapp://media/${msg.document.id}`
|
|
817
|
+
}
|
|
818
|
+
] : void 0;
|
|
819
|
+
case "audio":
|
|
820
|
+
return msg.audio ? [{ type: "audio", url: `whatsapp://media/${msg.audio.id}` }] : void 0;
|
|
821
|
+
case "video":
|
|
822
|
+
return msg.video ? [{ type: "video", url: `whatsapp://media/${msg.video.id}` }] : void 0;
|
|
823
|
+
case "location":
|
|
824
|
+
return msg.location ? [
|
|
825
|
+
{
|
|
826
|
+
type: "location",
|
|
827
|
+
url: `geo:${msg.location.latitude},${msg.location.longitude}`
|
|
828
|
+
}
|
|
829
|
+
] : void 0;
|
|
830
|
+
default:
|
|
831
|
+
return void 0;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
export {
|
|
835
|
+
DEFAULT_RETRY_CONFIG,
|
|
836
|
+
calculateRetryInterval,
|
|
837
|
+
createFetchSSETransport,
|
|
838
|
+
createLogger,
|
|
839
|
+
createSSETransport,
|
|
840
|
+
createWhatsAppTransport,
|
|
841
|
+
mapWhatsAppToChatEvent
|
|
842
|
+
};
|
|
843
|
+
//# sourceMappingURL=index.mjs.map
|