diff --git a/packages/chrome-extension/src/background.ts b/packages/chrome-extension/src/background.ts index b636452..d79a5bf 100644 --- a/packages/chrome-extension/src/background.ts +++ b/packages/chrome-extension/src/background.ts @@ -6,18 +6,29 @@ import ConfigStorageService from './services/config-storage-service'; let elementSender: ElementSenderService; let currentConfig: ExtensionConfig; +let initPromise: Promise | null = null; -// Initialize when service worker starts -async function initialize() { - currentConfig = await ConfigStorageService.load(); +// Idempotent initialization. The MV3 service worker is terminated when idle and +// woken by the very message we need to handle, so the onMessage listener must +// AWAIT this before touching elementSender/currentConfig — otherwise the first +// click after the SW sleeps races the async config load and is silently lost +// (elementSender is still undefined -> TypeError). +function ensureInitialized(): Promise { + if (!initPromise) { + initPromise = (async () => { + currentConfig = await ConfigStorageService.load(); - // Create the service (no connection on startup) - elementSender = new ElementSenderService(); + // Create the service (no connection on startup) + elementSender = new ElementSenderService(); - logger.info('🚀 MCP Pointer background script loaded', { - enabled: currentConfig.enabled, - port: currentConfig.websocket.port, - }); + logger.info('🚀 MCP Pointer background script loaded', { + enabled: currentConfig.enabled, + port: currentConfig.websocket.port, + }); + })(); + } + + return initPromise; } // Listen for config changes @@ -34,42 +45,57 @@ ConfigStorageService.onChange((newConfig: ExtensionConfig) => { } }); -// Listen for messages from content script +// Listen for messages from content script. +// NOTE: the listener itself is registered synchronously at the top level and is +// NOT async; the async work runs in an inner IIFE and we return true to keep the +// channel open (per MV3 messaging guidance). chrome.runtime.onMessage .addListener((request: any, _sender: any, sendResponse: (response: any) => void) => { if (request.type === 'DOM_ELEMENT_POINTED' && request.data) { - // Send element with current port and status callback - elementSender.sendElement( - request.data, - currentConfig.websocket.port, - (status, error) => { - // Status flow: CONNECTING -> CONNECTED -> SENDING -> SENT - switch (status) { - case ConnectionStatus.CONNECTING: - logger.info('🔄 Connecting to WebSocket...'); - break; - case ConnectionStatus.CONNECTED: - logger.info('✅ Connected'); - break; - case ConnectionStatus.SENDING: - logger.info('📤 Sending element...'); - break; - case ConnectionStatus.SENT: - logger.info('✓ Element sent successfully'); - break; - case ConnectionStatus.ERROR: - logger.error('❌ Failed:', error); - break; - default: - break; - } - }, - ); - - sendResponse({ success: true }); + (async () => { + try { + // Ensure config + sender are ready even on a cold SW start. + await ensureInitialized(); + + // Send element with current port and status callback + await elementSender.sendElement( + request.data, + currentConfig.websocket.port, + (status, error) => { + // Status flow: CONNECTING -> CONNECTED -> SENDING -> SENT + switch (status) { + case ConnectionStatus.CONNECTING: + logger.info('🔄 Connecting to WebSocket...'); + break; + case ConnectionStatus.CONNECTED: + logger.info('✅ Connected'); + break; + case ConnectionStatus.SENDING: + logger.info('📤 Sending element...'); + break; + case ConnectionStatus.SENT: + logger.info('✓ Element sent successfully'); + break; + case ConnectionStatus.ERROR: + logger.error('❌ Failed:', error); + break; + default: + break; + } + }, + ); + + sendResponse({ success: true }); + } catch (error) { + logger.error('❌ Failed to handle pointed element:', error); + sendResponse({ success: false, error: (error as Error).message }); + } + })(); + + return true; // Keep message channel open for async response } - return true; // Keep message channel open for async response + return true; }); // Handle extension install/update @@ -88,5 +114,5 @@ chrome.runtime.onInstalled.addListener((details) => { } }); -// Start initialization -initialize(); +// Best-effort warm start (messages also trigger ensureInitialized) +ensureInitialized();