@botonic/webchat-core 2.23.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +247 -0
  2. package/PR-LOCALE-SUPPORT.md +156 -0
  3. package/README.md +11 -0
  4. package/package.json +29 -0
  5. package/src/index.d.ts +11 -0
  6. package/src/index.js +15 -0
  7. package/src/index.js.map +1 -0
  8. package/src/lib/controllers/index.d.ts +2 -0
  9. package/src/lib/controllers/index.js +3 -0
  10. package/src/lib/controllers/index.js.map +1 -0
  11. package/src/lib/controllers/public.d.ts +383 -0
  12. package/src/lib/controllers/public.js +194 -0
  13. package/src/lib/controllers/public.js.map +1 -0
  14. package/src/lib/controllers/webchat.d.ts +414 -0
  15. package/src/lib/controllers/webchat.js +255 -0
  16. package/src/lib/controllers/webchat.js.map +1 -0
  17. package/src/lib/domain/index.d.ts +50 -0
  18. package/src/lib/domain/index.js +11 -0
  19. package/src/lib/domain/index.js.map +1 -0
  20. package/src/lib/infra/clients/hubtype-api.d.ts +53 -0
  21. package/src/lib/infra/clients/hubtype-api.js +189 -0
  22. package/src/lib/infra/clients/hubtype-api.js.map +1 -0
  23. package/src/lib/infra/clients/index.d.ts +3 -0
  24. package/src/lib/infra/clients/index.js +4 -0
  25. package/src/lib/infra/clients/index.js.map +1 -0
  26. package/src/lib/infra/clients/realtime.d.ts +92 -0
  27. package/src/lib/infra/clients/realtime.js +277 -0
  28. package/src/lib/infra/clients/realtime.js.map +1 -0
  29. package/src/lib/infra/clients/types.d.ts +21 -0
  30. package/src/lib/infra/clients/types.js +2 -0
  31. package/src/lib/infra/clients/types.js.map +1 -0
  32. package/src/lib/infra/repositories/index.d.ts +1 -0
  33. package/src/lib/infra/repositories/index.js +2 -0
  34. package/src/lib/infra/repositories/index.js.map +1 -0
  35. package/src/lib/infra/repositories/storage/base-storage.d.ts +23 -0
  36. package/src/lib/infra/repositories/storage/base-storage.js +102 -0
  37. package/src/lib/infra/repositories/storage/base-storage.js.map +1 -0
  38. package/src/lib/infra/repositories/storage/browser-storage.d.ts +9 -0
  39. package/src/lib/infra/repositories/storage/browser-storage.js +46 -0
  40. package/src/lib/infra/repositories/storage/browser-storage.js.map +1 -0
  41. package/src/lib/infra/repositories/storage/factory.d.ts +2 -0
  42. package/src/lib/infra/repositories/storage/factory.js +20 -0
  43. package/src/lib/infra/repositories/storage/factory.js.map +1 -0
  44. package/src/lib/infra/repositories/storage/in-memory.d.ts +9 -0
  45. package/src/lib/infra/repositories/storage/in-memory.js +21 -0
  46. package/src/lib/infra/repositories/storage/in-memory.js.map +1 -0
  47. package/src/lib/infra/repositories/storage/index.d.ts +7 -0
  48. package/src/lib/infra/repositories/storage/index.js +7 -0
  49. package/src/lib/infra/repositories/storage/index.js.map +1 -0
  50. package/src/lib/infra/repositories/storage/local-storage.d.ts +4 -0
  51. package/src/lib/infra/repositories/storage/local-storage.js +7 -0
  52. package/src/lib/infra/repositories/storage/local-storage.js.map +1 -0
  53. package/src/lib/infra/repositories/storage/session-storage.d.ts +4 -0
  54. package/src/lib/infra/repositories/storage/session-storage.js +7 -0
  55. package/src/lib/infra/repositories/storage/session-storage.js.map +1 -0
  56. package/src/lib/infra/repositories/storage/types.d.ts +30 -0
  57. package/src/lib/infra/repositories/storage/types.js +2 -0
  58. package/src/lib/infra/repositories/storage/types.js.map +1 -0
  59. package/src/lib/machines/message.d.ts +200 -0
  60. package/src/lib/machines/message.js +210 -0
  61. package/src/lib/machines/message.js.map +1 -0
  62. package/src/lib/machines/overlay.d.ts +299 -0
  63. package/src/lib/machines/overlay.js +217 -0
  64. package/src/lib/machines/overlay.js.map +1 -0
  65. package/src/lib/machines/webchat.d.ts +641 -0
  66. package/src/lib/machines/webchat.js +768 -0
  67. package/src/lib/machines/webchat.js.map +1 -0
  68. package/src/lib/services/webchat-service.d.ts +125 -0
  69. package/src/lib/services/webchat-service.js +339 -0
  70. package/src/lib/services/webchat-service.js.map +1 -0
  71. package/src/lib/utils/locale-detection.d.ts +2 -0
  72. package/src/lib/utils/locale-detection.js +20 -0
  73. package/src/lib/utils/locale-detection.js.map +1 -0
  74. package/src/lib/utils/timezone-to-country.d.ts +1 -0
  75. package/src/lib/utils/timezone-to-country.js +105 -0
  76. package/src/lib/utils/timezone-to-country.js.map +1 -0
  77. package/src/lib/webview/constants.d.ts +10 -0
  78. package/src/lib/webview/constants.js +10 -0
  79. package/src/lib/webview/constants.js.map +1 -0
  80. package/src/lib/webview/index.d.ts +3 -0
  81. package/src/lib/webview/index.js +4 -0
  82. package/src/lib/webview/index.js.map +1 -0
  83. package/src/lib/webview/types.d.ts +35 -0
  84. package/src/lib/webview/types.js +16 -0
  85. package/src/lib/webview/types.js.map +1 -0
  86. package/src/lib/webview/utils.d.ts +9 -0
  87. package/src/lib/webview/utils.js +33 -0
  88. package/src/lib/webview/utils.js.map +1 -0
@@ -0,0 +1,768 @@
1
+ import { __awaiter } from "tslib";
2
+ import { MessageSentBy, WebchatMessageFactory, } from '@botonic/shared';
3
+ import { v7 as uuidv7 } from 'uuid';
4
+ import { and, assertEvent, assign, createActor, emit, fromPromise, setup, } from 'xstate';
5
+ import { createStorageRepository, } from '../infra/repositories';
6
+ import { WebchatService, } from '../services/webchat-service';
7
+ import { detectUserLocale } from '../utils/locale-detection';
8
+ import { createWebchatMessageMachine, WebchatMessageStatus, } from './message';
9
+ import { OverlayMachine } from './overlay';
10
+ // Helper function to generate anonymous users with locale props
11
+ function generateAnonymousUser(localeProps, extraData) {
12
+ const userId = `${uuidv7()}`;
13
+ const userLocale = detectUserLocale(localeProps);
14
+ return Object.assign(Object.assign({ id: userId, name: 'Anonymous User' }, userLocale), { extra_data: extraData });
15
+ }
16
+ /**
17
+ * Helper function to create a postMessage actor for sending messages to the backend
18
+ */
19
+ function createPostMessageActor(webchatService) {
20
+ return fromPromise((_a) => __awaiter(this, [_a], void 0, function* ({ input: { message, user }, }) {
21
+ if (!webchatService) {
22
+ throw new Error('Webchat service not initialized');
23
+ }
24
+ const response = yield webchatService.sendMessage(message, user);
25
+ if (!response.success) {
26
+ throw new Error(response.error || `Failed to send message`);
27
+ }
28
+ return {
29
+ message: Promise.resolve({ detail: 'sent' }),
30
+ };
31
+ }));
32
+ }
33
+ /**
34
+ * Main webchat machine that handles the complete lifecycle
35
+ *
36
+ * Flow:
37
+ * 1. idle → initializing (setup services, load session)
38
+ * 2. initializing → operational (ready for messaging)
39
+ * 3. operational → (handle messages, connections, UI state)
40
+ */
41
+ export const WebchatMachine = setup({
42
+ types: {},
43
+ actors: {
44
+ // Initialize webchat service with all dependencies
45
+ initializeWebchatService: fromPromise((_a) => __awaiter(void 0, [_a], void 0, function* ({ input }) {
46
+ const { config } = input;
47
+ // Initialize storage repository with config
48
+ const storageRepository = createStorageRepository(config.storage);
49
+ const webchatServiceConfig = {
50
+ appId: config.appId,
51
+ apiUrl: config.apiUrl,
52
+ timeout: 10000,
53
+ };
54
+ const webchatService = new WebchatService(webchatServiceConfig);
55
+ return { webchatService, storageRepository };
56
+ })),
57
+ // Load messages from storage
58
+ loadMessages: fromPromise((_a) => __awaiter(void 0, [_a], void 0, function* ({ input, }) {
59
+ const { storageRepository, appId, userId } = input;
60
+ return yield storageRepository.findMessagesByConversation(appId, userId);
61
+ })),
62
+ // Load last user from storage
63
+ loadLastUser: fromPromise((_a) => __awaiter(void 0, [_a], void 0, function* ({ input, }) {
64
+ const { storageRepository, appId } = input;
65
+ const session = yield storageRepository.findLastUserSession(appId);
66
+ return session || null;
67
+ })),
68
+ // Initialize realtime connection
69
+ initializeRealtimeConnection: fromPromise((_a) => __awaiter(void 0, [_a], void 0, function* ({ input, }) {
70
+ const { webchatService, user, messages } = input;
71
+ const result = yield webchatService.connect(user, messages);
72
+ if (!result.success) {
73
+ throw new Error(result.error || 'Failed to initialize realtime connection');
74
+ }
75
+ return { success: true };
76
+ })),
77
+ // Reconnect to realtime
78
+ reconnectToRealtime: fromPromise((_a) => __awaiter(void 0, [_a], void 0, function* ({ input, }) {
79
+ const { webchatService, user, messages, attempt } = input;
80
+ // Calculate backoff delay
81
+ const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
82
+ // Wait before attempting reconnection
83
+ if (attempt > 0) {
84
+ yield new Promise(resolve => setTimeout(resolve, delay));
85
+ }
86
+ // Attempt to reconnect
87
+ const result = yield webchatService.connect(user, messages);
88
+ if (!result.success) {
89
+ throw new Error(result.error || 'Failed to reconnect');
90
+ }
91
+ return { success: true };
92
+ })),
93
+ },
94
+ actions: {
95
+ // Actions extracted from connection machine
96
+ assignWebchatService: assign({
97
+ webchatService: ({ event }) => event.output.webchatService,
98
+ storageRepository: ({ event }) => event.output.storageRepository,
99
+ appId: ({ context }) => context.config.appId,
100
+ }),
101
+ assignLoadedUser: assign({
102
+ user: ({ context, event }) => {
103
+ // If we already have a user in context (from createAnonymousUser), use it
104
+ if (context.user)
105
+ return context.user;
106
+ // Otherwise use the loaded user from storage
107
+ return event.output;
108
+ },
109
+ }),
110
+ createAnonymousUser: assign({
111
+ user: ({ context }) => {
112
+ // Extract locale props and extra_data from config
113
+ const { locale, country, system_locale, extra_data } = context.config;
114
+ const newUser = generateAnonymousUser({ locale, country, system_locale }, extra_data);
115
+ return newUser;
116
+ },
117
+ }),
118
+ assignLoadedMessages: assign({
119
+ messages: ({ event }) => event.output,
120
+ }),
121
+ spawnPersistedMessageActors: assign({
122
+ messageActors: ({ context, spawn }) => {
123
+ var _a;
124
+ const actors = new Map();
125
+ (_a = context.messages) === null || _a === void 0 ? void 0 : _a.forEach(storedMessage => {
126
+ const message = WebchatMessageFactory.fromStoredMessage(storedMessage);
127
+ // Create API actors using helper functions
128
+ const postMessageActor = createPostMessageActor(context.webchatService);
129
+ // Use factory function to create machine logic and spawn with input
130
+ const actor = spawn(createWebchatMessageMachine(postMessageActor), {
131
+ input: {
132
+ message,
133
+ user: context.user,
134
+ status: storedMessage.status, // Preserve stored status!
135
+ },
136
+ systemId: message.id,
137
+ });
138
+ actors.set(message.id, actor);
139
+ });
140
+ return actors;
141
+ },
142
+ }),
143
+ saveUser: (_a) => __awaiter(void 0, [_a], void 0, function* ({ context }) {
144
+ var _b;
145
+ if (!context.storageRepository || !context.user)
146
+ return;
147
+ try {
148
+ const channelData = (_b = context.webchatService) === null || _b === void 0 ? void 0 : _b.getChannelData();
149
+ const userToSave = channelData
150
+ ? Object.assign(Object.assign({}, context.user), { botId: channelData.botId, chatId: channelData.chatId }) : context.user;
151
+ yield context.storageRepository.saveUser(context.appId || context.config.appId, userToSave);
152
+ }
153
+ catch (error) {
154
+ console.error('Failed to save user:', error);
155
+ }
156
+ }),
157
+ // Forward WEBVIEW.OPEN events from message actors to the overlay actor
158
+ forwardWebviewOpenToOverlay: ({ context, event }) => {
159
+ assertEvent(event, 'WEBVIEW.OPEN');
160
+ context.overlayActor.send({
161
+ type: 'OPEN_WEBVIEW',
162
+ url: event.url,
163
+ title: event.title,
164
+ });
165
+ },
166
+ // Subscribe to WebchatService message callbacks
167
+ subscribeToWebchatService: ({ context, self }) => {
168
+ if (!context.webchatService) {
169
+ console.warn('⚠️ No webchat service available for subscriptions');
170
+ return;
171
+ }
172
+ // Subscribe to received messages from bot
173
+ const unsubscribeMessages = context.webchatService.onMessageReceived((message) => {
174
+ self.send({
175
+ type: 'MESSAGE_RECEIVED',
176
+ message,
177
+ });
178
+ });
179
+ // Subscribe to connection status changes (better than polling!)
180
+ const unsubscribeStatus = context.webchatService.onConnectionStatusChange(status => {
181
+ console.log(`🔄 Connection status changed: ${status}`);
182
+ self.send({
183
+ type: 'CONNECTION_STATUS_CHANGED',
184
+ status,
185
+ });
186
+ });
187
+ context._unsubscribeMessages = unsubscribeMessages;
188
+ context._unsubscribeStatus = unsubscribeStatus;
189
+ },
190
+ // Cleanup webchat service subscriptions
191
+ cleanupWebchatServiceSubscriptions: ({ context }) => {
192
+ if (context._unsubscribeMessages) {
193
+ ;
194
+ context._unsubscribeMessages();
195
+ }
196
+ if (context._unsubscribeStatus) {
197
+ ;
198
+ context._unsubscribeStatus();
199
+ }
200
+ },
201
+ // Toggle webchat open/close (direct handling)
202
+ toggleWebchat: assign({
203
+ isOpen: ({ context, event }) => {
204
+ assertEvent(event, 'TOGGLE_WEBCHAT');
205
+ return event.isOpen !== undefined ? event.isOpen : !context.isOpen;
206
+ },
207
+ }),
208
+ emitWebchatToggled: emit(({ context, event }) => {
209
+ assertEvent(event, 'TOGGLE_WEBCHAT');
210
+ const newIsOpen = event.isOpen !== undefined ? event.isOpen : !context.isOpen;
211
+ return newIsOpen
212
+ ? { type: 'WEBCHAT_OPENED' }
213
+ : { type: 'WEBCHAT_CLOSED' };
214
+ }),
215
+ // Update connection status in context
216
+ updateConnectionStatus: assign({
217
+ connectionStatus: ({ event }) => {
218
+ assertEvent(event, 'CONNECTION_STATUS_CHANGED');
219
+ return event.status;
220
+ },
221
+ }),
222
+ // Add message by spawning actor (single source of truth)
223
+ addMessage: assign({
224
+ messageActors: ({ context, event, spawn }) => {
225
+ assertEvent(event, ['SEND_MESSAGE', 'MESSAGE_RECEIVED']);
226
+ console.log('addMessage', event.message.type);
227
+ const message = event.message;
228
+ // Get unique identifier for the message
229
+ // Note: After domain refactoring, different message types use different ID fields:
230
+ // - UserInputMessage uses provider_id
231
+ // - RealtimeResponseMessage uses message_id
232
+ const messageId = message.id;
233
+ // Check if actor already exists
234
+ const currentActors = context.messageActors || new Map();
235
+ if (currentActors.has(messageId)) {
236
+ return currentActors;
237
+ }
238
+ // Determine status based on event type
239
+ // IMPORTANT: After domain refactoring, UserInputMessage doesn't have sent_by property
240
+ // Only RealtimeResponseMessage (bot messages) have sent_by
241
+ // So we determine status purely from event type:
242
+ // - SEND_MESSAGE = user input that needs to be sent via API (UNSENT)
243
+ // - MESSAGE_RECEIVED = bot response already sent via Pusher (SENT)
244
+ const status = event.type === 'MESSAGE_RECEIVED'
245
+ ? WebchatMessageStatus.SENT
246
+ : message.sent_by === MessageSentBy.User
247
+ ? WebchatMessageStatus.UNSENT
248
+ : WebchatMessageStatus.SENT;
249
+ // Create actors for API operations using helper functions
250
+ const postMessageActor = createPostMessageActor(context.webchatService);
251
+ // Use factory function to create machine logic and spawn with input
252
+ const actor = spawn(createWebchatMessageMachine(postMessageActor), {
253
+ input: {
254
+ message,
255
+ user: context.user,
256
+ status,
257
+ },
258
+ systemId: message.id,
259
+ });
260
+ // Update actors registry using the appropriate ID as key
261
+ const newActors = new Map(currentActors);
262
+ newActors.set(messageId, actor);
263
+ return newActors;
264
+ },
265
+ }),
266
+ // Save all messages to storage (derived from messageActors)
267
+ saveMessages: (_a) => __awaiter(void 0, [_a], void 0, function* ({ context }) {
268
+ if (!context.storageRepository || !context.user) {
269
+ console.warn('💾 PERSISTENCE: Cannot save - missing storage repository or user');
270
+ return;
271
+ }
272
+ try {
273
+ // Derive current messages from messageActors (single source of truth)
274
+ const currentMessages = [];
275
+ for (const [messageId, actor] of context.messageActors || new Map()) {
276
+ const snapshot = actor.getSnapshot();
277
+ const message = snapshot.context.message;
278
+ const status = snapshot.context.status;
279
+ // Create simple StoredMessage object
280
+ currentMessages.push(Object.assign({ status }, message));
281
+ }
282
+ // Use simple strings for the repository call
283
+ const appId = context.appId || context.config.appId;
284
+ const userId = context.user.id;
285
+ yield context.storageRepository.saveAllMessages(appId, userId, currentMessages);
286
+ }
287
+ catch (error) {
288
+ console.error('💾 PERSISTENCE: ❌ Failed to save messages:', error);
289
+ }
290
+ }),
291
+ // Emit message added event
292
+ emitMessageAdded: emit(({ context, event }) => {
293
+ assertEvent(event, ['SEND_MESSAGE', 'MESSAGE_RECEIVED']);
294
+ const message = event.message;
295
+ // Get ID based on message type (domain refactoring: different ID fields)
296
+ const messageId = message.id;
297
+ return {
298
+ type: 'MESSAGE_ADDED',
299
+ message: event.message,
300
+ // Lookup actor using the appropriate ID field
301
+ actor: (context.messageActors || new Map()).get(messageId),
302
+ };
303
+ }),
304
+ // New emit actions for enhanced event system
305
+ emitMessageSent: emit(({ event }) => {
306
+ assertEvent(event, 'SEND_MESSAGE');
307
+ return {
308
+ type: 'MESSAGE_SENT',
309
+ message: event.message,
310
+ };
311
+ }),
312
+ emitMessageReceived: emit(({ event }) => {
313
+ assertEvent(event, 'MESSAGE_RECEIVED');
314
+ return {
315
+ type: 'MESSAGE_RECEIVED',
316
+ message: event.message,
317
+ };
318
+ }),
319
+ emitMessageUpdated: emit(({ event }) => {
320
+ assertEvent(event, 'UPDATE_MESSAGE');
321
+ return {
322
+ type: 'MESSAGE_UPDATED',
323
+ messageId: event.messageId,
324
+ updates: event.updates,
325
+ };
326
+ }),
327
+ emitMessagesCleared: emit(() => ({
328
+ type: 'MESSAGES_CLEARED',
329
+ })),
330
+ emitUserUpdated: emit(({ event }) => {
331
+ assertEvent(event, 'UPDATE_USER');
332
+ return {
333
+ type: 'USER_UPDATED',
334
+ user: event.user,
335
+ };
336
+ }),
337
+ emitWebchatOpened: emit(() => ({
338
+ type: 'WEBCHAT_OPENED',
339
+ })),
340
+ emitWebchatClosed: emit(() => ({
341
+ type: 'WEBCHAT_CLOSED',
342
+ })),
343
+ // Initialization emit actions
344
+ emitInitializationStarted: emit(() => ({
345
+ type: 'INITIALIZATION_STARTED',
346
+ })),
347
+ emitInitializationCompleted: emit(({ context }) => ({
348
+ type: 'INITIALIZATION_COMPLETED',
349
+ context: context,
350
+ })),
351
+ emitInitializationFailed: emit(({ event }) => {
352
+ // Extract error from the event or provide a default
353
+ const error = event.error || 'Initialization failed';
354
+ return {
355
+ type: 'INITIALIZATION_FAILED',
356
+ error: typeof error === 'string' ? error : error.message || 'Unknown error',
357
+ };
358
+ }),
359
+ // Connection emit actions
360
+ emitConnectionChanged: emit(({ event }) => {
361
+ assertEvent(event, 'CONNECTION_STATUS_CHANGED');
362
+ return {
363
+ type: 'CONNECTION_CHANGED',
364
+ status: event.status,
365
+ };
366
+ }),
367
+ emitMessageDeleted: emit(({ event }) => {
368
+ assertEvent(event, 'DELETE_MESSAGE');
369
+ return {
370
+ type: 'MESSAGE_DELETED',
371
+ messageId: event.messageId,
372
+ };
373
+ }),
374
+ // New assign actions for state management
375
+ updateUser: assign({
376
+ user: ({ context, event }) => {
377
+ assertEvent(event, 'UPDATE_USER');
378
+ return Object.assign(Object.assign({}, context.user), event.user);
379
+ },
380
+ }),
381
+ clearMessages: assign({
382
+ messageActors: () => new Map(),
383
+ }),
384
+ updateMessage: assign({
385
+ messageActors: ({ context, event }) => {
386
+ assertEvent(event, 'UPDATE_MESSAGE');
387
+ const actors = new Map(context.messageActors);
388
+ const actor = actors.get(event.messageId);
389
+ // if (actor) {
390
+ // // Send update to the message actor
391
+ // actor.send({
392
+ // type: 'UPDATE_MESSAGE_INFO',
393
+ // message: {
394
+ // id: event.messageId,
395
+ // data: event.updates as any,
396
+ // },
397
+ // })
398
+ // }
399
+ return actors;
400
+ },
401
+ }),
402
+ deleteMessage: assign({
403
+ messageActors: ({ context, event }) => {
404
+ assertEvent(event, 'DELETE_MESSAGE');
405
+ const actors = new Map(context.messageActors);
406
+ actors.delete(event.messageId);
407
+ return actors;
408
+ },
409
+ }),
410
+ setTyping: assign({
411
+ isTypingOn: ({ event }) => {
412
+ assertEvent(event, 'MESSAGE_RECEIVED');
413
+ return event.message.isTypingOn;
414
+ },
415
+ }),
416
+ setTypingExternal: assign({
417
+ isTypingOn: ({ event }) => {
418
+ assertEvent(event, 'SET_TYPING');
419
+ return event.isTypingOn;
420
+ },
421
+ }),
422
+ setOnline: ({ context, event }) => {
423
+ assertEvent(event, 'SET_ONLINE');
424
+ // Handle online/offline status
425
+ if (context.webchatService) {
426
+ // TODO: Implement online status in WebchatService
427
+ // context.webchatService.setOnline(event.online)
428
+ }
429
+ },
430
+ },
431
+ guards: {
432
+ isInitialized: ({ context }) => !!context.user && !!context.webchatService,
433
+ hasLoadedUser: ({ event }) => {
434
+ // This is safe because this guard is only called in the context of loadLastUser's onDone
435
+ return event.output !== null;
436
+ },
437
+ isMaxReconnectAttempts: ({ context }) => (context.reconnectAttempt || 0) >= 5,
438
+ isTypingEvent: ({ event }) => {
439
+ assertEvent(event, ['MESSAGE_RECEIVED']);
440
+ return event.message.isTypingEvent();
441
+ },
442
+ },
443
+ }).createMachine({
444
+ /** @xstate-layout N4IgpgJg5mDOIC5QHcwCMDGALAhgFwDoBLCAGzAGIBJAOSoBUqBBAGSoC0BRAbQAYBdRKAAOAe1hE8RUQDshIAB6IAjACYArAQBs6gBy6ALAHYAnAGZel9Vq0AaEAE8V63kYImb6o731Gz65TMAXyD7VExcQiIZSSIcUiIAL2ioYhipeKSUgGUwACcANyIMOAoIWTA0gtEAa0rw7Hw02MzkmVToloS2qFzC4rgEaOqMfGkZPn5J+TEJKVl5JQQvAwI9Ay1eLVVdDVUTXXsnBGV1awI1VUsdU91edRMQsPRGqPS47pTmjM-2vqKSrAKPk8qI8gRhKR8AAzMEAWwIDUi3w+WXaKNaOXyAMGw1Eo3mEwE0yQIFmsQWpKWGkMBDMuxMqiMgQOZgM6iOKjMyguBkM9LMqgsrgMBieICRTU6PzRqVIohwECxsAksjKFSqtXqL2R0tRPQI8sVytVMiGMhGY1kkxJInEFLkVJU900m14yhMRjOWiMu1UnJOBjZBFMqlUxk9ljZYtCEp1UvemPRRqVfzgpvVMkqeLqiPjby6ssNCtTvXT43NlsJNuUglJ5MJi2dZ20lg9XusvrDAdOWlWylFGxMPh2ujU4slBZlBpTJvGwLyoPBkJh8LzEQThZnJbnskr+KtRKmAhm9sbToQGhMq3p+yZLN0bI5jhURjcntUOg7Pq0-l0E-zDFfjlHd2gAWXTHAYCBcos01XNJyAotZ3AyDoP3AlxhtE96zPcYmxOLRlB5LRdCMAwTEZPQB2sHsLC0Ol1A0LRKN4YctgAjcp31L4UKgCCVSg0oQTBCEoTwWE8gRRC9STEDjVQwT0LxTDrWJHC7TmfCL2UIiSLIiiqLHdk7BfQNmRDMwbDItRqMeWMZMTYCCFgYQcGQGJ2gABXyCRYDwSABNgISgVtMk8MpUBqVOXg1h8EwAl4GldHUAw6P2AhXGZMNfQCdl1E414kINDBZCzDApHaehRAAJTAeIpDhShYOzC0tXXIrZOc0qZHKyqoGquqGqIJqMMPbC600h0CJpG8GXvMxWXZHsmTcO4-AHDtTGCBzAK6oser6lJBvq0hGsoETl3EyTpL2pyDrKsAKuO2rTvOsbq3Uybwq0yLFEQWa6Xm5lFsfZazIHIU6TYyjRTuRbP0K5FDqewkADEcCIcgIAoGrOHoGqAE0AH0AGEAHkaBoThScYSmwobbSooBvRLN2HQ+TZIxwwDMc6TDExlFMX0WLDJGmjyJ7Hue9pM1a6oEMAyWUZlqAPqwr7T1+x1mcvci6WUSw+WIjn9DSiGqJDZRdCInQbFFa3xcIZXpf6hclzE1cpI65EXd61GUnVtTj2+xm-upPsDaNsddNSs2e0fMwCFFdQLHDQx7kd3auIIP2jtly7PYktdELzgP2iDo9uFrLXpovMN6WT31iPdCxh1SgMrkF9w7jOQWks5nbnhzsvVYoUmmBoUnOBYYm8YpqmafoBmIp1-6EHpd9eGN7x4v2DZO+31ZNh9E2kvIrQnYIURhHyQ94nHynqdpqhKeJ7J6CYegAFVsjJgAJSeABxTgAARFe2sCKb3cNvMcu9dCMmvKZY4qgWQEH0GoYc7IkrKCvjfO+hIH4L2fnTGg79P4-z-qTQBNAQHgJrrhSBF4LB82IsYMwFgDCG2tsg18XoCD7F-KGNifZDB4Nvnke+pBcY0yfkvCBdddbQKwXAywCD968MvKOIGLh2TXg4URAq2cir4MkYQ6R9ByZAKASwTgxMADqnAABC1Cv4KPPEogcPJt7Ch8XoQwmjY5JwMK4d014RzeFwcY5EpipEUGyJwGgoDiZgU4NkbITAQHuKZuvbkqdMpkW5J+D0VEzA9mMLoARZsGRESuBscRBDxgP1SekzJdj56cCoAANTAdk8OiBuRWQuC4Ji7pSK7CMHRXgSddKwxuFwhGDSzFNOkS0jJIC54006T0+hodV5QLZDyZkzdfzhl9G+Hs5g3AhP0acN8Y4NBLLiWstp5Cv6-wAcA3pGkfqKNyYbVQ7hU53GwR6XS-oIZXDWglJk3JSJcIMKoJ55iKDfy8qAr+djf6cBqn0teSw4WxTZAcIU60fTdghj4Bi1xLAiyYh4eyw8TESLiaTWxTAaopLSestJeKoEDiTn2Zkj4XD6GmZMsyVy4pXF-GYL02x-DIpWai9FmKuWtKyT8sO+KBkCuTkRN8wiDg2ADL6SplE-RbFTiwpF0SmixJRaAme+M7EvM1XsphnjRQwNIuREJewylmT7LFCiiKjC2ysmyf8drCAOuVQk+gxNKZsGpny5hulKmx3ymofw1qAyIqThw90HoHb6CZDGJlMSWUooTcTeghMvK0CAWmzxPoQz5UZD4AVTIAwBEqZsPk14zgUR0NGyt9rq3Kq8ji7IVAP6JOngQDJPT1U8pbf8ywFxhz7D8HpEWAZBSVJdNGQ9phPRKtkA-RxTiulUE4PYgg5Np00HXQS04qxPwILfElHQehO6ChDL3EljJDbGArXGHOcbL3SNoAwZgbAuCvpZpUvw7M45cx5hDX0gqPS-h9F4UwNgQixhkKICAcB5CTlrh49eABaTRtHNApQQXkt81hjCkSviQcg1GclLERT2UVFwnxfhSpRPQXH7o9F4-0y87I2afnQ8YTDxw2GrAorpH0XYsq2vHdxOSxUsT9EBDJnVl5zAKdNhh82qnU4mHQdtVK8KRRDwg51KTvFQJlhVDk7VBFHyAu8AlcNCUwMJUE6lTKlEPRypombSTW5PMKX4mhCjjC-lLBZDyPRfZiIBCKYEoMgWWJBgCOMoWCXpxfFcu5TyUAfJ5D8gFCAQUQqmYItbCwawnNbD8H2iFqnOH809F4Q29yUqVZ4uiFW-UTrDSau1+u4ZViCxsKcGLY5XDlI2D3Rk5ESkcPHDGggM3xgYyxpARbusdj622m2LYBbnyqf1vsAF28ErXnhlfUe-Urvr1pHKz7HhSKoKFiYTuqCmOkQ5oEMiZEL0yHiH96kCD7O6Q2KlbwVlryXJ2OgqMQoWLmFQdGkIQA */
445
+ id: 'webchat',
446
+ context: ({ input, spawn }) => ({
447
+ config: input,
448
+ // UI state
449
+ isOpen: false,
450
+ connectionStatus: 'disconnected',
451
+ // Operational state - messageActors is the single source of truth
452
+ messageActors: new Map(),
453
+ reconnectAttempt: 0,
454
+ // Initialized context properties (will be set after initialization)
455
+ user: undefined,
456
+ webchatService: undefined,
457
+ storageRepository: undefined,
458
+ appId: undefined,
459
+ isTypingOn: false,
460
+ // Overlay actor (spawned child machine)
461
+ overlayActor: spawn(OverlayMachine, {
462
+ systemId: 'overlay',
463
+ input: { initialState: null },
464
+ }),
465
+ }),
466
+ initial: 'idle',
467
+ states: {
468
+ idle: {
469
+ on: {
470
+ INITIALIZE: 'initializing',
471
+ },
472
+ },
473
+ initializing: {
474
+ initial: 'initializingServices',
475
+ entry: [
476
+ 'emitInitializationStarted',
477
+ assign({
478
+ connectionStatus: () => 'connecting',
479
+ }),
480
+ ],
481
+ states: {
482
+ initializingServices: {
483
+ invoke: {
484
+ src: 'initializeWebchatService',
485
+ input: ({ context }) => ({
486
+ config: context.config,
487
+ }),
488
+ onDone: {
489
+ target: 'loadingSession',
490
+ actions: 'assignWebchatService',
491
+ },
492
+ onError: {
493
+ target: '#webchat.connectionFailed',
494
+ },
495
+ },
496
+ },
497
+ loadingSession: {
498
+ invoke: {
499
+ src: 'loadLastUser',
500
+ input: ({ context }) => ({
501
+ storageRepository: context.storageRepository,
502
+ appId: context.config.appId,
503
+ }),
504
+ onDone: [
505
+ {
506
+ // Existing user found - load their messages
507
+ target: 'loadingMessages',
508
+ guard: 'hasLoadedUser',
509
+ actions: 'assignLoadedUser',
510
+ },
511
+ {
512
+ // No user found - create anonymous and skip to realtime
513
+ target: 'connectingToRealtime',
514
+ actions: ['createAnonymousUser', 'assignLoadedUser'],
515
+ },
516
+ ],
517
+ onError: '#webchat.connectionFailed',
518
+ },
519
+ },
520
+ loadingMessages: {
521
+ invoke: {
522
+ src: 'loadMessages',
523
+ input: ({ context }) => ({
524
+ storageRepository: context.storageRepository,
525
+ appId: context.config.appId,
526
+ userId: context.user.id,
527
+ }),
528
+ onDone: {
529
+ target: 'spawningPersistedMessages',
530
+ actions: 'assignLoadedMessages',
531
+ },
532
+ onError: {
533
+ // If loading messages fails, continue without them
534
+ target: 'connectingToRealtime',
535
+ },
536
+ },
537
+ },
538
+ spawningPersistedMessages: {
539
+ entry: 'spawnPersistedMessageActors',
540
+ always: 'connectingToRealtime',
541
+ },
542
+ connectingToRealtime: {
543
+ invoke: {
544
+ src: 'initializeRealtimeConnection',
545
+ input: ({ context }) => {
546
+ var _a;
547
+ return ({
548
+ webchatService: context.webchatService,
549
+ user: context.user,
550
+ messages: ((_a = context.messages) !== null && _a !== void 0 ? _a : []).map(WebchatMessageFactory.fromStoredMessage),
551
+ });
552
+ },
553
+ onDone: {
554
+ target: '#webchat.operational',
555
+ actions: [
556
+ assign({
557
+ connectionStatus: () => 'connected',
558
+ }),
559
+ 'saveUser',
560
+ 'emitInitializationCompleted',
561
+ ],
562
+ },
563
+ onError: {
564
+ // Continue without realtime if it fails (same as current behavior)
565
+ target: '#webchat.operational',
566
+ actions: [
567
+ assign({
568
+ connectionStatus: () => 'disconnected',
569
+ }),
570
+ 'saveUser',
571
+ 'emitInitializationCompleted',
572
+ ({ event }) => {
573
+ console.warn('⚠️ Realtime connection failed, continuing without realtime:', event);
574
+ },
575
+ ],
576
+ },
577
+ },
578
+ },
579
+ },
580
+ },
581
+ connectionFailed: {
582
+ on: {
583
+ RETRY_CONNECTION: 'initializing',
584
+ },
585
+ },
586
+ reconnecting: {
587
+ entry: assign({
588
+ connectionStatus: () => 'connecting',
589
+ }),
590
+ invoke: {
591
+ src: 'reconnectToRealtime',
592
+ input: ({ context }) => {
593
+ var _a;
594
+ return ({
595
+ webchatService: context.webchatService,
596
+ user: context.user,
597
+ messages: ((_a = context.messages) !== null && _a !== void 0 ? _a : []).map(WebchatMessageFactory.fromStoredMessage),
598
+ attempt: context.reconnectAttempt || 0,
599
+ });
600
+ },
601
+ onDone: {
602
+ target: 'operational',
603
+ actions: [
604
+ assign({
605
+ connectionStatus: () => 'connected',
606
+ reconnectAttempt: () => 0,
607
+ }),
608
+ () => {
609
+ console.log('✅ Reconnected successfully');
610
+ },
611
+ ],
612
+ },
613
+ onError: [
614
+ {
615
+ // Max retries reached
616
+ guard: 'isMaxReconnectAttempts',
617
+ target: 'connectionFailed',
618
+ actions: [
619
+ ({ event }) => {
620
+ console.error('❌ Max reconnection attempts reached:', event);
621
+ },
622
+ ],
623
+ },
624
+ {
625
+ // Retry with backoff
626
+ target: 'reconnecting',
627
+ reenter: true,
628
+ actions: [
629
+ assign({
630
+ reconnectAttempt: ({ context }) => (context.reconnectAttempt || 0) + 1,
631
+ }),
632
+ ({ context }) => {
633
+ const attempt = context.reconnectAttempt || 0;
634
+ const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
635
+ console.log(`⏳ Reconnection attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
636
+ },
637
+ ],
638
+ },
639
+ ],
640
+ },
641
+ on: {
642
+ CANCEL_RECONNECT: {
643
+ target: 'operational',
644
+ actions: [
645
+ assign({
646
+ connectionStatus: () => 'disconnected',
647
+ reconnectAttempt: () => 0,
648
+ }),
649
+ ],
650
+ },
651
+ },
652
+ },
653
+ operational: {
654
+ entry: 'subscribeToWebchatService',
655
+ exit: 'cleanupWebchatServiceSubscriptions',
656
+ on: {
657
+ // Connection monitoring events
658
+ CONNECTION_STATUS_CHANGED: [
659
+ {
660
+ // Auto-reconnect on disconnect
661
+ guard: ({ event }) => event.status === 'disconnected',
662
+ target: 'reconnecting',
663
+ actions: [
664
+ 'updateConnectionStatus',
665
+ 'emitConnectionChanged',
666
+ () => {
667
+ console.log('📡 Connection lost, attempting to reconnect...');
668
+ },
669
+ ],
670
+ },
671
+ {
672
+ // Other status changes just update context
673
+ actions: [
674
+ 'updateConnectionStatus',
675
+ 'emitConnectionChanged',
676
+ ({ event }) => {
677
+ console.log(`📡 Connection status updated: ${event.status}`);
678
+ },
679
+ ],
680
+ },
681
+ ],
682
+ // Allow manual reconnection trigger
683
+ RECONNECT: {
684
+ guard: 'isInitialized',
685
+ target: 'reconnecting',
686
+ },
687
+ // Handle operational events directly
688
+ TOGGLE_WEBCHAT: {
689
+ guard: 'isInitialized',
690
+ actions: ['toggleWebchat', 'emitWebchatToggled'],
691
+ },
692
+ SEND_MESSAGE: {
693
+ guard: 'isInitialized',
694
+ actions: [
695
+ 'addMessage',
696
+ 'saveMessages',
697
+ 'emitMessageAdded',
698
+ 'emitMessageSent',
699
+ ],
700
+ },
701
+ MESSAGE_RECEIVED: [
702
+ {
703
+ guard: and(['isInitialized', 'isTypingEvent']),
704
+ actions: ['setTyping'],
705
+ },
706
+ {
707
+ guard: 'isInitialized',
708
+ actions: [
709
+ 'addMessage',
710
+ 'saveMessages',
711
+ 'emitMessageAdded',
712
+ 'emitMessageReceived',
713
+ ],
714
+ },
715
+ ],
716
+ MESSAGE_STATUS_CHANGED: {
717
+ guard: 'isInitialized',
718
+ actions: ['saveMessages'],
719
+ },
720
+ // User management
721
+ UPDATE_USER: {
722
+ guard: 'isInitialized',
723
+ actions: ['updateUser', 'saveUser', 'emitUserUpdated'],
724
+ },
725
+ // Message management
726
+ CLEAR_MESSAGES: {
727
+ guard: 'isInitialized',
728
+ actions: ['clearMessages', 'saveMessages', 'emitMessagesCleared'],
729
+ },
730
+ UPDATE_MESSAGE: {
731
+ guard: 'isInitialized',
732
+ actions: ['updateMessage', 'saveMessages', 'emitMessageUpdated'],
733
+ },
734
+ DELETE_MESSAGE: {
735
+ guard: 'isInitialized',
736
+ actions: ['deleteMessage', 'saveMessages', 'emitMessageDeleted'],
737
+ },
738
+ // Connection management
739
+ SET_ONLINE: {
740
+ guard: 'isInitialized',
741
+ actions: ['setOnline'],
742
+ },
743
+ // Typing indicator control
744
+ SET_TYPING: {
745
+ guard: 'isInitialized',
746
+ actions: ['setTypingExternal'],
747
+ },
748
+ // Listen for persistence requests from message actors
749
+ 'PERSISTENCE.SAVE_MESSAGE': {
750
+ guard: 'isInitialized',
751
+ actions: ['saveMessages'],
752
+ },
753
+ // Forward WEBVIEW.OPEN from message actors to overlay actor
754
+ 'WEBVIEW.OPEN': {
755
+ guard: 'isInitialized',
756
+ actions: ['forwardWebviewOpenToOverlay'],
757
+ },
758
+ // Allow re-initialization if needed
759
+ INITIALIZE: 'initializing',
760
+ },
761
+ },
762
+ },
763
+ });
764
+ // Factory function for creating webchat machine actors
765
+ export function createWebchatMachine(webchatConfig) {
766
+ return createActor(WebchatMachine, { input: webchatConfig });
767
+ }
768
+ //# sourceMappingURL=webchat.js.map