Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 68 additions & 42 deletions packages/chrome-extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,29 @@ import ConfigStorageService from './services/config-storage-service';

let elementSender: ElementSenderService;
let currentConfig: ExtensionConfig;
let initPromise: Promise<void> | 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<void> {
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
Expand All @@ -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
Expand All @@ -88,5 +114,5 @@ chrome.runtime.onInstalled.addListener((details) => {
}
});

// Start initialization
initialize();
// Best-effort warm start (messages also trigger ensureInitialized)
ensureInitialized();