cdp-tunnel 2.7.9 → 2.8.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.
@@ -1,69 +1,75 @@
1
- var WebSocketManager = (function() {
2
- var _sendQueue = [];
3
- var _isSending = false;
4
- var _maxQueueSize = 100;
5
- var _bufferThreshold = 512 * 1024;
1
+ var WebSocketConnection = (function() {
2
+ function WebSocketConnection(connectionId, state, config) {
3
+ this.connectionId = connectionId;
4
+ this.state = state;
5
+ this.config = config;
6
+ this._sendQueue = [];
7
+ this._isSending = false;
8
+ this._maxQueueSize = 100;
9
+ this._bufferThreshold = 512 * 1024;
10
+ this._groupCreationPending = new Set();
11
+ }
6
12
 
7
- function connect() {
8
- var ws = State.getWs();
13
+ WebSocketConnection.prototype.connect = function() {
14
+ var self = this;
15
+ var ws = self.state.getWs();
9
16
  if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
10
17
  return;
11
18
  }
12
19
 
13
- Config.getWsUrl(function(wsUrl) {
14
- Config.getPluginId(function(pluginId) {
15
- if (pluginId) {
16
- var sep = wsUrl.indexOf('?') >= 0 ? '&' : '?';
17
- wsUrl += sep + 'pluginId=' + encodeURIComponent(pluginId);
18
- }
19
- Logger.info('[WS] Connecting to', wsUrl);
20
- setBadgeStatus('ON');
20
+ var wsUrl = self.config.url;
21
+ Config.getPluginId(function(pluginId) {
22
+ if (pluginId) {
23
+ var sep = wsUrl.indexOf('?') >= 0 ? '&' : '?';
24
+ wsUrl += sep + 'pluginId=' + encodeURIComponent(pluginId);
25
+ }
26
+ Logger.info('[WS:' + self.connectionId + '] Connecting to', wsUrl);
27
+ setBadgeStatus('ON');
21
28
 
22
- try {
23
- ws = new WebSocket(wsUrl);
24
- State.setWs(ws);
25
-
26
- ws.onopen = function() {
27
- Logger.info('[WS] Connected');
28
- setBadgeStatus('ON');
29
- State.clearReconnectTimer();
30
- processQueue();
31
- broadcastStateUpdate();
32
- var extVersion = (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.getManifest)
33
- ? chrome.runtime.getManifest().version : 'unknown';
34
- send({ type: 'plugin-hello', version: extVersion });
35
- };
36
-
37
- ws.onclose = function(event) {
38
- Logger.info('[WS] Closed:', event.code, event.reason);
39
- setBadgeStatus('OFF');
40
- scheduleReconnect();
41
- broadcastStateUpdate();
42
- };
43
-
44
- ws.onerror = function(error) {
45
- Logger.error('[WS] Error:', error);
46
- setBadgeStatus('ERR');
47
- broadcastStateUpdate();
48
- };
49
-
50
- ws.onmessage = function(event) {
51
- handleRawMessage(event.data);
52
- };
53
- } catch (error) {
54
- Logger.error('[WS] Failed to create:', error);
29
+ try {
30
+ ws = new WebSocket(wsUrl);
31
+ self.state.setWs(ws);
32
+
33
+ ws.onopen = function() {
34
+ Logger.info('[WS:' + self.connectionId + '] Connected');
35
+ setBadgeStatus('ON');
36
+ self.state.clearReconnectTimer();
37
+ self._processQueue();
38
+ self._broadcastStateUpdate();
39
+ var extVersion = (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.getManifest)
40
+ ? chrome.runtime.getManifest().version : 'unknown';
41
+ self.send({ type: 'plugin-hello', version: extVersion });
42
+ };
43
+
44
+ ws.onclose = function(event) {
45
+ Logger.info('[WS:' + self.connectionId + '] Closed:', event.code, event.reason);
46
+ setBadgeStatus('OFF');
47
+ self._scheduleReconnect();
48
+ self._broadcastStateUpdate();
49
+ };
50
+
51
+ ws.onerror = function(error) {
52
+ Logger.error('[WS:' + self.connectionId + '] Error:', error);
55
53
  setBadgeStatus('ERR');
56
- scheduleReconnect();
57
- }
58
- });
54
+ self._broadcastStateUpdate();
55
+ };
56
+
57
+ ws.onmessage = function(event) {
58
+ self._handleRawMessage(event.data);
59
+ };
60
+ } catch (error) {
61
+ Logger.error('[WS:' + self.connectionId + '] Failed to create:', error);
62
+ setBadgeStatus('ERR');
63
+ self._scheduleReconnect();
64
+ }
59
65
  });
60
- }
66
+ };
61
67
 
62
- function send(message) {
63
- var ws = State.getWs();
68
+ WebSocketConnection.prototype.send = function(message) {
69
+ var ws = this.state.getWs();
64
70
  var wsState = ws ? ws.readyState : 'no ws';
65
71
  if (!ws || ws.readyState !== WebSocket.OPEN) {
66
- Logger.warn('[WS] Cannot send, WebSocket not open. State:', wsState);
72
+ Logger.warn('[WS:' + this.connectionId + '] Cannot send, WebSocket not open. State:', wsState);
67
73
  return false;
68
74
  }
69
75
 
@@ -71,91 +77,91 @@ var WebSocketManager = (function() {
71
77
  try {
72
78
  jsonStr = JSON.stringify(message);
73
79
  } catch (e) {
74
- Logger.error('[WS] Failed to stringify message:', e);
80
+ Logger.error('[WS:' + this.connectionId + '] Failed to stringify message:', e);
75
81
  return false;
76
82
  }
77
83
 
78
84
  var msgSize = jsonStr.length;
79
-
80
85
  if (msgSize > 1024 * 1024) {
81
- Logger.warn('[WS] Large message:', msgSize, 'bytes, method:', message.method || message.type);
86
+ Logger.warn('[WS:' + this.connectionId + '] Large message:', msgSize, 'bytes, method:', message.method || message.type);
82
87
  }
83
88
 
84
- if (ws.bufferedAmount > _bufferThreshold) {
85
- Logger.warn('[WS] Buffer full, queuing message. Buffered:', ws.bufferedAmount);
86
- if (_sendQueue.length < _maxQueueSize) {
87
- _sendQueue.push(jsonStr);
89
+ if (ws.bufferedAmount > this._bufferThreshold) {
90
+ Logger.warn('[WS:' + this.connectionId + '] Buffer full, queuing message. Buffered:', ws.bufferedAmount);
91
+ if (this._sendQueue.length < this._maxQueueSize) {
92
+ this._sendQueue.push(jsonStr);
88
93
  } else {
89
- Logger.error('[WS] Queue full, dropping message');
94
+ Logger.error('[WS:' + this.connectionId + '] Queue full, dropping message');
90
95
  }
91
96
  return false;
92
97
  }
93
98
 
94
99
  try {
95
100
  ws.send(jsonStr);
96
- Logger.info('[WS] SEND: ' + jsonStr.substring(0, 200));
101
+ Logger.info('[WS:' + this.connectionId + '] SEND: ' + jsonStr.substring(0, 200));
97
102
  return true;
98
103
  } catch (e) {
99
- Logger.error('[WS] Send error:', e);
104
+ Logger.error('[WS:' + this.connectionId + '] Send error:', e);
100
105
  return false;
101
106
  }
102
- }
107
+ };
103
108
 
104
- function processQueue() {
105
- var ws = State.getWs();
106
- if (!ws || ws.readyState !== WebSocket.OPEN) {
107
- return;
108
- }
109
+ WebSocketConnection.prototype._processQueue = function() {
110
+ var ws = this.state.getWs();
111
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
109
112
 
110
- while (_sendQueue.length > 0 && ws.bufferedAmount < _bufferThreshold) {
111
- var data = _sendQueue.shift();
113
+ while (this._sendQueue.length > 0 && ws.bufferedAmount < this._bufferThreshold) {
114
+ var data = this._sendQueue.shift();
112
115
  try {
113
116
  ws.send(data);
114
117
  } catch (e) {
115
- Logger.error('[WS] Queue send error:', e);
118
+ Logger.error('[WS:' + this.connectionId + '] Queue send error:', e);
116
119
  break;
117
120
  }
118
121
  }
119
122
 
120
- if (_sendQueue.length > 0) {
121
- setTimeout(processQueue, 100);
123
+ if (this._sendQueue.length > 0) {
124
+ setTimeout(this._processQueue.bind(this), 100);
122
125
  }
123
- }
126
+ };
124
127
 
125
- function scheduleReconnect() {
126
- State.clearReconnectTimer();
128
+ WebSocketConnection.prototype._scheduleReconnect = function() {
129
+ this.state.clearReconnectTimer();
130
+ var self = this;
127
131
  var timer = setTimeout(function() {
128
- Logger.info('[WS] Attempting to reconnect...');
129
- connect();
132
+ Logger.info('[WS:' + self.connectionId + '] Attempting to reconnect...');
133
+ self.connect();
130
134
  }, Config.RECONNECT_DELAY);
131
- State.setReconnectTimer(timer);
132
- }
135
+ self.state.setReconnectTimer(timer);
136
+ };
133
137
 
134
- function handleRawMessage(data) {
138
+ WebSocketConnection.prototype._handleRawMessage = function(data) {
139
+ var self = this;
135
140
  try {
136
141
  if (data instanceof Blob) {
137
142
  data.text().then(function(text) {
138
143
  try {
139
- handleMessage(JSON.parse(text));
144
+ self._handleMessage(JSON.parse(text));
140
145
  } catch (e) {
141
- Logger.error('[WS] Failed to parse Blob message:', e);
146
+ Logger.error('[WS:' + self.connectionId + '] Failed to parse Blob message:', e);
142
147
  }
143
148
  }).catch(function(e) {
144
- Logger.error('[WS] Failed to read Blob:', e);
149
+ Logger.error('[WS:' + self.connectionId + '] Failed to read Blob:', e);
145
150
  });
146
151
  } else {
147
152
  try {
148
- handleMessage(JSON.parse(data));
153
+ self._handleMessage(JSON.parse(data));
149
154
  } catch (e) {
150
- Logger.error('[WS] Failed to parse message:', e);
155
+ Logger.error('[WS:' + self.connectionId + '] Failed to parse message:', e);
151
156
  }
152
157
  }
153
158
  } catch (e) {
154
- Logger.error('[WS] handleRawMessage error:', e);
159
+ Logger.error('[WS:' + self.connectionId + '] handleRawMessage error:', e);
155
160
  }
156
- }
161
+ };
157
162
 
158
- function handleMessage(message) {
163
+ WebSocketConnection.prototype._handleMessage = function(message) {
164
+ var self = this;
159
165
  var type = message.type;
160
166
  var method = message.method;
161
167
  var params = message.params;
@@ -166,80 +172,81 @@ var WebSocketManager = (function() {
166
172
  switch (type) {
167
173
  case 'connected':
168
174
  if (message.fresh) {
169
- Logger.info('[WS] Received fresh connection from server');
170
- handleServerRestart();
175
+ Logger.info('[WS:' + self.connectionId + '] Received fresh connection from server');
176
+ self._handleServerRestart();
171
177
  }
172
178
  break;
173
179
 
174
180
  case 'ping':
175
- send({ type: 'pong' });
181
+ self.send({ type: 'pong' });
176
182
  break;
177
183
 
178
184
  case 'attach':
179
- var attachTabId = tabId || State.getCurrentTabId();
180
- DebuggerManager.attach(attachTabId).then(function(success) {
181
- send({ type: 'attach_result', tabId: attachTabId, success: success });
185
+ var attachTabId = tabId || self.state.getCurrentTabId();
186
+ DebuggerManager.attach(attachTabId, self.state).then(function(success) {
187
+ self.send({ type: 'attach_result', tabId: attachTabId, success: success });
182
188
  });
183
189
  break;
184
190
 
185
191
  case 'detach':
186
- var detachTabId = tabId || State.getCurrentTabId();
187
- DebuggerManager.detach(detachTabId).then(function() {
188
- send({ type: 'detach_result', tabId: detachTabId, success: true });
192
+ var detachTabId = tabId || self.state.getCurrentTabId();
193
+ DebuggerManager.detach(detachTabId, self.state).then(function() {
194
+ self.send({ type: 'detach_result', tabId: detachTabId, success: true });
189
195
  });
190
196
  break;
191
197
 
192
198
  case 'browser-close':
193
- handleBrowserClose(message.sessions, message.clientId);
199
+ self._handleBrowserClose(message.sessions, message.clientId);
194
200
  break;
195
201
 
196
202
  case 'client-connected':
197
- Logger.info('[WS] Client connected, resuming event forwarding');
198
- State.setHasConnectedClient(true);
199
- State.addCDPClient(message.clientId, message.clientId);
200
- createGroupForClient(message.clientId);
201
- broadcastStateUpdate();
203
+ Logger.info('[WS:' + self.connectionId + '] Client connected, resuming event forwarding');
204
+ self.state.setHasConnectedClient(true);
205
+ self.state.addCDPClient(message.clientId, message.clientId);
206
+ self._createGroupForClient(message.clientId);
207
+ self._broadcastStateUpdate();
202
208
  break;
203
209
 
204
210
  case 'client-disconnected':
205
- Logger.info('[WS] Client disconnected:', message.clientId);
211
+ Logger.info('[WS:' + self.connectionId + '] Client disconnected:', message.clientId);
206
212
  var discClientId = message.clientId;
207
- closeTabGroupByClientId(discClientId).then(function() {
213
+ self._groupCreationPending.delete(discClientId);
214
+ self._closeTabGroupByClientId(discClientId).then(function() {
208
215
  return new Promise(function(resolve) {
209
- closeTabsByClientId(discClientId, resolve);
216
+ self._closeTabsByClientId(discClientId, resolve);
210
217
  });
211
218
  }).then(function() {
212
- var preExistingTabs = State.getPreExistingTabs();
213
- var clientPreExisting = preExistingTabs.filter(function(tabId) {
214
- return State.getClientIdByTabId(tabId) === discClientId;
219
+ var preExistingTabs = self.state.getPreExistingTabs();
220
+ var clientPreExisting = preExistingTabs.filter(function(tid) {
221
+ return self.state.getClientIdByTabId(tid) === discClientId;
215
222
  });
216
- clientPreExisting.forEach(function(tabId) {
217
- chrome.debugger.detach({ tabId: tabId }).catch(function() {});
218
- State.removeAttachedTab(tabId);
223
+ clientPreExisting.forEach(function(tid) {
224
+ chrome.debugger.detach({ tabId: tid }).catch(function() {});
225
+ self.state.removeAttachedTab(tid);
219
226
  });
220
- State.clearPreExistingTabsForClient(discClientId);
221
- State.removeCDPClient(discClientId);
222
- if (State.getCDPClients().length === 0) {
223
- State.setHasConnectedClient(false);
227
+ self.state.clearPreExistingTabsForClient(discClientId);
228
+ self.state.removeCDPClient(discClientId);
229
+ if (self.state.getCDPClients().length === 0) {
230
+ self.state.setHasConnectedClient(false);
224
231
  }
225
- broadcastStateUpdate();
232
+ self._broadcastStateUpdate();
226
233
  });
227
234
  break;
228
-
235
+
229
236
  case 'client-list':
230
- Logger.info('[WS] Received client list:', message.clients);
231
- State.setCDPClients(message.clients || []);
232
- State.setHasConnectedClient((message.clients || []).length > 0);
233
- broadcastStateUpdate();
237
+ Logger.info('[WS:' + self.connectionId + '] Received client list:', message.clients);
238
+ self.state.setCDPClients(message.clients || []);
239
+ self.state.setHasConnectedClient((message.clients || []).length > 0);
240
+ self._broadcastStateUpdate();
234
241
  break;
235
242
 
236
243
  case 'plugin-disconnected':
237
- Logger.info('[WS] Plugin disconnected from server');
244
+ Logger.info('[WS:' + self.connectionId + '] Plugin disconnected from server');
238
245
  break;
239
246
 
240
247
  case 'server-restart':
241
- Logger.info('[WS] Server restart detected, cleaning up...');
242
- handleServerRestart();
248
+ Logger.info('[WS:' + self.connectionId + '] Server restart detected, cleaning up...');
249
+ self._handleServerRestart();
243
250
  break;
244
251
 
245
252
  case 'cdp':
@@ -250,8 +257,9 @@ var WebSocketManager = (function() {
250
257
  params: params,
251
258
  tabId: tabId,
252
259
  sessionId: sessionId,
253
- clientId: message.__clientId
254
- });
260
+ clientId: message.__clientId,
261
+ connectionId: self.connectionId
262
+ }, self.state, self);
255
263
  }
256
264
  break;
257
265
 
@@ -263,111 +271,115 @@ var WebSocketManager = (function() {
263
271
  params: params,
264
272
  tabId: tabId,
265
273
  sessionId: sessionId,
266
- clientId: message.__clientId
267
- });
274
+ clientId: message.__clientId,
275
+ connectionId: self.connectionId
276
+ }, self.state, self);
268
277
  }
269
278
  }
270
- }
279
+ };
271
280
 
272
- function closeTabGroupByClientId(clientId) {
281
+ WebSocketConnection.prototype._closeTabGroupByClientId = function(clientId) {
282
+ var self = this;
273
283
  if (!clientId) return Promise.resolve();
274
-
275
- Logger.info('[WS] Closing tab group for client:', clientId);
276
-
284
+
285
+ Logger.info('[WS:' + self.connectionId + '] Closing tab group for client:', clientId);
286
+
277
287
  return new Promise(function(resolve) {
278
288
  var timeoutId = setTimeout(function() {
279
- Logger.warn('[WS] closeTabGroupByClientId timeout for client:', clientId, '— forcing cleanup');
280
- cleanupStaleState(clientId);
289
+ Logger.warn('[WS:' + self.connectionId + '] closeTabGroupByClientId timeout for client:', clientId, '— forcing cleanup');
290
+ self._cleanupStaleState(clientId);
281
291
  resolve();
282
292
  }, 5000);
283
-
284
- var groupId = State.getGroupIdForClient(clientId);
285
-
293
+
294
+ var groupId = self.state.getGroupIdForClient(clientId);
295
+
286
296
  if (groupId) {
287
- closeGroupById(groupId, clientId, function() {
297
+ self._closeGroupById(groupId, clientId, function() {
288
298
  clearTimeout(timeoutId);
289
- cleanupStaleState(clientId);
299
+ self._cleanupStaleState(clientId);
290
300
  resolve();
291
301
  });
292
302
  } else {
293
- var baseName = CDPUtils.getGroupBaseName(clientId);
303
+ var baseName = CDPUtils.getGroupBaseName(clientId, self.config ? self.config.tag : null);
294
304
  chrome.tabGroups.query({}, function(allGroups) {
295
305
  var match = CDPUtils.findGroupByName(allGroups, baseName);
296
306
  if (match) {
297
- closeGroupById(match.id, clientId, function() {
307
+ self._closeGroupById(match.id, clientId, function() {
298
308
  clearTimeout(timeoutId);
299
- cleanupStaleState(clientId);
309
+ self._cleanupStaleState(clientId);
300
310
  resolve();
301
311
  });
302
312
  } else {
303
- Logger.info('[WS] No tab group found, closing tabs by clientId:', clientId);
304
- closeTabsByClientId(clientId, function() {
313
+ Logger.info('[WS:' + self.connectionId + '] No tab group found, closing tabs by clientId:', clientId);
314
+ self._closeTabsByClientId(clientId, function() {
305
315
  clearTimeout(timeoutId);
306
- cleanupStaleState(clientId);
316
+ self._cleanupStaleState(clientId);
307
317
  resolve();
308
318
  });
309
319
  }
310
320
  });
311
321
  }
312
322
  });
313
- }
323
+ };
314
324
 
315
- function cleanupStaleState(clientId) {
325
+ WebSocketConnection.prototype._cleanupStaleState = function(clientId) {
316
326
  if (!clientId) return;
317
- var attachedTabs = State.getAttachedTabIds();
327
+ var self = this;
328
+ var attachedTabs = self.state.getAttachedTabIds();
318
329
  attachedTabs.forEach(function(tabId) {
319
- if (State.getClientIdByTabId(tabId) === clientId) {
320
- State.removeTabIdToClientId(tabId);
330
+ if (self.state.getClientIdByTabId(tabId) === clientId) {
331
+ self.state.removeTabIdToClientId(tabId);
321
332
  }
322
333
  });
323
- }
334
+ };
324
335
 
325
- function closeGroupById(groupId, clientId, resolve) {
326
- Logger.info('[WS] closeGroupById: groupId=' + groupId + ' clientId=' + clientId);
336
+ WebSocketConnection.prototype._closeGroupById = function(groupId, clientId, resolve) {
337
+ var self = this;
338
+ Logger.info('[WS:' + self.connectionId + '] closeGroupById: groupId=' + groupId + ' clientId=' + clientId);
327
339
  chrome.tabs.query({ groupId: groupId }, function(tabs) {
328
- if (!tabs || tabs.length === 0) {
329
- Logger.info('[WS] No tabs in group:', groupId);
330
- State.removeGroupForClient(clientId);
331
- removeEmptyGroup(groupId);
332
- resolve();
333
- return;
334
- }
335
-
336
- var ownTabs = tabs.filter(function(tab) {
337
- return State.getClientIdByTabId(tab.id) === clientId;
338
- });
339
- var otherTabs = tabs.filter(function(tab) {
340
- return State.getClientIdByTabId(tab.id) !== clientId;
341
- });
342
- var tabIds = ownTabs.map(function(tab) { return tab.id; });
343
- Logger.info('[WS] Closing ' + tabIds.length + ' tabs in group (skipping ' + otherTabs.length + ' from other clients):', groupId);
344
-
345
- if (tabIds.length === 0) {
346
- Logger.info('[WS] No own tabs to close in group:', groupId);
347
- State.removeGroupForClient(clientId);
348
- resolve();
349
- return;
340
+ if (!tabs || tabs.length === 0) {
341
+ Logger.info('[WS:' + self.connectionId + '] No tabs in group:', groupId);
342
+ self.state.removeGroupForClient(clientId);
343
+ self._removeEmptyGroup(groupId);
344
+ resolve();
345
+ return;
346
+ }
347
+
348
+ var ownTabs = tabs.filter(function(tab) {
349
+ return self.state.getClientIdByTabId(tab.id) === clientId;
350
+ });
351
+ var otherTabs = tabs.filter(function(tab) {
352
+ return self.state.getClientIdByTabId(tab.id) !== clientId;
353
+ });
354
+ var tabIds = ownTabs.map(function(tab) { return tab.id; });
355
+ Logger.info('[WS:' + self.connectionId + '] Closing ' + tabIds.length + ' tabs in group (skipping ' + otherTabs.length + ' from other clients):', groupId);
356
+
357
+ if (tabIds.length === 0) {
358
+ Logger.info('[WS:' + self.connectionId + '] No own tabs to close in group:', groupId);
359
+ self.state.removeGroupForClient(clientId);
360
+ resolve();
361
+ return;
362
+ }
363
+
364
+ chrome.tabs.remove(tabIds, function() {
365
+ if (chrome.runtime.lastError) {
366
+ Logger.error('[WS:' + self.connectionId + '] Failed to close tabs:', chrome.runtime.lastError.message);
367
+ } else {
368
+ Logger.info('[WS:' + self.connectionId + '] Successfully closed ' + tabIds.length + ' tabs');
350
369
  }
351
370
 
352
- chrome.tabs.remove(tabIds, function() {
353
- if (chrome.runtime.lastError) {
354
- Logger.error('[WS] Failed to close tabs:', chrome.runtime.lastError.message);
355
- } else {
356
- Logger.info('[WS] Successfully closed ' + tabIds.length + ' tabs');
357
- }
358
-
359
- tabIds.forEach(function(tabId) {
360
- chrome.debugger.detach({ tabId: tabId }).catch(function() {});
361
- });
362
-
363
- State.removeGroupForClient(clientId);
364
- removeEmptyGroup(groupId);
365
- resolve();
371
+ tabIds.forEach(function(tabId) {
372
+ chrome.debugger.detach({ tabId: tabId }).catch(function() {});
366
373
  });
374
+
375
+ self.state.removeGroupForClient(clientId);
376
+ self._removeEmptyGroup(groupId);
377
+ resolve();
378
+ });
367
379
  });
368
- }
380
+ };
369
381
 
370
- function removeEmptyGroup(groupId) {
382
+ WebSocketConnection.prototype._removeEmptyGroup = function(groupId) {
371
383
  if (!groupId || !chrome.tabGroups) return;
372
384
  setTimeout(function() {
373
385
  chrome.tabGroups.query({ groupId: groupId }, function(groups) {
@@ -386,166 +398,173 @@ var WebSocketManager = (function() {
386
398
  }
387
399
  });
388
400
  }, 500);
389
- }
401
+ };
390
402
 
391
- function closeTabsByClientId(clientId, resolve) {
392
- var attachedTabs = State.getAttachedTabIds();
393
- var cdpCreatedTabs = State.getCDPCreatedTabIds();
403
+ WebSocketConnection.prototype._closeTabsByClientId = function(clientId, resolve) {
404
+ var self = this;
405
+ var attachedTabs = self.state.getAttachedTabIds();
406
+ var cdpCreatedTabs = self.state.getCDPCreatedTabIds();
394
407
  var tabsToClose = [];
395
408
  var tabsToCloseSet = new Set();
396
-
397
- Logger.info('[WS] closeTabsByClientId: clientId=' + clientId + ' attachedTabs=' + JSON.stringify(attachedTabs) + ' cdpCreatedTabs=' + JSON.stringify(cdpCreatedTabs));
398
-
409
+
410
+ Logger.info('[WS:' + self.connectionId + '] closeTabsByClientId: clientId=' + clientId + ' attachedTabs=' + JSON.stringify(attachedTabs) + ' cdpCreatedTabs=' + JSON.stringify(cdpCreatedTabs));
411
+
399
412
  attachedTabs.forEach(function(tabId) {
400
- var tabClientId = State.getClientIdByTabId(tabId);
401
- var isPre = State.isPreExistingTab(tabId);
402
- var isCDP = State.isCDPCreatedTab(tabId);
403
- Logger.info('[WS] [attached] tabId=' + tabId + ' clientId=' + tabClientId + ' isPre=' + isPre + ' isCDP=' + isCDP);
413
+ var tabClientId = self.state.getClientIdByTabId(tabId);
414
+ var isPre = self.state.isPreExistingTab(tabId);
415
+ var isCDP = self.state.isCDPCreatedTab(tabId);
416
+ Logger.info('[WS:' + self.connectionId + '] [attached] tabId=' + tabId + ' clientId=' + tabClientId + ' isPre=' + isPre + ' isCDP=' + isCDP);
404
417
  if (tabClientId === clientId && !isPre) {
405
418
  tabsToCloseSet.add(tabId);
406
419
  }
407
420
  });
408
-
421
+
409
422
  cdpCreatedTabs.forEach(function(tabId) {
410
423
  if (tabsToCloseSet.has(tabId)) return;
411
-
412
- var tabClientId = State.getClientIdByTabId(tabId);
413
- var isPre = State.isPreExistingTab(tabId);
414
- Logger.info('[WS] [cdpCreated] tabId=' + tabId + ' clientId=' + tabClientId + ' isPre=' + isPre + ' isAttached=' + attachedTabs.includes(tabId));
424
+
425
+ var tabClientId = self.state.getClientIdByTabId(tabId);
426
+ var isPre = self.state.isPreExistingTab(tabId);
427
+ Logger.info('[WS:' + self.connectionId + '] [cdpCreated] tabId=' + tabId + ' clientId=' + tabClientId + ' isPre=' + isPre + ' isAttached=' + attachedTabs.includes(tabId));
415
428
  if (tabClientId === clientId && !isPre && !attachedTabs.includes(tabId)) {
416
429
  tabsToCloseSet.add(tabId);
417
- Logger.info('[WS] -> Added to close list (not yet attached)');
430
+ Logger.info('[WS:' + self.connectionId + '] -> Added to close list (not yet attached)');
418
431
  }
419
432
  });
420
-
433
+
421
434
  tabsToClose = Array.from(tabsToCloseSet);
422
-
423
- Logger.info('[WS] closeTabsByClientId: will close ' + tabsToClose.length + ' tabs');
424
-
435
+ Logger.info('[WS:' + self.connectionId + '] closeTabsByClientId: will close ' + tabsToClose.length + ' tabs');
436
+
425
437
  if (tabsToClose.length === 0) {
426
- Logger.info('[WS] No tabs found for clientId:', clientId);
438
+ Logger.info('[WS:' + self.connectionId + '] No tabs found for clientId:', clientId);
427
439
  resolve();
428
440
  return;
429
441
  }
430
442
 
431
- doCloseTabs(tabsToClose, clientId, resolve);
432
- }
443
+ self._doCloseTabs(tabsToClose, clientId, resolve);
444
+ };
433
445
 
434
- function doCloseTabs(tabIds, clientId, resolve) {
446
+ WebSocketConnection.prototype._doCloseTabs = function(tabIds, clientId, resolve) {
447
+ var self = this;
435
448
  if (tabIds.length === 0) {
436
449
  resolve();
437
450
  return;
438
451
  }
439
- Logger.info('[WS] Closing ' + tabIds.length + ' attached tabs for clientId:', clientId);
452
+ Logger.info('[WS:' + self.connectionId + '] Closing ' + tabIds.length + ' attached tabs for clientId:', clientId);
440
453
  var pending = tabIds.length;
441
454
  tabIds.forEach(function(tabId) {
442
455
  chrome.tabs.remove(tabId, function() {
443
456
  if (chrome.runtime.lastError) {
444
- Logger.info('[WS] Tab already closed:', tabId);
457
+ Logger.info('[WS:' + self.connectionId + '] Tab already closed:', tabId);
445
458
  }
446
459
  chrome.debugger.detach({ tabId: tabId }).catch(function() {});
447
- State.removeAttachedTab(tabId);
460
+ self.state.removeAttachedTab(tabId);
448
461
  pending--;
449
- if (pending === 0) {
450
- resolve();
451
- }
462
+ if (pending === 0) resolve();
452
463
  });
453
464
  });
454
- }
465
+ };
455
466
 
456
- function createGroupForClient(clientId) {
467
+ WebSocketConnection.prototype._createGroupForClient = function(clientId) {
468
+ var self = this;
457
469
  if (!clientId || !chrome.tabGroups) return;
458
470
 
459
- var existingGroupId = State.getGroupIdForClient(clientId);
471
+ if (self._groupCreationPending.has(clientId)) {
472
+ Logger.info('[WS:' + self.connectionId + '] Group creation already pending for client:', clientId);
473
+ return;
474
+ }
475
+
476
+ var existingGroupId = self.state.getGroupIdForClient(clientId);
460
477
  if (existingGroupId) {
461
- Logger.info('[WS] Group already cached for client:', clientId, 'groupId:', existingGroupId);
478
+ Logger.info('[WS:' + self.connectionId + '] Group already cached for client:', clientId, 'groupId:', existingGroupId);
462
479
  return;
463
480
  }
464
481
 
465
- var baseName = CDPUtils.getGroupBaseName(clientId);
482
+ self._groupCreationPending.add(clientId);
483
+
484
+ var baseName = CDPUtils.getGroupBaseName(clientId, self.config ? self.config.tag : null);
466
485
  chrome.tabs.query({ currentWindow: true }, function(tabs) {
467
486
  if (!tabs || tabs.length === 0) {
468
- Logger.warn('[WS] No tabs found for group creation');
487
+ Logger.warn('[WS:' + self.connectionId + '] No tabs found for group creation');
488
+ self._groupCreationPending.delete(clientId);
469
489
  return;
470
490
  }
471
491
  var windowId = tabs[0].windowId;
472
492
  chrome.tabs.group({ createProperties: { windowId: windowId } }, function(groupId) {
473
493
  if (chrome.runtime.lastError) {
474
- Logger.warn('[WS] Failed to create group on connect:', chrome.runtime.lastError.message);
494
+ Logger.warn('[WS:' + self.connectionId + '] Failed to create group on connect:', chrome.runtime.lastError.message);
495
+ self._groupCreationPending.delete(clientId);
475
496
  return;
476
497
  }
498
+ self._groupCreationPending.delete(clientId);
477
499
  chrome.tabGroups.update(groupId, {
478
500
  title: baseName,
479
501
  color: CDPUtils.getGroupColorForClient(clientId),
480
502
  collapsed: true
481
503
  }, function() {
482
504
  if (chrome.runtime.lastError) {
483
- Logger.warn('[WS] Failed to set group title:', chrome.runtime.lastError.message);
505
+ Logger.warn('[WS:' + self.connectionId + '] Failed to set group title:', chrome.runtime.lastError.message);
484
506
  }
485
507
  });
486
- State.setGroupIdForClient(clientId, groupId);
487
- Logger.info('[WS] Created group for client:', clientId, 'groupId:', groupId, 'title:', baseName);
508
+ self.state.setGroupIdForClient(clientId, groupId);
509
+ Logger.info('[WS:' + self.connectionId + '] Created group for client:', clientId, 'groupId:', groupId, 'title:', baseName);
488
510
  });
489
511
  });
490
- }
512
+ };
491
513
 
492
- function handleServerRestart() {
493
- Logger.info('[WS] Server restarted, cleaning up all state...');
514
+ WebSocketConnection.prototype._handleServerRestart = function() {
515
+ var self = this;
516
+ Logger.info('[WS:' + self.connectionId + '] Server restarted, cleaning up all state...');
494
517
 
495
- var attachedTabIds = State.getAttachedTabIds();
518
+ var attachedTabIds = self.state.getAttachedTabIds();
496
519
  var promises = attachedTabIds.map(function(tabId) {
497
520
  return chrome.debugger.detach({ tabId: tabId }).catch(function(e) {
498
- Logger.info('[WS] Detach failed for tab', tabId, ':', e.message);
521
+ Logger.info('[WS:' + self.connectionId + '] Detach failed for tab', tabId, ':', e.message);
499
522
  });
500
523
  });
501
524
 
502
525
  Promise.all(promises).then(function() {
503
- State.clearAllState();
504
- State.persist(null, false);
505
- Logger.info('[WS] State cleaned up after server restart');
526
+ self.state.clearAllState();
527
+ self.state.persist(null, false);
528
+ Logger.info('[WS:' + self.connectionId + '] State cleaned up after server restart');
506
529
  });
507
- }
530
+ };
531
+
532
+ WebSocketConnection.prototype._handleBrowserClose = function(sessions, clientId) {
533
+ var self = this;
534
+ Logger.info('[WS:' + self.connectionId + '] Browser.close received, cleaning up... clientId:', clientId);
508
535
 
509
- function handleBrowserClose(sessions, clientId) {
510
- Logger.info('[WS] Browser.close received, cleaning up... clientId:', clientId);
511
-
512
- closeTabGroupByClientId(clientId).then(function() {
536
+ self._closeTabGroupByClientId(clientId).then(function() {
513
537
  return new Promise(function(resolve) {
514
- closeTabsByClientId(clientId, resolve);
538
+ self._closeTabsByClientId(clientId, resolve);
515
539
  });
516
540
  }).then(function() {
517
- var preExistingTabs = State.getPreExistingTabs();
541
+ var preExistingTabs = self.state.getPreExistingTabs();
518
542
  var clientPreExisting = preExistingTabs.filter(function(tabId) {
519
- return State.getClientIdByTabId(tabId) === clientId;
543
+ return self.state.getClientIdByTabId(tabId) === clientId;
520
544
  });
521
545
  clientPreExisting.forEach(function(tabId) {
522
546
  chrome.debugger.detach({ tabId: tabId }).catch(function() {});
523
- State.removeAttachedTab(tabId);
547
+ self.state.removeAttachedTab(tabId);
524
548
  });
525
- State.clearPreExistingTabsForClient(clientId);
549
+ self.state.clearPreExistingTabsForClient(clientId);
526
550
 
527
- State.removeCDPClient(clientId);
528
- if (State.getCDPClients().length === 0) {
529
- State.clearAllState();
530
- State.persist(null, false);
551
+ self.state.removeCDPClient(clientId);
552
+ if (self.state.getCDPClients().length === 0) {
553
+ self.state.clearAllState();
554
+ self.state.persist(null, false);
531
555
  }
532
- broadcastStateUpdate();
533
- Logger.info('[WS] Browser.close cleanup complete for client:', clientId);
556
+ self._broadcastStateUpdate();
557
+ Logger.info('[WS:' + self.connectionId + '] Browser.close cleanup complete for client:', clientId);
534
558
  });
535
- }
536
-
537
- function setBadgeStatus(status) {
538
- var colors = Config.BADGE_COLORS;
539
- chrome.action.setBadgeText({ text: status });
540
- chrome.action.setBadgeBackgroundColor({ color: colors[status] || colors.OFF });
541
- }
559
+ };
542
560
 
543
- function broadcastStateUpdate() {
544
- var ws = State.getWs();
561
+ WebSocketConnection.prototype._broadcastStateUpdate = function() {
562
+ var self = this;
563
+ var ws = self.state.getWs();
545
564
  var isConnected = ws && ws.readyState === WebSocket.OPEN;
546
- var cdpClients = State.getCDPClients() || [];
547
- var attachedTabIds = State.getAttachedTabIds();
548
-
565
+ var cdpClients = self.state.getCDPClients() || [];
566
+ var attachedTabIds = self.state.getAttachedTabIds();
567
+
549
568
  if (attachedTabIds.length === 0) {
550
569
  chrome.runtime.sendMessage({
551
570
  type: 'stateUpdate',
@@ -578,14 +597,61 @@ var WebSocketManager = (function() {
578
597
  }
579
598
  });
580
599
  });
581
- }
600
+ };
582
601
 
583
- function getQueueStats() {
602
+ WebSocketConnection.prototype.getQueueStats = function() {
584
603
  return {
585
- queueLength: _sendQueue.length,
586
- maxQueueSize: _maxQueueSize,
587
- bufferThreshold: _bufferThreshold
604
+ queueLength: this._sendQueue.length,
605
+ maxQueueSize: this._maxQueueSize,
606
+ bufferThreshold: this._bufferThreshold
588
607
  };
608
+ };
609
+
610
+ return WebSocketConnection;
611
+ })();
612
+
613
+ function setBadgeStatus(status) {
614
+ var colors = Config.BADGE_COLORS;
615
+ chrome.action.setBadgeText({ text: status });
616
+ chrome.action.setBadgeBackgroundColor({ color: colors[status] || colors.OFF });
617
+ }
618
+
619
+ var WebSocketManager = (function() {
620
+ function connect() {
621
+ ConnectionManager.connectAll();
622
+ }
623
+
624
+ function send(message) {
625
+ var sent = false;
626
+ ConnectionManager.forEachConnection(function(entry) {
627
+ if (entry.wsManager.send(message)) {
628
+ sent = true;
629
+ }
630
+ });
631
+ return sent;
632
+ }
633
+
634
+ function scheduleReconnect() {
635
+ ConnectionManager.forEachConnection(function(entry) {
636
+ entry.wsManager._scheduleReconnect();
637
+ });
638
+ }
639
+
640
+ function getQueueStats() {
641
+ var stats = [];
642
+ ConnectionManager.forEachConnection(function(entry) {
643
+ stats.push({
644
+ connectionId: entry.id,
645
+ stats: entry.wsManager.getQueueStats()
646
+ });
647
+ });
648
+ return stats;
649
+ }
650
+
651
+ function processQueue() {
652
+ ConnectionManager.forEachConnection(function(entry) {
653
+ entry.wsManager._processQueue();
654
+ });
589
655
  }
590
656
 
591
657
  return {