/** * KeepAlive service to handle widget disappearing due to external JS files * This service helps ensure that the widget remains visible even if other scripts * accidentally remove it from the DOM. */ import { Debug } from '../debug'; interface KeepAliveOptions { targetSelector: string; onDisappear: (element: HTMLElement | null) => void; maxAttempts?: number; checkInterval?: number; } export class KeepAlive { private targetSelector: string; private onDisappear: (element: HTMLElement | null) => void; private maxAttempts: number; private checkInterval: number; private attempts: number = 0; private observer: MutationObserver | null = null; private intervalId: number | null = null; private originalHTML: string | null = null; private originalElement: HTMLElement | null = null; /** * Creates a KeepAlive instance * * @param options Configuration options * @param options.targetSelector CSS selector for the target element to watch * @param options.onDisappear Callback to execute when target disappears * @param options.maxAttempts Maximum number of recovery attempts (default: 10) * @param options.checkInterval Interval in ms to check element visibility (default: 500) */ constructor(options: KeepAliveOptions) { this.targetSelector = options.targetSelector; this.onDisappear = options.onDisappear; this.maxAttempts = options.maxAttempts || 10; this.checkInterval = options.checkInterval || 500; } /** * Starts monitoring the target element * This combines both mutation observation and interval checking */ public start(): void { Debug.log('Starting keep-alive monitoring'); this.originalElement = document.querySelector(this.targetSelector); if (!this.originalElement) { Debug.error('KeepAlive: Target element not found'); return; } // Store original state this.originalHTML = this.originalElement.innerHTML; // Set up mutation observer this.setupMutationObserver(); // Start interval check as a backup detection method this.startIntervalCheck(); } /** * Stops all monitoring */ public stop(): void { Debug.log('Stopping keep-alive monitoring'); if (this.observer) { this.observer.disconnect(); this.observer = null; } if (this.intervalId !== null) { window.clearInterval(this.intervalId); this.intervalId = null; } } /** * Set up mutation observer to detect DOM changes, scoped to the parent of the target element. */ private setupMutationObserver(): void { if (!this.originalElement) return; // Observe the parent, if not present observe the html element const observeTarget = this.originalElement.parentNode || document.documentElement; this.observer = new MutationObserver(() => { const targetElement = document.querySelector(this.targetSelector); // If our element was removed, trigger recovery logic if (!targetElement || !document.body.contains(targetElement)) { Debug.log('Widget element removed from DOM by mutation', { targetElement }); this.handleDisappearance(targetElement); } }); // Observe only direct children of the parent to minimize performance impact this.observer.observe(observeTarget, { childList: true, subtree: false, attributes: false, characterData: false, }); } /** * Start interval checking as a backup mechanism after several consecutive failed checks */ private startIntervalCheck(): void { let consecutiveMisses = 0; const REQUIRED_MISSES = 3; this.intervalId = window.setInterval(() => { const targetElement = document.querySelector(this.targetSelector); const missing = !targetElement || !document.body.contains(targetElement); if (missing) { consecutiveMisses += 1; if (consecutiveMisses >= REQUIRED_MISSES) { consecutiveMisses = 0; Debug.log('Widget element not found during interval check'); this.handleDisappearance(targetElement); } } else { consecutiveMisses = 0; } }, this.checkInterval); } /** * Handle when element disappears */ private handleDisappearance(currentElement: HTMLElement | null): void { if (this.attempts >= this.maxAttempts) { Debug.log('KeepAlive: Max recovery attempts reached'); this.stop(); return; } this.attempts++; Debug.log(`KeepAlive: Attempting recovery (${this.attempts}/${this.maxAttempts})`); try { this.onDisappear(currentElement); } catch (error) { Debug.error('Error in onDisappear callback', error); } } } export default KeepAlive;