@btraut/browser-bridge 0.11.1 → 0.12.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.
@@ -36,7 +36,7 @@ body.bb-page {
36
36
  body.bb-page.bb-page--popup {
37
37
  height: auto;
38
38
  padding: 10px 10px 12px;
39
- min-width: 260px;
39
+ min-width: 300px;
40
40
  background: var(--bb-surface);
41
41
  }
42
42
 
@@ -68,6 +68,37 @@ body.bb-page.bb-page--popup {
68
68
  padding: 2px;
69
69
  }
70
70
 
71
+ .bb-connection {
72
+ border: 1px solid var(--bb-border-2);
73
+ border-radius: var(--bb-radius-sm);
74
+ background: var(--bb-bg);
75
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.06);
76
+ padding: 10px 12px;
77
+ margin: 0 0 10px;
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: space-between;
81
+ }
82
+
83
+ .bb-connection-label {
84
+ font-size: 14px;
85
+ font-weight: 600;
86
+ color: var(--bb-ink);
87
+ }
88
+
89
+ .bb-connection-dot {
90
+ width: 11px;
91
+ height: 11px;
92
+ border-radius: 999px;
93
+ border: 1px solid rgba(162, 43, 43, 0.35);
94
+ background: #c93939;
95
+ }
96
+
97
+ .bb-connection-dot[data-connected='true'] {
98
+ border-color: rgba(31, 111, 63, 0.4);
99
+ background: #2b9a52;
100
+ }
101
+
71
102
  .bb-popup-head {
72
103
  display: flex;
73
104
  align-items: center;
@@ -410,10 +410,86 @@ var PermissionPromptController = class {
410
410
  }
411
411
  };
412
412
 
413
+ // packages/extension/src/connection-state.ts
414
+ var toIso = (ms) => new Date(ms).toISOString();
415
+ var ConnectionStateTracker = class {
416
+ constructor(now = Date.now, failureLogThrottleMs = 15e3) {
417
+ this.now = now;
418
+ this.failureLogThrottleMs = failureLogThrottleMs;
419
+ this.state = "disconnected";
420
+ this.consecutiveFailures = 0;
421
+ this.lastFailureLogAt = 0;
422
+ this.suppressedFailureLogs = 0;
423
+ }
424
+ setEndpoint(endpoint) {
425
+ this.endpoint = endpoint;
426
+ }
427
+ markConnecting() {
428
+ this.state = "connecting";
429
+ this.reconnectDelayMs = void 0;
430
+ this.retryAt = void 0;
431
+ }
432
+ markConnected() {
433
+ this.state = "connected";
434
+ this.lastConnectedAt = toIso(this.now());
435
+ this.reconnectDelayMs = void 0;
436
+ this.retryAt = void 0;
437
+ this.consecutiveFailures = 0;
438
+ }
439
+ markDisconnected() {
440
+ this.state = "disconnected";
441
+ this.reconnectDelayMs = void 0;
442
+ this.retryAt = void 0;
443
+ this.lastDisconnectedAt = toIso(this.now());
444
+ }
445
+ markBackoff(delayMs2) {
446
+ this.state = "backoff";
447
+ this.reconnectDelayMs = delayMs2;
448
+ this.retryAt = toIso(this.now() + Math.max(0, delayMs2));
449
+ }
450
+ recordFailure(message) {
451
+ this.lastErrorAt = toIso(this.now());
452
+ this.lastErrorMessage = message;
453
+ this.consecutiveFailures += 1;
454
+ }
455
+ consumeFailureLogBudget() {
456
+ const now = this.now();
457
+ if (this.lastFailureLogAt === 0 || now - this.lastFailureLogAt >= this.failureLogThrottleMs) {
458
+ const suppressedCount = this.suppressedFailureLogs;
459
+ this.suppressedFailureLogs = 0;
460
+ this.lastFailureLogAt = now;
461
+ return { shouldLog: true, suppressedCount };
462
+ }
463
+ this.suppressedFailureLogs += 1;
464
+ return { shouldLog: false, suppressedCount: this.suppressedFailureLogs };
465
+ }
466
+ flushSuppressedFailureLogs() {
467
+ const count = this.suppressedFailureLogs;
468
+ this.suppressedFailureLogs = 0;
469
+ return count;
470
+ }
471
+ getStatus() {
472
+ return {
473
+ state: this.state,
474
+ endpoint: this.endpoint,
475
+ ws_url: this.endpoint ? `ws://${this.endpoint.host}:${this.endpoint.port}/drive` : void 0,
476
+ reconnect_delay_ms: this.reconnectDelayMs,
477
+ retry_at: this.retryAt,
478
+ last_connected_at: this.lastConnectedAt,
479
+ last_disconnected_at: this.lastDisconnectedAt,
480
+ last_error_at: this.lastErrorAt,
481
+ last_error_message: this.lastErrorMessage,
482
+ consecutive_failures: this.consecutiveFailures
483
+ };
484
+ }
485
+ };
486
+
413
487
  // packages/extension/src/background.ts
414
488
  var DEFAULT_CORE_PORT = 3210;
415
489
  var CORE_PORT_KEY = "corePort";
416
490
  var CORE_WS_PATH = "/drive";
491
+ var CORE_HEALTH_PATH = "/health";
492
+ var CORE_HEALTH_TIMEOUT_MS = 1200;
417
493
  var DEBUGGER_PROTOCOL_VERSION = "1.3";
418
494
  var DEBUGGER_IDLE_TIMEOUT_KEY = "debuggerIdleTimeoutMs";
419
495
  var DEFAULT_DEBUGGER_IDLE_TIMEOUT_MS = 15e3;
@@ -598,24 +674,36 @@ var renderDataUrlToFormat = async (dataUrl, format, quality) => {
598
674
  bitmap.close();
599
675
  }
600
676
  };
601
- var readCorePort = async () => {
677
+ var readCoreEndpointConfig = async () => {
602
678
  return await new Promise((resolve) => {
603
679
  chrome.storage.local.get(
604
680
  [CORE_PORT_KEY],
605
681
  (result) => {
606
682
  const raw = result?.[CORE_PORT_KEY];
607
683
  if (typeof raw === "number" && Number.isFinite(raw)) {
608
- resolve(raw);
684
+ resolve({
685
+ host: "127.0.0.1",
686
+ port: raw,
687
+ portSource: "storage"
688
+ });
609
689
  return;
610
690
  }
611
691
  if (typeof raw === "string") {
612
692
  const parsed = Number(raw);
613
693
  if (Number.isFinite(parsed)) {
614
- resolve(parsed);
694
+ resolve({
695
+ host: "127.0.0.1",
696
+ port: parsed,
697
+ portSource: "storage"
698
+ });
615
699
  return;
616
700
  }
617
701
  }
618
- resolve(DEFAULT_CORE_PORT);
702
+ resolve({
703
+ host: "127.0.0.1",
704
+ port: DEFAULT_CORE_PORT,
705
+ portSource: "default"
706
+ });
619
707
  }
620
708
  );
621
709
  });
@@ -1060,10 +1148,14 @@ var waitForDomContentLoaded = async (tabId, timeoutMs) => {
1060
1148
  }, timeoutMs);
1061
1149
  });
1062
1150
  };
1063
- var getWsUrl = async () => {
1064
- const port = await readCorePort();
1065
- return `ws://127.0.0.1:${port}${CORE_WS_PATH}`;
1151
+ var getWsEndpoint = async () => {
1152
+ const endpoint = await readCoreEndpointConfig();
1153
+ return {
1154
+ endpoint,
1155
+ url: `ws://${endpoint.host}:${endpoint.port}${CORE_WS_PATH}`
1156
+ };
1066
1157
  };
1158
+ var getHealthEndpoint = (endpoint) => `http://${endpoint.host}:${endpoint.port}${CORE_HEALTH_PATH}`;
1067
1159
  var DriveSocket = class {
1068
1160
  constructor() {
1069
1161
  this.socket = null;
@@ -1074,10 +1166,12 @@ var DriveSocket = class {
1074
1166
  this.keepAliveIntervalMs = 3e4;
1075
1167
  this.debuggerSessions = /* @__PURE__ */ new Map();
1076
1168
  this.debuggerIdleTimeoutMs = null;
1169
+ this.connection = new ConnectionStateTracker();
1077
1170
  }
1078
1171
  start() {
1172
+ this.connection.markConnecting();
1079
1173
  void this.connect().catch((error) => {
1080
- console.error("DriveSocket connect failed:", error);
1174
+ this.recordConnectionFailure("initial connect", error);
1081
1175
  });
1082
1176
  }
1083
1177
  stop() {
@@ -1090,6 +1184,7 @@ var DriveSocket = class {
1090
1184
  this.socket.close();
1091
1185
  this.socket = null;
1092
1186
  }
1187
+ this.connection.markDisconnected();
1093
1188
  }
1094
1189
  sendTabReport() {
1095
1190
  void this.emitTabReport().catch((error) => {
@@ -1101,10 +1196,12 @@ var DriveSocket = class {
1101
1196
  return;
1102
1197
  }
1103
1198
  const delay2 = this.reconnectDelayMs;
1199
+ this.connection.markBackoff(delay2);
1104
1200
  this.reconnectTimer = self.setTimeout(() => {
1105
1201
  this.reconnectTimer = null;
1202
+ this.connection.markConnecting();
1106
1203
  void this.connect().catch((error) => {
1107
- console.error("DriveSocket reconnect failed:", error);
1204
+ this.recordConnectionFailure("reconnect", error);
1108
1205
  });
1109
1206
  }, delay2);
1110
1207
  this.reconnectDelayMs = Math.min(
@@ -1113,12 +1210,30 @@ var DriveSocket = class {
1113
1210
  );
1114
1211
  }
1115
1212
  async connect() {
1116
- const url = await getWsUrl();
1213
+ const { endpoint, url } = await getWsEndpoint();
1214
+ this.connection.setEndpoint(endpoint);
1215
+ const health = await this.checkCoreHealth(endpoint);
1216
+ if (!health.ok) {
1217
+ this.connection.markDisconnected();
1218
+ this.recordConnectionFailure(
1219
+ "core unavailable",
1220
+ new Error(health.detail)
1221
+ );
1222
+ this.scheduleReconnect();
1223
+ return;
1224
+ }
1117
1225
  try {
1118
1226
  const socket2 = new WebSocket(url);
1119
1227
  this.socket = socket2;
1120
1228
  socket2.addEventListener("open", () => {
1121
1229
  this.reconnectDelayMs = 1e3;
1230
+ this.connection.markConnected();
1231
+ const suppressed = this.connection.flushSuppressedFailureLogs();
1232
+ if (suppressed > 0) {
1233
+ console.info(
1234
+ `DriveSocket reconnected after suppressing ${suppressed} repeated connection failures.`
1235
+ );
1236
+ }
1122
1237
  this.startKeepAlive();
1123
1238
  void this.sendHello().catch((error) => {
1124
1239
  console.error("DriveSocket hello failed:", error);
@@ -1128,22 +1243,82 @@ var DriveSocket = class {
1128
1243
  this.handleMessage(event.data);
1129
1244
  });
1130
1245
  socket2.addEventListener("close", () => {
1131
- this.socket = null;
1132
- this.stopKeepAlive();
1133
- this.scheduleReconnect();
1246
+ this.handleSocketUnavailable(socket2, "socket closed");
1134
1247
  });
1135
1248
  socket2.addEventListener("error", () => {
1136
- this.socket = null;
1137
- this.stopKeepAlive();
1138
- this.scheduleReconnect();
1249
+ this.handleSocketUnavailable(socket2, "socket error");
1139
1250
  });
1140
1251
  } catch (error) {
1141
- console.debug("DriveSocket connect failed, scheduling reconnect.", error);
1252
+ this.recordConnectionFailure("connect", error);
1253
+ this.connection.markDisconnected();
1142
1254
  this.scheduleReconnect();
1143
1255
  }
1144
1256
  }
1257
+ async checkCoreHealth(endpoint) {
1258
+ const controller = new AbortController();
1259
+ const timeoutId = self.setTimeout(() => {
1260
+ controller.abort();
1261
+ }, CORE_HEALTH_TIMEOUT_MS);
1262
+ try {
1263
+ const response = await fetch(getHealthEndpoint(endpoint), {
1264
+ method: "GET",
1265
+ cache: "no-store",
1266
+ signal: controller.signal
1267
+ });
1268
+ if (!response.ok) {
1269
+ return {
1270
+ ok: false,
1271
+ detail: `health returned HTTP ${response.status}`
1272
+ };
1273
+ }
1274
+ return { ok: true, detail: "ok" };
1275
+ } catch (error) {
1276
+ if (error instanceof DOMException && error.name === "AbortError") {
1277
+ return {
1278
+ ok: false,
1279
+ detail: `health timed out after ${CORE_HEALTH_TIMEOUT_MS}ms`
1280
+ };
1281
+ }
1282
+ return {
1283
+ ok: false,
1284
+ detail: error instanceof Error && error.message.length > 0 ? error.message : "health check failed"
1285
+ };
1286
+ } finally {
1287
+ clearTimeout(timeoutId);
1288
+ }
1289
+ }
1290
+ getConnectionStatus() {
1291
+ return this.connection.getStatus();
1292
+ }
1293
+ handleSocketUnavailable(socket2, reason) {
1294
+ if (this.socket !== socket2) {
1295
+ return;
1296
+ }
1297
+ this.socket = null;
1298
+ this.stopKeepAlive();
1299
+ this.connection.markDisconnected();
1300
+ this.recordConnectionFailure(reason, new Error(reason));
1301
+ this.scheduleReconnect();
1302
+ }
1303
+ recordConnectionFailure(context, error) {
1304
+ const detail = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
1305
+ this.connection.recordFailure(`${context}: ${detail}`);
1306
+ const budget = this.connection.consumeFailureLogBudget();
1307
+ if (!budget.shouldLog) {
1308
+ return;
1309
+ }
1310
+ if (budget.suppressedCount > 0) {
1311
+ console.warn(
1312
+ `DriveSocket ${context} failed (${budget.suppressedCount} repeated failures suppressed).`,
1313
+ error
1314
+ );
1315
+ return;
1316
+ }
1317
+ console.warn(`DriveSocket ${context} failed.`, error);
1318
+ }
1145
1319
  async sendHello() {
1146
1320
  const manifest = chrome.runtime.getManifest();
1321
+ const endpoint = await readCoreEndpointConfig();
1147
1322
  let tabs = [];
1148
1323
  try {
1149
1324
  tabs = await queryTabs();
@@ -1153,6 +1328,9 @@ var DriveSocket = class {
1153
1328
  }
1154
1329
  const params = {
1155
1330
  version: manifest.version,
1331
+ core_host: endpoint.host,
1332
+ core_port: endpoint.port,
1333
+ core_port_source: endpoint.portSource,
1156
1334
  tabs
1157
1335
  };
1158
1336
  this.sendEvent("drive.hello", params);
@@ -3188,5 +3366,17 @@ chrome.debugger.onDetach.addListener(
3188
3366
  });
3189
3367
  }
3190
3368
  );
3369
+ chrome.runtime.onMessage.addListener(
3370
+ (message, _sender, sendResponse) => {
3371
+ if (!message || typeof message !== "object" || message.action !== "drive.connection_status") {
3372
+ return void 0;
3373
+ }
3374
+ sendResponse({
3375
+ ok: true,
3376
+ result: socket.getConnectionStatus()
3377
+ });
3378
+ return true;
3379
+ }
3380
+ );
3191
3381
  socket.start();
3192
3382
  //# sourceMappingURL=background.js.map