@bootdesk/js-web-adapter-core 0.1.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,1042 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ChatEvent: () => ChatEvent,
24
+ DMRequestedEvent: () => DMRequestedEvent,
25
+ HttpClient: () => HttpClient,
26
+ LaravelEchoBroadcastClient: () => LaravelEchoBroadcastClient,
27
+ MessageDeletedEvent: () => MessageDeletedEvent,
28
+ MessageEditedEvent: () => MessageEditedEvent,
29
+ MessagePostedEvent: () => MessagePostedEvent,
30
+ PushManager: () => PushManager,
31
+ PusherBroadcastClient: () => PusherBroadcastClient,
32
+ ReactionAddedEvent: () => ReactionAddedEvent,
33
+ ReactionRemovedEvent: () => ReactionRemovedEvent,
34
+ StreamingChunkEvent: () => StreamingChunkEvent,
35
+ TypingStartedEvent: () => TypingStartedEvent,
36
+ UnknownEvent: () => UnknownEvent,
37
+ WebChatClient: () => WebChatClient,
38
+ createPushSubscriptionHandlers: () => createPushSubscriptionHandlers,
39
+ generateConversationId: () => generateConversationId,
40
+ generateId: () => generateId,
41
+ parseChatEvent: () => parseChatEvent
42
+ });
43
+ module.exports = __toCommonJS(index_exports);
44
+
45
+ // src/client/HttpClient.ts
46
+ var HttpClient = class {
47
+ constructor(config) {
48
+ const headers = { ...config.headers };
49
+ if (config.verifyToken) {
50
+ headers["X-Verify-Token"] = config.verifyToken;
51
+ }
52
+ this.config = { apiUrl: config.apiUrl, timeout: config.timeout ?? 3e4, headers };
53
+ }
54
+ async get(url, signal) {
55
+ const controller = new AbortController();
56
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
57
+ try {
58
+ const fullUrl = this.resolve(url);
59
+ const combined = signal ? AbortSignal.any([controller.signal, signal]) : controller.signal;
60
+ const response = await fetch(fullUrl, {
61
+ method: "GET",
62
+ headers: this.config.headers,
63
+ signal: combined
64
+ });
65
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
66
+ return response.json();
67
+ } finally {
68
+ clearTimeout(timeoutId);
69
+ }
70
+ }
71
+ async post(url, body, signal) {
72
+ const controller = new AbortController();
73
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
74
+ try {
75
+ const fullUrl = this.resolve(url);
76
+ const combined = signal ? AbortSignal.any([controller.signal, signal]) : controller.signal;
77
+ const response = await fetch(fullUrl, {
78
+ method: "POST",
79
+ headers: { "Content-Type": "application/json", ...this.config.headers },
80
+ signal: combined,
81
+ body: JSON.stringify(body)
82
+ });
83
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
84
+ return response.json();
85
+ } finally {
86
+ clearTimeout(timeoutId);
87
+ }
88
+ }
89
+ async delete(url, signal) {
90
+ const controller = new AbortController();
91
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
92
+ try {
93
+ const fullUrl = this.resolve(url);
94
+ const combined = signal ? AbortSignal.any([controller.signal, signal]) : controller.signal;
95
+ const response = await fetch(fullUrl, {
96
+ method: "DELETE",
97
+ headers: this.config.headers,
98
+ signal: combined
99
+ });
100
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
101
+ const text = await response.text();
102
+ return text ? JSON.parse(text) : void 0;
103
+ } finally {
104
+ clearTimeout(timeoutId);
105
+ }
106
+ }
107
+ async sendMessage(messages, endpoint = "/api/webhooks/web", conversationId) {
108
+ return this.post(endpoint, { id: conversationId, messages });
109
+ }
110
+ async sendAction(actionId, value, messageId, conversationId, endpoint = "/api/webhooks/web") {
111
+ return this.post(endpoint, {
112
+ id: conversationId,
113
+ action: { actionId, value, messageId }
114
+ });
115
+ }
116
+ async editMessage(messageId, newText, endpointTemplate = "/api/chat/messages/{id}/edit") {
117
+ const url = this.expandTemplate(endpointTemplate, { id: messageId });
118
+ await this.post(url, { text: newText });
119
+ }
120
+ async deleteMessage(messageId, endpointTemplate = "/api/chat/messages/{id}") {
121
+ const url = this.expandTemplate(endpointTemplate, { id: messageId });
122
+ await this.delete(url);
123
+ }
124
+ async addReaction(messageId, emoji, endpointTemplate = "/api/chat/messages/{id}/reactions") {
125
+ const url = this.expandTemplate(endpointTemplate, { id: messageId });
126
+ await this.post(url, { emoji });
127
+ }
128
+ async removeReaction(messageId, emoji, endpointTemplate = "/api/chat/messages/{id}/reactions/{emoji}") {
129
+ const url = this.expandTemplate(endpointTemplate, { id: messageId, emoji });
130
+ await this.delete(url);
131
+ }
132
+ resolve(url) {
133
+ return /^https?:\/\//.test(url) ? url : `${this.config.apiUrl}${url}`;
134
+ }
135
+ expandTemplate(template, params) {
136
+ let url = template;
137
+ for (const [key, value] of Object.entries(params)) {
138
+ url = url.replace(`{${key}}`, encodeURIComponent(value));
139
+ }
140
+ return this.resolve(url);
141
+ }
142
+ };
143
+
144
+ // src/events/base/ChatEvent.ts
145
+ var ChatEvent = class {
146
+ constructor(type, threadId, timestamp) {
147
+ this.type = type;
148
+ this.threadId = threadId;
149
+ this.timestamp = timestamp;
150
+ }
151
+ };
152
+ var UnknownEvent = class extends ChatEvent {
153
+ constructor(type, threadId, data, timestamp) {
154
+ super(type, threadId, timestamp);
155
+ this.data = data;
156
+ }
157
+ };
158
+
159
+ // src/events/MessagePostedEvent.ts
160
+ var MessagePostedEvent = class extends ChatEvent {
161
+ constructor(threadId, messageId, text, author, card, attachments, timestamp) {
162
+ super("message.posted", threadId, timestamp ?? Date.now());
163
+ this.messageId = messageId;
164
+ this.text = text;
165
+ this.author = author;
166
+ this.card = card;
167
+ this.attachments = attachments;
168
+ }
169
+ };
170
+
171
+ // src/events/MessageEditedEvent.ts
172
+ var MessageEditedEvent = class extends ChatEvent {
173
+ constructor(threadId, messageId, newText, card, timestamp) {
174
+ super("message.edited", threadId, timestamp ?? Date.now());
175
+ this.messageId = messageId;
176
+ this.newText = newText;
177
+ this.card = card;
178
+ }
179
+ };
180
+
181
+ // src/events/MessageDeletedEvent.ts
182
+ var MessageDeletedEvent = class extends ChatEvent {
183
+ constructor(threadId, messageId, timestamp) {
184
+ super("message.deleted", threadId, timestamp ?? Date.now());
185
+ this.messageId = messageId;
186
+ }
187
+ };
188
+
189
+ // src/events/ReactionAddedEvent.ts
190
+ var ReactionAddedEvent = class extends ChatEvent {
191
+ constructor(threadId, messageId, emoji, user, timestamp) {
192
+ super("reaction.added", threadId, timestamp ?? Date.now());
193
+ this.messageId = messageId;
194
+ this.emoji = emoji;
195
+ this.user = user;
196
+ }
197
+ };
198
+
199
+ // src/events/ReactionRemovedEvent.ts
200
+ var ReactionRemovedEvent = class extends ChatEvent {
201
+ constructor(threadId, messageId, emoji, user, timestamp) {
202
+ super("reaction.removed", threadId, timestamp ?? Date.now());
203
+ this.messageId = messageId;
204
+ this.emoji = emoji;
205
+ this.user = user;
206
+ }
207
+ };
208
+
209
+ // src/events/TypingStartedEvent.ts
210
+ var TypingStartedEvent = class extends ChatEvent {
211
+ constructor(threadId, userId, timestamp) {
212
+ super("typing.started", threadId, timestamp ?? Date.now());
213
+ this.userId = userId;
214
+ }
215
+ };
216
+
217
+ // src/events/StreamingChunkEvent.ts
218
+ var StreamingChunkEvent = class extends ChatEvent {
219
+ constructor(threadId, messageId, chunk, isFinal, timestamp) {
220
+ super("streaming.chunk", threadId, timestamp ?? Date.now());
221
+ this.messageId = messageId;
222
+ this.chunk = chunk;
223
+ this.isFinal = isFinal;
224
+ }
225
+ };
226
+
227
+ // src/events/DMRequestedEvent.ts
228
+ var DMRequestedEvent = class extends ChatEvent {
229
+ constructor(threadId, userId, timestamp) {
230
+ super("dm.requested", threadId, timestamp ?? Date.now());
231
+ this.userId = userId;
232
+ }
233
+ };
234
+
235
+ // src/events/ChatEventFactory.ts
236
+ function parseCard(value) {
237
+ if (!value || typeof value !== "object") return void 0;
238
+ const obj = value;
239
+ if (typeof obj.type !== "string") return void 0;
240
+ return obj;
241
+ }
242
+ function parseChatEvent(json) {
243
+ const type = json.type;
244
+ const threadId = json.threadId;
245
+ const timestamp = json.timestamp;
246
+ const data = json.data ?? {};
247
+ switch (type) {
248
+ case "message.posted":
249
+ return new MessagePostedEvent(
250
+ threadId,
251
+ data.messageId,
252
+ data.text,
253
+ data.author,
254
+ parseCard(data.card),
255
+ data.attachments,
256
+ timestamp
257
+ );
258
+ case "message.edited":
259
+ return new MessageEditedEvent(
260
+ threadId,
261
+ data.messageId,
262
+ data.newText,
263
+ parseCard(data.card),
264
+ timestamp
265
+ );
266
+ case "message.deleted":
267
+ return new MessageDeletedEvent(threadId, data.messageId, timestamp);
268
+ case "reaction.added":
269
+ return new ReactionAddedEvent(
270
+ threadId,
271
+ data.messageId,
272
+ data.emoji,
273
+ data.user,
274
+ timestamp
275
+ );
276
+ case "reaction.removed":
277
+ return new ReactionRemovedEvent(
278
+ threadId,
279
+ data.messageId,
280
+ data.emoji,
281
+ data.user,
282
+ timestamp
283
+ );
284
+ case "typing.started":
285
+ return new TypingStartedEvent(threadId, data.userId, timestamp);
286
+ case "streaming.chunk":
287
+ return new StreamingChunkEvent(
288
+ threadId,
289
+ data.messageId,
290
+ data.chunk,
291
+ data.isFinal,
292
+ timestamp
293
+ );
294
+ case "dm.requested":
295
+ return new DMRequestedEvent(threadId, data.userId, timestamp);
296
+ default:
297
+ return new UnknownEvent(type, threadId, data, timestamp);
298
+ }
299
+ }
300
+ ChatEvent.fromJSON = parseChatEvent;
301
+
302
+ // src/utils/eventIdGenerator.ts
303
+ function generateId() {
304
+ return `msg-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
305
+ }
306
+ function generateConversationId() {
307
+ return `conv-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
308
+ }
309
+
310
+ // src/client/WebChatClient.ts
311
+ var WebChatClient = class {
312
+ constructor(config) {
313
+ this.messages = [];
314
+ this.eventHandlers = {};
315
+ this.streamingMessages = /* @__PURE__ */ new Map();
316
+ this.pendingTyping = null;
317
+ this.subscribers = /* @__PURE__ */ new Map();
318
+ this.config = config;
319
+ this.httpClient = new HttpClient({
320
+ apiUrl: config.apiUrl,
321
+ headers: {
322
+ "X-User-Id": config.userId,
323
+ "X-User-Name": config.userName,
324
+ ...config.headers ?? {}
325
+ },
326
+ verifyToken: config.verifyToken
327
+ });
328
+ this.broadcastClient = config.broadcastClient;
329
+ this.conversationId = config.conversationId ?? generateConversationId();
330
+ this.currentUserId = config.userId;
331
+ }
332
+ async connect() {
333
+ if (this.broadcastClient) {
334
+ this.broadcastClient.connect();
335
+ const threadId = this.getThreadId();
336
+ const threadEvents = {
337
+ onMessagePosted: (event) => this.handleMessagePosted(event)
338
+ };
339
+ if (this.config.features?.editMessages) {
340
+ threadEvents.onMessageEdited = (event) => this.handleMessageEdited(event);
341
+ }
342
+ if (this.config.features?.deleteMessages) {
343
+ threadEvents.onMessageDeleted = (event) => this.handleMessageDeleted(event);
344
+ }
345
+ if (this.config.features?.reactions) {
346
+ threadEvents.onReactionAdded = (event) => this.handleReactionAdded(event);
347
+ threadEvents.onReactionRemoved = (event) => this.handleReactionRemoved(event);
348
+ }
349
+ this.broadcastClient.subscribe(threadId, threadEvents);
350
+ this.unsubscribeUserChannel = this.broadcastClient.subscribeToUser(
351
+ threadId,
352
+ this.currentUserId,
353
+ {
354
+ onTypingStarted: (event) => this.handleTypingStarted(event),
355
+ onStreamingChunk: (event) => this.handleStreamingChunk(event),
356
+ onDMRequested: (event) => this.handleDMRequested(event)
357
+ }
358
+ );
359
+ }
360
+ }
361
+ disconnect() {
362
+ this.unsubscribeUserChannel?.();
363
+ this.unsubscribeUserChannel = void 0;
364
+ this.broadcastClient?.disconnect();
365
+ this.streamingMessages.clear();
366
+ }
367
+ async loadMessages(options, signal) {
368
+ const endpoint = this.config.endpoints?.loadMessages ?? "/api/chat/messages";
369
+ const threadId = this.getThreadId();
370
+ const params = new URLSearchParams({
371
+ threadId,
372
+ limit: String(options?.limit ?? 50)
373
+ });
374
+ if (options?.before) params.set("before", String(options.before));
375
+ if (options?.after) params.set("after", String(options.after));
376
+ const response = await this.httpClient.get(
377
+ `${endpoint}?${params.toString()}`,
378
+ signal
379
+ );
380
+ const messages = (response.messages || []).map((msg) => ({
381
+ id: msg.id,
382
+ threadId,
383
+ content: { text: msg.text, cards: msg.card ? [msg.card] : void 0 },
384
+ author: {
385
+ id: msg.author.id,
386
+ name: msg.author.name,
387
+ isBot: msg.author.isBot ?? false,
388
+ isMe: msg.author.id === this.currentUserId
389
+ },
390
+ timestamp: msg.timestamp,
391
+ attachments: msg.attachments?.map((a) => ({
392
+ id: `att-${msg.id}-${a.url}`,
393
+ url: a.url,
394
+ name: a.name,
395
+ type: a.type,
396
+ mimeType: a.mime_type,
397
+ size: a.size
398
+ })),
399
+ reactions: msg.reactions ?? []
400
+ }));
401
+ if (!options?.before && !options?.after && !options?.skipStateSeed) {
402
+ this.messages = messages;
403
+ this.notifySubscribers("messages:loaded", messages);
404
+ }
405
+ return {
406
+ messages,
407
+ hasMore: response.hasMore ?? false,
408
+ nextCursor: response.nextCursor,
409
+ prevCursor: response.prevCursor
410
+ };
411
+ }
412
+ async sendMessage(text, attachments = []) {
413
+ const messageId = generateId();
414
+ const userMessage = {
415
+ id: messageId,
416
+ threadId: this.getThreadId(),
417
+ content: { text },
418
+ author: { id: this.currentUserId, name: this.config.userName, isMe: true },
419
+ timestamp: Date.now(),
420
+ attachments: attachments.length > 0 ? attachments.map((a, i) => ({
421
+ id: `att-${messageId}-${i}`,
422
+ name: a.name || "",
423
+ url: a.url,
424
+ size: a.size,
425
+ mimeType: a.mimeType
426
+ })) : void 0
427
+ };
428
+ this.messages.push(userMessage);
429
+ this.notifySubscribers("message:added", userMessage);
430
+ const endpoint = this.config.endpoints?.sendMessage ?? "/api/webhooks/web";
431
+ const response = await this.httpClient.sendMessage(
432
+ [
433
+ {
434
+ id: messageId,
435
+ role: "user",
436
+ text,
437
+ attachments: attachments?.map((a) => ({
438
+ url: a.url,
439
+ name: a.name,
440
+ mime_type: a.mimeType,
441
+ size: a.size
442
+ }))
443
+ }
444
+ ],
445
+ endpoint,
446
+ this.conversationId
447
+ );
448
+ if (response.events) {
449
+ response.events.forEach((eventData) => {
450
+ const event = parseChatEvent(eventData);
451
+ this.dispatchEvent(event);
452
+ });
453
+ }
454
+ if (response.text && !response.events?.some((e) => e.type === "message.posted")) {
455
+ const assistantMessage = {
456
+ id: response.id || generateId(),
457
+ threadId: this.getThreadId(),
458
+ content: { text: response.text },
459
+ author: { id: "assistant", name: "Assistant", isBot: true },
460
+ timestamp: Date.now(),
461
+ attachments: response.attachments?.map((a, i) => ({
462
+ id: `att-${response.id || "msg"}-${i}`,
463
+ name: a.name || "",
464
+ url: a.url || "",
465
+ type: a.type,
466
+ mimeType: a.mime_type,
467
+ size: a.size
468
+ }))
469
+ };
470
+ this.messages.push(assistantMessage);
471
+ this.notifySubscribers("message:added", assistantMessage);
472
+ }
473
+ }
474
+ async sendAction(messageId, actionId, value) {
475
+ const endpoint = this.config.endpoints?.sendMessage ?? "/api/webhooks/web";
476
+ const response = await this.httpClient.sendAction(
477
+ actionId,
478
+ value,
479
+ messageId,
480
+ this.conversationId,
481
+ endpoint
482
+ );
483
+ if (response.events) {
484
+ response.events.forEach((eventData) => {
485
+ const event = parseChatEvent(eventData);
486
+ this.dispatchEvent(event);
487
+ });
488
+ }
489
+ }
490
+ async editMessage(messageId, newText) {
491
+ if (!this.config.features?.editMessages) {
492
+ throw new Error("Edit messages not enabled. Set features.editMessages = true in config.");
493
+ }
494
+ const endpoint = this.config.endpoints?.editMessage ?? "/api/chat/messages/{id}/edit";
495
+ await this.httpClient.editMessage(messageId, newText, endpoint);
496
+ }
497
+ async deleteMessage(messageId) {
498
+ if (!this.config.features?.deleteMessages) {
499
+ throw new Error("Delete messages not enabled. Set features.deleteMessages = true in config.");
500
+ }
501
+ const endpoint = this.config.endpoints?.deleteMessage ?? "/api/chat/messages/{id}";
502
+ await this.httpClient.deleteMessage(messageId, endpoint);
503
+ }
504
+ async addReaction(messageId, emoji) {
505
+ if (!this.config.features?.reactions) {
506
+ throw new Error("Reactions not enabled. Set features.reactions = true in config.");
507
+ }
508
+ const endpoint = this.config.endpoints?.addReaction ?? "/api/chat/messages/{id}/reactions";
509
+ await this.httpClient.addReaction(messageId, emoji, endpoint);
510
+ }
511
+ async removeReaction(messageId, emoji) {
512
+ if (!this.config.features?.reactions) {
513
+ throw new Error("Reactions not enabled. Set features.reactions = true in config.");
514
+ }
515
+ const endpoint = this.config.endpoints?.removeReaction ?? "/api/chat/messages/{id}/reactions/{emoji}";
516
+ await this.httpClient.removeReaction(messageId, emoji, endpoint);
517
+ }
518
+ onMessagePosted(handler) {
519
+ return this.addEventListener("message.posted", handler);
520
+ }
521
+ onStreamingChunk(handler) {
522
+ return this.addEventListener("streaming.chunk", handler);
523
+ }
524
+ onTypingStarted(handler) {
525
+ return this.addEventListener("typing.started", handler);
526
+ }
527
+ getConversationId() {
528
+ return this.conversationId;
529
+ }
530
+ getMessages() {
531
+ return [...this.messages];
532
+ }
533
+ getThreadId() {
534
+ return `web:${this.currentUserId}:${this.conversationId}`;
535
+ }
536
+ getCurrentUserId() {
537
+ return this.currentUserId;
538
+ }
539
+ getFeatures() {
540
+ return this.config.features ?? {};
541
+ }
542
+ getEndpoints() {
543
+ return this.config.endpoints ?? {};
544
+ }
545
+ getHttpClient() {
546
+ return this.httpClient;
547
+ }
548
+ addEventListener(eventType, handler) {
549
+ if (!this.subscribers.has(eventType)) this.subscribers.set(eventType, []);
550
+ this.subscribers.get(eventType).push(handler);
551
+ return () => {
552
+ const handlers = this.subscribers.get(eventType);
553
+ if (handlers) {
554
+ const index = handlers.indexOf(handler);
555
+ if (index !== -1) handlers.splice(index, 1);
556
+ }
557
+ };
558
+ }
559
+ handleMessagePosted(event) {
560
+ if (this.messages.some((m) => m.id === event.messageId)) return;
561
+ if (this.streamingMessages.has(event.messageId)) return;
562
+ const message = {
563
+ id: event.messageId,
564
+ threadId: event.threadId,
565
+ content: { text: event.text, cards: event.card ? [event.card] : void 0 },
566
+ author: event.author,
567
+ timestamp: event.timestamp,
568
+ attachments: event.attachments?.map((a) => ({
569
+ id: `att-${event.messageId}-${Math.random().toString(36).slice(2, 8)}`,
570
+ name: a.name || "",
571
+ url: a.url || "",
572
+ type: a.type,
573
+ mimeType: a.mimeType,
574
+ size: a.size
575
+ }))
576
+ };
577
+ this.messages.push(message);
578
+ this.notifySubscribers("message:added", message);
579
+ this.notifySubscribers("message.posted", event);
580
+ }
581
+ handleMessageEdited(event) {
582
+ const message = this.messages.find((m) => m.id === event.messageId);
583
+ if (message?.content) {
584
+ message.content.text = event.newText;
585
+ this.notifySubscribers("message:edited", {
586
+ messageId: event.messageId,
587
+ newText: event.newText
588
+ });
589
+ }
590
+ }
591
+ handleMessageDeleted(event) {
592
+ const index = this.messages.findIndex((m) => m.id === event.messageId);
593
+ if (index !== -1) {
594
+ this.messages.splice(index, 1);
595
+ this.notifySubscribers("message:deleted", { messageId: event.messageId });
596
+ }
597
+ }
598
+ handleReactionAdded(event) {
599
+ const message = this.messages.find((m) => m.id === event.messageId);
600
+ if (!message) return;
601
+ if (!message.reactions) message.reactions = [];
602
+ const existing = message.reactions.find((r) => r.emoji === event.emoji);
603
+ if (existing) {
604
+ existing.count++;
605
+ existing.users.push(event.user.id);
606
+ } else {
607
+ message.reactions.push({ emoji: event.emoji, count: 1, users: [event.user.id] });
608
+ }
609
+ this.notifySubscribers("reaction:added", { messageId: event.messageId, emoji: event.emoji });
610
+ }
611
+ handleReactionRemoved(event) {
612
+ const message = this.messages.find((m) => m.id === event.messageId);
613
+ if (!message?.reactions) return;
614
+ const index = message.reactions.findIndex((r) => r.emoji === event.emoji);
615
+ if (index !== -1) {
616
+ const reaction = message.reactions[index];
617
+ reaction.count--;
618
+ reaction.users = reaction.users.filter((id) => id !== event.user.id);
619
+ if (reaction.count === 0) message.reactions.splice(index, 1);
620
+ }
621
+ this.notifySubscribers("reaction:removed", { messageId: event.messageId, emoji: event.emoji });
622
+ }
623
+ handleStreamingChunk(event) {
624
+ const { messageId, chunk, isFinal } = event;
625
+ if (!this.streamingMessages.has(messageId)) {
626
+ this.streamingMessages.set(messageId, { messageId, accumulatedText: "", isComplete: false });
627
+ this.notifySubscribers("streaming:started", { messageId });
628
+ }
629
+ const state = this.streamingMessages.get(messageId);
630
+ state.accumulatedText += chunk;
631
+ if (isFinal) {
632
+ state.isComplete = true;
633
+ if (!this.messages.some((m) => m.id === messageId)) {
634
+ const message = {
635
+ id: messageId,
636
+ threadId: event.threadId,
637
+ content: { text: state.accumulatedText },
638
+ author: { id: "assistant", name: "Assistant", isBot: true },
639
+ timestamp: event.timestamp
640
+ };
641
+ this.messages.push(message);
642
+ this.notifySubscribers("message:added", message);
643
+ }
644
+ this.streamingMessages.delete(messageId);
645
+ this.notifySubscribers("streaming:complete", { messageId, text: state.accumulatedText });
646
+ } else {
647
+ this.notifySubscribers("streaming:chunk", {
648
+ messageId,
649
+ chunk,
650
+ fullText: state.accumulatedText
651
+ });
652
+ }
653
+ this.notifySubscribers("streaming.chunk", event);
654
+ }
655
+ handleTypingStarted(event) {
656
+ if (this.pendingTyping) clearTimeout(this.pendingTyping);
657
+ this.notifySubscribers("typing:started", { userId: event.userId });
658
+ this.pendingTyping = setTimeout(() => {
659
+ this.notifySubscribers("typing:stopped", { userId: event.userId });
660
+ }, 3e3);
661
+ this.notifySubscribers("typing.started", event);
662
+ }
663
+ handleDMRequested(event) {
664
+ this.notifySubscribers("dm.requested", { userId: event.userId, threadId: event.threadId });
665
+ }
666
+ notifySubscribers(eventType, data) {
667
+ this.subscribers.get(eventType)?.forEach((handler) => handler(data));
668
+ }
669
+ dispatchEvent(event) {
670
+ switch (event.type) {
671
+ case "message.posted":
672
+ this.handleMessagePosted(event);
673
+ break;
674
+ case "message.edited":
675
+ this.handleMessageEdited(event);
676
+ break;
677
+ case "message.deleted":
678
+ this.handleMessageDeleted(event);
679
+ break;
680
+ case "reaction.added":
681
+ this.handleReactionAdded(event);
682
+ break;
683
+ case "reaction.removed":
684
+ this.handleReactionRemoved(event);
685
+ break;
686
+ case "typing.started":
687
+ this.handleTypingStarted(event);
688
+ break;
689
+ case "streaming.chunk":
690
+ this.handleStreamingChunk(event);
691
+ break;
692
+ case "dm.requested":
693
+ this.handleDMRequested(event);
694
+ break;
695
+ }
696
+ }
697
+ };
698
+
699
+ // src/client/PusherBroadcastClient.ts
700
+ var PusherBroadcastClient = class {
701
+ constructor(pusherOrConfig, channelPrefix = "chat", channelTypes) {
702
+ this.subscriptions = /* @__PURE__ */ new Map();
703
+ this.channelPrefix = channelPrefix;
704
+ this.threadChannelType = channelTypes?.threadChannel ?? "public";
705
+ this.userChannelType = channelTypes?.userChannel ?? "private";
706
+ if ("key" in pusherOrConfig) {
707
+ const PusherCtor = globalThis.Pusher ?? globalThis.pusherJs;
708
+ if (!PusherCtor) {
709
+ throw new Error("pusher-js not found. Install it or pass a Pusher instance.");
710
+ }
711
+ this.pusher = new PusherCtor(pusherOrConfig.key, pusherOrConfig);
712
+ } else {
713
+ this.pusher = pusherOrConfig;
714
+ }
715
+ }
716
+ buildChannelName(base, type) {
717
+ switch (type) {
718
+ case "private":
719
+ return `private-${base}`;
720
+ case "presence":
721
+ return `presence-${base}`;
722
+ default:
723
+ return base;
724
+ }
725
+ }
726
+ connect() {
727
+ if (this.pusher.connection?.state !== "connected") {
728
+ this.pusher.connect();
729
+ }
730
+ }
731
+ disconnect() {
732
+ this.subscriptions.forEach((channel) => channel.unbind_all?.() ?? channel.unsubscribe?.());
733
+ this.subscriptions.clear();
734
+ this.pusher.disconnect?.();
735
+ }
736
+ subscribe(threadId, handlers) {
737
+ const baseName = `${this.channelPrefix}.${threadId}`;
738
+ const channelName = this.buildChannelName(baseName, this.threadChannelType);
739
+ const channel = this.pusher.subscribe(channelName);
740
+ const key = `thread:${threadId}`;
741
+ this.subscriptions.set(key, channel);
742
+ const threadEvents = [
743
+ "message.posted",
744
+ "message.edited",
745
+ "message.deleted",
746
+ "reaction.added",
747
+ "reaction.removed"
748
+ ];
749
+ threadEvents.forEach((eventType) => {
750
+ const eventName = `${this.channelPrefix}.${eventType}`;
751
+ channel.bind(eventName, (data) => {
752
+ const event = parseChatEvent(data);
753
+ this.dispatchToHandler(event, handlers);
754
+ });
755
+ });
756
+ return () => {
757
+ channel.unbind_all?.();
758
+ this.pusher.unsubscribe(channelName);
759
+ this.subscriptions.delete(key);
760
+ };
761
+ }
762
+ subscribeToUser(threadId, userId, handlers) {
763
+ const baseName = `${this.channelPrefix}.${threadId}.${userId}`;
764
+ const channelName = this.buildChannelName(baseName, this.userChannelType);
765
+ const channel = this.pusher.subscribe(channelName);
766
+ const key = `user:${threadId}:${userId}`;
767
+ this.subscriptions.set(key, channel);
768
+ const userEvents = ["typing.started", "streaming.chunk", "dm.requested"];
769
+ userEvents.forEach((eventType) => {
770
+ const eventName = `${this.channelPrefix}.${eventType}`;
771
+ channel.bind(eventName, (data) => {
772
+ const event = parseChatEvent(data);
773
+ this.dispatchToHandler(event, handlers);
774
+ });
775
+ });
776
+ return () => {
777
+ channel.unbind_all?.();
778
+ this.pusher.unsubscribe(channelName);
779
+ this.subscriptions.delete(key);
780
+ };
781
+ }
782
+ isConnected() {
783
+ return this.pusher.connection?.state === "connected";
784
+ }
785
+ dispatchToHandler(event, handlers) {
786
+ switch (event.type) {
787
+ case "message.posted":
788
+ handlers.onMessagePosted?.(event);
789
+ break;
790
+ case "message.edited":
791
+ handlers.onMessageEdited?.(event);
792
+ break;
793
+ case "message.deleted":
794
+ handlers.onMessageDeleted?.(event);
795
+ break;
796
+ case "reaction.added":
797
+ handlers.onReactionAdded?.(event);
798
+ break;
799
+ case "reaction.removed":
800
+ handlers.onReactionRemoved?.(event);
801
+ break;
802
+ case "typing.started":
803
+ handlers.onTypingStarted?.(event);
804
+ break;
805
+ case "streaming.chunk":
806
+ handlers.onStreamingChunk?.(event);
807
+ break;
808
+ case "dm.requested":
809
+ handlers.onDMRequested?.(event);
810
+ break;
811
+ }
812
+ }
813
+ };
814
+
815
+ // src/client/LaravelEchoBroadcastClient.ts
816
+ var LaravelEchoBroadcastClient = class {
817
+ constructor(echo, channelPrefix = "chat", channelTypes) {
818
+ this.subscriptions = /* @__PURE__ */ new Map();
819
+ this.echo = echo;
820
+ this.channelPrefix = channelPrefix;
821
+ this.threadChannelType = channelTypes?.threadChannel ?? "public";
822
+ this.userChannelType = channelTypes?.userChannel ?? "private";
823
+ }
824
+ subscribeToEcho(name, type) {
825
+ switch (type) {
826
+ case "private":
827
+ return this.echo.private(name);
828
+ case "presence":
829
+ return this.echo.join(name);
830
+ default:
831
+ return this.echo.channel(name);
832
+ }
833
+ }
834
+ connect() {
835
+ return Promise.resolve();
836
+ }
837
+ disconnect() {
838
+ this.subscriptions.forEach((channel) => {
839
+ channel.unsubscribe?.();
840
+ channel.stopListening?.();
841
+ });
842
+ this.subscriptions.clear();
843
+ }
844
+ subscribe(threadId, handlers) {
845
+ const channelName = `${this.channelPrefix}.${threadId}`;
846
+ const channel = this.subscribeToEcho(channelName, this.threadChannelType);
847
+ const key = `thread:${threadId}`;
848
+ this.subscriptions.set(key, channel);
849
+ const threadEvents = [
850
+ { type: "message.posted", handler: "onMessagePosted" },
851
+ { type: "message.edited", handler: "onMessageEdited" },
852
+ { type: "message.deleted", handler: "onMessageDeleted" },
853
+ { type: "reaction.added", handler: "onReactionAdded" },
854
+ { type: "reaction.removed", handler: "onReactionRemoved" }
855
+ ];
856
+ threadEvents.forEach(({ type, handler }) => {
857
+ const eventName = `.${this.channelPrefix}.${type}`;
858
+ channel.listen(eventName, (data) => {
859
+ const event = parseChatEvent(data);
860
+ handlers[handler]?.(event);
861
+ });
862
+ });
863
+ return () => {
864
+ channel.unsubscribe?.();
865
+ this.subscriptions.delete(key);
866
+ };
867
+ }
868
+ subscribeToUser(threadId, userId, handlers) {
869
+ const channelName = `${this.channelPrefix}.${threadId}.${userId}`;
870
+ const channel = this.subscribeToEcho(channelName, this.userChannelType);
871
+ const key = `user:${threadId}:${userId}`;
872
+ this.subscriptions.set(key, channel);
873
+ const userEvents = [
874
+ { type: "typing.started", handler: "onTypingStarted" },
875
+ { type: "streaming.chunk", handler: "onStreamingChunk" },
876
+ { type: "dm.requested", handler: "onDMRequested" }
877
+ ];
878
+ userEvents.forEach(({ type, handler }) => {
879
+ const eventName = `.${this.channelPrefix}.${type}`;
880
+ channel.listen(eventName, (data) => {
881
+ const event = parseChatEvent(data);
882
+ handlers[handler]?.(event);
883
+ });
884
+ });
885
+ return () => {
886
+ channel.unsubscribe?.();
887
+ this.subscriptions.delete(key);
888
+ };
889
+ }
890
+ isConnected() {
891
+ try {
892
+ const connector = this.echo.connector;
893
+ if (!connector) return false;
894
+ if (connector.pusher?.connection?.state === "connected") return true;
895
+ if (connector.socket?.connected) return true;
896
+ return false;
897
+ } catch {
898
+ return false;
899
+ }
900
+ }
901
+ };
902
+
903
+ // src/push/PushManager.ts
904
+ var PushManager = class _PushManager {
905
+ constructor(config) {
906
+ this.registration = null;
907
+ this.status = "unsupported";
908
+ this.statusListeners = /* @__PURE__ */ new Set();
909
+ this.messageListeners = /* @__PURE__ */ new Set();
910
+ this.config = config;
911
+ }
912
+ static isSupported() {
913
+ return typeof navigator !== "undefined" && "serviceWorker" in navigator && "PushManager" in window && "Notification" in window;
914
+ }
915
+ getStatus() {
916
+ return this.status;
917
+ }
918
+ onStatusChange(listener) {
919
+ this.statusListeners.add(listener);
920
+ return () => {
921
+ this.statusListeners.delete(listener);
922
+ };
923
+ }
924
+ onMessage(listener) {
925
+ this.messageListeners.add(listener);
926
+ return () => {
927
+ this.messageListeners.delete(listener);
928
+ };
929
+ }
930
+ async initialize() {
931
+ if (!_PushManager.isSupported()) {
932
+ this.setStatus("unsupported");
933
+ return;
934
+ }
935
+ if (Notification.permission === "denied") {
936
+ this.setStatus("denied");
937
+ return;
938
+ }
939
+ try {
940
+ this.registration = await navigator.serviceWorker.register(
941
+ this.config.serviceWorkerUrl || "/chat-service-worker.js",
942
+ { scope: this.config.serviceWorkerScope || "/" }
943
+ );
944
+ await navigator.serviceWorker.ready;
945
+ const subscription = await this.registration.pushManager.getSubscription();
946
+ this.setStatus(subscription ? "subscribed" : "default");
947
+ } catch {
948
+ this.setStatus("error");
949
+ }
950
+ }
951
+ async subscribe() {
952
+ if (!this.registration)
953
+ throw new Error("PushManager not initialized. Call initialize() first.");
954
+ this.setStatus("subscribing");
955
+ try {
956
+ let subscription = await this.registration.pushManager.getSubscription();
957
+ if (!subscription) {
958
+ const permission = await Notification.requestPermission();
959
+ if (permission !== "granted") {
960
+ this.setStatus(permission === "denied" ? "denied" : "default");
961
+ return;
962
+ }
963
+ const vapidPublicKey = await this.config.getVapidPublicKey();
964
+ const convertedKey = this.urlBase64ToUint8Array(vapidPublicKey);
965
+ subscription = await this.registration.pushManager.subscribe({
966
+ userVisibleOnly: true,
967
+ applicationServerKey: convertedKey.buffer
968
+ });
969
+ }
970
+ await this.config.onSubscribe(subscription.toJSON());
971
+ this.setStatus("subscribed");
972
+ } catch {
973
+ this.setStatus("error");
974
+ throw new Error("Push subscription failed");
975
+ }
976
+ }
977
+ async unsubscribe() {
978
+ if (!this.registration) return;
979
+ try {
980
+ const subscription = await this.registration.pushManager.getSubscription();
981
+ if (subscription) {
982
+ await this.config.onUnsubscribe(subscription.toJSON());
983
+ await subscription.unsubscribe();
984
+ }
985
+ this.setStatus("default");
986
+ } catch {
987
+ this.setStatus("error");
988
+ throw new Error("Push unsubscription failed");
989
+ }
990
+ }
991
+ urlBase64ToUint8Array(base64String) {
992
+ const padding = "=".repeat((4 - base64String.length % 4) % 4);
993
+ const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
994
+ const rawData = atob(base64);
995
+ return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
996
+ }
997
+ setStatus(status) {
998
+ this.status = status;
999
+ this.statusListeners.forEach((listener) => listener(status));
1000
+ }
1001
+ };
1002
+
1003
+ // src/push/PushSubscriptionManager.ts
1004
+ function createPushSubscriptionHandlers(httpClient, userId) {
1005
+ return {
1006
+ onSubscribe: async (subscription) => {
1007
+ await httpClient.post("/api/push/subscriptions", {
1008
+ userId,
1009
+ subscription,
1010
+ userAgent: navigator.userAgent
1011
+ });
1012
+ },
1013
+ onUnsubscribe: async (subscription) => {
1014
+ await httpClient.delete(
1015
+ `/api/push/subscriptions?userId=${encodeURIComponent(userId)}&endpoint=${encodeURIComponent(subscription.endpoint || "")}`
1016
+ );
1017
+ }
1018
+ };
1019
+ }
1020
+ // Annotate the CommonJS export names for ESM import in node:
1021
+ 0 && (module.exports = {
1022
+ ChatEvent,
1023
+ DMRequestedEvent,
1024
+ HttpClient,
1025
+ LaravelEchoBroadcastClient,
1026
+ MessageDeletedEvent,
1027
+ MessageEditedEvent,
1028
+ MessagePostedEvent,
1029
+ PushManager,
1030
+ PusherBroadcastClient,
1031
+ ReactionAddedEvent,
1032
+ ReactionRemovedEvent,
1033
+ StreamingChunkEvent,
1034
+ TypingStartedEvent,
1035
+ UnknownEvent,
1036
+ WebChatClient,
1037
+ createPushSubscriptionHandlers,
1038
+ generateConversationId,
1039
+ generateId,
1040
+ parseChatEvent
1041
+ });
1042
+ //# sourceMappingURL=index.cjs.map