@chaterafrikang/sdk 0.1.0-beta.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/dist/index.cjs ADDED
@@ -0,0 +1,2119 @@
1
+ 'use strict';
2
+
3
+ var eventemitter3 = require('eventemitter3');
4
+ var uuid = require('uuid');
5
+
6
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
7
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
+ }) : x)(function(x) {
10
+ if (typeof require !== "undefined") return require.apply(this, arguments);
11
+ throw Error('Dynamic require of "' + x + '" is not supported');
12
+ });
13
+
14
+ // src/core/ConnectionState.ts
15
+ var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
16
+ ConnectionState2["DISCONNECTED"] = "disconnected";
17
+ ConnectionState2["CONNECTING"] = "connecting";
18
+ ConnectionState2["CONNECTED"] = "connected";
19
+ ConnectionState2["RECONNECTING"] = "reconnecting";
20
+ ConnectionState2["ERROR"] = "error";
21
+ return ConnectionState2;
22
+ })(ConnectionState || {});
23
+
24
+ // src/utils/logger.ts
25
+ var Logger = class {
26
+ constructor(enabled = false) {
27
+ this.enabled = enabled;
28
+ }
29
+ setEnabled(enabled) {
30
+ this.enabled = enabled;
31
+ }
32
+ debug(...args) {
33
+ if (this.enabled) {
34
+ console.debug("[ChatAfrika SDK]", ...args);
35
+ }
36
+ }
37
+ info(...args) {
38
+ if (this.enabled) {
39
+ console.info("[ChatAfrika SDK]", ...args);
40
+ }
41
+ }
42
+ warn(...args) {
43
+ if (this.enabled) {
44
+ console.warn("[ChatAfrika SDK]", ...args);
45
+ }
46
+ }
47
+ error(...args) {
48
+ if (this.enabled) {
49
+ console.error("[ChatAfrika SDK]", ...args);
50
+ }
51
+ }
52
+ };
53
+
54
+ // src/websocket/WebSocketClient.ts
55
+ var isNodeEnv = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
56
+ var wsModule = null;
57
+ var wsLoading = null;
58
+ async function getWsModule() {
59
+ if (!isNodeEnv) return null;
60
+ if (wsModule) return wsModule;
61
+ if (wsLoading) return wsLoading;
62
+ wsLoading = (async () => {
63
+ try {
64
+ try {
65
+ const ws = await import('ws');
66
+ wsModule = ws;
67
+ return wsModule;
68
+ } catch (esmErr) {
69
+ if (typeof __require !== "undefined") {
70
+ const { createRequire } = await import('module');
71
+ const requireFn = createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)) || __filename);
72
+ wsModule = requireFn("ws");
73
+ return wsModule;
74
+ }
75
+ throw esmErr;
76
+ }
77
+ } catch (err) {
78
+ return null;
79
+ } finally {
80
+ wsLoading = null;
81
+ }
82
+ })();
83
+ return wsLoading;
84
+ }
85
+ var WebSocketClient = class extends eventemitter3.EventEmitter {
86
+ constructor(url, options = {}) {
87
+ super();
88
+ this.ws = null;
89
+ this.token = null;
90
+ this.reconnectAttempts = 0;
91
+ this.reconnectTimer = null;
92
+ this.isManuallyDisconnected = false;
93
+ this.isTokenExpired = false;
94
+ this.url = url;
95
+ this.autoReconnect = options.autoReconnect ?? true;
96
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
97
+ this.logger = new Logger(options.debug ?? false);
98
+ }
99
+ /**
100
+ * Connect to the WebSocket server with authentication token
101
+ */
102
+ connect(token) {
103
+ if (!token || token.length === 0) {
104
+ const error = new Error("Token is required for connection");
105
+ this.emit("error", error);
106
+ return Promise.reject(error);
107
+ }
108
+ this.token = token;
109
+ this.isManuallyDisconnected = false;
110
+ this.isTokenExpired = false;
111
+ return this.attemptConnection();
112
+ }
113
+ /**
114
+ * Attempt a WebSocket connection
115
+ */
116
+ async attemptConnection() {
117
+ const wsUrl = new URL(this.url);
118
+ const token = this.token ?? "";
119
+ if (isNodeEnv) {
120
+ const ws = await getWsModule();
121
+ this.logger.debug("ws module loaded:", !!ws);
122
+ if (ws && ws.WebSocket) {
123
+ this.logger.debug("Using ws package with Authorization header");
124
+ return new Promise((resolve, reject) => {
125
+ try {
126
+ const wsInstance = new ws.WebSocket(wsUrl.toString(), {
127
+ headers: {
128
+ "Authorization": `Bearer ${token}`
129
+ }
130
+ });
131
+ this.ws = wsInstance;
132
+ let handshakeErrorHandled = false;
133
+ const errorHandler = (error) => {
134
+ if (handshakeErrorHandled) return;
135
+ handshakeErrorHandled = true;
136
+ const errorMsg = String(error?.message || error || "").toLowerCase();
137
+ if (errorMsg.includes("401") || errorMsg.includes("unauthorized") || error?.code && String(error.code).includes("401")) {
138
+ this.handleTokenExpiration();
139
+ const tokenError = new Error("Token expired or invalid. Please refresh your token and reconnect.");
140
+ tokenError.name = "TokenExpiredError";
141
+ this.emit("error", tokenError);
142
+ reject(tokenError);
143
+ return;
144
+ }
145
+ };
146
+ if (typeof wsInstance.addEventListener === "function") {
147
+ wsInstance.addEventListener("error", errorHandler);
148
+ wsInstance.addEventListener("open", () => {
149
+ wsInstance.removeEventListener("error", errorHandler);
150
+ });
151
+ } else if (typeof wsInstance.once === "function") {
152
+ wsInstance.once("error", errorHandler);
153
+ wsInstance.once("open", () => {
154
+ });
155
+ }
156
+ this.setupEventHandlers(resolve, reject);
157
+ } catch (error) {
158
+ this.logger.error("Failed to create ws.WebSocket:", error);
159
+ reject(error);
160
+ }
161
+ });
162
+ } else {
163
+ this.logger.warn("ws package not available, falling back to native WebSocket");
164
+ }
165
+ }
166
+ this.logger.debug("Using native WebSocket with query parameter", { url: wsUrl.toString(), hasToken: !!token });
167
+ return new Promise((resolve, reject) => {
168
+ try {
169
+ if (!token || token.length === 0) {
170
+ const error = new Error("Token is required for WebSocket connection");
171
+ this.logger.error(error.message);
172
+ reject(error);
173
+ return;
174
+ }
175
+ const urlWithToken = new URL(wsUrl.toString());
176
+ urlWithToken.searchParams.set("token", token);
177
+ this.ws = new globalThis.WebSocket(urlWithToken.toString());
178
+ this.logger.debug("WebSocket instance created", {
179
+ readyState: this.ws.readyState,
180
+ url: this.ws.url,
181
+ protocol: this.ws.protocol
182
+ });
183
+ const wsRef = this.ws;
184
+ this.setupEventHandlers(resolve, reject);
185
+ setTimeout(() => {
186
+ if (wsRef.readyState === WebSocket.CLOSED || wsRef.readyState === WebSocket.CLOSING) {
187
+ this.logger.warn("WebSocket closed immediately after creation", {
188
+ readyState: wsRef.readyState,
189
+ url: wsRef.url
190
+ });
191
+ }
192
+ }, 100);
193
+ } catch (error) {
194
+ this.logger.error("Failed to create native WebSocket:", error);
195
+ reject(error);
196
+ }
197
+ });
198
+ }
199
+ /**
200
+ * Set up WebSocket event handlers
201
+ */
202
+ setupEventHandlers(resolve, reject) {
203
+ if (!this.ws) return;
204
+ this.ws.onopen = () => {
205
+ this.reconnectAttempts = 0;
206
+ this.isTokenExpired = false;
207
+ const ws = this.ws;
208
+ if (!ws) {
209
+ this.logger.error("WebSocket is null in onopen handler");
210
+ reject(new Error("WebSocket instance lost"));
211
+ return;
212
+ }
213
+ this.logger.debug("WebSocket connected", {
214
+ readyState: ws.readyState,
215
+ url: ws.url,
216
+ protocol: ws.protocol,
217
+ extensions: ws.extensions
218
+ });
219
+ if (ws.readyState !== WebSocket.OPEN) {
220
+ this.logger.error("WebSocket readyState is not OPEN after onopen", { readyState: ws.readyState });
221
+ reject(new Error(`WebSocket not open: readyState=${ws.readyState}`));
222
+ return;
223
+ }
224
+ this.emit("connected");
225
+ resolve();
226
+ };
227
+ this.ws.onmessage = (event) => {
228
+ try {
229
+ const message = JSON.parse(event.data);
230
+ this.logger.debug("Received message:", message);
231
+ this.emit("message", message);
232
+ } catch (error) {
233
+ const parseError = error instanceof Error ? error : new Error("Failed to parse message");
234
+ this.logger.error("Failed to parse message:", parseError);
235
+ this.emit("error", parseError);
236
+ }
237
+ };
238
+ this.ws.onerror = (error) => {
239
+ this.logger.error("WebSocket error:", error);
240
+ let errorToEmit;
241
+ if (error instanceof Error) {
242
+ errorToEmit = error;
243
+ } else if (error && error.message) {
244
+ errorToEmit = error instanceof Error ? error : new Error(String(error.message));
245
+ } else {
246
+ errorToEmit = new Error("WebSocket connection error");
247
+ }
248
+ if (isNodeEnv && error && error.message) {
249
+ const errorMsg = String(error.message).toLowerCase();
250
+ if (errorMsg.includes("401") || errorMsg.includes("unauthorized")) {
251
+ this.handleTokenExpiration();
252
+ const tokenError = new Error("Token expired or invalid. Please refresh your token and reconnect.");
253
+ tokenError.name = "TokenExpiredError";
254
+ this.emit("error", tokenError);
255
+ reject(tokenError);
256
+ return;
257
+ }
258
+ }
259
+ this.emit("error", errorToEmit);
260
+ };
261
+ this.ws.onclose = (event) => {
262
+ this.logger.debug("WebSocket closed:", {
263
+ code: event.code,
264
+ reason: event.reason,
265
+ wasClean: event.wasClean
266
+ });
267
+ if (event.code === 1006) {
268
+ this.logger.warn("Abnormal closure (1006) - connection closed without proper handshake. Possible causes: network issue, server closed connection, CORS issue, or protocol mismatch.");
269
+ }
270
+ const isAuthFailure = event.code === 1008 || event.code === 1002 || event.reason && event.reason.toLowerCase().includes("unauthorized") || event.reason && event.reason.toLowerCase().includes("401");
271
+ if (isAuthFailure) {
272
+ this.handleTokenExpiration();
273
+ const tokenError = new Error("Token expired or invalid. Please refresh your token and reconnect.");
274
+ tokenError.name = "TokenExpiredError";
275
+ this.emit("error", tokenError);
276
+ this.emit("disconnected", { code: event.code, reason: event.reason, tokenExpired: true });
277
+ return;
278
+ }
279
+ this.emit("disconnected", { code: event.code, reason: event.reason });
280
+ if (!this.isManuallyDisconnected && !this.isTokenExpired && this.autoReconnect) {
281
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
282
+ this.scheduleReconnect();
283
+ } else {
284
+ this.logger.warn("Max reconnection attempts reached");
285
+ this.emit("error", new Error("Max reconnection attempts reached"));
286
+ }
287
+ } else if (this.isTokenExpired) {
288
+ this.logger.warn("Not reconnecting: token is expired");
289
+ }
290
+ };
291
+ }
292
+ /**
293
+ * Handle token expiration - stop reconnection attempts
294
+ */
295
+ handleTokenExpiration() {
296
+ this.isTokenExpired = true;
297
+ this.autoReconnect = false;
298
+ if (this.reconnectTimer) {
299
+ clearTimeout(this.reconnectTimer);
300
+ this.reconnectTimer = null;
301
+ }
302
+ }
303
+ /**
304
+ * Disconnect from the WebSocket server
305
+ */
306
+ disconnect() {
307
+ this.isManuallyDisconnected = true;
308
+ this.autoReconnect = false;
309
+ if (this.reconnectTimer) {
310
+ clearTimeout(this.reconnectTimer);
311
+ this.reconnectTimer = null;
312
+ }
313
+ if (this.ws) {
314
+ this.ws.close();
315
+ this.ws = null;
316
+ }
317
+ this.logger.debug("WebSocket disconnected");
318
+ }
319
+ /**
320
+ * Send a message to the server
321
+ */
322
+ send(message) {
323
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
324
+ const error = new Error("WebSocket is not connected");
325
+ this.logger.error(error.message);
326
+ this.emit("error", error);
327
+ throw error;
328
+ }
329
+ try {
330
+ const messageStr = JSON.stringify(message);
331
+ this.logger.debug("Sending message:", message);
332
+ this.ws.send(messageStr);
333
+ } catch (error) {
334
+ const sendError = error instanceof Error ? error : new Error("Failed to send message");
335
+ this.logger.error("Send error:", sendError);
336
+ this.emit("error", sendError);
337
+ throw sendError;
338
+ }
339
+ }
340
+ /**
341
+ * Check if the WebSocket is connected
342
+ */
343
+ isConnected() {
344
+ return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
345
+ }
346
+ /**
347
+ * Get current reconnection attempt count
348
+ */
349
+ getReconnectAttempts() {
350
+ return this.reconnectAttempts;
351
+ }
352
+ /**
353
+ * Schedule a reconnection attempt with exponential backoff
354
+ * Backoff: 1s → 2s → 5s → 10s (max)
355
+ */
356
+ scheduleReconnect() {
357
+ if (this.reconnectTimer || !this.token) {
358
+ return;
359
+ }
360
+ this.reconnectAttempts++;
361
+ const backoffDelays = [1e3, 2e3, 5e3, 1e4];
362
+ const delay = backoffDelays[Math.min(this.reconnectAttempts - 1, backoffDelays.length - 1)];
363
+ this.logger.debug(`Scheduling reconnect attempt ${this.reconnectAttempts} in ${delay}ms`);
364
+ this.emit("reconnecting", { attempt: this.reconnectAttempts, delay });
365
+ this.reconnectTimer = setTimeout(() => {
366
+ this.reconnectTimer = null;
367
+ this.attemptConnection().catch(() => {
368
+ });
369
+ }, delay);
370
+ }
371
+ };
372
+
373
+ // src/auth/TokenManager.ts
374
+ var TokenManager = class {
375
+ constructor() {
376
+ this.token = null;
377
+ }
378
+ /**
379
+ * Set the SDK token
380
+ */
381
+ setToken(token) {
382
+ if (!this.isValidFormat(token)) {
383
+ throw new Error("Invalid token format");
384
+ }
385
+ this.token = token;
386
+ }
387
+ /**
388
+ * Get the current SDK token
389
+ */
390
+ getToken() {
391
+ return this.token;
392
+ }
393
+ /**
394
+ * Check if a token is set
395
+ */
396
+ hasToken() {
397
+ return this.token !== null && this.token.length > 0;
398
+ }
399
+ /**
400
+ * Clear the token
401
+ */
402
+ clearToken() {
403
+ this.token = null;
404
+ }
405
+ /**
406
+ * Replace the current token with a new one (token rotation)
407
+ */
408
+ rotateToken(newToken) {
409
+ if (!this.isValidFormat(newToken)) {
410
+ throw new Error("Invalid token format");
411
+ }
412
+ this.token = newToken;
413
+ }
414
+ /**
415
+ * Validate token format (basic validation)
416
+ */
417
+ isValidFormat(token) {
418
+ return typeof token === "string" && token.length > 0;
419
+ }
420
+ /**
421
+ * Validate that a token is present before operations
422
+ */
423
+ validateTokenPresence() {
424
+ if (!this.hasToken()) {
425
+ throw new Error("Token is required but not set");
426
+ }
427
+ }
428
+ };
429
+
430
+ // src/messages/Message.ts
431
+ var MessageFactory = class {
432
+ /**
433
+ * Create an optimistic message (client-generated, not yet confirmed by server)
434
+ */
435
+ static createOptimistic(conversationId, messageId, content, messageType = "text", metadata) {
436
+ return {
437
+ id: messageId,
438
+ conversationId,
439
+ content,
440
+ messageType,
441
+ optimistic: true,
442
+ receiptState: "sent",
443
+ createdAt: /* @__PURE__ */ new Date(),
444
+ metadata
445
+ };
446
+ }
447
+ /**
448
+ * Create a message from server payload (confirmed message)
449
+ */
450
+ static fromServerPayload(conversationId, payload) {
451
+ return {
452
+ id: payload.message_id,
453
+ conversationId,
454
+ content: payload.content,
455
+ senderId: payload.sender_id,
456
+ senderType: payload.sender_type,
457
+ messageType: payload.message_type === "system" ? "system" : "text",
458
+ optimistic: false,
459
+ receiptState: "sent",
460
+ createdAt: payload.sent_at ? new Date(payload.sent_at) : /* @__PURE__ */ new Date()
461
+ };
462
+ }
463
+ /**
464
+ * Convert an optimistic message to confirmed (reconciliation)
465
+ */
466
+ static confirmOptimistic(optimisticMessage, serverPayload) {
467
+ return {
468
+ ...optimisticMessage,
469
+ optimistic: false,
470
+ senderId: serverPayload.sender_id,
471
+ senderType: serverPayload.sender_type,
472
+ receiptState: optimisticMessage.receiptState ?? "sent",
473
+ createdAt: serverPayload.sent_at ? new Date(serverPayload.sent_at) : optimisticMessage.createdAt
474
+ };
475
+ }
476
+ /**
477
+ * Update receipt state on a message (immutable update)
478
+ */
479
+ static updateReceiptState(message, receiptState) {
480
+ return {
481
+ ...message,
482
+ receiptState
483
+ };
484
+ }
485
+ };
486
+ var ReceiptManager = class extends eventemitter3.EventEmitter {
487
+ constructor(debug = false) {
488
+ super();
489
+ /**
490
+ * Track receipt state per message
491
+ * Map<messageId, ReceiptState>
492
+ */
493
+ this.receiptState = /* @__PURE__ */ new Map();
494
+ this.logger = new Logger(debug);
495
+ }
496
+ /**
497
+ * Get receipt state for a message
498
+ */
499
+ getReceiptState(messageId) {
500
+ return this.receiptState.get(messageId);
501
+ }
502
+ /**
503
+ * Handle a delivered receipt
504
+ *
505
+ * Rules:
506
+ * - Only update if current state < delivered
507
+ * - Ignore duplicate or regressive updates
508
+ */
509
+ handleDelivered(messageId, userId) {
510
+ if (!messageId) {
511
+ this.logger.warn("Invalid delivered receipt: missing messageId");
512
+ return false;
513
+ }
514
+ const currentState = this.receiptState.get(messageId) ?? "sent";
515
+ if (this.compareStates(currentState, "delivered") < 0) {
516
+ this.receiptState.set(messageId, "delivered");
517
+ this.emit("receipt", {
518
+ messageId,
519
+ state: "delivered",
520
+ userId
521
+ });
522
+ this.logger.debug("Receipt delivered:", messageId);
523
+ return true;
524
+ } else {
525
+ this.logger.debug("Delivered receipt ignored (already delivered or read):", messageId);
526
+ return false;
527
+ }
528
+ }
529
+ /**
530
+ * Handle a read receipt
531
+ *
532
+ * Rules:
533
+ * - Only update if current state < read
534
+ * - Ignore duplicate or regressive updates
535
+ */
536
+ handleRead(messageId, userId) {
537
+ if (!messageId) {
538
+ this.logger.warn("Invalid read receipt: missing messageId");
539
+ return false;
540
+ }
541
+ const currentState = this.receiptState.get(messageId) ?? "sent";
542
+ if (this.compareStates(currentState, "read") < 0) {
543
+ this.receiptState.set(messageId, "read");
544
+ this.emit("receipt", {
545
+ messageId,
546
+ state: "read",
547
+ userId
548
+ });
549
+ this.logger.debug("Receipt read:", messageId);
550
+ return true;
551
+ } else {
552
+ this.logger.debug("Read receipt ignored (already read):", messageId);
553
+ return false;
554
+ }
555
+ }
556
+ /**
557
+ * Compare two receipt states
558
+ * Returns: -1 if a < b, 0 if a === b, 1 if a > b
559
+ */
560
+ compareStates(a, b) {
561
+ const order = ["sent", "delivered", "read"];
562
+ const aIndex = order.indexOf(a);
563
+ const bIndex = order.indexOf(b);
564
+ if (aIndex === -1 || bIndex === -1) {
565
+ return 0;
566
+ }
567
+ return aIndex - bIndex;
568
+ }
569
+ /**
570
+ * Clear receipt state for a conversation
571
+ * Note: We track by messageId, so we need to clear all messages for a conversation
572
+ * This is called when leaving a conversation
573
+ */
574
+ clearConversation(conversationId, messageIds) {
575
+ let cleared = 0;
576
+ for (const messageId of messageIds) {
577
+ if (this.receiptState.has(messageId)) {
578
+ this.receiptState.delete(messageId);
579
+ cleared++;
580
+ }
581
+ }
582
+ if (cleared > 0) {
583
+ this.logger.debug("Cleared receipt state for conversation:", conversationId, `(${cleared} messages)`);
584
+ }
585
+ }
586
+ /**
587
+ * Clear receipt state for a specific message
588
+ */
589
+ clearMessage(messageId) {
590
+ if (this.receiptState.delete(messageId)) {
591
+ this.logger.debug("Cleared receipt state for message:", messageId);
592
+ }
593
+ }
594
+ /**
595
+ * Clear all receipt state
596
+ */
597
+ clear() {
598
+ this.receiptState.clear();
599
+ this.logger.debug("Cleared all receipt state");
600
+ }
601
+ };
602
+
603
+ // src/messages/MessageManager.ts
604
+ var MessageManager = class extends eventemitter3.EventEmitter {
605
+ constructor(debug = false) {
606
+ super();
607
+ /**
608
+ * Track seen message IDs per conversation to prevent duplicates
609
+ * Map<conversationId, Set<message_id>>
610
+ */
611
+ this.seenMessageIds = /* @__PURE__ */ new Map();
612
+ /**
613
+ * Track optimistic messages that are waiting for server confirmation
614
+ * Map<conversationId, Map<message_id, Message>>
615
+ */
616
+ this.optimisticMessages = /* @__PURE__ */ new Map();
617
+ /**
618
+ * Track confirmed messages for receipt updates
619
+ * Map<conversationId, Map<message_id, Message>>
620
+ */
621
+ this.confirmedMessages = /* @__PURE__ */ new Map();
622
+ this.logger = new Logger(debug);
623
+ this.receiptManager = new ReceiptManager(debug);
624
+ this.receiptManager.on("receipt", (event) => {
625
+ this.updateMessageReceiptState(event);
626
+ });
627
+ }
628
+ /**
629
+ * Create an optimistic message and emit it immediately
630
+ *
631
+ * @returns The created optimistic message
632
+ */
633
+ createOptimisticMessage(conversationId, messageId, content, messageType = "text", metadata) {
634
+ const message = MessageFactory.createOptimistic(
635
+ conversationId,
636
+ messageId,
637
+ content,
638
+ messageType,
639
+ metadata
640
+ );
641
+ this.trackOptimisticMessage(conversationId, messageId, message);
642
+ this.trackMessageId(conversationId, messageId);
643
+ this.emit("message", message);
644
+ this.logger.debug("Created optimistic message:", messageId);
645
+ return message;
646
+ }
647
+ /**
648
+ * Handle an incoming message from the server
649
+ *
650
+ * This method:
651
+ * - Checks for duplicates
652
+ * - Reconciles optimistic messages if message_id matches
653
+ * - Emits confirmed messages
654
+ *
655
+ * @returns The message (reconciled if optimistic, new if not), or null if duplicate
656
+ */
657
+ handleIncomingMessage(conversationId, serverPayload) {
658
+ const messageId = serverPayload.message_id;
659
+ const optimisticMessage = this.getOptimisticMessage(conversationId, messageId);
660
+ if (optimisticMessage) {
661
+ const reconciledMessage = this.reconcileOptimisticMessage(optimisticMessage, serverPayload);
662
+ this.removeOptimisticMessage(conversationId, messageId);
663
+ this.trackMessageId(conversationId, messageId);
664
+ this.trackConfirmedMessage(conversationId, messageId, reconciledMessage);
665
+ return reconciledMessage;
666
+ }
667
+ if (this.hasSeenMessage(conversationId, messageId)) {
668
+ this.logger.debug("Duplicate message ignored:", messageId);
669
+ return null;
670
+ }
671
+ this.trackMessageId(conversationId, messageId);
672
+ const message = MessageFactory.fromServerPayload(conversationId, serverPayload);
673
+ this.trackConfirmedMessage(conversationId, messageId, message);
674
+ this.emit("message", message);
675
+ this.logger.debug("Handled incoming message:", messageId);
676
+ return message;
677
+ }
678
+ /**
679
+ * Handle a receipt update
680
+ *
681
+ * This method:
682
+ * - Routes receipt to ReceiptManager
683
+ * - Updates message receipt state if message exists
684
+ * - Emits updated message
685
+ */
686
+ handleReceipt(_conversationId, messageId, receiptState, userId) {
687
+ if (receiptState === "delivered") {
688
+ this.receiptManager.handleDelivered(messageId, userId);
689
+ } else if (receiptState === "read") {
690
+ this.receiptManager.handleRead(messageId, userId);
691
+ }
692
+ }
693
+ /**
694
+ * Update message receipt state and emit updated message
695
+ */
696
+ updateMessageReceiptState(event) {
697
+ for (const [, messages] of this.confirmedMessages.entries()) {
698
+ const message = messages.get(event.messageId);
699
+ if (message) {
700
+ const updatedMessage = MessageFactory.updateReceiptState(
701
+ message,
702
+ event.state
703
+ );
704
+ messages.set(event.messageId, updatedMessage);
705
+ this.emit("message", updatedMessage);
706
+ this.logger.debug("Updated message receipt state:", event.messageId, event.state);
707
+ return;
708
+ }
709
+ }
710
+ for (const [, messages] of this.optimisticMessages.entries()) {
711
+ const message = messages.get(event.messageId);
712
+ if (message) {
713
+ const updatedMessage = MessageFactory.updateReceiptState(
714
+ message,
715
+ event.state
716
+ );
717
+ messages.set(event.messageId, updatedMessage);
718
+ this.emit("message", updatedMessage);
719
+ this.logger.debug("Updated optimistic message receipt state:", event.messageId, event.state);
720
+ return;
721
+ }
722
+ }
723
+ this.logger.warn("Receipt for unknown message:", event.messageId);
724
+ }
725
+ /**
726
+ * Reconcile an optimistic message with a server confirmation
727
+ *
728
+ * This is called when we receive a server message with the same message_id
729
+ * as an optimistic message we previously created.
730
+ *
731
+ * @returns The reconciled (confirmed) message
732
+ */
733
+ reconcileOptimisticMessage(optimisticMessage, serverPayload) {
734
+ const confirmedMessage = MessageFactory.confirmOptimistic(optimisticMessage, serverPayload);
735
+ this.emit("message", confirmedMessage);
736
+ this.logger.debug("Reconciled optimistic message:", optimisticMessage.id);
737
+ return confirmedMessage;
738
+ }
739
+ /**
740
+ * Check if we've already seen a message ID for a conversation
741
+ */
742
+ hasSeenMessage(conversationId, messageId) {
743
+ const seenIds = this.seenMessageIds.get(conversationId);
744
+ return seenIds?.has(messageId) ?? false;
745
+ }
746
+ /**
747
+ * Track a message ID for a conversation (for deduplication)
748
+ */
749
+ trackMessageId(conversationId, messageId) {
750
+ let seenIds = this.seenMessageIds.get(conversationId);
751
+ if (!seenIds) {
752
+ seenIds = /* @__PURE__ */ new Set();
753
+ this.seenMessageIds.set(conversationId, seenIds);
754
+ }
755
+ seenIds.add(messageId);
756
+ }
757
+ /**
758
+ * Track an optimistic message for later reconciliation
759
+ */
760
+ trackOptimisticMessage(conversationId, messageId, message) {
761
+ let conversationOptimistic = this.optimisticMessages.get(conversationId);
762
+ if (!conversationOptimistic) {
763
+ conversationOptimistic = /* @__PURE__ */ new Map();
764
+ this.optimisticMessages.set(conversationId, conversationOptimistic);
765
+ }
766
+ conversationOptimistic.set(messageId, message);
767
+ }
768
+ /**
769
+ * Get an optimistic message by ID
770
+ */
771
+ getOptimisticMessage(conversationId, messageId) {
772
+ const conversationOptimistic = this.optimisticMessages.get(conversationId);
773
+ return conversationOptimistic?.get(messageId);
774
+ }
775
+ /**
776
+ * Remove an optimistic message from tracking
777
+ */
778
+ removeOptimisticMessage(conversationId, messageId) {
779
+ const conversationOptimistic = this.optimisticMessages.get(conversationId);
780
+ if (conversationOptimistic) {
781
+ conversationOptimistic.delete(messageId);
782
+ if (conversationOptimistic.size === 0) {
783
+ this.optimisticMessages.delete(conversationId);
784
+ }
785
+ }
786
+ }
787
+ /**
788
+ * Track a confirmed message for receipt updates
789
+ */
790
+ trackConfirmedMessage(conversationId, messageId, message) {
791
+ let conversationMessages = this.confirmedMessages.get(conversationId);
792
+ if (!conversationMessages) {
793
+ conversationMessages = /* @__PURE__ */ new Map();
794
+ this.confirmedMessages.set(conversationId, conversationMessages);
795
+ }
796
+ conversationMessages.set(messageId, message);
797
+ }
798
+ /**
799
+ * Get all message IDs for a conversation (for receipt clearing)
800
+ */
801
+ getMessageIds(conversationId) {
802
+ const confirmed = this.confirmedMessages.get(conversationId);
803
+ const optimistic = this.optimisticMessages.get(conversationId);
804
+ const ids = /* @__PURE__ */ new Set();
805
+ if (confirmed) {
806
+ for (const id of confirmed.keys()) {
807
+ ids.add(id);
808
+ }
809
+ }
810
+ if (optimistic) {
811
+ for (const id of optimistic.keys()) {
812
+ ids.add(id);
813
+ }
814
+ }
815
+ return Array.from(ids);
816
+ }
817
+ /**
818
+ * Clear tracked message IDs and optimistic messages for a conversation
819
+ */
820
+ clearConversation(conversationId) {
821
+ this.seenMessageIds.delete(conversationId);
822
+ this.optimisticMessages.delete(conversationId);
823
+ const messageIds = this.getMessageIds(conversationId);
824
+ this.receiptManager.clearConversation(conversationId, messageIds);
825
+ this.confirmedMessages.delete(conversationId);
826
+ this.logger.debug("Cleared message tracking for conversation:", conversationId);
827
+ }
828
+ /**
829
+ * Clear all tracked message IDs and optimistic messages
830
+ */
831
+ clear() {
832
+ this.seenMessageIds.clear();
833
+ this.optimisticMessages.clear();
834
+ this.confirmedMessages.clear();
835
+ this.receiptManager.clear();
836
+ this.logger.debug("Cleared all message tracking");
837
+ }
838
+ };
839
+ var TypingManager = class extends eventemitter3.EventEmitter {
840
+ // 5 seconds
841
+ constructor(debug = false) {
842
+ super();
843
+ /**
844
+ * Track active typing timers per conversation per user
845
+ * Map<conversationId, Map<userId, timeout>>
846
+ */
847
+ this.typingTimers = /* @__PURE__ */ new Map();
848
+ /**
849
+ * Track who is currently typing per conversation
850
+ * Map<conversationId, Set<userId>>
851
+ */
852
+ this.activeTyping = /* @__PURE__ */ new Map();
853
+ this.TYPING_TIMEOUT_MS = 5e3;
854
+ this.logger = new Logger(debug);
855
+ }
856
+ /**
857
+ * Handle a typing_start event
858
+ *
859
+ * Rules:
860
+ * - Emit only once per user until stopped
861
+ * - Restart timeout if typing_start repeats
862
+ */
863
+ handleTypingStart(conversationId, userId) {
864
+ if (!conversationId || !userId) {
865
+ this.logger.warn("Invalid typing_start: missing conversationId or userId");
866
+ return;
867
+ }
868
+ let conversationTyping = this.activeTyping.get(conversationId);
869
+ if (!conversationTyping) {
870
+ conversationTyping = /* @__PURE__ */ new Set();
871
+ this.activeTyping.set(conversationId, conversationTyping);
872
+ }
873
+ let conversationTimers = this.typingTimers.get(conversationId);
874
+ if (!conversationTimers) {
875
+ conversationTimers = /* @__PURE__ */ new Map();
876
+ this.typingTimers.set(conversationId, conversationTimers);
877
+ }
878
+ const wasTyping = conversationTyping.has(userId);
879
+ const existingTimer = conversationTimers.get(userId);
880
+ if (existingTimer) {
881
+ clearTimeout(existingTimer);
882
+ }
883
+ conversationTyping.add(userId);
884
+ const timer = setTimeout(() => {
885
+ this.handleTypingStop(conversationId, userId);
886
+ }, this.TYPING_TIMEOUT_MS);
887
+ conversationTimers.set(userId, timer);
888
+ if (!wasTyping) {
889
+ this.emit("typing", {
890
+ conversationId,
891
+ userId,
892
+ state: "start"
893
+ });
894
+ this.logger.debug("Typing started:", conversationId, userId);
895
+ } else {
896
+ this.logger.debug("Typing timer restarted:", conversationId, userId);
897
+ }
898
+ }
899
+ /**
900
+ * Handle a typing_stop event
901
+ *
902
+ * Rules:
903
+ * - Emit only if user was typing
904
+ */
905
+ handleTypingStop(conversationId, userId) {
906
+ if (!conversationId || !userId) {
907
+ this.logger.warn("Invalid typing_stop: missing conversationId or userId");
908
+ return;
909
+ }
910
+ const conversationTyping = this.activeTyping.get(conversationId);
911
+ const conversationTimers = this.typingTimers.get(conversationId);
912
+ if (!conversationTyping || !conversationTyping.has(userId)) {
913
+ this.logger.debug("Typing stop ignored (user not typing):", conversationId, userId);
914
+ return;
915
+ }
916
+ const timer = conversationTimers?.get(userId);
917
+ if (timer) {
918
+ clearTimeout(timer);
919
+ conversationTimers?.delete(userId);
920
+ }
921
+ conversationTyping.delete(userId);
922
+ if (conversationTyping.size === 0) {
923
+ this.activeTyping.delete(conversationId);
924
+ }
925
+ if (conversationTimers && conversationTimers.size === 0) {
926
+ this.typingTimers.delete(conversationId);
927
+ }
928
+ this.emit("typing", {
929
+ conversationId,
930
+ userId,
931
+ state: "stop"
932
+ });
933
+ this.logger.debug("Typing stopped:", conversationId, userId);
934
+ }
935
+ /**
936
+ * Clear all typing state for a conversation
937
+ */
938
+ clearConversation(conversationId) {
939
+ const conversationTyping = this.activeTyping.get(conversationId);
940
+ const conversationTimers = this.typingTimers.get(conversationId);
941
+ if (conversationTyping) {
942
+ for (const userId of conversationTyping) {
943
+ this.emit("typing", {
944
+ conversationId,
945
+ userId,
946
+ state: "stop"
947
+ });
948
+ }
949
+ this.activeTyping.delete(conversationId);
950
+ }
951
+ if (conversationTimers) {
952
+ for (const timer of conversationTimers.values()) {
953
+ clearTimeout(timer);
954
+ }
955
+ this.typingTimers.delete(conversationId);
956
+ }
957
+ this.logger.debug("Cleared typing state for conversation:", conversationId);
958
+ }
959
+ /**
960
+ * Clear all typing state
961
+ */
962
+ clear() {
963
+ for (const conversationTimers of this.typingTimers.values()) {
964
+ for (const timer of conversationTimers.values()) {
965
+ clearTimeout(timer);
966
+ }
967
+ }
968
+ this.typingTimers.clear();
969
+ this.activeTyping.clear();
970
+ this.logger.debug("Cleared all typing state");
971
+ }
972
+ /**
973
+ * Get currently typing users for a conversation
974
+ */
975
+ getTypingUsers(conversationId) {
976
+ const conversationTyping = this.activeTyping.get(conversationId);
977
+ return conversationTyping ? Array.from(conversationTyping) : [];
978
+ }
979
+ };
980
+
981
+ // src/conversations/Conversation.ts
982
+ var Conversation = class extends eventemitter3.EventEmitter {
983
+ constructor(conversationId, debug = false) {
984
+ super();
985
+ this._isJoined = false;
986
+ this.id = conversationId;
987
+ this.messageManager = new MessageManager(debug);
988
+ this.typingManager = new TypingManager(debug);
989
+ this.messageManager.on("message", (message) => {
990
+ if (message.conversationId === this.id) {
991
+ this.emit("message", message);
992
+ }
993
+ });
994
+ this.messageManager.on("receipt", (event) => {
995
+ this.emit("receipt", event);
996
+ });
997
+ this.typingManager.on("typing", (event) => {
998
+ if (event.conversationId === this.id) {
999
+ this.emit("typing", {
1000
+ userId: event.userId,
1001
+ state: event.state
1002
+ });
1003
+ }
1004
+ });
1005
+ }
1006
+ /**
1007
+ * Check if this conversation is currently joined
1008
+ */
1009
+ get isJoined() {
1010
+ return this._isJoined;
1011
+ }
1012
+ /**
1013
+ * Set the send message handler (called by ConversationManager)
1014
+ */
1015
+ setSendMessageHandler(handler) {
1016
+ this.sendMessageHandler = handler;
1017
+ }
1018
+ /**
1019
+ * Set the send typing handler (called by ConversationManager)
1020
+ */
1021
+ setSendTypingHandler(handler) {
1022
+ this.sendTypingHandler = handler;
1023
+ }
1024
+ /**
1025
+ * Mark conversation as joined
1026
+ */
1027
+ markJoined() {
1028
+ if (!this._isJoined) {
1029
+ this._isJoined = true;
1030
+ this.emit("joined", { conversation_id: this.id });
1031
+ }
1032
+ }
1033
+ /**
1034
+ * Mark conversation as left
1035
+ */
1036
+ markLeft() {
1037
+ if (this._isJoined) {
1038
+ this._isJoined = false;
1039
+ this.emit("left", { conversation_id: this.id });
1040
+ }
1041
+ }
1042
+ /**
1043
+ * Send a message to this conversation
1044
+ *
1045
+ * This method:
1046
+ * 1. Generates a message_id (UUID v4)
1047
+ * 2. Creates an optimistic Message
1048
+ * 3. Emits the optimistic message immediately
1049
+ * 4. Sends the message via transport
1050
+ */
1051
+ sendMessage(content, messageType = "text", metadata) {
1052
+ if (!this.sendMessageHandler) {
1053
+ const error = new Error("Conversation is not connected. Call join() first.");
1054
+ this.emit("error", error);
1055
+ throw error;
1056
+ }
1057
+ if (!this._isJoined) {
1058
+ const error = new Error("Conversation is not joined. Call join() first.");
1059
+ this.emit("error", error);
1060
+ throw error;
1061
+ }
1062
+ const messageId = uuid.v4();
1063
+ this.messageManager.createOptimisticMessage(
1064
+ this.id,
1065
+ messageId,
1066
+ content,
1067
+ messageType,
1068
+ metadata
1069
+ );
1070
+ const protocolMessage = {
1071
+ type: "message",
1072
+ conversation_id: this.id,
1073
+ payload: {
1074
+ message_id: messageId,
1075
+ content,
1076
+ message_type: messageType
1077
+ }
1078
+ };
1079
+ try {
1080
+ this.sendMessageHandler(protocolMessage);
1081
+ } catch (error) {
1082
+ const sendError = error instanceof Error ? error : new Error("Failed to send message");
1083
+ this.emit("error", sendError);
1084
+ throw sendError;
1085
+ }
1086
+ }
1087
+ /**
1088
+ * Handle an incoming message from the server
1089
+ * Called by ConversationManager when routing messages
1090
+ */
1091
+ handleIncomingMessage(payload) {
1092
+ this.messageManager.handleIncomingMessage(this.id, payload);
1093
+ }
1094
+ /**
1095
+ * Handle a receipt update
1096
+ * Called by ConversationManager when routing receipt messages
1097
+ */
1098
+ handleReceipt(messageId, receiptState, userId) {
1099
+ this.messageManager.handleReceipt(this.id, messageId, receiptState, userId);
1100
+ }
1101
+ /**
1102
+ * Handle a typing event from the server
1103
+ * Called by ConversationManager when routing typing messages
1104
+ */
1105
+ handleTyping(payload) {
1106
+ if (payload.state === "start") {
1107
+ this.typingManager.handleTypingStart(this.id, payload.user_id);
1108
+ } else if (payload.state === "stop") {
1109
+ this.typingManager.handleTypingStop(this.id, payload.user_id);
1110
+ }
1111
+ }
1112
+ /**
1113
+ * Emit a presence event (called by ConversationManager when routing)
1114
+ * Note: Presence is SDK-global, but we emit it here for convenience
1115
+ */
1116
+ emitPresence(payload) {
1117
+ this.emit("presence", {
1118
+ userId: payload.user_id,
1119
+ state: payload.state
1120
+ });
1121
+ }
1122
+ /**
1123
+ * Clear message tracking for this conversation
1124
+ */
1125
+ clearMessages() {
1126
+ this.messageManager.clearConversation(this.id);
1127
+ }
1128
+ /**
1129
+ * Clear typing state for this conversation
1130
+ */
1131
+ clearTyping() {
1132
+ this.typingManager.clearConversation(this.id);
1133
+ }
1134
+ /**
1135
+ * Start typing indicator
1136
+ *
1137
+ * This method sends a typing_start message to the server.
1138
+ * The TypingManager will handle debouncing and auto-expiry.
1139
+ */
1140
+ startTyping() {
1141
+ if (!this.sendTypingHandler) {
1142
+ const error = new Error("Conversation is not connected. Call join() first.");
1143
+ this.emit("error", error);
1144
+ throw error;
1145
+ }
1146
+ if (!this._isJoined) {
1147
+ const error = new Error("Conversation is not joined. Call join() first.");
1148
+ this.emit("error", error);
1149
+ throw error;
1150
+ }
1151
+ const typingMessage = {
1152
+ type: "typing_start",
1153
+ conversation_id: this.id
1154
+ };
1155
+ try {
1156
+ this.sendTypingHandler(typingMessage);
1157
+ } catch (error) {
1158
+ const sendError = error instanceof Error ? error : new Error("Failed to send typing_start");
1159
+ this.emit("error", sendError);
1160
+ throw sendError;
1161
+ }
1162
+ }
1163
+ /**
1164
+ * Stop typing indicator
1165
+ *
1166
+ * This method sends a typing_stop message to the server.
1167
+ */
1168
+ stopTyping() {
1169
+ if (!this.sendTypingHandler) {
1170
+ const error = new Error("Conversation is not connected. Call join() first.");
1171
+ this.emit("error", error);
1172
+ throw error;
1173
+ }
1174
+ if (!this._isJoined) {
1175
+ const error = new Error("Conversation is not joined. Call join() first.");
1176
+ this.emit("error", error);
1177
+ throw error;
1178
+ }
1179
+ const typingMessage = {
1180
+ type: "typing_stop",
1181
+ conversation_id: this.id
1182
+ };
1183
+ try {
1184
+ this.sendTypingHandler(typingMessage);
1185
+ } catch (error) {
1186
+ const sendError = error instanceof Error ? error : new Error("Failed to send typing_stop");
1187
+ this.emit("error", sendError);
1188
+ throw sendError;
1189
+ }
1190
+ }
1191
+ };
1192
+
1193
+ // src/conversations/ConversationManager.ts
1194
+ var ConversationManager = class extends eventemitter3.EventEmitter {
1195
+ constructor(sendMessageHandler, debug = false, supportMessageHandler, sendTypingHandler) {
1196
+ super();
1197
+ this.conversations = /* @__PURE__ */ new Map();
1198
+ this.sendMessageHandler = sendMessageHandler;
1199
+ this.sendTypingHandler = sendTypingHandler;
1200
+ this.supportMessageHandler = supportMessageHandler;
1201
+ this.debug = debug;
1202
+ this.logger = new Logger(debug);
1203
+ }
1204
+ /**
1205
+ * Set the support message handler (called by ChatAfrika)
1206
+ */
1207
+ setSupportMessageHandler(handler) {
1208
+ this.supportMessageHandler = handler;
1209
+ }
1210
+ /**
1211
+ * Set the send typing handler (called by ChatAfrika)
1212
+ */
1213
+ setSendTypingHandler(handler) {
1214
+ this.sendTypingHandler = handler;
1215
+ for (const conversation of this.conversations.values()) {
1216
+ conversation.setSendTypingHandler(handler);
1217
+ }
1218
+ }
1219
+ /**
1220
+ * Get or create a Conversation instance
1221
+ * Conversations are lazy-created
1222
+ */
1223
+ get(conversationId) {
1224
+ if (!conversationId || conversationId.length === 0) {
1225
+ throw new Error("Conversation ID is required");
1226
+ }
1227
+ let conversation = this.conversations.get(conversationId);
1228
+ if (!conversation) {
1229
+ conversation = new Conversation(conversationId, this.debug);
1230
+ if (this.sendMessageHandler) {
1231
+ conversation.setSendMessageHandler(this.sendMessageHandler);
1232
+ }
1233
+ if (this.sendTypingHandler) {
1234
+ conversation.setSendTypingHandler(this.sendTypingHandler);
1235
+ }
1236
+ this.conversations.set(conversationId, conversation);
1237
+ this.logger.debug("Created conversation instance:", conversationId);
1238
+ }
1239
+ return conversation;
1240
+ }
1241
+ /**
1242
+ * Check if a conversation instance exists
1243
+ */
1244
+ has(conversationId) {
1245
+ return this.conversations.has(conversationId);
1246
+ }
1247
+ /**
1248
+ * Join a conversation
1249
+ * This delegates to the transport layer (ChatAfrika)
1250
+ */
1251
+ async join(conversationId, joinHandler) {
1252
+ if (!conversationId || conversationId.length === 0) {
1253
+ throw new Error("Conversation ID is required");
1254
+ }
1255
+ this.get(conversationId);
1256
+ await joinHandler(conversationId);
1257
+ this.logger.debug("Join requested for conversation:", conversationId);
1258
+ }
1259
+ /**
1260
+ * Leave a conversation
1261
+ * This delegates to the transport layer (ChatAfrika)
1262
+ */
1263
+ async leave(conversationId, leaveHandler) {
1264
+ if (!conversationId || conversationId.length === 0) {
1265
+ throw new Error("Conversation ID is required");
1266
+ }
1267
+ const conversation = this.conversations.get(conversationId);
1268
+ if (conversation) {
1269
+ conversation.clearTyping();
1270
+ await leaveHandler(conversationId);
1271
+ conversation.markLeft();
1272
+ this.logger.debug("Left conversation:", conversationId);
1273
+ } else {
1274
+ await leaveHandler(conversationId);
1275
+ this.logger.warn("Attempted to leave unknown conversation:", conversationId);
1276
+ }
1277
+ }
1278
+ /**
1279
+ * Route an incoming protocol message to the appropriate Conversation
1280
+ *
1281
+ * Messages are routed through MessageManager for lifecycle handling.
1282
+ * Typing events are routed through TypingManager.
1283
+ * Only messages for joined conversations are processed.
1284
+ */
1285
+ routeIncomingMessage(message) {
1286
+ let conversationId;
1287
+ switch (message.type) {
1288
+ case "joined":
1289
+ conversationId = message.payload?.conversation_id || message.conversation_id;
1290
+ if (conversationId) {
1291
+ const conversation = this.get(conversationId);
1292
+ conversation.markJoined();
1293
+ }
1294
+ break;
1295
+ case "message":
1296
+ conversationId = message.conversation_id;
1297
+ if (conversationId) {
1298
+ const conversation = this.conversations.get(conversationId);
1299
+ if (conversation) {
1300
+ if (conversation.isJoined) {
1301
+ conversation.handleIncomingMessage(message.payload);
1302
+ } else {
1303
+ this.logger.warn("Received message for conversation that is not joined:", conversationId);
1304
+ }
1305
+ } else {
1306
+ this.logger.warn("Received message for unknown conversation:", conversationId);
1307
+ }
1308
+ }
1309
+ break;
1310
+ case "typing":
1311
+ conversationId = message.conversation_id;
1312
+ if (conversationId) {
1313
+ const conversation = this.conversations.get(conversationId);
1314
+ if (conversation) {
1315
+ if (conversation.isJoined) {
1316
+ conversation.handleTyping(message.payload);
1317
+ } else {
1318
+ this.logger.warn("Received typing indicator for conversation that is not joined:", conversationId);
1319
+ }
1320
+ } else {
1321
+ this.logger.warn("Received typing indicator for unknown conversation:", conversationId);
1322
+ }
1323
+ }
1324
+ break;
1325
+ case "receipt":
1326
+ conversationId = message.conversation_id;
1327
+ if (conversationId) {
1328
+ const conversation = this.conversations.get(conversationId);
1329
+ if (conversation) {
1330
+ if (conversation.isJoined) {
1331
+ const receiptMsg = message;
1332
+ conversation.handleReceipt(
1333
+ receiptMsg.payload.message_id,
1334
+ receiptMsg.payload.state,
1335
+ receiptMsg.payload.user_id
1336
+ );
1337
+ } else {
1338
+ this.logger.warn("Received receipt for conversation that is not joined:", conversationId);
1339
+ }
1340
+ } else {
1341
+ this.logger.warn("Received receipt for unknown conversation:", conversationId);
1342
+ }
1343
+ }
1344
+ break;
1345
+ case "support":
1346
+ break;
1347
+ case "presence":
1348
+ conversationId = message.conversation_id;
1349
+ if (conversationId) {
1350
+ const conversation = this.conversations.get(conversationId);
1351
+ if (conversation && conversation.isJoined) {
1352
+ conversation.emitPresence(message.payload);
1353
+ }
1354
+ }
1355
+ break;
1356
+ case "error":
1357
+ this.logger.error("Received error message:", message.error);
1358
+ break;
1359
+ }
1360
+ }
1361
+ /**
1362
+ * Route a support protocol message to SupportManager
1363
+ */
1364
+ routeSupportMessage(message) {
1365
+ if (!this.supportMessageHandler) {
1366
+ this.logger.warn("Support message handler not set, ignoring support message");
1367
+ return;
1368
+ }
1369
+ const conversationId = message.conversation_id;
1370
+ if (!conversationId) {
1371
+ this.logger.warn("Support message missing conversation_id");
1372
+ return;
1373
+ }
1374
+ const eventType = message.payload?.event;
1375
+ if (!eventType) {
1376
+ this.logger.warn("Support message missing event type");
1377
+ return;
1378
+ }
1379
+ this.supportMessageHandler(conversationId, eventType, message.payload);
1380
+ }
1381
+ /**
1382
+ * Remove a conversation instance
1383
+ */
1384
+ remove(conversationId) {
1385
+ const conversation = this.conversations.get(conversationId);
1386
+ if (conversation) {
1387
+ conversation.markLeft();
1388
+ conversation.clearMessages();
1389
+ conversation.clearTyping();
1390
+ this.conversations.delete(conversationId);
1391
+ this.logger.debug("Removed conversation instance:", conversationId);
1392
+ }
1393
+ }
1394
+ /**
1395
+ * Get all conversation IDs
1396
+ */
1397
+ getAllIds() {
1398
+ return Array.from(this.conversations.keys());
1399
+ }
1400
+ /**
1401
+ * Clear all conversations
1402
+ */
1403
+ clear() {
1404
+ for (const conversation of this.conversations.values()) {
1405
+ conversation.markLeft();
1406
+ conversation.clearMessages();
1407
+ conversation.clearTyping();
1408
+ }
1409
+ this.conversations.clear();
1410
+ this.logger.debug("Cleared all conversations");
1411
+ }
1412
+ };
1413
+ var PresenceManager = class extends eventemitter3.EventEmitter {
1414
+ constructor(debug = false) {
1415
+ super();
1416
+ /**
1417
+ * Track current presence state per user
1418
+ * Map<userId, "online" | "offline">
1419
+ */
1420
+ this.presenceState = /* @__PURE__ */ new Map();
1421
+ this.logger = new Logger(debug);
1422
+ }
1423
+ /**
1424
+ * Handle a presence event
1425
+ *
1426
+ * Rules:
1427
+ * - Emit only on state change
1428
+ * - Ignore duplicate online/online or offline/offline events
1429
+ */
1430
+ handlePresence(userId, state) {
1431
+ if (!userId) {
1432
+ this.logger.warn("Invalid presence event: missing userId");
1433
+ return;
1434
+ }
1435
+ if (state !== "online" && state !== "offline") {
1436
+ this.logger.warn("Invalid presence state:", state);
1437
+ return;
1438
+ }
1439
+ const currentState = this.presenceState.get(userId);
1440
+ if (currentState !== state) {
1441
+ this.presenceState.set(userId, state);
1442
+ this.emit("presence", {
1443
+ userId,
1444
+ state
1445
+ });
1446
+ this.logger.debug("Presence changed:", userId, state);
1447
+ } else {
1448
+ this.logger.debug("Duplicate presence event ignored:", userId, state);
1449
+ }
1450
+ }
1451
+ /**
1452
+ * Get current presence state for a user
1453
+ */
1454
+ getPresence(userId) {
1455
+ return this.presenceState.get(userId);
1456
+ }
1457
+ /**
1458
+ * Get presence state for multiple users
1459
+ */
1460
+ getPresences(userIds) {
1461
+ const result = /* @__PURE__ */ new Map();
1462
+ for (const userId of userIds) {
1463
+ const state = this.presenceState.get(userId);
1464
+ if (state) {
1465
+ result.set(userId, state);
1466
+ }
1467
+ }
1468
+ return result;
1469
+ }
1470
+ /**
1471
+ * Clear presence for a user
1472
+ */
1473
+ clearPresence(userId) {
1474
+ const hadState = this.presenceState.has(userId);
1475
+ this.presenceState.delete(userId);
1476
+ if (hadState) {
1477
+ this.logger.debug("Cleared presence for user:", userId);
1478
+ }
1479
+ }
1480
+ /**
1481
+ * Clear all presence state
1482
+ */
1483
+ clear() {
1484
+ this.presenceState.clear();
1485
+ this.logger.debug("Cleared all presence state");
1486
+ }
1487
+ };
1488
+ var SupportSession = class extends eventemitter3.EventEmitter {
1489
+ constructor(conversationId, conversation, role, debug = false) {
1490
+ super();
1491
+ this.botActive = false;
1492
+ this.status = "queued";
1493
+ this.conversationId = conversationId;
1494
+ this.conversation = conversation;
1495
+ this.role = role;
1496
+ this.logger = new Logger(debug);
1497
+ this.conversation.on("message", (msg) => this.emit("message", msg));
1498
+ this.conversation.on("typing", (event) => this.emit("typing", event));
1499
+ this.conversation.on("presence", (event) => this.emit("presence", event));
1500
+ this.conversation.on("joined", (event) => this.emit("joined", event));
1501
+ this.conversation.on("left", (event) => this.emit("left", event));
1502
+ this.conversation.on("error", (error) => this.emit("error", error));
1503
+ }
1504
+ /**
1505
+ * Get the underlying Conversation instance
1506
+ */
1507
+ getConversation() {
1508
+ return this.conversation;
1509
+ }
1510
+ /**
1511
+ * Check if this session is for a customer
1512
+ */
1513
+ isCustomer() {
1514
+ return this.role === "customer";
1515
+ }
1516
+ /**
1517
+ * Check if this session is for an agent
1518
+ */
1519
+ isAgent() {
1520
+ return this.role === "agent";
1521
+ }
1522
+ /**
1523
+ * Get the current role
1524
+ */
1525
+ getRole() {
1526
+ return this.role;
1527
+ }
1528
+ /**
1529
+ * Check if bot is currently active
1530
+ */
1531
+ isBotActive() {
1532
+ return this.botActive;
1533
+ }
1534
+ /**
1535
+ * Get the assigned agent ID (if any)
1536
+ */
1537
+ getAssignedAgent() {
1538
+ return this.assignedAgentId;
1539
+ }
1540
+ /**
1541
+ * Get the current status
1542
+ */
1543
+ getStatus() {
1544
+ return this.status;
1545
+ }
1546
+ /**
1547
+ * Handle assignment of an agent
1548
+ */
1549
+ handleAssigned(agentId) {
1550
+ if (this.assignedAgentId === agentId) {
1551
+ return;
1552
+ }
1553
+ const previousAgentId = this.assignedAgentId;
1554
+ const wasAssigned = previousAgentId !== void 0;
1555
+ this.assignedAgentId = agentId;
1556
+ if (this.status === "queued") {
1557
+ this.status = "assigned";
1558
+ } else if (this.status === "assigned" || this.status === "active") ;
1559
+ if (!wasAssigned) {
1560
+ this.emit("assigned", { agentId });
1561
+ this.logger.debug("Support session assigned:", this.conversationId, agentId);
1562
+ } else {
1563
+ this.emit("assigned", { agentId, previousAgentId });
1564
+ this.logger.debug("Support session reassigned:", this.conversationId, agentId, "from", previousAgentId);
1565
+ }
1566
+ }
1567
+ /**
1568
+ * Handle unassignment of an agent
1569
+ */
1570
+ handleUnassigned() {
1571
+ if (this.assignedAgentId === void 0) {
1572
+ return;
1573
+ }
1574
+ const previousAgentId = this.assignedAgentId;
1575
+ this.assignedAgentId = void 0;
1576
+ if (this.status === "assigned" || this.status === "active") {
1577
+ this.status = "queued";
1578
+ }
1579
+ this.emit("unassigned", { previousAgentId });
1580
+ this.logger.debug("Support session unassigned:", this.conversationId);
1581
+ }
1582
+ /**
1583
+ * Handle bot takeover
1584
+ */
1585
+ handleBotTakeover() {
1586
+ if (this.botActive) {
1587
+ return;
1588
+ }
1589
+ this.botActive = true;
1590
+ this.emit("bot_takeover", {});
1591
+ this.logger.debug("Bot took over support session:", this.conversationId);
1592
+ }
1593
+ /**
1594
+ * Handle bot release
1595
+ */
1596
+ handleBotReleased() {
1597
+ if (!this.botActive) {
1598
+ return;
1599
+ }
1600
+ this.botActive = false;
1601
+ this.emit("bot_released", {});
1602
+ this.logger.debug("Bot released support session:", this.conversationId);
1603
+ }
1604
+ /**
1605
+ * Handle session closed
1606
+ */
1607
+ handleClosed() {
1608
+ if (this.status === "closed") {
1609
+ return;
1610
+ }
1611
+ const previousStatus = this.status;
1612
+ this.status = "closed";
1613
+ this.emit("closed", { previousStatus });
1614
+ this.logger.debug("Support session closed:", this.conversationId);
1615
+ }
1616
+ /**
1617
+ * Handle session reopened
1618
+ */
1619
+ handleReopened() {
1620
+ if (this.status !== "closed") {
1621
+ return;
1622
+ }
1623
+ this.status = this.assignedAgentId ? "assigned" : "queued";
1624
+ this.emit("reopened", {});
1625
+ this.logger.debug("Support session reopened:", this.conversationId);
1626
+ }
1627
+ /**
1628
+ * Send a message (delegates to Conversation)
1629
+ */
1630
+ sendMessage(content, messageType = "text", metadata) {
1631
+ this.conversation.sendMessage(content, messageType, metadata);
1632
+ }
1633
+ /**
1634
+ * Join the conversation (delegates to Conversation)
1635
+ */
1636
+ async join() {
1637
+ }
1638
+ /**
1639
+ * Leave the conversation (delegates to Conversation)
1640
+ */
1641
+ async leave() {
1642
+ }
1643
+ };
1644
+
1645
+ // src/support/SupportManager.ts
1646
+ var SupportManager = class extends eventemitter3.EventEmitter {
1647
+ constructor(getConversationHandler, debug = false) {
1648
+ super();
1649
+ this.sessions = /* @__PURE__ */ new Map();
1650
+ this.getConversationHandler = getConversationHandler;
1651
+ this.debug = debug;
1652
+ this.logger = new Logger(debug);
1653
+ }
1654
+ /**
1655
+ * Get or create a SupportSession for a conversation
1656
+ *
1657
+ * Note: This assumes the conversation is a support conversation.
1658
+ * The role is determined by the SDK user's context (not inferred).
1659
+ *
1660
+ * @param conversationId - The conversation ID
1661
+ * @param role - The role of the current user ('customer' or 'agent')
1662
+ * @returns SupportSession instance
1663
+ */
1664
+ get(conversationId, role = "customer") {
1665
+ if (!conversationId || conversationId.length === 0) {
1666
+ throw new Error("Conversation ID is required");
1667
+ }
1668
+ let session = this.sessions.get(conversationId);
1669
+ if (!session) {
1670
+ const conversation = this.getConversationHandler(conversationId);
1671
+ session = new SupportSession(conversationId, conversation, role, this.debug);
1672
+ session.on("assigned", (data) => this.emit("assigned", { conversationId, ...data }));
1673
+ session.on("unassigned", (data) => this.emit("unassigned", { conversationId, ...data }));
1674
+ session.on("bot_takeover", () => this.emit("bot_takeover", { conversationId }));
1675
+ session.on("bot_released", () => this.emit("bot_released", { conversationId }));
1676
+ session.on("closed", (data) => this.emit("closed", { conversationId, ...data }));
1677
+ session.on("reopened", () => this.emit("reopened", { conversationId }));
1678
+ this.sessions.set(conversationId, session);
1679
+ this.logger.debug("Created support session:", conversationId, role);
1680
+ }
1681
+ return session;
1682
+ }
1683
+ /**
1684
+ * Check if a support session exists
1685
+ */
1686
+ has(conversationId) {
1687
+ return this.sessions.has(conversationId);
1688
+ }
1689
+ /**
1690
+ * Get a support session if it exists
1691
+ */
1692
+ getSession(conversationId) {
1693
+ return this.sessions.get(conversationId);
1694
+ }
1695
+ /**
1696
+ * Handle a support protocol event
1697
+ */
1698
+ handleSupportEvent(conversationId, eventType, payload) {
1699
+ const session = this.sessions.get(conversationId);
1700
+ if (!session) {
1701
+ if (eventType === "assigned" && payload?.agent_id) {
1702
+ this.logger.debug("Support event for unknown session, will be handled when session is created:", conversationId);
1703
+ } else {
1704
+ this.logger.warn("Support event for unknown session:", conversationId, eventType);
1705
+ }
1706
+ return;
1707
+ }
1708
+ switch (eventType) {
1709
+ case "assigned":
1710
+ if (payload?.agent_id && typeof payload.agent_id === "string") {
1711
+ session.handleAssigned(payload.agent_id);
1712
+ }
1713
+ break;
1714
+ case "unassigned":
1715
+ session.handleUnassigned();
1716
+ break;
1717
+ case "bot_takeover":
1718
+ session.handleBotTakeover();
1719
+ break;
1720
+ case "bot_released":
1721
+ session.handleBotReleased();
1722
+ break;
1723
+ case "closed":
1724
+ session.handleClosed();
1725
+ break;
1726
+ case "reopened":
1727
+ session.handleReopened();
1728
+ break;
1729
+ }
1730
+ }
1731
+ /**
1732
+ * Remove a support session
1733
+ */
1734
+ remove(conversationId) {
1735
+ const session = this.sessions.get(conversationId);
1736
+ if (session) {
1737
+ this.sessions.delete(conversationId);
1738
+ this.logger.debug("Removed support session:", conversationId);
1739
+ }
1740
+ }
1741
+ /**
1742
+ * Clear all support sessions
1743
+ */
1744
+ clear() {
1745
+ this.sessions.clear();
1746
+ this.logger.debug("Cleared all support sessions");
1747
+ }
1748
+ };
1749
+
1750
+ // src/core/ChatAfrika.ts
1751
+ var ChatAfrika = class extends eventemitter3.EventEmitter {
1752
+ constructor(config) {
1753
+ super();
1754
+ this.connectionState = "disconnected" /* DISCONNECTED */;
1755
+ this.wsClient = null;
1756
+ this.config = {
1757
+ autoReconnect: true,
1758
+ maxReconnectAttempts: 5,
1759
+ reconnectDelay: 1e3,
1760
+ debug: false,
1761
+ apiUrl: "",
1762
+ ...config
1763
+ };
1764
+ this.tokenManager = new TokenManager();
1765
+ this.tokenManager.setToken(config.token);
1766
+ this.logger = new Logger(this.config.debug);
1767
+ this.wsClient = new WebSocketClient(this.config.wsUrl, {
1768
+ autoReconnect: this.config.autoReconnect,
1769
+ maxReconnectAttempts: this.config.maxReconnectAttempts,
1770
+ reconnectDelay: this.config.reconnectDelay,
1771
+ debug: this.config.debug
1772
+ });
1773
+ this.supportManager = new SupportManager(
1774
+ (conversationId) => this.conversationManager.get(conversationId),
1775
+ this.config.debug
1776
+ );
1777
+ this.conversationManager = new ConversationManager(
1778
+ (message) => {
1779
+ if (!this.wsClient) {
1780
+ throw new Error("WebSocket client not initialized");
1781
+ }
1782
+ this.wsClient.send(message);
1783
+ },
1784
+ this.config.debug,
1785
+ void 0,
1786
+ // supportMessageHandler (set later)
1787
+ (message) => {
1788
+ if (!this.wsClient) {
1789
+ throw new Error("WebSocket client not initialized");
1790
+ }
1791
+ this.wsClient.send(message);
1792
+ }
1793
+ );
1794
+ this.presenceManager = new PresenceManager(this.config.debug);
1795
+ this.presenceManager.on("presence", (event) => {
1796
+ this.emit("presence", event);
1797
+ });
1798
+ this.setupWebSocketHandlers();
1799
+ }
1800
+ /**
1801
+ * Set up WebSocket event handlers
1802
+ */
1803
+ setupWebSocketHandlers() {
1804
+ if (!this.wsClient) {
1805
+ return;
1806
+ }
1807
+ this.wsClient.on("connected", () => {
1808
+ this.connectionState = "connected" /* CONNECTED */;
1809
+ this.emit("connected");
1810
+ this.emit("state", this.connectionState);
1811
+ this.logger.debug("SDK connected");
1812
+ });
1813
+ this.wsClient.on("disconnected", (data) => {
1814
+ this.connectionState = "disconnected" /* DISCONNECTED */;
1815
+ this.emit("disconnected", data);
1816
+ this.emit("state", this.connectionState);
1817
+ this.logger.debug("SDK disconnected");
1818
+ });
1819
+ this.wsClient.on("reconnecting", (data) => {
1820
+ this.connectionState = "reconnecting" /* RECONNECTING */;
1821
+ this.emit("reconnecting", data);
1822
+ this.emit("state", this.connectionState);
1823
+ this.logger.debug("SDK reconnecting", data);
1824
+ });
1825
+ this.wsClient.on("message", (message) => {
1826
+ this.emit("message", message);
1827
+ this.conversationManager.routeIncomingMessage(message);
1828
+ if (message.type === "presence") {
1829
+ const presenceMsg = message;
1830
+ if (presenceMsg.payload) {
1831
+ this.presenceManager.handlePresence(
1832
+ presenceMsg.payload.user_id,
1833
+ presenceMsg.payload.state
1834
+ );
1835
+ }
1836
+ }
1837
+ if (message.type === "support") {
1838
+ this.conversationManager.routeSupportMessage(message);
1839
+ }
1840
+ switch (message.type) {
1841
+ case "joined":
1842
+ this.emit("joined", message);
1843
+ break;
1844
+ case "message":
1845
+ this.emit("message", message);
1846
+ break;
1847
+ case "typing":
1848
+ this.emit("typing", message);
1849
+ break;
1850
+ case "receipt":
1851
+ break;
1852
+ case "support":
1853
+ break;
1854
+ case "presence":
1855
+ break;
1856
+ case "error":
1857
+ this.emit("error", message);
1858
+ break;
1859
+ }
1860
+ });
1861
+ this.wsClient.on("error", (error) => {
1862
+ this.connectionState = "error" /* ERROR */;
1863
+ const errorMessage = error instanceof Error ? error.message : "";
1864
+ const errorName = error instanceof Error ? error.name : "";
1865
+ if (errorName === "TokenExpiredError" || errorMessage && errorMessage.includes("Token expired")) {
1866
+ this.logger.error("Token expired:", errorMessage);
1867
+ this.logger.warn("To reconnect, call sdk.updateToken(newToken) then sdk.connect()");
1868
+ } else {
1869
+ this.logger.error("SDK error:", error);
1870
+ }
1871
+ const errorToEmit = error instanceof Error ? error : new Error("WebSocket connection error");
1872
+ this.emit("error", errorToEmit);
1873
+ this.emit("state", this.connectionState);
1874
+ });
1875
+ }
1876
+ /**
1877
+ * Get the current connection state
1878
+ */
1879
+ getState() {
1880
+ return this.connectionState;
1881
+ }
1882
+ /**
1883
+ * Get the current configuration
1884
+ */
1885
+ getConfig() {
1886
+ return { ...this.config };
1887
+ }
1888
+ /**
1889
+ * Connect to the ChatAfrika service
1890
+ */
1891
+ async connect() {
1892
+ if (this.connectionState === "connected" /* CONNECTED */) {
1893
+ this.logger.warn("Already connected");
1894
+ return;
1895
+ }
1896
+ if (this.connectionState === "connecting" /* CONNECTING */) {
1897
+ this.logger.warn("Connection already in progress");
1898
+ return;
1899
+ }
1900
+ this.tokenManager.validateTokenPresence();
1901
+ const token = this.tokenManager.getToken();
1902
+ if (!token) {
1903
+ throw new Error("Token is required for connection");
1904
+ }
1905
+ this.connectionState = "connecting" /* CONNECTING */;
1906
+ this.emit("state", this.connectionState);
1907
+ this.logger.debug("Connecting...");
1908
+ try {
1909
+ if (!this.wsClient) {
1910
+ throw new Error("WebSocket client not initialized");
1911
+ }
1912
+ await this.wsClient.connect(token);
1913
+ } catch (error) {
1914
+ this.connectionState = "error" /* ERROR */;
1915
+ this.emit("state", this.connectionState);
1916
+ const connectionError = error instanceof Error ? error : new Error("Connection failed");
1917
+ this.emit("error", connectionError);
1918
+ throw connectionError;
1919
+ }
1920
+ }
1921
+ /**
1922
+ * Disconnect from the ChatAfrika service
1923
+ */
1924
+ async disconnect() {
1925
+ if (this.connectionState === "disconnected" /* DISCONNECTED */) {
1926
+ return;
1927
+ }
1928
+ this.logger.debug("Disconnecting...");
1929
+ this.conversationManager.clear();
1930
+ this.presenceManager.clear();
1931
+ this.supportManager.clear();
1932
+ if (this.wsClient) {
1933
+ this.wsClient.disconnect();
1934
+ }
1935
+ this.connectionState = "disconnected" /* DISCONNECTED */;
1936
+ this.emit("disconnected");
1937
+ this.emit("state", this.connectionState);
1938
+ }
1939
+ /**
1940
+ * Check if the client is connected
1941
+ */
1942
+ isConnected() {
1943
+ return this.connectionState === "connected" /* CONNECTED */ && this.wsClient?.isConnected() === true;
1944
+ }
1945
+ /**
1946
+ * Update the authentication token
1947
+ * Use this when your token expires and you need to refresh it
1948
+ * After updating, call connect() to reconnect with the new token
1949
+ */
1950
+ updateToken(newToken) {
1951
+ if (!newToken || newToken.length === 0) {
1952
+ throw new Error("Token cannot be empty");
1953
+ }
1954
+ this.tokenManager.setToken(newToken);
1955
+ this.logger.debug("Token updated");
1956
+ if (this.isConnected()) {
1957
+ this.logger.warn("Token updated while connected. Disconnecting to reconnect with new token...");
1958
+ this.disconnect();
1959
+ }
1960
+ }
1961
+ /**
1962
+ * Get the conversations manager
1963
+ * Exposes: sdk.conversations.get(id), sdk.conversations.join(id), etc.
1964
+ */
1965
+ get conversations() {
1966
+ return {
1967
+ get: (id) => this.conversationManager.get(id),
1968
+ has: (id) => this.conversationManager.has(id),
1969
+ join: (id) => this.conversationManager.join(id, (conversationId) => this.joinConversation(conversationId)),
1970
+ leave: (id) => this.conversationManager.leave(id, (conversationId) => this.leaveConversation(conversationId))
1971
+ };
1972
+ }
1973
+ /**
1974
+ * Get the support manager
1975
+ * Exposes: sdk.support.get(conversationId, role?)
1976
+ */
1977
+ get support() {
1978
+ return {
1979
+ get: (id, role = "customer") => this.supportManager.get(id, role),
1980
+ has: (id) => this.supportManager.has(id)
1981
+ };
1982
+ }
1983
+ /**
1984
+ * Join a conversation room (internal method used by ConversationManager)
1985
+ */
1986
+ async joinConversation(conversationId) {
1987
+ if (!this.isConnected()) {
1988
+ throw new Error("Must be connected before joining a conversation");
1989
+ }
1990
+ if (!conversationId || conversationId.length === 0) {
1991
+ throw new Error("Conversation ID is required");
1992
+ }
1993
+ if (!this.wsClient) {
1994
+ throw new Error("WebSocket client not initialized");
1995
+ }
1996
+ const message = {
1997
+ type: "join",
1998
+ conversation_id: conversationId
1999
+ };
2000
+ try {
2001
+ this.wsClient.send(message);
2002
+ this.logger.debug("Join conversation sent:", conversationId);
2003
+ } catch (error) {
2004
+ const sendError = error instanceof Error ? error : new Error("Failed to join conversation");
2005
+ this.emit("error", sendError);
2006
+ throw sendError;
2007
+ }
2008
+ }
2009
+ /**
2010
+ * Leave a conversation room (internal method used by ConversationManager)
2011
+ */
2012
+ async leaveConversation(conversationId) {
2013
+ if (!this.isConnected()) {
2014
+ throw new Error("Must be connected before leaving a conversation");
2015
+ }
2016
+ if (!conversationId || conversationId.length === 0) {
2017
+ throw new Error("Conversation ID is required");
2018
+ }
2019
+ if (!this.wsClient) {
2020
+ throw new Error("WebSocket client not initialized");
2021
+ }
2022
+ const message = {
2023
+ type: "leave",
2024
+ conversation_id: conversationId
2025
+ };
2026
+ try {
2027
+ this.wsClient.send(message);
2028
+ this.logger.debug("Leave conversation sent:", conversationId);
2029
+ } catch (error) {
2030
+ const sendError = error instanceof Error ? error : new Error("Failed to leave conversation");
2031
+ this.emit("error", sendError);
2032
+ throw sendError;
2033
+ }
2034
+ }
2035
+ /**
2036
+ * Rotate the authentication token
2037
+ */
2038
+ rotateToken(newToken) {
2039
+ this.tokenManager.rotateToken(newToken);
2040
+ this.logger.debug("Token rotated");
2041
+ if (this.isConnected()) {
2042
+ this.logger.warn("Token rotated while connected. Reconnect may be required.");
2043
+ }
2044
+ }
2045
+ /**
2046
+ * Get the token manager (for advanced use cases)
2047
+ */
2048
+ getTokenManager() {
2049
+ return this.tokenManager;
2050
+ }
2051
+ };
2052
+
2053
+ // src/utils/retry.ts
2054
+ async function retry(fn, options = {}) {
2055
+ const {
2056
+ maxAttempts = 3,
2057
+ delay = 1e3,
2058
+ backoff = true,
2059
+ onRetry
2060
+ } = options;
2061
+ let lastError = null;
2062
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2063
+ try {
2064
+ return await fn();
2065
+ } catch (error) {
2066
+ lastError = error instanceof Error ? error : new Error(String(error));
2067
+ if (attempt < maxAttempts) {
2068
+ const waitTime = backoff ? delay * Math.pow(2, attempt - 1) : delay;
2069
+ onRetry?.(attempt, lastError);
2070
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
2071
+ }
2072
+ }
2073
+ }
2074
+ throw lastError ?? new Error("Retry failed");
2075
+ }
2076
+
2077
+ // src/utils/guards.ts
2078
+ function isNonEmptyString(value) {
2079
+ return typeof value === "string" && value.length > 0;
2080
+ }
2081
+ function isValidUrl(value) {
2082
+ if (!isNonEmptyString(value)) {
2083
+ return false;
2084
+ }
2085
+ try {
2086
+ new URL(value);
2087
+ return true;
2088
+ } catch {
2089
+ return false;
2090
+ }
2091
+ }
2092
+ function isNumber(value) {
2093
+ return typeof value === "number" && !isNaN(value);
2094
+ }
2095
+ function isPositiveInteger(value) {
2096
+ return isNumber(value) && Number.isInteger(value) && value > 0;
2097
+ }
2098
+
2099
+ exports.ChatAfrika = ChatAfrika;
2100
+ exports.ConnectionState = ConnectionState;
2101
+ exports.Conversation = Conversation;
2102
+ exports.ConversationManager = ConversationManager;
2103
+ exports.Logger = Logger;
2104
+ exports.MessageFactory = MessageFactory;
2105
+ exports.MessageManager = MessageManager;
2106
+ exports.PresenceManager = PresenceManager;
2107
+ exports.ReceiptManager = ReceiptManager;
2108
+ exports.SupportManager = SupportManager;
2109
+ exports.SupportSession = SupportSession;
2110
+ exports.TokenManager = TokenManager;
2111
+ exports.TypingManager = TypingManager;
2112
+ exports.WebSocketClient = WebSocketClient;
2113
+ exports.isNonEmptyString = isNonEmptyString;
2114
+ exports.isNumber = isNumber;
2115
+ exports.isPositiveInteger = isPositiveInteger;
2116
+ exports.isValidUrl = isValidUrl;
2117
+ exports.retry = retry;
2118
+ //# sourceMappingURL=index.cjs.map
2119
+ //# sourceMappingURL=index.cjs.map