Skip to content

Commit 8eac37e

Browse files
committed
fix: Prevent infinite recursion in widget close handlers
- Add isClosing flag to prevent re-entry in handleClose() - Add isClosing flag to ChatInterface.close() to prevent callback recursion - Properly disconnect LiveKit client before closing chat interface - Fixes 'Maximum call stack size exceeded' error when closing widget
1 parent f0d6dfe commit 8eac37e

2 files changed

Lines changed: 53 additions & 16 deletions

File tree

widget/src/ui/chat-interface.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export class ChatInterface {
3434
private emojiPicker: HTMLDivElement | null = null
3535
private emojiButton: HTMLButtonElement | null = null
3636
private isEmojiPickerOpen = false
37+
private isClosing = false // Prevent infinite recursion in close callback
3738

3839
constructor(config: ChatInterfaceConfig) {
3940
this.config = config
@@ -1359,16 +1360,30 @@ export class ChatInterface {
13591360
* Close chat window
13601361
*/
13611362
close(): void {
1362-
if (this.window) {
1363-
this.window.style.display = 'none'
1364-
this.isOpen = false
1363+
// Prevent infinite recursion
1364+
if (this.isClosing) {
1365+
return
13651366
}
1366-
// Close emoji picker if open
1367-
this.closeEmojiPicker()
1368-
// Stop audio monitoring when closing
1369-
this.stopAudioMonitoring()
1370-
if (this.config.onClose) {
1371-
this.config.onClose()
1367+
this.isClosing = true
1368+
1369+
try {
1370+
if (this.window) {
1371+
this.window.style.display = 'none'
1372+
this.isOpen = false
1373+
}
1374+
// Close emoji picker if open
1375+
this.closeEmojiPicker()
1376+
// Stop audio monitoring when closing
1377+
this.stopAudioMonitoring()
1378+
// Call callback (the widget's handleClose has its own guard to prevent recursion)
1379+
if (this.config.onClose) {
1380+
this.config.onClose()
1381+
}
1382+
} finally {
1383+
// Reset flag after a short delay to allow cleanup
1384+
setTimeout(() => {
1385+
this.isClosing = false
1386+
}, 100)
13721387
}
13731388
}
13741389

widget/src/widget.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export class SynteraWidget {
2222
private isInitialized = false
2323
private consentData: ConsentData | null = null
2424
private gdprModal: GDPRConsentModal | null = null
25+
private isClosing = false // Prevent infinite recursion in close handlers
2526

2627
constructor(config: WidgetConfig) {
2728
this.config = config
@@ -377,15 +378,36 @@ export class SynteraWidget {
377378
* Handle closing the widget
378379
*/
379380
private handleClose(): void {
380-
// Disconnect WebSocket
381-
if (this.wsClient) {
382-
this.wsClient.disconnect()
383-
this.wsClient = null
381+
// Prevent infinite recursion
382+
if (this.isClosing) {
383+
return
384384
}
385+
this.isClosing = true
385386

386-
// Close chat interface
387-
if (this.chatInterface) {
388-
this.chatInterface.close()
387+
try {
388+
// Disconnect LiveKit client first
389+
if (this.liveKitClient) {
390+
this.liveKitClient.disconnect().catch((error) => {
391+
logger.error('Error disconnecting LiveKit client:', error)
392+
})
393+
this.liveKitClient = null
394+
}
395+
396+
// Disconnect WebSocket
397+
if (this.wsClient) {
398+
this.wsClient.disconnect()
399+
this.wsClient = null
400+
}
401+
402+
// Close chat interface (this will trigger onClose callback, but we're protected by isClosing flag)
403+
if (this.chatInterface) {
404+
this.chatInterface.close()
405+
}
406+
} finally {
407+
// Reset flag after a short delay to allow cleanup
408+
setTimeout(() => {
409+
this.isClosing = false
410+
}, 100)
389411
}
390412
}
391413

0 commit comments

Comments
 (0)