cdp-tunnel 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.github/workflows/publish.yml +92 -0
  2. package/.github/workflows/release-assets.yml +50 -0
  3. package/LICENSE +81 -0
  4. package/PUBLISH.md +65 -0
  5. package/README.md +228 -0
  6. package/cli/guide.html +753 -0
  7. package/cli/icon.svg +13 -0
  8. package/cli/icon128.png +0 -0
  9. package/cli/index.js +357 -0
  10. package/docs/README_CN.md +204 -0
  11. package/docs/config-page-screenshot.png +0 -0
  12. package/extension-new/background.js +294 -0
  13. package/extension-new/cdp/handler/forward.js +44 -0
  14. package/extension-new/cdp/handler/local.js +233 -0
  15. package/extension-new/cdp/handler/special.js +442 -0
  16. package/extension-new/cdp/index.js +104 -0
  17. package/extension-new/cdp/response.js +49 -0
  18. package/extension-new/config-page-preview.html +769 -0
  19. package/extension-new/config-page.js +318 -0
  20. package/extension-new/core/debugger.js +310 -0
  21. package/extension-new/core/state.js +384 -0
  22. package/extension-new/core/websocket.js +326 -0
  23. package/extension-new/features/automation-badge.js +113 -0
  24. package/extension-new/features/screencast.js +221 -0
  25. package/extension-new/icons/icon128.png +0 -0
  26. package/extension-new/icons/icon16.png +0 -0
  27. package/extension-new/icons/icon48.png +0 -0
  28. package/extension-new/manifest.json +39 -0
  29. package/extension-new/popup.html +72 -0
  30. package/extension-new/popup.js +34 -0
  31. package/extension-new/utils/config.js +20 -0
  32. package/extension-new/utils/diagnostics.js +560 -0
  33. package/extension-new/utils/helpers.js +25 -0
  34. package/extension-new/utils/logger.js +64 -0
  35. package/package.json +42 -0
  36. package/server/modules/config.js +28 -0
  37. package/server/modules/logger.js +197 -0
  38. package/server/proxy-server.js +1431 -0
  39. package/tests/playwright-demo.js +45 -0
  40. package/tests/playwright-interactive.js +261 -0
  41. package/tests/playwright-multi-demo.js +60 -0
  42. package/tests/playwright-multi.js +85 -0
  43. package/tests/playwright-single.js +41 -0
  44. package/tests/screenshot-config.js +35 -0
  45. package/tests/test-client.js +89 -0
  46. package/tests/test-multi-client.js +129 -0
@@ -0,0 +1,384 @@
1
+ var State = (function() {
2
+ var _state = {
3
+ ws: null,
4
+ reconnectTimer: null,
5
+ hasConnectedClient: false,
6
+ cdpClients: [],
7
+ sessionIdToTabId: new Map(),
8
+ sessionIdToTargetId: new Map(),
9
+ attachedTabIds: new Set(),
10
+ emittedTargets: new Set(),
11
+ browserContextIds: new Set(['default']),
12
+ autoAttachConfig: {
13
+ autoAttach: false,
14
+ waitForDebuggerOnStart: false,
15
+ flatten: true
16
+ },
17
+ discoverTargetsEnabled: false,
18
+ pendingDebuggerTabs: new Set(),
19
+ screencastPollingSessions: new Map(),
20
+ automatedTabs: new Set(),
21
+ currentTabId: null,
22
+ isAttached: false,
23
+ pendingCreatedTabUrls: new Set(),
24
+ clientIdToTabId: new Map(),
25
+ clientIdToSessionId: new Map()
26
+ };
27
+
28
+ function mapSession(sessionId, tabId, targetId) {
29
+ _state.sessionIdToTabId.set(sessionId, tabId);
30
+ _state.sessionIdToTargetId.set(sessionId, targetId);
31
+ _state.attachedTabIds.add(tabId);
32
+ }
33
+
34
+ function unmapSession(sessionId) {
35
+ var tabId = _state.sessionIdToTabId.get(sessionId);
36
+ _state.sessionIdToTabId.delete(sessionId);
37
+ _state.sessionIdToTargetId.delete(sessionId);
38
+ return tabId;
39
+ }
40
+
41
+ function getTabIdBySession(sessionId) {
42
+ return _state.sessionIdToTabId.get(sessionId);
43
+ }
44
+
45
+ function getTargetIdBySession(sessionId) {
46
+ return _state.sessionIdToTargetId.get(sessionId);
47
+ }
48
+
49
+ function findSessionByTabId(tabId) {
50
+ var entries = _state.sessionIdToTabId.entries();
51
+ var entry = entries.next();
52
+ while (!entry.done) {
53
+ if (entry.value[1] === tabId) {
54
+ return entry.value[0];
55
+ }
56
+ entry = entries.next();
57
+ }
58
+ return null;
59
+ }
60
+
61
+ function findSessionByTargetId(targetId) {
62
+ var entries = _state.sessionIdToTargetId.entries();
63
+ var entry = entries.next();
64
+ while (!entry.done) {
65
+ if (entry.value[1] === targetId) {
66
+ return entry.value[0];
67
+ }
68
+ entry = entries.next();
69
+ }
70
+ return null;
71
+ }
72
+
73
+ function getTabIdByTargetId(targetId) {
74
+ var sessionId = findSessionByTargetId(targetId);
75
+ if (sessionId) {
76
+ return _state.sessionIdToTabId.get(sessionId);
77
+ }
78
+ return null;
79
+ }
80
+
81
+ function hasOtherSessionForTab(tabId) {
82
+ var count = 0;
83
+ _state.sessionIdToTabId.forEach(function(mappedTabId) {
84
+ if (mappedTabId === tabId) {
85
+ count++;
86
+ }
87
+ });
88
+ return count > 0;
89
+ }
90
+
91
+ function addAttachedTab(tabId) {
92
+ _state.attachedTabIds.add(tabId);
93
+ }
94
+
95
+ function removeAttachedTab(tabId) {
96
+ _state.attachedTabIds.delete(tabId);
97
+ }
98
+
99
+ function isTabAttached(tabId) {
100
+ return _state.attachedTabIds.has(tabId);
101
+ }
102
+
103
+ function getAttachedTabIds() {
104
+ return Array.from(_state.attachedTabIds);
105
+ }
106
+
107
+ function addEmittedTarget(targetId) {
108
+ _state.emittedTargets.add(targetId);
109
+ }
110
+
111
+ function hasEmittedTarget(targetId) {
112
+ return _state.emittedTargets.has(targetId);
113
+ }
114
+
115
+ function setAutoAttachConfig(config) {
116
+ Object.assign(_state.autoAttachConfig, config);
117
+ }
118
+
119
+ function getAutoAttachConfig() {
120
+ return Object.assign({}, _state.autoAttachConfig);
121
+ }
122
+
123
+ function setDiscoverTargets(enabled) {
124
+ _state.discoverTargetsEnabled = enabled;
125
+ }
126
+
127
+ function addPendingDebuggerTab(tabId) {
128
+ _state.pendingDebuggerTabs.add(tabId);
129
+ }
130
+
131
+ function removePendingDebuggerTab(tabId) {
132
+ _state.pendingDebuggerTabs.delete(tabId);
133
+ }
134
+
135
+ function isPendingDebuggerTab(tabId) {
136
+ return _state.pendingDebuggerTabs.has(tabId);
137
+ }
138
+
139
+ function addBrowserContext(id) {
140
+ _state.browserContextIds.add(id);
141
+ }
142
+
143
+ function removeBrowserContext(id) {
144
+ _state.browserContextIds.delete(id);
145
+ }
146
+
147
+ function getBrowserContexts() {
148
+ return Array.from(_state.browserContextIds);
149
+ }
150
+
151
+ function loadPersisted() {
152
+ return new Promise(function(resolve) {
153
+ chrome.storage.local.get(['currentTabId', 'isAttached'], function(result) {
154
+ _state.currentTabId = result.currentTabId || null;
155
+ _state.isAttached = result.isAttached || false;
156
+ resolve(result);
157
+ });
158
+ });
159
+ }
160
+
161
+ function persist(tabId, attached) {
162
+ _state.currentTabId = tabId;
163
+ _state.isAttached = attached;
164
+ return new Promise(function(resolve) {
165
+ chrome.storage.local.set({ currentTabId: tabId, isAttached: attached }, resolve);
166
+ });
167
+ }
168
+
169
+ function getCurrentTabId() {
170
+ return _state.currentTabId;
171
+ }
172
+
173
+ function setCurrentTabId(tabId) {
174
+ _state.currentTabId = tabId;
175
+ }
176
+
177
+ function getWs() {
178
+ return _state.ws;
179
+ }
180
+
181
+ function setWs(ws) {
182
+ _state.ws = ws;
183
+ }
184
+
185
+ function getReconnectTimer() {
186
+ return _state.reconnectTimer;
187
+ }
188
+
189
+ function setReconnectTimer(timer) {
190
+ _state.reconnectTimer = timer;
191
+ }
192
+
193
+ function clearReconnectTimer() {
194
+ if (_state.reconnectTimer) {
195
+ clearTimeout(_state.reconnectTimer);
196
+ _state.reconnectTimer = null;
197
+ }
198
+ }
199
+
200
+ function hasConnectedClient() {
201
+ return _state.hasConnectedClient;
202
+ }
203
+
204
+ function setHasConnectedClient(value) {
205
+ _state.hasConnectedClient = value;
206
+ }
207
+
208
+ function addPendingCreatedTabUrl(url) {
209
+ _state.pendingCreatedTabUrls.add(url);
210
+ }
211
+
212
+ function removePendingCreatedTabUrl(url) {
213
+ _state.pendingCreatedTabUrls.delete(url);
214
+ }
215
+
216
+ function hasPendingCreatedTabUrl(url) {
217
+ return _state.pendingCreatedTabUrls.has(url);
218
+ }
219
+
220
+ function getScreencastSession(tabId) {
221
+ return _state.screencastPollingSessions.get(tabId);
222
+ }
223
+
224
+ function setScreencastSession(tabId, session) {
225
+ _state.screencastPollingSessions.set(tabId, session);
226
+ }
227
+
228
+ function deleteScreencastSession(tabId) {
229
+ _state.screencastPollingSessions.delete(tabId);
230
+ }
231
+
232
+ function addAutomatedTab(tabId) {
233
+ _state.automatedTabs.add(tabId);
234
+ }
235
+
236
+ function removeAutomatedTab(tabId) {
237
+ _state.automatedTabs.delete(tabId);
238
+ }
239
+
240
+ function getAutomatedTabs() {
241
+ return Array.from(_state.automatedTabs);
242
+ }
243
+
244
+ function clearSessionState() {
245
+ _state.sessionIdToTabId.clear();
246
+ _state.sessionIdToTargetId.clear();
247
+ _state.pendingDebuggerTabs.clear();
248
+ _state.emittedTargets.clear();
249
+ }
250
+
251
+ function clearAllState() {
252
+ clearSessionState();
253
+ _state.attachedTabIds.clear();
254
+ _state.screencastPollingSessions.clear();
255
+ _state.browserContextIds = new Set(['default']);
256
+ _state.autoAttachConfig = {
257
+ autoAttach: false,
258
+ waitForDebuggerOnStart: false,
259
+ flatten: true
260
+ };
261
+ _state.discoverTargetsEnabled = false;
262
+ _state.hasConnectedClient = false;
263
+ }
264
+
265
+ function cleanupAllTabs() {
266
+ return new Promise(function(resolve) {
267
+ var tabIds = Array.from(_state.attachedTabIds);
268
+ var promises = tabIds.map(function(tabId) {
269
+ return chrome.debugger.detach({ tabId: tabId }).catch(function() {});
270
+ });
271
+ Promise.all(promises).then(function() {
272
+ clearAllState();
273
+ persist(null, false).then(resolve);
274
+ });
275
+ });
276
+ }
277
+
278
+ function mapClientIdToTab(clientId, tabId, sessionId) {
279
+ _state.clientIdToTabId.set(clientId, tabId);
280
+ if (sessionId) {
281
+ _state.clientIdToSessionId.set(clientId, sessionId);
282
+ }
283
+ }
284
+
285
+ function getTabIdByClientId(clientId) {
286
+ return _state.clientIdToTabId.get(clientId);
287
+ }
288
+
289
+ function getSessionIdByClientId(clientId) {
290
+ return _state.clientIdToSessionId.get(clientId);
291
+ }
292
+
293
+ function unmapClientId(clientId) {
294
+ _state.clientIdToTabId.delete(clientId);
295
+ _state.clientIdToSessionId.delete(clientId);
296
+ }
297
+
298
+ function addCDPClient(clientId, info) {
299
+ var exists = false;
300
+ for (var i = 0; i < _state.cdpClients.length; i++) {
301
+ if (_state.cdpClients[i].id === clientId) {
302
+ exists = true;
303
+ break;
304
+ }
305
+ }
306
+ if (!exists) {
307
+ _state.cdpClients.push({
308
+ id: clientId,
309
+ connectedAt: Date.now()
310
+ });
311
+ }
312
+ }
313
+
314
+ function removeCDPClient(clientId) {
315
+ var newClients = [];
316
+ for (var i = 0; i < _state.cdpClients.length; i++) {
317
+ if (_state.cdpClients[i].id !== clientId) {
318
+ newClients.push(_state.cdpClients[i]);
319
+ }
320
+ }
321
+ _state.cdpClients = newClients;
322
+ }
323
+
324
+ function getCDPClients() {
325
+ return _state.cdpClients;
326
+ }
327
+
328
+ function setCDPClients(clients) {
329
+ _state.cdpClients = clients || [];
330
+ }
331
+
332
+ return {
333
+ mapSession: mapSession,
334
+ unmapSession: unmapSession,
335
+ getTabIdBySession: getTabIdBySession,
336
+ getTargetIdBySession: getTargetIdBySession,
337
+ findSessionByTabId: findSessionByTabId,
338
+ findSessionByTargetId: findSessionByTargetId,
339
+ getTabIdByTargetId: getTabIdByTargetId,
340
+ hasOtherSessionForTab: hasOtherSessionForTab,
341
+ addAttachedTab: addAttachedTab,
342
+ removeAttachedTab: removeAttachedTab,
343
+ isTabAttached: isTabAttached,
344
+ getAttachedTabIds: getAttachedTabIds,
345
+ addEmittedTarget: addEmittedTarget,
346
+ hasEmittedTarget: hasEmittedTarget,
347
+ setAutoAttachConfig: setAutoAttachConfig,
348
+ getAutoAttachConfig: getAutoAttachConfig,
349
+ setDiscoverTargets: setDiscoverTargets,
350
+ addPendingDebuggerTab: addPendingDebuggerTab,
351
+ removePendingDebuggerTab: removePendingDebuggerTab,
352
+ isPendingDebuggerTab: isPendingDebuggerTab,
353
+ addBrowserContext: addBrowserContext,
354
+ removeBrowserContext: removeBrowserContext,
355
+ getBrowserContexts: getBrowserContexts,
356
+ loadPersisted: loadPersisted,
357
+ persist: persist,
358
+ getCurrentTabId: getCurrentTabId,
359
+ setCurrentTabId: setCurrentTabId,
360
+ getWs: getWs,
361
+ setWs: setWs,
362
+ getReconnectTimer: getReconnectTimer,
363
+ setReconnectTimer: setReconnectTimer,
364
+ clearReconnectTimer: clearReconnectTimer,
365
+ hasConnectedClient: hasConnectedClient,
366
+ setHasConnectedClient: setHasConnectedClient,
367
+ addCDPClient: addCDPClient,
368
+ removeCDPClient: removeCDPClient,
369
+ getCDPClients: getCDPClients,
370
+ setCDPClients: setCDPClients,
371
+ addPendingCreatedTabUrl: addPendingCreatedTabUrl,
372
+ removePendingCreatedTabUrl: removePendingCreatedTabUrl,
373
+ hasPendingCreatedTabUrl: hasPendingCreatedTabUrl,
374
+ getScreencastSession: getScreencastSession,
375
+ setScreencastSession: setScreencastSession,
376
+ deleteScreencastSession: deleteScreencastSession,
377
+ addAutomatedTab: addAutomatedTab,
378
+ removeAutomatedTab: removeAutomatedTab,
379
+ getAutomatedTabs: getAutomatedTabs,
380
+ clearSessionState: clearSessionState,
381
+ clearAllState: clearAllState,
382
+ cleanupAllTabs: cleanupAllTabs
383
+ };
384
+ })();
@@ -0,0 +1,326 @@
1
+ var WebSocketManager = (function() {
2
+ var _sendQueue = [];
3
+ var _isSending = false;
4
+ var _maxQueueSize = 100;
5
+ var _bufferThreshold = 512 * 1024;
6
+
7
+ function connect() {
8
+ var ws = State.getWs();
9
+ if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
10
+ return;
11
+ }
12
+
13
+ Config.getWsUrl(function(wsUrl) {
14
+ Logger.info('[WS] Connecting to', wsUrl);
15
+ setBadgeStatus('ON');
16
+
17
+ try {
18
+ ws = new WebSocket(wsUrl);
19
+ State.setWs(ws);
20
+
21
+ ws.onopen = function() {
22
+ Logger.info('[WS] Connected');
23
+ setBadgeStatus('ON');
24
+ State.clearReconnectTimer();
25
+ processQueue();
26
+ broadcastStateUpdate();
27
+ };
28
+
29
+ ws.onclose = function(event) {
30
+ Logger.info('[WS] Closed:', event.code, event.reason);
31
+ setBadgeStatus('OFF');
32
+ scheduleReconnect();
33
+ broadcastStateUpdate();
34
+ };
35
+
36
+ ws.onerror = function(error) {
37
+ Logger.error('[WS] Error:', error);
38
+ setBadgeStatus('ERR');
39
+ broadcastStateUpdate();
40
+ };
41
+
42
+ ws.onmessage = function(event) {
43
+ handleRawMessage(event.data);
44
+ };
45
+ } catch (error) {
46
+ Logger.error('[WS] Failed to create:', error);
47
+ setBadgeStatus('ERR');
48
+ scheduleReconnect();
49
+ }
50
+ });
51
+ }
52
+
53
+ function send(message) {
54
+ var ws = State.getWs();
55
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
56
+ Logger.warn('[WS] Cannot send, WebSocket not open');
57
+ return false;
58
+ }
59
+
60
+ var jsonStr;
61
+ try {
62
+ jsonStr = JSON.stringify(message);
63
+ } catch (e) {
64
+ Logger.error('[WS] Failed to stringify message:', e);
65
+ return false;
66
+ }
67
+
68
+ var msgSize = jsonStr.length;
69
+
70
+ if (msgSize > 1024 * 1024) {
71
+ Logger.warn('[WS] Large message:', msgSize, 'bytes, method:', message.method || message.type);
72
+ }
73
+
74
+ if (ws.bufferedAmount > _bufferThreshold) {
75
+ Logger.warn('[WS] Buffer full, queuing message. Buffered:', ws.bufferedAmount);
76
+ if (_sendQueue.length < _maxQueueSize) {
77
+ _sendQueue.push(jsonStr);
78
+ } else {
79
+ Logger.error('[WS] Queue full, dropping message');
80
+ }
81
+ return false;
82
+ }
83
+
84
+ try {
85
+ ws.send(jsonStr);
86
+ Logger.info('[WS] SEND: ' + jsonStr.substring(0, 200));
87
+ return true;
88
+ } catch (e) {
89
+ Logger.error('[WS] Send error:', e);
90
+ return false;
91
+ }
92
+ }
93
+
94
+ function processQueue() {
95
+ var ws = State.getWs();
96
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
97
+ return;
98
+ }
99
+
100
+ while (_sendQueue.length > 0 && ws.bufferedAmount < _bufferThreshold) {
101
+ var data = _sendQueue.shift();
102
+ try {
103
+ ws.send(data);
104
+ } catch (e) {
105
+ Logger.error('[WS] Queue send error:', e);
106
+ break;
107
+ }
108
+ }
109
+
110
+ if (_sendQueue.length > 0) {
111
+ setTimeout(processQueue, 100);
112
+ }
113
+ }
114
+
115
+ function scheduleReconnect() {
116
+ State.clearReconnectTimer();
117
+ var timer = setTimeout(function() {
118
+ Logger.info('[WS] Attempting to reconnect...');
119
+ connect();
120
+ }, Config.RECONNECT_DELAY);
121
+ State.setReconnectTimer(timer);
122
+ }
123
+
124
+ function handleRawMessage(data) {
125
+ try {
126
+ if (data instanceof Blob) {
127
+ data.text().then(function(text) {
128
+ try {
129
+ handleMessage(JSON.parse(text));
130
+ } catch (e) {
131
+ Logger.error('[WS] Failed to parse Blob message:', e);
132
+ }
133
+ }).catch(function(e) {
134
+ Logger.error('[WS] Failed to read Blob:', e);
135
+ });
136
+ } else {
137
+ try {
138
+ handleMessage(JSON.parse(data));
139
+ } catch (e) {
140
+ Logger.error('[WS] Failed to parse message:', e);
141
+ }
142
+ }
143
+ } catch (e) {
144
+ Logger.error('[WS] handleRawMessage error:', e);
145
+ }
146
+ }
147
+
148
+ function handleMessage(message) {
149
+ var type = message.type;
150
+ var method = message.method;
151
+ var params = message.params;
152
+ var id = message.id;
153
+ var tabId = message.tabId;
154
+ var sessionId = message.sessionId;
155
+
156
+ switch (type) {
157
+ case 'connected':
158
+ if (message.fresh) {
159
+ Logger.info('[WS] Received fresh connection from server');
160
+ handleServerRestart();
161
+ }
162
+ break;
163
+
164
+ case 'ping':
165
+ send({ type: 'pong' });
166
+ break;
167
+
168
+ case 'attach':
169
+ var attachTabId = tabId || State.getCurrentTabId();
170
+ DebuggerManager.attach(attachTabId).then(function(success) {
171
+ send({ type: 'attach_result', tabId: attachTabId, success: success });
172
+ });
173
+ break;
174
+
175
+ case 'detach':
176
+ var detachTabId = tabId || State.getCurrentTabId();
177
+ DebuggerManager.detach(detachTabId).then(function() {
178
+ send({ type: 'detach_result', tabId: detachTabId, success: true });
179
+ });
180
+ break;
181
+
182
+ case 'browser-close':
183
+ handleBrowserClose(message.sessions);
184
+ break;
185
+
186
+ case 'client-connected':
187
+ Logger.info('[WS] Client connected, resuming event forwarding');
188
+ State.setHasConnectedClient(true);
189
+ State.addCDPClient(message.clientId, message.clientId);
190
+ broadcastStateUpdate();
191
+ break;
192
+
193
+ case 'client-disconnected':
194
+ Logger.info('[WS] Client disconnected:', message.clientId);
195
+ State.removeCDPClient(message.clientId);
196
+ if (State.getCDPClients().length === 0) {
197
+ State.setHasConnectedClient(false);
198
+ }
199
+ broadcastStateUpdate();
200
+ break;
201
+
202
+ case 'client-list':
203
+ Logger.info('[WS] Received client list:', message.clients);
204
+ State.setCDPClients(message.clients || []);
205
+ State.setHasConnectedClient((message.clients || []).length > 0);
206
+ broadcastStateUpdate();
207
+ break;
208
+
209
+ case 'plugin-disconnected':
210
+ Logger.info('[WS] Plugin disconnected from server');
211
+ break;
212
+
213
+ case 'server-restart':
214
+ Logger.info('[WS] Server restart detected, cleaning up...');
215
+ handleServerRestart();
216
+ break;
217
+
218
+ case 'cdp':
219
+ if (method) {
220
+ routeCDPCommand({
221
+ id: id,
222
+ method: method,
223
+ params: params,
224
+ tabId: tabId,
225
+ sessionId: sessionId
226
+ });
227
+ }
228
+ break;
229
+
230
+ default:
231
+ if (method) {
232
+ routeCDPCommand({
233
+ id: id,
234
+ method: method,
235
+ params: params,
236
+ tabId: tabId,
237
+ sessionId: sessionId
238
+ });
239
+ }
240
+ }
241
+ }
242
+
243
+ function handleServerRestart() {
244
+ Logger.info('[WS] Server restarted, cleaning up all state...');
245
+
246
+ var attachedTabIds = State.getAttachedTabIds();
247
+ var promises = attachedTabIds.map(function(tabId) {
248
+ return chrome.debugger.detach({ tabId: tabId }).catch(function(e) {
249
+ Logger.info('[WS] Detach failed for tab', tabId, ':', e.message);
250
+ });
251
+ });
252
+
253
+ Promise.all(promises).then(function() {
254
+ State.clearAllState();
255
+ State.persist(null, false);
256
+ Logger.info('[WS] State cleaned up after server restart');
257
+ });
258
+ }
259
+
260
+ function handleBrowserClose(sessions) {
261
+ Logger.info('[WS] Browser.close received, cleaning up...');
262
+
263
+ var attachedTabIds = State.getAttachedTabIds();
264
+ var promises = attachedTabIds.map(function(tabId) {
265
+ return chrome.debugger.detach({ tabId: tabId }).catch(function(e) {
266
+ Logger.info('[WS] Detach failed for tab', tabId, ':', e.message);
267
+ });
268
+ });
269
+
270
+ Promise.all(promises).then(function() {
271
+ State.clearAllState();
272
+ State.persist(null, false);
273
+ Logger.info('[WS] Browser.close cleanup complete');
274
+ });
275
+ }
276
+
277
+ function setBadgeStatus(status) {
278
+ var colors = Config.BADGE_COLORS;
279
+ chrome.action.setBadgeText({ text: status });
280
+ chrome.action.setBadgeBackgroundColor({ color: colors[status] || colors.OFF });
281
+ }
282
+
283
+ function broadcastStateUpdate() {
284
+ var ws = State.getWs();
285
+ var isConnected = ws && ws.readyState === WebSocket.OPEN;
286
+ var cdpClients = State.getCDPClients() || [];
287
+ var attachedTabIds = State.getAttachedTabIds();
288
+
289
+ var attachedPages = [];
290
+ attachedTabIds.forEach(function(tabId) {
291
+ chrome.tabs.get(tabId, function(tab) {
292
+ if (tab && !chrome.runtime.lastError) {
293
+ attachedPages.push({
294
+ tabId: tabId,
295
+ title: tab.title || 'Untitled',
296
+ url: tab.url || ''
297
+ });
298
+ }
299
+ });
300
+ });
301
+
302
+ chrome.runtime.sendMessage({
303
+ type: 'stateUpdate',
304
+ connected: isConnected,
305
+ cdpClients: cdpClients,
306
+ attachedPages: attachedPages
307
+ }).catch(function() {});
308
+ }
309
+
310
+ function getQueueStats() {
311
+ return {
312
+ queueLength: _sendQueue.length,
313
+ maxQueueSize: _maxQueueSize,
314
+ bufferThreshold: _bufferThreshold
315
+ };
316
+ }
317
+
318
+ return {
319
+ connect: connect,
320
+ send: send,
321
+ scheduleReconnect: scheduleReconnect,
322
+ setBadgeStatus: setBadgeStatus,
323
+ getQueueStats: getQueueStats,
324
+ processQueue: processQueue
325
+ };
326
+ })();