@scalemule/chat 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/{ChatClient-COmdEJ11.d.ts → ChatClient-DQPHdUHX.d.cts} +21 -2
  2. package/dist/{ChatClient-BoZaTtyM.d.cts → ChatClient-DtUKF-4c.d.ts} +21 -2
  3. package/dist/chat.embed.global.js +1 -1
  4. package/dist/chat.umd.global.js +288 -12
  5. package/dist/{chunk-ZLMMNFZL.js → chunk-5O5YLRJL.js} +386 -16
  6. package/dist/chunk-GTMAK3IA.js +285 -0
  7. package/dist/chunk-TRCELAZQ.cjs +287 -0
  8. package/dist/{chunk-YDLRISR7.cjs → chunk-W2PWFS3E.cjs} +386 -15
  9. package/dist/constants.d.ts +9 -0
  10. package/dist/constants.d.ts.map +1 -0
  11. package/dist/core/ChatClient.d.ts +96 -0
  12. package/dist/core/ChatClient.d.ts.map +1 -0
  13. package/dist/core/EventEmitter.d.ts +11 -0
  14. package/dist/core/EventEmitter.d.ts.map +1 -0
  15. package/dist/core/MessageCache.d.ts +18 -0
  16. package/dist/core/MessageCache.d.ts.map +1 -0
  17. package/dist/core/OfflineQueue.d.ts +19 -0
  18. package/dist/core/OfflineQueue.d.ts.map +1 -0
  19. package/dist/element.cjs +542 -51
  20. package/dist/element.d.ts +2 -2
  21. package/dist/element.d.ts.map +1 -0
  22. package/dist/element.js +541 -50
  23. package/dist/embed/index.d.ts +2 -0
  24. package/dist/embed/index.d.ts.map +1 -0
  25. package/dist/factory.d.ts +8 -0
  26. package/dist/factory.d.ts.map +1 -0
  27. package/dist/iframe.d.cts +1 -1
  28. package/dist/iframe.d.ts +3 -5
  29. package/dist/iframe.d.ts.map +1 -0
  30. package/dist/index.cjs +34 -5
  31. package/dist/index.d.cts +93 -4
  32. package/dist/index.d.ts +8 -77
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +29 -4
  35. package/dist/react-components/ChatInput.d.ts +16 -0
  36. package/dist/react-components/ChatInput.d.ts.map +1 -0
  37. package/dist/react-components/ChatMessageItem.d.ts +13 -0
  38. package/dist/react-components/ChatMessageItem.d.ts.map +1 -0
  39. package/dist/react-components/ChatMessageList.d.ts +15 -0
  40. package/dist/react-components/ChatMessageList.d.ts.map +1 -0
  41. package/dist/react-components/ChatThread.d.ts +12 -0
  42. package/dist/react-components/ChatThread.d.ts.map +1 -0
  43. package/dist/react-components/ConversationList.d.ts +13 -0
  44. package/dist/react-components/ConversationList.d.ts.map +1 -0
  45. package/dist/react-components/EmojiPicker.d.ts +8 -0
  46. package/dist/react-components/EmojiPicker.d.ts.map +1 -0
  47. package/dist/react-components/index.d.ts +8 -0
  48. package/dist/react-components/index.d.ts.map +1 -0
  49. package/dist/react-components/theme.d.ts +19 -0
  50. package/dist/react-components/theme.d.ts.map +1 -0
  51. package/dist/react-components/utils.d.ts +4 -0
  52. package/dist/react-components/utils.d.ts.map +1 -0
  53. package/dist/react.cjs +1212 -50
  54. package/dist/react.d.cts +100 -4
  55. package/dist/react.d.ts +38 -15
  56. package/dist/react.d.ts.map +1 -0
  57. package/dist/react.js +1164 -13
  58. package/dist/shared/ChatController.d.ts +65 -0
  59. package/dist/shared/ChatController.d.ts.map +1 -0
  60. package/dist/shared/upload.d.ts +3 -0
  61. package/dist/shared/upload.d.ts.map +1 -0
  62. package/dist/support-widget.global.js +485 -157
  63. package/dist/support.d.ts +99 -0
  64. package/dist/support.d.ts.map +1 -0
  65. package/dist/transport/HttpTransport.d.ts +20 -0
  66. package/dist/transport/HttpTransport.d.ts.map +1 -0
  67. package/dist/transport/WebSocketTransport.d.ts +78 -0
  68. package/dist/transport/WebSocketTransport.d.ts.map +1 -0
  69. package/dist/{types-BmD7f1gV.d.cts → types-COPVrm3K.d.cts} +25 -1
  70. package/dist/{types-BmD7f1gV.d.ts → types-COPVrm3K.d.ts} +25 -1
  71. package/dist/types.d.ts +271 -0
  72. package/dist/types.d.ts.map +1 -0
  73. package/dist/umd.d.ts +6 -0
  74. package/dist/umd.d.ts.map +1 -0
  75. package/dist/version.d.ts +2 -0
  76. package/dist/version.d.ts.map +1 -0
  77. package/dist/widget/icons.d.ts +8 -0
  78. package/dist/widget/icons.d.ts.map +1 -0
  79. package/dist/widget/index.d.ts +2 -0
  80. package/dist/widget/index.d.ts.map +1 -0
  81. package/dist/widget/storage.d.ts +5 -0
  82. package/dist/widget/storage.d.ts.map +1 -0
  83. package/dist/widget/styles.d.ts +3 -0
  84. package/dist/widget/styles.d.ts.map +1 -0
  85. package/package.json +5 -2
@@ -63,6 +63,9 @@ var MessageCache = class {
63
63
  getMessages(conversationId) {
64
64
  return this.cache.get(conversationId) ?? [];
65
65
  }
66
+ getMessage(conversationId, messageId) {
67
+ return this.getMessages(conversationId).find((message) => message.id === messageId);
68
+ }
66
69
  setMessages(conversationId, messages) {
67
70
  this.cache.set(conversationId, messages.slice(0, this.maxMessages));
68
71
  this.evictOldConversations();
@@ -77,6 +80,13 @@ var MessageCache = class {
77
80
  }
78
81
  this.cache.set(conversationId, messages);
79
82
  }
83
+ upsertMessage(conversationId, message) {
84
+ if (this.getMessage(conversationId, message.id)) {
85
+ this.updateMessage(conversationId, message);
86
+ return;
87
+ }
88
+ this.addMessage(conversationId, message);
89
+ }
80
90
  updateMessage(conversationId, message) {
81
91
  const messages = this.cache.get(conversationId);
82
92
  if (!messages) return;
@@ -85,6 +95,23 @@ var MessageCache = class {
85
95
  messages[idx] = message;
86
96
  }
87
97
  }
98
+ reconcileOptimisticMessage(conversationId, message) {
99
+ const messages = this.cache.get(conversationId);
100
+ if (!messages) return message;
101
+ const incomingAttachmentIds = (message.attachments ?? []).map((attachment) => attachment.file_id).sort();
102
+ const pendingIndex = messages.findIndex((cached) => {
103
+ if (!cached.id.startsWith("pending-")) return false;
104
+ if (cached.sender_id !== message.sender_id) return false;
105
+ if (cached.content !== message.content) return false;
106
+ const cachedAttachmentIds = (cached.attachments ?? []).map((attachment) => attachment.file_id).sort();
107
+ if (cachedAttachmentIds.length !== incomingAttachmentIds.length) return false;
108
+ return cachedAttachmentIds.every((fileId, index) => fileId === incomingAttachmentIds[index]);
109
+ });
110
+ if (pendingIndex >= 0) {
111
+ messages[pendingIndex] = message;
112
+ }
113
+ return message;
114
+ }
88
115
  removeMessage(conversationId, messageId) {
89
116
  const messages = this.cache.get(conversationId);
90
117
  if (!messages) return;
@@ -200,8 +227,7 @@ var HttpTransport = class {
200
227
  method,
201
228
  headers,
202
229
  body: body ? JSON.stringify(body) : void 0,
203
- signal: controller.signal,
204
- credentials: "include"
230
+ signal: controller.signal
205
231
  });
206
232
  clearTimeout(timeoutId);
207
233
  if (response.status === 204) {
@@ -348,8 +374,7 @@ var WebSocketTransport = class extends EventEmitter {
348
374
  try {
349
375
  const response = await fetch(this.ticketUrl, {
350
376
  method: "POST",
351
- headers,
352
- credentials: "include"
377
+ headers
353
378
  });
354
379
  if (!response.ok) return null;
355
380
  const json = await response.json();
@@ -431,14 +456,14 @@ var WebSocketTransport = class extends EventEmitter {
431
456
  }
432
457
  this.setStatus("reconnecting");
433
458
  this.emit("reconnecting", { attempt: this.reconnectAttempt + 1 });
434
- const delay = Math.min(
459
+ const delay2 = Math.min(
435
460
  this.baseDelay * Math.pow(2, this.reconnectAttempt) + Math.random() * this.baseDelay * 0.3,
436
461
  this.maxDelay
437
462
  );
438
463
  this.reconnectTimer = setTimeout(() => {
439
464
  this.reconnectAttempt++;
440
465
  this.connect();
441
- }, delay);
466
+ }, delay2);
442
467
  }
443
468
  setStatus(status) {
444
469
  if (this.status !== status) {
@@ -448,6 +473,173 @@ var WebSocketTransport = class extends EventEmitter {
448
473
  }
449
474
  };
450
475
 
476
+ // src/shared/upload.ts
477
+ var RETRY_DELAYS_MS = [0, 1e3, 3e3];
478
+ var STALL_TIMEOUT_MS = 45e3;
479
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([0, 408, 429, 500, 502, 503, 504]);
480
+ function delay(ms) {
481
+ return new Promise((resolve) => setTimeout(resolve, ms));
482
+ }
483
+ function shouldRetry(status, code) {
484
+ if (code === "aborted") {
485
+ return false;
486
+ }
487
+ return RETRYABLE_STATUS_CODES.has(status) || code === "upload_stalled";
488
+ }
489
+ function uploadOnce(url, file, onProgress, signal) {
490
+ return new Promise((resolve) => {
491
+ if (typeof XMLHttpRequest === "undefined") {
492
+ resolve({
493
+ data: null,
494
+ error: {
495
+ code: "unsupported_environment",
496
+ message: "XMLHttpRequest is not available in this environment",
497
+ status: 0
498
+ }
499
+ });
500
+ return;
501
+ }
502
+ const xhr = new XMLHttpRequest();
503
+ let settled = false;
504
+ let stallTimer = null;
505
+ let lastLoaded = 0;
506
+ let totalBytes = file.size;
507
+ const finish = (result) => {
508
+ if (settled) return;
509
+ settled = true;
510
+ if (stallTimer) {
511
+ clearTimeout(stallTimer);
512
+ stallTimer = null;
513
+ }
514
+ resolve(result);
515
+ };
516
+ const resetStallTimer = () => {
517
+ if (stallTimer) {
518
+ clearTimeout(stallTimer);
519
+ }
520
+ stallTimer = setTimeout(() => {
521
+ xhr.abort();
522
+ finish({
523
+ data: null,
524
+ error: {
525
+ code: "upload_stalled",
526
+ message: `Upload stalled (no progress for ${STALL_TIMEOUT_MS / 1e3}s)`,
527
+ status: 0,
528
+ details: {
529
+ bytes_sent: lastLoaded,
530
+ total_bytes: totalBytes
531
+ }
532
+ }
533
+ });
534
+ }, STALL_TIMEOUT_MS);
535
+ };
536
+ if (signal) {
537
+ if (signal.aborted) {
538
+ finish({
539
+ data: null,
540
+ error: { code: "aborted", message: "Upload aborted", status: 0 }
541
+ });
542
+ return;
543
+ }
544
+ signal.addEventListener(
545
+ "abort",
546
+ () => {
547
+ xhr.abort();
548
+ finish({
549
+ data: null,
550
+ error: { code: "aborted", message: "Upload aborted", status: 0 }
551
+ });
552
+ },
553
+ { once: true }
554
+ );
555
+ }
556
+ xhr.upload.addEventListener("progress", (event) => {
557
+ resetStallTimer();
558
+ lastLoaded = event.loaded;
559
+ totalBytes = event.total || totalBytes;
560
+ if (event.lengthComputable) {
561
+ onProgress?.(Math.round(event.loaded / event.total * 100));
562
+ }
563
+ });
564
+ xhr.addEventListener("load", () => {
565
+ if (xhr.status >= 200 && xhr.status < 300) {
566
+ onProgress?.(100);
567
+ finish({ data: null, error: null });
568
+ return;
569
+ }
570
+ finish({
571
+ data: null,
572
+ error: {
573
+ code: "upload_error",
574
+ message: `S3 upload failed: ${xhr.status}`,
575
+ status: xhr.status,
576
+ details: {
577
+ bytes_sent: lastLoaded,
578
+ total_bytes: totalBytes
579
+ }
580
+ }
581
+ });
582
+ });
583
+ xhr.addEventListener("error", () => {
584
+ finish({
585
+ data: null,
586
+ error: {
587
+ code: "upload_error",
588
+ message: "S3 upload failed",
589
+ status: xhr.status || 0,
590
+ details: {
591
+ bytes_sent: lastLoaded,
592
+ total_bytes: totalBytes
593
+ }
594
+ }
595
+ });
596
+ });
597
+ xhr.addEventListener("abort", () => {
598
+ if (settled) return;
599
+ finish({
600
+ data: null,
601
+ error: { code: "aborted", message: "Upload aborted", status: 0 }
602
+ });
603
+ });
604
+ xhr.open("PUT", url, true);
605
+ if (file.type) {
606
+ xhr.setRequestHeader("Content-Type", file.type);
607
+ }
608
+ resetStallTimer();
609
+ xhr.send(file);
610
+ });
611
+ }
612
+ async function uploadToPresignedUrl(url, file, onProgress, signal) {
613
+ let lastError = null;
614
+ for (const [attempt, delayMs] of RETRY_DELAYS_MS.entries()) {
615
+ if (delayMs > 0) {
616
+ await delay(delayMs);
617
+ }
618
+ const result = await uploadOnce(url, file, onProgress, signal);
619
+ if (!result.error) {
620
+ return result;
621
+ }
622
+ lastError = {
623
+ ...result.error,
624
+ details: {
625
+ ...result.error.details,
626
+ attempt: attempt + 1
627
+ }
628
+ };
629
+ if (!shouldRetry(result.error.status, result.error.code)) {
630
+ break;
631
+ }
632
+ }
633
+ return {
634
+ data: null,
635
+ error: lastError ?? {
636
+ code: "upload_error",
637
+ message: "Upload failed",
638
+ status: 0
639
+ }
640
+ };
641
+ }
642
+
451
643
  // src/core/ChatClient.ts
452
644
  var ChatClient = class extends EventEmitter {
453
645
  constructor(config) {
@@ -455,6 +647,7 @@ var ChatClient = class extends EventEmitter {
455
647
  this.conversationSubs = /* @__PURE__ */ new Map();
456
648
  this.conversationTypes = /* @__PURE__ */ new Map();
457
649
  const baseUrl = config.apiBaseUrl ?? DEFAULT_API_BASE_URL;
650
+ this.currentUserId = config.userId;
458
651
  this.http = new HttpTransport({
459
652
  baseUrl,
460
653
  apiKey: config.apiKey,
@@ -513,6 +706,9 @@ var ChatClient = class extends EventEmitter {
513
706
  get status() {
514
707
  return this.ws.getStatus();
515
708
  }
709
+ get userId() {
710
+ return this.currentUserId;
711
+ }
516
712
  connect() {
517
713
  this.ws.connect();
518
714
  }
@@ -564,9 +760,15 @@ var ChatClient = class extends EventEmitter {
564
760
  }
565
761
  );
566
762
  if (result.data) {
567
- this.cache.addMessage(conversationId, result.data);
763
+ const reconciled = this.cache.reconcileOptimisticMessage(conversationId, result.data);
764
+ this.cache.upsertMessage(conversationId, reconciled);
568
765
  } else if (result.error?.status === 0) {
569
- this.offlineQueue.enqueue(conversationId, options.content, options.message_type ?? "text");
766
+ this.offlineQueue.enqueue(
767
+ conversationId,
768
+ options.content,
769
+ options.message_type ?? "text",
770
+ options.attachments
771
+ );
570
772
  }
571
773
  return result;
572
774
  }
@@ -595,9 +797,60 @@ var ChatClient = class extends EventEmitter {
595
797
  async deleteMessage(messageId) {
596
798
  return this.http.del(`/v1/chat/messages/${messageId}`);
597
799
  }
800
+ async uploadAttachment(file, onProgress, signal) {
801
+ const filename = typeof File !== "undefined" && file instanceof File ? file.name : "attachment";
802
+ const contentType = file.type || "application/octet-stream";
803
+ const initResult = await this.http.post("/v1/storage/signed-url/upload", {
804
+ filename,
805
+ content_type: contentType,
806
+ size_bytes: file.size,
807
+ is_public: false,
808
+ metadata: {
809
+ source: "chat_sdk"
810
+ }
811
+ });
812
+ if (initResult.error || !initResult.data) {
813
+ return { data: null, error: initResult.error };
814
+ }
815
+ const uploadResult = await uploadToPresignedUrl(
816
+ initResult.data.upload_url,
817
+ file,
818
+ onProgress,
819
+ signal
820
+ );
821
+ if (uploadResult.error) {
822
+ return { data: null, error: uploadResult.error };
823
+ }
824
+ const completeResult = await this.http.post("/v1/storage/signed-url/complete", {
825
+ file_id: initResult.data.file_id,
826
+ completion_token: initResult.data.completion_token
827
+ });
828
+ if (completeResult.error || !completeResult.data) {
829
+ return { data: null, error: completeResult.error };
830
+ }
831
+ return {
832
+ data: {
833
+ file_id: completeResult.data.file_id,
834
+ file_name: completeResult.data.filename,
835
+ file_size: completeResult.data.size_bytes,
836
+ mime_type: completeResult.data.content_type,
837
+ presigned_url: completeResult.data.url
838
+ },
839
+ error: null
840
+ };
841
+ }
842
+ async refreshAttachmentUrl(messageId, fileId) {
843
+ return this.http.get(
844
+ `/v1/chat/messages/${messageId}/attachment/${fileId}/url`
845
+ );
846
+ }
598
847
  getCachedMessages(conversationId) {
599
848
  return this.cache.getMessages(conversationId);
600
849
  }
850
+ stageOptimisticMessage(conversationId, message) {
851
+ this.cache.upsertMessage(conversationId, message);
852
+ return message;
853
+ }
601
854
  // ============ Reactions ============
602
855
  async addReaction(messageId, emoji) {
603
856
  return this.http.post(`/v1/chat/messages/${messageId}/reactions`, { emoji });
@@ -605,6 +858,20 @@ var ChatClient = class extends EventEmitter {
605
858
  async removeReaction(messageId, emoji) {
606
859
  return this.http.del(`/v1/chat/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`);
607
860
  }
861
+ async reportMessage(messageId, reason, description) {
862
+ return this.http.post(`/v1/chat/messages/${messageId}/report`, {
863
+ reason,
864
+ description
865
+ });
866
+ }
867
+ async muteConversation(conversationId, mutedUntil) {
868
+ return this.http.post(`/v1/chat/conversations/${conversationId}/mute`, {
869
+ muted_until: mutedUntil
870
+ });
871
+ }
872
+ async unmuteConversation(conversationId) {
873
+ return this.http.del(`/v1/chat/conversations/${conversationId}/mute`);
874
+ }
608
875
  // ============ Unread Count ============
609
876
  async getUnreadTotal() {
610
877
  return this.http.get("/v1/chat/conversations/unread-total");
@@ -780,6 +1047,91 @@ var ChatClient = class extends EventEmitter {
780
1047
  }
781
1048
  }
782
1049
  }
1050
+ normalizeMessage(payload) {
1051
+ return {
1052
+ id: payload.id ?? payload.message_id ?? "",
1053
+ content: payload.content ?? "",
1054
+ message_type: payload.message_type ?? "text",
1055
+ sender_id: payload.sender_id ?? payload.sender_user_id ?? "",
1056
+ sender_type: payload.sender_type,
1057
+ sender_agent_model: payload.sender_agent_model,
1058
+ attachments: payload.attachments,
1059
+ reactions: payload.reactions,
1060
+ is_edited: Boolean(payload.is_edited ?? false),
1061
+ created_at: payload.created_at ?? payload.updated_at ?? payload.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
1062
+ };
1063
+ }
1064
+ buildEditedMessage(conversationId, update) {
1065
+ const messageId = update.message_id ?? update.id;
1066
+ if (!messageId) {
1067
+ return null;
1068
+ }
1069
+ const existing = this.cache.getMessage(conversationId, messageId);
1070
+ return {
1071
+ id: existing?.id ?? messageId,
1072
+ content: update.content ?? update.new_content ?? existing?.content ?? "",
1073
+ message_type: existing?.message_type ?? "text",
1074
+ sender_id: existing?.sender_id ?? "",
1075
+ sender_type: existing?.sender_type,
1076
+ sender_agent_model: existing?.sender_agent_model,
1077
+ attachments: existing?.attachments,
1078
+ reactions: existing?.reactions,
1079
+ is_edited: true,
1080
+ created_at: existing?.created_at ?? update.updated_at ?? update.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
1081
+ };
1082
+ }
1083
+ applyReactionEvent(conversationId, reactionEvent) {
1084
+ const reaction = {
1085
+ id: `${reactionEvent.message_id}:${reactionEvent.user_id}:${reactionEvent.emoji}`,
1086
+ message_id: reactionEvent.message_id,
1087
+ user_id: reactionEvent.user_id,
1088
+ emoji: reactionEvent.emoji,
1089
+ action: reactionEvent.action,
1090
+ timestamp: reactionEvent.timestamp
1091
+ };
1092
+ const existingMessage = this.cache.getMessage(conversationId, reactionEvent.message_id);
1093
+ if (!existingMessage) {
1094
+ return reaction;
1095
+ }
1096
+ const nextReactions = [...existingMessage.reactions ?? []];
1097
+ const reactionIndex = nextReactions.findIndex((entry) => entry.emoji === reactionEvent.emoji);
1098
+ if (reactionEvent.action === "added") {
1099
+ if (reactionIndex >= 0) {
1100
+ const current = nextReactions[reactionIndex];
1101
+ if (!current.user_ids.includes(reactionEvent.user_id)) {
1102
+ const user_ids = [...current.user_ids, reactionEvent.user_id];
1103
+ nextReactions[reactionIndex] = {
1104
+ ...current,
1105
+ user_ids,
1106
+ count: user_ids.length
1107
+ };
1108
+ }
1109
+ } else {
1110
+ nextReactions.push({
1111
+ emoji: reactionEvent.emoji,
1112
+ count: 1,
1113
+ user_ids: [reactionEvent.user_id]
1114
+ });
1115
+ }
1116
+ } else if (reactionIndex >= 0) {
1117
+ const current = nextReactions[reactionIndex];
1118
+ const user_ids = current.user_ids.filter((userId) => userId !== reactionEvent.user_id);
1119
+ if (user_ids.length === 0) {
1120
+ nextReactions.splice(reactionIndex, 1);
1121
+ } else {
1122
+ nextReactions[reactionIndex] = {
1123
+ ...current,
1124
+ user_ids,
1125
+ count: user_ids.length
1126
+ };
1127
+ }
1128
+ }
1129
+ this.cache.updateMessage(conversationId, {
1130
+ ...existingMessage,
1131
+ reactions: nextReactions
1132
+ });
1133
+ return reaction;
1134
+ }
783
1135
  handleConversationMessage(channel, data) {
784
1136
  const conversationId = channel.replace(/^conversation:(?:lr:|bc:|support:)?/, "");
785
1137
  const raw = data;
@@ -788,15 +1140,32 @@ var ChatClient = class extends EventEmitter {
788
1140
  const payload = raw.data ?? raw;
789
1141
  switch (event) {
790
1142
  case "new_message": {
791
- const message = payload;
792
- this.cache.addMessage(conversationId, message);
793
- this.emit("message", { message, conversationId });
1143
+ const message = this.normalizeMessage(payload);
1144
+ const reconciled = this.cache.reconcileOptimisticMessage(conversationId, message);
1145
+ this.cache.upsertMessage(conversationId, reconciled);
1146
+ this.emit("message", { message: reconciled, conversationId });
794
1147
  break;
795
1148
  }
796
1149
  case "message_edited": {
797
- const message = payload;
798
- this.cache.updateMessage(conversationId, message);
799
- this.emit("message:updated", { message, conversationId });
1150
+ const update = payload;
1151
+ const message = this.buildEditedMessage(conversationId, update);
1152
+ if (message) {
1153
+ this.cache.upsertMessage(conversationId, message);
1154
+ this.emit("message:updated", { message, conversationId, update });
1155
+ }
1156
+ break;
1157
+ }
1158
+ case "reaction": {
1159
+ const reactionEvent = payload;
1160
+ if (!reactionEvent.message_id || !reactionEvent.user_id || !reactionEvent.emoji) {
1161
+ break;
1162
+ }
1163
+ const reaction = this.applyReactionEvent(conversationId, reactionEvent);
1164
+ this.emit("reaction", {
1165
+ reaction,
1166
+ conversationId,
1167
+ action: reactionEvent.action
1168
+ });
800
1169
  break;
801
1170
  }
802
1171
  case "message_deleted": {
@@ -857,10 +1226,12 @@ var ChatClient = class extends EventEmitter {
857
1226
  for (const item of queued) {
858
1227
  await this.sendMessage(item.conversationId, {
859
1228
  content: item.content,
860
- message_type: item.message_type
1229
+ message_type: item.message_type,
1230
+ attachments: item.attachments
861
1231
  });
862
1232
  }
863
1233
  }
864
1234
  };
865
1235
 
866
1236
  exports.ChatClient = ChatClient;
1237
+ exports.EventEmitter = EventEmitter;
@@ -0,0 +1,9 @@
1
+ export declare const DEFAULT_API_BASE_URL = "https://api.scalemule.com";
2
+ export declare const DEFAULT_WS_RECONNECT_BASE_DELAY = 1000;
3
+ export declare const DEFAULT_WS_RECONNECT_MAX_DELAY = 30000;
4
+ export declare const DEFAULT_WS_RECONNECT_MAX_RETRIES: number;
5
+ export declare const DEFAULT_WS_HEARTBEAT_INTERVAL = 30000;
6
+ export declare const DEFAULT_MESSAGE_CACHE_SIZE = 200;
7
+ export declare const DEFAULT_MAX_CONVERSATIONS_CACHED = 50;
8
+ export declare const DEFAULT_REQUEST_TIMEOUT = 10000;
9
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oBAAoB,8BAA8B,CAAC;AAChE,eAAO,MAAM,+BAA+B,OAAO,CAAC;AACpD,eAAO,MAAM,8BAA8B,QAAQ,CAAC;AACpD,eAAO,MAAM,gCAAgC,QAAW,CAAC;AACzD,eAAO,MAAM,6BAA6B,QAAQ,CAAC;AACnD,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAC9C,eAAO,MAAM,gCAAgC,KAAK,CAAC;AACnD,eAAO,MAAM,uBAAuB,QAAQ,CAAC"}
@@ -0,0 +1,96 @@
1
+ import { EventEmitter } from './EventEmitter';
2
+ import type { Attachment, ApiResponse, ChatConfig, ChatEventMap, ChatMessage, ChannelWithSettings, ConnectionStatus, Conversation, CreateConversationOptions, CreateEphemeralChannelOptions, CreateLargeRoomOptions, GetMessagesOptions, ListConversationsOptions, MessagesResponse, ReadStatus, SendMessageOptions, ChannelSettings, UnreadTotalResponse } from '../types';
3
+ export declare class ChatClient extends EventEmitter<ChatEventMap> {
4
+ private http;
5
+ private ws;
6
+ private cache;
7
+ private offlineQueue;
8
+ private conversationSubs;
9
+ private conversationTypes;
10
+ private currentUserId?;
11
+ constructor(config: ChatConfig);
12
+ get status(): ConnectionStatus;
13
+ get userId(): string | undefined;
14
+ connect(): void;
15
+ disconnect(): void;
16
+ createConversation(options: CreateConversationOptions): Promise<ApiResponse<Conversation>>;
17
+ listConversations(options?: ListConversationsOptions): Promise<ApiResponse<Conversation[]>>;
18
+ getConversation(id: string): Promise<ApiResponse<Conversation>>;
19
+ private trackConversationType;
20
+ sendMessage(conversationId: string, options: SendMessageOptions): Promise<ApiResponse<ChatMessage>>;
21
+ getMessages(conversationId: string, options?: GetMessagesOptions): Promise<ApiResponse<MessagesResponse>>;
22
+ editMessage(messageId: string, content: string): Promise<ApiResponse<void>>;
23
+ deleteMessage(messageId: string): Promise<ApiResponse<void>>;
24
+ uploadAttachment(file: File | Blob, onProgress?: (percent: number) => void, signal?: AbortSignal): Promise<ApiResponse<Attachment>>;
25
+ refreshAttachmentUrl(messageId: string, fileId: string): Promise<ApiResponse<{
26
+ url: string;
27
+ }>>;
28
+ getCachedMessages(conversationId: string): ChatMessage[];
29
+ stageOptimisticMessage(conversationId: string, message: ChatMessage): ChatMessage;
30
+ addReaction(messageId: string, emoji: string): Promise<ApiResponse<void>>;
31
+ removeReaction(messageId: string, emoji: string): Promise<ApiResponse<void>>;
32
+ reportMessage(messageId: string, reason: 'spam' | 'harassment' | 'hate' | 'violence' | 'other', description?: string): Promise<ApiResponse<{
33
+ reported: boolean;
34
+ }>>;
35
+ muteConversation(conversationId: string, mutedUntil?: string): Promise<ApiResponse<{
36
+ muted: boolean;
37
+ }>>;
38
+ unmuteConversation(conversationId: string): Promise<ApiResponse<{
39
+ muted: boolean;
40
+ }>>;
41
+ getUnreadTotal(): Promise<ApiResponse<UnreadTotalResponse>>;
42
+ sendTyping(conversationId: string, isTyping?: boolean): Promise<void>;
43
+ markRead(conversationId: string): Promise<void>;
44
+ getReadStatus(conversationId: string): Promise<ApiResponse<{
45
+ statuses: ReadStatus[];
46
+ }>>;
47
+ addParticipant(conversationId: string, userId: string): Promise<ApiResponse<void>>;
48
+ removeParticipant(conversationId: string, userId: string): Promise<ApiResponse<void>>;
49
+ joinPresence(conversationId: string, userData?: unknown): void;
50
+ leavePresence(conversationId: string): void;
51
+ /** Update presence status (online/away/dnd) without leaving the channel. */
52
+ updatePresence(conversationId: string, status: 'online' | 'away' | 'dnd', userData?: unknown): void;
53
+ getChannelSettings(channelId: string): Promise<ApiResponse<ChannelSettings>>;
54
+ /**
55
+ * Set the conversation type for channel name routing.
56
+ * Large rooms use `conversation:lr:` prefix to skip MySQL in the realtime service.
57
+ */
58
+ setConversationType(conversationId: string, type: Conversation['conversation_type']): void;
59
+ /**
60
+ * Find an active ephemeral or large_room channel by linked session ID.
61
+ * Returns null (not an error) if no active channel exists.
62
+ */
63
+ findChannelBySessionId(linkedSessionId: string): Promise<ChannelWithSettings | null>;
64
+ /**
65
+ * Self-join an ephemeral or large_room channel. Idempotent.
66
+ */
67
+ joinChannel(channelId: string): Promise<ApiResponse<{
68
+ participant_id: string;
69
+ role: string;
70
+ joined_at: string;
71
+ }>>;
72
+ /**
73
+ * Create an ephemeral channel tied to a session (e.g., a video snap).
74
+ */
75
+ createEphemeralChannel(options: CreateEphemeralChannelOptions): Promise<ApiResponse<ChannelWithSettings>>;
76
+ /**
77
+ * Create a large room channel (high-concurrency, skips MySQL tracking in realtime).
78
+ */
79
+ createLargeRoom(options: CreateLargeRoomOptions): Promise<ApiResponse<ChannelWithSettings>>;
80
+ /**
81
+ * Get the global concurrent subscriber count for a conversation.
82
+ */
83
+ getSubscriberCount(conversationId: string): Promise<number>;
84
+ subscribeToConversation(conversationId: string): () => void;
85
+ /** Build channel name with correct prefix based on conversation type. */
86
+ private channelName;
87
+ destroy(): void;
88
+ private handleRealtimeMessage;
89
+ private handlePrivateMessage;
90
+ private normalizeMessage;
91
+ private buildEditedMessage;
92
+ private applyReactionEvent;
93
+ private handleConversationMessage;
94
+ private flushOfflineQueue;
95
+ }
96
+ //# sourceMappingURL=ChatClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatClient.d.ts","sourceRoot":"","sources":["../../src/core/ChatClient.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ type Listener<T> = (data: T) => void;
2
+ export declare class EventEmitter<EventMap extends Record<string, any>> {
3
+ private listeners;
4
+ on<K extends keyof EventMap>(event: K, callback: Listener<EventMap[K]>): () => void;
5
+ off<K extends keyof EventMap>(event: K, callback: Listener<EventMap[K]>): void;
6
+ once<K extends keyof EventMap>(event: K, callback: Listener<EventMap[K]>): () => void;
7
+ emit<K extends keyof EventMap>(event: K, ...[data]: EventMap[K] extends void ? [] : [EventMap[K]]): void;
8
+ removeAllListeners(event?: keyof EventMap): void;
9
+ }
10
+ export {};
11
+ //# sourceMappingURL=EventEmitter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EventEmitter.d.ts","sourceRoot":"","sources":["../../src/core/EventEmitter.ts"],"names":[],"mappings":"AAAA,KAAK,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;AAGrC,qBAAa,YAAY,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAC5D,OAAO,CAAC,SAAS,CAAqD;IAEtE,EAAE,CAAC,CAAC,SAAS,MAAM,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAQnF,GAAG,CAAC,CAAC,SAAS,MAAM,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;IAI9E,IAAI,CAAC,CAAC,SAAS,MAAM,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAQrF,IAAI,CAAC,CAAC,SAAS,MAAM,QAAQ,EAC3B,KAAK,EAAE,CAAC,EACR,GAAG,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GACvD,IAAI;IAaP,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,QAAQ,GAAG,IAAI;CAOjD"}
@@ -0,0 +1,18 @@
1
+ import type { ChatMessage } from '../types';
2
+ export declare class MessageCache {
3
+ private cache;
4
+ private maxMessages;
5
+ private maxConversations;
6
+ constructor(maxMessages?: number, maxConversations?: number);
7
+ getMessages(conversationId: string): ChatMessage[];
8
+ getMessage(conversationId: string, messageId: string): ChatMessage | undefined;
9
+ setMessages(conversationId: string, messages: ChatMessage[]): void;
10
+ addMessage(conversationId: string, message: ChatMessage): void;
11
+ upsertMessage(conversationId: string, message: ChatMessage): void;
12
+ updateMessage(conversationId: string, message: ChatMessage): void;
13
+ reconcileOptimisticMessage(conversationId: string, message: ChatMessage): ChatMessage;
14
+ removeMessage(conversationId: string, messageId: string): void;
15
+ clear(conversationId?: string): void;
16
+ private evictOldConversations;
17
+ }
18
+ //# sourceMappingURL=MessageCache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageCache.d.ts","sourceRoot":"","sources":["../../src/core/MessageCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAG5C,qBAAa,YAAY;IACvB,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,gBAAgB,CAAS;gBAErB,WAAW,CAAC,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM;IAK3D,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,WAAW,EAAE;IAIlD,UAAU,CAAC,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAI9E,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,IAAI;IAKlE,UAAU,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI;IAc9D,aAAa,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI;IASjE,aAAa,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI;IASjE,0BAA0B,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,WAAW;IAyBrF,aAAa,CAAC,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAS9D,KAAK,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI;IAQpC,OAAO,CAAC,qBAAqB;CAM9B"}