@elqnt/chat 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/use-websocket-chat-admin.js +829 -4
- package/dist/hooks/use-websocket-chat-admin.js.map +1 -1
- package/dist/hooks/use-websocket-chat-admin.mjs +805 -4
- package/dist/hooks/use-websocket-chat-admin.mjs.map +1 -1
- package/dist/hooks/use-websocket-chat-base.js +661 -6
- package/dist/hooks/use-websocket-chat-base.js.map +1 -1
- package/dist/hooks/use-websocket-chat-base.mjs +634 -3
- package/dist/hooks/use-websocket-chat-base.mjs.map +1 -1
- package/dist/hooks/use-websocket-chat-customer.js +726 -5
- package/dist/hooks/use-websocket-chat-customer.js.map +1 -1
- package/dist/hooks/use-websocket-chat-customer.mjs +701 -4
- package/dist/hooks/use-websocket-chat-customer.mjs.map +1 -1
- package/dist/index.js +1685 -590
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1064 -197
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-3PXNBY7J.js +0 -73
- package/dist/chunk-3PXNBY7J.js.map +0 -1
- package/dist/chunk-AC5J5LX5.mjs +0 -529
- package/dist/chunk-AC5J5LX5.mjs.map +0 -1
- package/dist/chunk-DTFTLFSY.js +0 -637
- package/dist/chunk-DTFTLFSY.js.map +0 -1
- package/dist/chunk-E2FJX52R.js +0 -529
- package/dist/chunk-E2FJX52R.js.map +0 -1
- package/dist/chunk-F6OOS4ZM.mjs +0 -637
- package/dist/chunk-F6OOS4ZM.mjs.map +0 -1
- package/dist/chunk-XVYABY2Z.mjs +0 -73
- package/dist/chunk-XVYABY2Z.mjs.map +0 -1
|
@@ -1,9 +1,730 @@
|
|
|
1
|
-
"use
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
"use client";
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
2
21
|
|
|
22
|
+
// hooks/use-websocket-chat-customer.ts
|
|
23
|
+
var use_websocket_chat_customer_exports = {};
|
|
24
|
+
__export(use_websocket_chat_customer_exports, {
|
|
25
|
+
useWebSocketChatCustomer: () => useWebSocketChatCustomer
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(use_websocket_chat_customer_exports);
|
|
28
|
+
var import_react2 = require("react");
|
|
3
29
|
|
|
4
|
-
|
|
5
|
-
require(
|
|
30
|
+
// hooks/use-websocket-chat-base.ts
|
|
31
|
+
var import_react = require("react");
|
|
32
|
+
var createDefaultLogger = (debug) => ({
|
|
33
|
+
debug: debug ? console.log : () => {
|
|
34
|
+
},
|
|
35
|
+
info: console.info,
|
|
36
|
+
warn: console.warn,
|
|
37
|
+
error: console.error
|
|
38
|
+
});
|
|
39
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
40
|
+
maxRetries: 10,
|
|
41
|
+
intervals: [1e3, 2e3, 5e3],
|
|
42
|
+
backoffMultiplier: 1.5,
|
|
43
|
+
maxBackoffTime: 3e4
|
|
44
|
+
};
|
|
45
|
+
var DEFAULT_QUEUE_CONFIG = {
|
|
46
|
+
maxSize: 100,
|
|
47
|
+
dropStrategy: "oldest"
|
|
48
|
+
};
|
|
49
|
+
var DEFAULT_HEARTBEAT_INTERVAL = 3e4;
|
|
50
|
+
var DEFAULT_HEARTBEAT_TIMEOUT = 5e3;
|
|
51
|
+
function isChatEvent(data) {
|
|
52
|
+
return data && typeof data === "object" && (typeof data.type === "string" || data.message);
|
|
53
|
+
}
|
|
54
|
+
var useWebSocketChatBase = ({
|
|
55
|
+
serverBaseUrl,
|
|
56
|
+
orgId,
|
|
57
|
+
clientType,
|
|
58
|
+
product,
|
|
59
|
+
onMessage,
|
|
60
|
+
retryConfig = DEFAULT_RETRY_CONFIG,
|
|
61
|
+
queueConfig = DEFAULT_QUEUE_CONFIG,
|
|
62
|
+
debug = false,
|
|
63
|
+
logger = createDefaultLogger(debug),
|
|
64
|
+
heartbeatInterval = DEFAULT_HEARTBEAT_INTERVAL,
|
|
65
|
+
heartbeatTimeout = DEFAULT_HEARTBEAT_TIMEOUT
|
|
66
|
+
}) => {
|
|
67
|
+
const [connectionState, setConnectionState] = (0, import_react.useState)("disconnected");
|
|
68
|
+
const [error, setError] = (0, import_react.useState)(void 0);
|
|
69
|
+
const [startTime, setStartTime] = (0, import_react.useState)(void 0);
|
|
70
|
+
const [metrics, setMetrics] = (0, import_react.useState)({
|
|
71
|
+
latency: 0,
|
|
72
|
+
messagesSent: 0,
|
|
73
|
+
messagesReceived: 0,
|
|
74
|
+
messagesQueued: 0,
|
|
75
|
+
reconnectCount: 0
|
|
76
|
+
});
|
|
77
|
+
const wsRef = (0, import_react.useRef)(void 0);
|
|
78
|
+
const reconnectTimeoutRef = (0, import_react.useRef)(void 0);
|
|
79
|
+
const retryCountRef = (0, import_react.useRef)(0);
|
|
80
|
+
const messageQueueRef = (0, import_react.useRef)([]);
|
|
81
|
+
const mountedRef = (0, import_react.useRef)(false);
|
|
82
|
+
const currentChatKeyRef = (0, import_react.useRef)(void 0);
|
|
83
|
+
const currentUserIdRef = (0, import_react.useRef)(void 0);
|
|
84
|
+
const intentionalDisconnectRef = (0, import_react.useRef)(false);
|
|
85
|
+
const eventHandlersRef = (0, import_react.useRef)(
|
|
86
|
+
/* @__PURE__ */ new Map()
|
|
87
|
+
);
|
|
88
|
+
const onMessageRef = (0, import_react.useRef)(onMessage);
|
|
89
|
+
const pendingMessagesRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
90
|
+
const heartbeatIntervalRef = (0, import_react.useRef)(void 0);
|
|
91
|
+
const heartbeatTimeoutRef = (0, import_react.useRef)(void 0);
|
|
92
|
+
const lastPongRef = (0, import_react.useRef)(Date.now());
|
|
93
|
+
const chatCreationPromiseRef = (0, import_react.useRef)(null);
|
|
94
|
+
const loadChatRetryMapRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
95
|
+
(0, import_react.useEffect)(() => {
|
|
96
|
+
onMessageRef.current = onMessage;
|
|
97
|
+
}, [onMessage]);
|
|
98
|
+
const isConnected = connectionState === "connected";
|
|
99
|
+
const on = (0, import_react.useCallback)((eventType, handler) => {
|
|
100
|
+
if (!eventHandlersRef.current.has(eventType)) {
|
|
101
|
+
eventHandlersRef.current.set(eventType, /* @__PURE__ */ new Set());
|
|
102
|
+
}
|
|
103
|
+
eventHandlersRef.current.get(eventType).add(handler);
|
|
104
|
+
return () => off(eventType, handler);
|
|
105
|
+
}, []);
|
|
106
|
+
const off = (0, import_react.useCallback)((eventType, handler) => {
|
|
107
|
+
const handlers = eventHandlersRef.current.get(eventType);
|
|
108
|
+
if (handlers) {
|
|
109
|
+
handlers.delete(handler);
|
|
110
|
+
if (handlers.size === 0) {
|
|
111
|
+
eventHandlersRef.current.delete(eventType);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}, []);
|
|
115
|
+
const emit = (0, import_react.useCallback)(
|
|
116
|
+
(eventType, data) => {
|
|
117
|
+
const handlers = eventHandlersRef.current.get(eventType);
|
|
118
|
+
if (handlers) {
|
|
119
|
+
handlers.forEach((handler) => {
|
|
120
|
+
try {
|
|
121
|
+
handler(data);
|
|
122
|
+
} catch (error2) {
|
|
123
|
+
logger.error(`Error in event handler for ${eventType}:`, error2);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
[logger]
|
|
129
|
+
);
|
|
130
|
+
const updateMetrics = (0, import_react.useCallback)((updates) => {
|
|
131
|
+
setMetrics((prev) => ({ ...prev, ...updates }));
|
|
132
|
+
}, []);
|
|
133
|
+
const addToQueue = (0, import_react.useCallback)(
|
|
134
|
+
(event) => {
|
|
135
|
+
const currentQueueSize = messageQueueRef.current.length;
|
|
136
|
+
const maxSize = queueConfig.maxSize || DEFAULT_QUEUE_CONFIG.maxSize;
|
|
137
|
+
if (currentQueueSize >= maxSize) {
|
|
138
|
+
if (queueConfig.dropStrategy === "newest") {
|
|
139
|
+
logger.warn("Message queue full, dropping new message");
|
|
140
|
+
return false;
|
|
141
|
+
} else {
|
|
142
|
+
const dropped = messageQueueRef.current.shift();
|
|
143
|
+
logger.warn("Message queue full, dropped oldest message", dropped);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
messageQueueRef.current.push(event);
|
|
147
|
+
updateMetrics({ messagesQueued: messageQueueRef.current.length });
|
|
148
|
+
return true;
|
|
149
|
+
},
|
|
150
|
+
[queueConfig, logger, updateMetrics]
|
|
151
|
+
);
|
|
152
|
+
const calculateRetryInterval = (0, import_react.useCallback)(
|
|
153
|
+
(retryCount) => {
|
|
154
|
+
const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig };
|
|
155
|
+
const {
|
|
156
|
+
intervals = [],
|
|
157
|
+
backoffMultiplier = 1.5,
|
|
158
|
+
maxBackoffTime = 3e4
|
|
159
|
+
} = config;
|
|
160
|
+
if (retryCount < intervals.length) {
|
|
161
|
+
return intervals[retryCount];
|
|
162
|
+
}
|
|
163
|
+
const baseInterval = intervals[intervals.length - 1] || 5e3;
|
|
164
|
+
const backoffTime = baseInterval * Math.pow(backoffMultiplier, retryCount - intervals.length + 1);
|
|
165
|
+
return Math.min(backoffTime, maxBackoffTime);
|
|
166
|
+
},
|
|
167
|
+
[retryConfig]
|
|
168
|
+
);
|
|
169
|
+
const startHeartbeat = (0, import_react.useCallback)(() => {
|
|
170
|
+
if (!heartbeatInterval || heartbeatInterval <= 0) return;
|
|
171
|
+
const sendPing = () => {
|
|
172
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
173
|
+
const pingTime = Date.now();
|
|
174
|
+
wsRef.current.send(
|
|
175
|
+
JSON.stringify({ type: "ping", timestamp: pingTime })
|
|
176
|
+
);
|
|
177
|
+
heartbeatTimeoutRef.current = setTimeout(() => {
|
|
178
|
+
logger.warn("Heartbeat timeout - no pong received");
|
|
179
|
+
if (wsRef.current) {
|
|
180
|
+
wsRef.current.close(4e3, "Heartbeat timeout");
|
|
181
|
+
}
|
|
182
|
+
}, heartbeatTimeout);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
if (heartbeatIntervalRef.current) {
|
|
186
|
+
clearInterval(heartbeatIntervalRef.current);
|
|
187
|
+
}
|
|
188
|
+
heartbeatIntervalRef.current = setInterval(sendPing, heartbeatInterval);
|
|
189
|
+
logger.debug("Heartbeat started");
|
|
190
|
+
}, [heartbeatInterval, heartbeatTimeout, logger]);
|
|
191
|
+
const stopHeartbeat = (0, import_react.useCallback)(() => {
|
|
192
|
+
if (heartbeatIntervalRef.current) {
|
|
193
|
+
clearInterval(heartbeatIntervalRef.current);
|
|
194
|
+
heartbeatIntervalRef.current = void 0;
|
|
195
|
+
}
|
|
196
|
+
if (heartbeatTimeoutRef.current) {
|
|
197
|
+
clearTimeout(heartbeatTimeoutRef.current);
|
|
198
|
+
heartbeatTimeoutRef.current = void 0;
|
|
199
|
+
}
|
|
200
|
+
logger.debug("Heartbeat stopped");
|
|
201
|
+
}, [logger]);
|
|
202
|
+
const cleanup = (0, import_react.useCallback)(() => {
|
|
203
|
+
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
204
|
+
wsRef.current.close(1e3, "Cleanup");
|
|
205
|
+
}
|
|
206
|
+
wsRef.current = void 0;
|
|
207
|
+
if (reconnectTimeoutRef.current) {
|
|
208
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
209
|
+
reconnectTimeoutRef.current = void 0;
|
|
210
|
+
}
|
|
211
|
+
stopHeartbeat();
|
|
212
|
+
pendingMessagesRef.current.forEach(({ reject, timeout }) => {
|
|
213
|
+
clearTimeout(timeout);
|
|
214
|
+
reject(new Error("Connection closed"));
|
|
215
|
+
});
|
|
216
|
+
pendingMessagesRef.current.clear();
|
|
217
|
+
loadChatRetryMapRef.current.forEach((retryState) => {
|
|
218
|
+
if (retryState.timeoutId) {
|
|
219
|
+
clearTimeout(retryState.timeoutId);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
loadChatRetryMapRef.current.clear();
|
|
223
|
+
}, [stopHeartbeat]);
|
|
224
|
+
const connect = (0, import_react.useCallback)(
|
|
225
|
+
async (userId) => {
|
|
226
|
+
if (!mountedRef.current) {
|
|
227
|
+
mountedRef.current = true;
|
|
228
|
+
}
|
|
229
|
+
if (!orgId) {
|
|
230
|
+
const error2 = {
|
|
231
|
+
code: "CONNECTION_FAILED",
|
|
232
|
+
message: "Cannot connect: orgId is undefined",
|
|
233
|
+
retryable: false,
|
|
234
|
+
timestamp: Date.now()
|
|
235
|
+
};
|
|
236
|
+
logger.error("Cannot connect: orgId is undefined");
|
|
237
|
+
setError(error2);
|
|
238
|
+
return Promise.reject(error2);
|
|
239
|
+
}
|
|
240
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
241
|
+
logger.debug("Already connected");
|
|
242
|
+
return Promise.resolve();
|
|
243
|
+
}
|
|
244
|
+
if (connectionState === "connecting" || connectionState === "reconnecting") {
|
|
245
|
+
logger.debug("Connection already in progress");
|
|
246
|
+
return Promise.resolve();
|
|
247
|
+
}
|
|
248
|
+
const maxRetries = retryConfig.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries;
|
|
249
|
+
if (retryCountRef.current >= maxRetries && !intentionalDisconnectRef.current) {
|
|
250
|
+
const error2 = {
|
|
251
|
+
code: "CONNECTION_FAILED",
|
|
252
|
+
message: `Max retries (${maxRetries}) exceeded`,
|
|
253
|
+
retryable: false,
|
|
254
|
+
timestamp: Date.now()
|
|
255
|
+
};
|
|
256
|
+
setError(error2);
|
|
257
|
+
updateMetrics({ lastError: error2 });
|
|
258
|
+
return Promise.reject(error2);
|
|
259
|
+
}
|
|
260
|
+
cleanup();
|
|
261
|
+
setConnectionState(
|
|
262
|
+
retryCountRef.current > 0 ? "reconnecting" : "connecting"
|
|
263
|
+
);
|
|
264
|
+
intentionalDisconnectRef.current = false;
|
|
265
|
+
return new Promise((resolve, reject) => {
|
|
266
|
+
try {
|
|
267
|
+
const wsUrl = `${serverBaseUrl}?orgId=${orgId}&userId=${userId}&clientType=${clientType}&product=${product}`;
|
|
268
|
+
const connectionStartTime = Date.now();
|
|
269
|
+
logger.debug("Connecting to WebSocket:", wsUrl);
|
|
270
|
+
console.log(`\u23F3 Initiating WebSocket connection to ${serverBaseUrl}...`);
|
|
271
|
+
const ws = new WebSocket(wsUrl);
|
|
272
|
+
ws.onopen = () => {
|
|
273
|
+
if (!mountedRef.current) {
|
|
274
|
+
ws.close(1e3, "Component unmounted");
|
|
275
|
+
reject(new Error("Component unmounted"));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const connectionTimeMs = Date.now() - connectionStartTime;
|
|
279
|
+
const connectionTimeSec = (connectionTimeMs / 1e3).toFixed(2);
|
|
280
|
+
logger.info("\u2705 WebSocket connected", {
|
|
281
|
+
userId,
|
|
282
|
+
retryCount: retryCountRef.current,
|
|
283
|
+
connectionTime: `${connectionTimeSec}s (${connectionTimeMs}ms)`
|
|
284
|
+
});
|
|
285
|
+
console.log(`\u{1F50C} WebSocket connection established in ${connectionTimeSec} seconds`);
|
|
286
|
+
setConnectionState("connected");
|
|
287
|
+
setError(void 0);
|
|
288
|
+
const wasReconnecting = retryCountRef.current > 0;
|
|
289
|
+
retryCountRef.current = 0;
|
|
290
|
+
updateMetrics({
|
|
291
|
+
connectedAt: Date.now(),
|
|
292
|
+
latency: connectionTimeMs,
|
|
293
|
+
reconnectCount: wasReconnecting ? metrics.reconnectCount + 1 : metrics.reconnectCount
|
|
294
|
+
});
|
|
295
|
+
while (messageQueueRef.current.length > 0 && ws.readyState === WebSocket.OPEN) {
|
|
296
|
+
const event = messageQueueRef.current.shift();
|
|
297
|
+
if (event) {
|
|
298
|
+
ws.send(JSON.stringify({ ...event, timestamp: Date.now() }));
|
|
299
|
+
updateMetrics({
|
|
300
|
+
messagesSent: metrics.messagesSent + 1,
|
|
301
|
+
messagesQueued: messageQueueRef.current.length
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
currentUserIdRef.current = userId;
|
|
306
|
+
if (currentChatKeyRef.current) {
|
|
307
|
+
if (!orgId) {
|
|
308
|
+
logger.error("Cannot resubscribe to chat: orgId is undefined");
|
|
309
|
+
} else {
|
|
310
|
+
logger.info(
|
|
311
|
+
"Resubscribing to chat after reconnection:",
|
|
312
|
+
currentChatKeyRef.current
|
|
313
|
+
);
|
|
314
|
+
const resubscribeEvent = {
|
|
315
|
+
type: "load_chat",
|
|
316
|
+
orgId,
|
|
317
|
+
chatKey: currentChatKeyRef.current,
|
|
318
|
+
userId,
|
|
319
|
+
timestamp: Date.now(),
|
|
320
|
+
data: {}
|
|
321
|
+
};
|
|
322
|
+
ws.send(JSON.stringify(resubscribeEvent));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
startHeartbeat();
|
|
326
|
+
emit("connected", { userId, wasReconnecting });
|
|
327
|
+
resolve();
|
|
328
|
+
};
|
|
329
|
+
ws.onmessage = (event) => {
|
|
330
|
+
if (!mountedRef.current) return;
|
|
331
|
+
try {
|
|
332
|
+
const data = JSON.parse(event.data);
|
|
333
|
+
if (!isChatEvent(data)) {
|
|
334
|
+
logger.warn("Received invalid message format:", data);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const chatEvent = data;
|
|
338
|
+
logger.debug("Message received:", chatEvent.type);
|
|
339
|
+
updateMetrics({
|
|
340
|
+
messagesReceived: metrics.messagesReceived + 1,
|
|
341
|
+
lastMessageAt: Date.now()
|
|
342
|
+
});
|
|
343
|
+
if (chatEvent.type === "pong") {
|
|
344
|
+
if (heartbeatTimeoutRef.current) {
|
|
345
|
+
clearTimeout(heartbeatTimeoutRef.current);
|
|
346
|
+
heartbeatTimeoutRef.current = void 0;
|
|
347
|
+
}
|
|
348
|
+
const latency = Date.now() - (chatEvent.timestamp || Date.now());
|
|
349
|
+
lastPongRef.current = Date.now();
|
|
350
|
+
updateMetrics({ latency });
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
switch (chatEvent.type) {
|
|
354
|
+
case "new_chat_created":
|
|
355
|
+
const newChatKey = chatEvent.data?.chatKey;
|
|
356
|
+
if (newChatKey) {
|
|
357
|
+
logger.info("New chat created with key:", newChatKey);
|
|
358
|
+
currentChatKeyRef.current = newChatKey;
|
|
359
|
+
if (chatCreationPromiseRef.current) {
|
|
360
|
+
chatCreationPromiseRef.current.resolve(newChatKey);
|
|
361
|
+
chatCreationPromiseRef.current = null;
|
|
362
|
+
}
|
|
363
|
+
if (!orgId) {
|
|
364
|
+
logger.error("Cannot send load_chat: orgId is undefined");
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const loadChatEvent = {
|
|
368
|
+
type: "load_chat",
|
|
369
|
+
orgId,
|
|
370
|
+
chatKey: newChatKey,
|
|
371
|
+
userId: currentUserIdRef.current || userId,
|
|
372
|
+
timestamp: Date.now(),
|
|
373
|
+
data: {}
|
|
374
|
+
};
|
|
375
|
+
ws.send(JSON.stringify(loadChatEvent));
|
|
376
|
+
}
|
|
377
|
+
break;
|
|
378
|
+
case "load_chat_response":
|
|
379
|
+
const chat = chatEvent.data?.chat;
|
|
380
|
+
if (chat && chat.key) {
|
|
381
|
+
logger.info("Chat loaded with key:", chat.key);
|
|
382
|
+
currentChatKeyRef.current = chat.key;
|
|
383
|
+
const retryState = loadChatRetryMapRef.current.get(chat.key);
|
|
384
|
+
if (retryState) {
|
|
385
|
+
if (retryState.timeoutId) {
|
|
386
|
+
clearTimeout(retryState.timeoutId);
|
|
387
|
+
}
|
|
388
|
+
loadChatRetryMapRef.current.delete(chat.key);
|
|
389
|
+
}
|
|
390
|
+
} else if (!chat) {
|
|
391
|
+
logger.warn("Chat load failed, clearing key");
|
|
392
|
+
currentChatKeyRef.current = void 0;
|
|
393
|
+
}
|
|
394
|
+
break;
|
|
395
|
+
case "chat_ended":
|
|
396
|
+
logger.info("Chat ended, clearing key");
|
|
397
|
+
currentChatKeyRef.current = void 0;
|
|
398
|
+
break;
|
|
399
|
+
case "error":
|
|
400
|
+
const errorMessage = chatEvent.data?.message || "Unknown error";
|
|
401
|
+
logger.error("Received error from server:", errorMessage);
|
|
402
|
+
if (errorMessage.includes("nats: key not found") || errorMessage.includes("Failed to load chat")) {
|
|
403
|
+
const chatKeyFromError = currentChatKeyRef.current;
|
|
404
|
+
if (chatKeyFromError) {
|
|
405
|
+
const maxRetries2 = 5;
|
|
406
|
+
let retryState = loadChatRetryMapRef.current.get(chatKeyFromError);
|
|
407
|
+
if (!retryState) {
|
|
408
|
+
retryState = { retryCount: 0, timeoutId: null };
|
|
409
|
+
loadChatRetryMapRef.current.set(chatKeyFromError, retryState);
|
|
410
|
+
}
|
|
411
|
+
if (retryState.retryCount < maxRetries2) {
|
|
412
|
+
const delay = 200 * Math.pow(2, retryState.retryCount);
|
|
413
|
+
retryState.retryCount++;
|
|
414
|
+
logger.info(
|
|
415
|
+
`Chat load failed, retrying (${retryState.retryCount}/${maxRetries2}) in ${delay}ms...`,
|
|
416
|
+
chatKeyFromError
|
|
417
|
+
);
|
|
418
|
+
retryState.timeoutId = setTimeout(() => {
|
|
419
|
+
if (!wsRef.current || !mountedRef.current) return;
|
|
420
|
+
if (!orgId) {
|
|
421
|
+
logger.error("Cannot retry load_chat: orgId is undefined", chatKeyFromError);
|
|
422
|
+
loadChatRetryMapRef.current.delete(chatKeyFromError);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
logger.debug("Retrying load_chat:", chatKeyFromError);
|
|
426
|
+
const retryLoadEvent = {
|
|
427
|
+
type: "load_chat",
|
|
428
|
+
orgId,
|
|
429
|
+
chatKey: chatKeyFromError,
|
|
430
|
+
userId: currentUserIdRef.current || "",
|
|
431
|
+
timestamp: Date.now(),
|
|
432
|
+
data: {}
|
|
433
|
+
};
|
|
434
|
+
ws.send(JSON.stringify(retryLoadEvent));
|
|
435
|
+
}, delay);
|
|
436
|
+
} else {
|
|
437
|
+
logger.error("Max retries reached for loading chat:", chatKeyFromError);
|
|
438
|
+
loadChatRetryMapRef.current.delete(chatKeyFromError);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const wsError = {
|
|
443
|
+
code: "NETWORK_ERROR",
|
|
444
|
+
message: errorMessage,
|
|
445
|
+
retryable: true,
|
|
446
|
+
timestamp: Date.now()
|
|
447
|
+
};
|
|
448
|
+
setError(wsError);
|
|
449
|
+
updateMetrics({ lastError: wsError });
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
emit(chatEvent.type || "message", chatEvent);
|
|
453
|
+
if (onMessageRef.current) {
|
|
454
|
+
onMessageRef.current(chatEvent);
|
|
455
|
+
}
|
|
456
|
+
} catch (error2) {
|
|
457
|
+
logger.error("Failed to parse WebSocket message:", error2);
|
|
458
|
+
const parseError = {
|
|
459
|
+
code: "PARSE_ERROR",
|
|
460
|
+
message: "Failed to parse message",
|
|
461
|
+
retryable: false,
|
|
462
|
+
timestamp: Date.now()
|
|
463
|
+
};
|
|
464
|
+
setError(parseError);
|
|
465
|
+
updateMetrics({ lastError: parseError });
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
ws.onclose = (event) => {
|
|
469
|
+
if (!mountedRef.current) return;
|
|
470
|
+
logger.info("WebSocket closed", {
|
|
471
|
+
code: event.code,
|
|
472
|
+
reason: event.reason
|
|
473
|
+
});
|
|
474
|
+
setConnectionState("disconnected");
|
|
475
|
+
wsRef.current = void 0;
|
|
476
|
+
stopHeartbeat();
|
|
477
|
+
emit("disconnected", { code: event.code, reason: event.reason });
|
|
478
|
+
if (event.code !== 1e3 && !intentionalDisconnectRef.current && mountedRef.current) {
|
|
479
|
+
const retryInterval = calculateRetryInterval(
|
|
480
|
+
retryCountRef.current
|
|
481
|
+
);
|
|
482
|
+
retryCountRef.current++;
|
|
483
|
+
logger.info(
|
|
484
|
+
`Reconnecting in ${retryInterval}ms (attempt ${retryCountRef.current})`
|
|
485
|
+
);
|
|
486
|
+
if (reconnectTimeoutRef.current) {
|
|
487
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
488
|
+
}
|
|
489
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
490
|
+
connect(userId);
|
|
491
|
+
}, retryInterval);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
ws.onerror = (error2) => {
|
|
495
|
+
logger.error("WebSocket error:", error2);
|
|
496
|
+
if (!mountedRef.current) return;
|
|
497
|
+
const wsError = {
|
|
498
|
+
code: "CONNECTION_FAILED",
|
|
499
|
+
message: "WebSocket connection failed",
|
|
500
|
+
retryable: true,
|
|
501
|
+
timestamp: Date.now()
|
|
502
|
+
};
|
|
503
|
+
setError(wsError);
|
|
504
|
+
updateMetrics({ lastError: wsError });
|
|
505
|
+
reject(wsError);
|
|
506
|
+
};
|
|
507
|
+
wsRef.current = ws;
|
|
508
|
+
} catch (error2) {
|
|
509
|
+
logger.error("Failed to create WebSocket:", error2);
|
|
510
|
+
const wsError = {
|
|
511
|
+
code: "CONNECTION_FAILED",
|
|
512
|
+
message: error2 instanceof Error ? error2.message : "Failed to create connection",
|
|
513
|
+
retryable: true,
|
|
514
|
+
timestamp: Date.now()
|
|
515
|
+
};
|
|
516
|
+
setError(wsError);
|
|
517
|
+
updateMetrics({ lastError: wsError });
|
|
518
|
+
reject(wsError);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
},
|
|
522
|
+
[
|
|
523
|
+
serverBaseUrl,
|
|
524
|
+
orgId,
|
|
525
|
+
clientType,
|
|
526
|
+
product,
|
|
527
|
+
connectionState,
|
|
528
|
+
logger,
|
|
529
|
+
retryConfig,
|
|
530
|
+
metrics,
|
|
531
|
+
updateMetrics,
|
|
532
|
+
cleanup,
|
|
533
|
+
calculateRetryInterval,
|
|
534
|
+
startHeartbeat,
|
|
535
|
+
emit
|
|
536
|
+
]
|
|
537
|
+
);
|
|
538
|
+
const sendMessage = (0, import_react.useCallback)(
|
|
539
|
+
(event, overrideUserId) => {
|
|
540
|
+
return new Promise((resolve, reject) => {
|
|
541
|
+
if (!mountedRef.current) {
|
|
542
|
+
reject(new Error("Component not mounted"));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const fullEvent = {
|
|
546
|
+
...event,
|
|
547
|
+
timestamp: Date.now()
|
|
548
|
+
};
|
|
549
|
+
const messageId = `${fullEvent.type}_${fullEvent.timestamp}_${Math.random()}`;
|
|
550
|
+
logger.debug("Sending message:", fullEvent.type);
|
|
551
|
+
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
|
|
552
|
+
if (addToQueue(fullEvent)) {
|
|
553
|
+
logger.debug("Message queued, attempting to connect");
|
|
554
|
+
if (connectionState === "disconnected" && overrideUserId) {
|
|
555
|
+
connect(overrideUserId).then(() => resolve()).catch(reject);
|
|
556
|
+
} else {
|
|
557
|
+
resolve();
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
reject(new Error("Message queue full"));
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
wsRef.current.send(JSON.stringify(fullEvent));
|
|
566
|
+
updateMetrics({ messagesSent: metrics.messagesSent + 1 });
|
|
567
|
+
logger.debug("Message sent successfully");
|
|
568
|
+
resolve();
|
|
569
|
+
} catch (error2) {
|
|
570
|
+
logger.error("Failed to send message:", error2);
|
|
571
|
+
if (addToQueue(fullEvent)) {
|
|
572
|
+
resolve();
|
|
573
|
+
} else {
|
|
574
|
+
const sendError = {
|
|
575
|
+
code: "SEND_FAILED",
|
|
576
|
+
message: error2 instanceof Error ? error2.message : "Failed to send message",
|
|
577
|
+
retryable: true,
|
|
578
|
+
timestamp: Date.now()
|
|
579
|
+
};
|
|
580
|
+
setError(sendError);
|
|
581
|
+
updateMetrics({ lastError: sendError });
|
|
582
|
+
reject(sendError);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
},
|
|
587
|
+
[connectionState, connect, addToQueue, logger, metrics, updateMetrics]
|
|
588
|
+
);
|
|
589
|
+
const startNewChat = (0, import_react.useCallback)(
|
|
590
|
+
(userId, data) => {
|
|
591
|
+
return new Promise((resolve, reject) => {
|
|
592
|
+
if (!userId) {
|
|
593
|
+
reject(new Error("User ID is required"));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
logger.info("Requesting new chat from server with userId:", userId);
|
|
597
|
+
setStartTime(/* @__PURE__ */ new Date());
|
|
598
|
+
chatCreationPromiseRef.current = { resolve, reject };
|
|
599
|
+
sendMessage(
|
|
600
|
+
{
|
|
601
|
+
type: "new_chat",
|
|
602
|
+
orgId,
|
|
603
|
+
chatKey: "",
|
|
604
|
+
// Server will generate
|
|
605
|
+
userId,
|
|
606
|
+
data: data ?? {}
|
|
607
|
+
},
|
|
608
|
+
userId
|
|
609
|
+
).catch((error2) => {
|
|
610
|
+
chatCreationPromiseRef.current = null;
|
|
611
|
+
reject(error2);
|
|
612
|
+
});
|
|
613
|
+
setTimeout(() => {
|
|
614
|
+
if (chatCreationPromiseRef.current) {
|
|
615
|
+
chatCreationPromiseRef.current = null;
|
|
616
|
+
reject(new Error("Chat creation timed out"));
|
|
617
|
+
}
|
|
618
|
+
}, 3e4);
|
|
619
|
+
});
|
|
620
|
+
},
|
|
621
|
+
[sendMessage, orgId, logger]
|
|
622
|
+
);
|
|
623
|
+
const disconnect = (0, import_react.useCallback)(
|
|
624
|
+
(intentional = true) => {
|
|
625
|
+
logger.info("Disconnecting WebSocket", { intentional });
|
|
626
|
+
intentionalDisconnectRef.current = intentional;
|
|
627
|
+
cleanup();
|
|
628
|
+
setConnectionState("disconnected");
|
|
629
|
+
messageQueueRef.current = [];
|
|
630
|
+
retryCountRef.current = 0;
|
|
631
|
+
mountedRef.current = false;
|
|
632
|
+
currentChatKeyRef.current = void 0;
|
|
633
|
+
currentUserIdRef.current = void 0;
|
|
634
|
+
},
|
|
635
|
+
[cleanup, logger]
|
|
636
|
+
);
|
|
637
|
+
const clearError = (0, import_react.useCallback)(() => {
|
|
638
|
+
setError(void 0);
|
|
639
|
+
}, []);
|
|
640
|
+
(0, import_react.useEffect)(() => {
|
|
641
|
+
mountedRef.current = true;
|
|
642
|
+
return () => {
|
|
643
|
+
mountedRef.current = false;
|
|
644
|
+
disconnect(true);
|
|
645
|
+
};
|
|
646
|
+
}, []);
|
|
647
|
+
return {
|
|
648
|
+
connectionState,
|
|
649
|
+
isConnected,
|
|
650
|
+
sendMessage,
|
|
651
|
+
error,
|
|
652
|
+
connect,
|
|
653
|
+
startNewChat,
|
|
654
|
+
startTime,
|
|
655
|
+
disconnect,
|
|
656
|
+
metrics,
|
|
657
|
+
on,
|
|
658
|
+
off,
|
|
659
|
+
clearError
|
|
660
|
+
};
|
|
661
|
+
};
|
|
6
662
|
|
|
7
|
-
|
|
8
|
-
|
|
663
|
+
// hooks/use-websocket-chat-customer.ts
|
|
664
|
+
var useWebSocketChatCustomer = ({
|
|
665
|
+
serverBaseUrl,
|
|
666
|
+
orgId,
|
|
667
|
+
chatKey,
|
|
668
|
+
product
|
|
669
|
+
}) => {
|
|
670
|
+
const [currentChat, setCurrentChat] = (0, import_react2.useState)(void 0);
|
|
671
|
+
const handleMessage = (0, import_react2.useCallback)((chatEvent) => {
|
|
672
|
+
console.log("Received event:", chatEvent.type);
|
|
673
|
+
switch (chatEvent.type) {
|
|
674
|
+
case "message":
|
|
675
|
+
if (!chatEvent.message) return;
|
|
676
|
+
console.log(
|
|
677
|
+
"got message:",
|
|
678
|
+
chatEvent.message.role,
|
|
679
|
+
":",
|
|
680
|
+
chatEvent.message.content
|
|
681
|
+
);
|
|
682
|
+
setCurrentChat((prev) => {
|
|
683
|
+
if (!prev) return prev;
|
|
684
|
+
return {
|
|
685
|
+
...prev,
|
|
686
|
+
messages: [...prev.messages, chatEvent.message]
|
|
687
|
+
};
|
|
688
|
+
});
|
|
689
|
+
break;
|
|
690
|
+
case "chat_updated":
|
|
691
|
+
const chat = chatEvent.data?.chat?.value;
|
|
692
|
+
if (chat) {
|
|
693
|
+
setCurrentChat(chat);
|
|
694
|
+
}
|
|
695
|
+
break;
|
|
696
|
+
case "load_chat":
|
|
697
|
+
const history = chatEvent.data?.chat;
|
|
698
|
+
if (!history) return;
|
|
699
|
+
setCurrentChat(history);
|
|
700
|
+
break;
|
|
701
|
+
default:
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
}, []);
|
|
705
|
+
const base = useWebSocketChatBase({
|
|
706
|
+
serverBaseUrl,
|
|
707
|
+
orgId,
|
|
708
|
+
clientType: "customer",
|
|
709
|
+
onMessage: handleMessage,
|
|
710
|
+
product
|
|
711
|
+
});
|
|
712
|
+
return {
|
|
713
|
+
...base,
|
|
714
|
+
chatKey,
|
|
715
|
+
title: currentChat?.title,
|
|
716
|
+
messages: currentChat?.messages ?? [],
|
|
717
|
+
users: currentChat?.users ?? [],
|
|
718
|
+
isWaiting: currentChat?.isWaiting ?? false,
|
|
719
|
+
isWaitingForAgent: currentChat?.isWaitingForAgent ?? false,
|
|
720
|
+
aiEngaged: currentChat?.aiEngaged ?? false,
|
|
721
|
+
humanAgentEngaged: currentChat?.humanAgentEngaged ?? false,
|
|
722
|
+
metadata: currentChat?.metadata ?? {},
|
|
723
|
+
status: currentChat?.status
|
|
724
|
+
};
|
|
725
|
+
};
|
|
726
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
727
|
+
0 && (module.exports = {
|
|
728
|
+
useWebSocketChatCustomer
|
|
729
|
+
});
|
|
9
730
|
//# sourceMappingURL=use-websocket-chat-customer.js.map
|