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