Skip to content

Commit cd13239

Browse files
fix(extension): await init in onMessage to fix lost click after SW sleeps
The MV3 service worker is terminated when idle (the WebSocket idle timer disconnects after 10s, removing the last keep-alive). The next Option+Click wakes the SW via runtime.sendMessage, but the onMessage handler used elementSender / currentConfig directly while initialize() was still running its async config load. On a cold start the handler fires before init completes, so elementSender is undefined and the click is silently lost — users had to open the SW console (which keeps it alive) to make it work. Make initialization idempotent (ensureInitialized) and await it inside the listener before sending. The listener stays synchronous and returns true to keep the channel open for the async sendResponse, per MV3 messaging guidance. Fixes #19
1 parent 59601d1 commit cd13239

1 file changed

Lines changed: 68 additions & 42 deletions

File tree

packages/chrome-extension/src/background.ts

Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,29 @@ import ConfigStorageService from './services/config-storage-service';
66

77
let elementSender: ElementSenderService;
88
let currentConfig: ExtensionConfig;
9+
let initPromise: Promise<void> | null = null;
910

10-
// Initialize when service worker starts
11-
async function initialize() {
12-
currentConfig = await ConfigStorageService.load();
11+
// Idempotent initialization. The MV3 service worker is terminated when idle and
12+
// woken by the very message we need to handle, so the onMessage listener must
13+
// AWAIT this before touching elementSender/currentConfig — otherwise the first
14+
// click after the SW sleeps races the async config load and is silently lost
15+
// (elementSender is still undefined -> TypeError).
16+
function ensureInitialized(): Promise<void> {
17+
if (!initPromise) {
18+
initPromise = (async () => {
19+
currentConfig = await ConfigStorageService.load();
1320

14-
// Create the service (no connection on startup)
15-
elementSender = new ElementSenderService();
21+
// Create the service (no connection on startup)
22+
elementSender = new ElementSenderService();
1623

17-
logger.info('🚀 MCP Pointer background script loaded', {
18-
enabled: currentConfig.enabled,
19-
port: currentConfig.websocket.port,
20-
});
24+
logger.info('🚀 MCP Pointer background script loaded', {
25+
enabled: currentConfig.enabled,
26+
port: currentConfig.websocket.port,
27+
});
28+
})();
29+
}
30+
31+
return initPromise;
2132
}
2233

2334
// Listen for config changes
@@ -34,42 +45,57 @@ ConfigStorageService.onChange((newConfig: ExtensionConfig) => {
3445
}
3546
});
3647

37-
// Listen for messages from content script
48+
// Listen for messages from content script.
49+
// NOTE: the listener itself is registered synchronously at the top level and is
50+
// NOT async; the async work runs in an inner IIFE and we return true to keep the
51+
// channel open (per MV3 messaging guidance).
3852
chrome.runtime.onMessage
3953
.addListener((request: any, _sender: any, sendResponse: (response: any) => void) => {
4054
if (request.type === 'DOM_ELEMENT_POINTED' && request.data) {
41-
// Send element with current port and status callback
42-
elementSender.sendElement(
43-
request.data,
44-
currentConfig.websocket.port,
45-
(status, error) => {
46-
// Status flow: CONNECTING -> CONNECTED -> SENDING -> SENT
47-
switch (status) {
48-
case ConnectionStatus.CONNECTING:
49-
logger.info('🔄 Connecting to WebSocket...');
50-
break;
51-
case ConnectionStatus.CONNECTED:
52-
logger.info('✅ Connected');
53-
break;
54-
case ConnectionStatus.SENDING:
55-
logger.info('📤 Sending element...');
56-
break;
57-
case ConnectionStatus.SENT:
58-
logger.info('✓ Element sent successfully');
59-
break;
60-
case ConnectionStatus.ERROR:
61-
logger.error('❌ Failed:', error);
62-
break;
63-
default:
64-
break;
65-
}
66-
},
67-
);
68-
69-
sendResponse({ success: true });
55+
(async () => {
56+
try {
57+
// Ensure config + sender are ready even on a cold SW start.
58+
await ensureInitialized();
59+
60+
// Send element with current port and status callback
61+
await elementSender.sendElement(
62+
request.data,
63+
currentConfig.websocket.port,
64+
(status, error) => {
65+
// Status flow: CONNECTING -> CONNECTED -> SENDING -> SENT
66+
switch (status) {
67+
case ConnectionStatus.CONNECTING:
68+
logger.info('🔄 Connecting to WebSocket...');
69+
break;
70+
case ConnectionStatus.CONNECTED:
71+
logger.info('✅ Connected');
72+
break;
73+
case ConnectionStatus.SENDING:
74+
logger.info('📤 Sending element...');
75+
break;
76+
case ConnectionStatus.SENT:
77+
logger.info('✓ Element sent successfully');
78+
break;
79+
case ConnectionStatus.ERROR:
80+
logger.error('❌ Failed:', error);
81+
break;
82+
default:
83+
break;
84+
}
85+
},
86+
);
87+
88+
sendResponse({ success: true });
89+
} catch (error) {
90+
logger.error('❌ Failed to handle pointed element:', error);
91+
sendResponse({ success: false, error: (error as Error).message });
92+
}
93+
})();
94+
95+
return true; // Keep message channel open for async response
7096
}
7197

72-
return true; // Keep message channel open for async response
98+
return true;
7399
});
74100

75101
// Handle extension install/update
@@ -88,5 +114,5 @@ chrome.runtime.onInstalled.addListener((details) => {
88114
}
89115
});
90116

91-
// Start initialization
92-
initialize();
117+
// Best-effort warm start (messages also trigger ensureInitialized)
118+
ensureInitialized();

0 commit comments

Comments
 (0)