cdp-tunnel 2.7.10 → 2.8.1

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