tabctl 0.6.0-rc.11 → 0.6.0-rc.12

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.
@@ -783,7 +783,12 @@
783
783
  var RECONNECT_ALARM = "tabctl-reconnect";
784
784
  var KEEPALIVE_INTERVAL_MINUTES = 1;
785
785
  var BROWSER_STATE_SYNC_DEBOUNCE_MS = 750;
786
- var ACTIVE_PAGE_CACHE_DEBOUNCE_MS = 1e3;
786
+ var ACTIVE_PAGE_CACHE_LOADING_RETRY_MS = 100;
787
+ var ACTIVE_PAGE_CACHE_SETTLING_RETRY_MS = 150;
788
+ var ACTIVE_PAGE_CACHE_FIRST_QUIESCENT_TIMEOUT_MS = 750;
789
+ var ACTIVE_PAGE_CACHE_FIRST_QUIESCENT_SAMPLE_MS = 75;
790
+ var ACTIVE_PAGE_CACHE_FIRST_LOADING_MAX_ATTEMPTS = 300;
791
+ var ACTIVE_PAGE_CACHE_FIRST_SETTLING_MAX_ATTEMPTS = 200;
787
792
  var ACTIVE_PAGE_CACHE_TIMEOUT_MS = 5e3;
788
793
  var MAX_PAGE_HTML_CHARS = 10 * 1024 * 1024;
789
794
  var ACTIVE_PAGE_CACHE_MAX_HTML_CHARS = MAX_PAGE_HTML_CHARS;
@@ -837,7 +842,15 @@
837
842
  lastQuiescentCapturedKey: null,
838
843
  lastQuiescentCapturedAt: 0,
839
844
  statusRequests: /* @__PURE__ */ new Map(),
840
- diagnostics: /* @__PURE__ */ new Map()
845
+ states: /* @__PURE__ */ new Map()
846
+ };
847
+ var ACTIVE_PAGE_CACHE_STATE_DETAILS = {
848
+ checking: "checking status",
849
+ loading: "page still loading",
850
+ settling: "waiting for page settle",
851
+ capturing: "capture pending",
852
+ cached: "page cache available",
853
+ error: "capture failed"
841
854
  };
842
855
  function log(...args) {
843
856
  console.log("[tabctl]", ...args);
@@ -1040,20 +1053,40 @@
1040
1053
  const key = activePageCacheKeyForTabId(tabId, url);
1041
1054
  return activePageCache.inFlightKeys.has(key) || activePageCache.pending?.key === key || activePageCache.quiescentPending?.key === key;
1042
1055
  }
1043
- function setActivePageCacheDiagnostic(tabId, url, kind, detail) {
1044
- activePageCache.diagnostics.set(activePageCacheKeyForTabId(tabId, url), { kind, detail });
1056
+ function setActivePageCacheState(tabId, url, kind, detail = ACTIVE_PAGE_CACHE_STATE_DETAILS[kind]) {
1057
+ activePageCache.states.set(activePageCacheKeyForTabId(tabId, url), { kind, detail });
1058
+ }
1059
+ function activePageCacheState(tabId, url) {
1060
+ return activePageCache.states.get(activePageCacheKeyForTabId(tabId, url));
1045
1061
  }
1046
- function clearActivePageCacheDiagnostic(tabId, url) {
1047
- activePageCache.diagnostics.delete(activePageCacheKeyForTabId(tabId, url));
1062
+ function clearActivePageCacheState(tabId, url) {
1063
+ activePageCache.states.delete(activePageCacheKeyForTabId(tabId, url));
1048
1064
  }
1049
- function clearActivePageCacheDiagnosticsForTab(tabId) {
1065
+ function clearActivePageCacheStatesForTab(tabId) {
1050
1066
  const prefix = `${tabId}:`;
1051
- for (const key of activePageCache.diagnostics.keys()) {
1067
+ for (const key of activePageCache.states.keys()) {
1052
1068
  if (key.startsWith(prefix)) {
1053
- activePageCache.diagnostics.delete(key);
1069
+ activePageCache.states.delete(key);
1054
1070
  }
1055
1071
  }
1056
1072
  }
1073
+ function clearPendingFirstActivePageCacheCapture(key) {
1074
+ if (activePageCache.pending?.key === key) {
1075
+ activePageCache.pending = null;
1076
+ }
1077
+ }
1078
+ function failFirstActivePageCacheCapture(pending, tab, url, detail) {
1079
+ clearPendingFirstActivePageCacheCapture(pending.key);
1080
+ setActivePageCacheState(tab.id, url, "error", detail);
1081
+ void setCacheErrorIndicator(tab.id, url, detail);
1082
+ void requestPageCacheStatusForTab(tab.id, `first-capture:${detail.replace(/\s+/g, "-")}`);
1083
+ }
1084
+ function waitingDetailForActivePageCacheState(state2) {
1085
+ if (state2?.kind === "checking" || state2?.kind === "loading" || state2?.kind === "settling" || state2?.kind === "capturing") {
1086
+ return state2.detail;
1087
+ }
1088
+ return null;
1089
+ }
1057
1090
  async function applyPageCacheStatus(tabId, expectedUrl, available) {
1058
1091
  try {
1059
1092
  const tab = await chrome.tabs.get(tabId);
@@ -1065,23 +1098,26 @@
1065
1098
  return;
1066
1099
  }
1067
1100
  if (!available) {
1068
- const diagnostic = activePageCache.diagnostics.get(activePageCacheKeyForTabId(tabId, expectedUrl));
1069
- if (diagnostic?.kind === "waiting") {
1070
- await setCacheWaitingIndicator(tabId, expectedUrl, diagnostic.detail);
1101
+ const cacheState = activePageCacheState(tabId, expectedUrl);
1102
+ const waitingDetail = waitingDetailForActivePageCacheState(cacheState);
1103
+ if (waitingDetail) {
1104
+ await setCacheWaitingIndicator(tabId, expectedUrl, waitingDetail);
1071
1105
  return;
1072
1106
  }
1073
- if (diagnostic?.kind === "error") {
1074
- await setCacheErrorIndicator(tabId, expectedUrl, diagnostic.detail);
1107
+ if (cacheState?.kind === "error") {
1108
+ await setCacheErrorIndicator(tabId, expectedUrl, cacheState.detail);
1075
1109
  return;
1076
1110
  }
1077
1111
  if (hasPendingActivePageCacheWork(tabId, expectedUrl)) {
1078
- await setCacheWaitingIndicator(tabId, expectedUrl, "capture pending");
1112
+ setActivePageCacheState(tabId, expectedUrl, "capturing");
1113
+ await setCacheWaitingIndicator(tabId, expectedUrl, ACTIVE_PAGE_CACHE_STATE_DETAILS.capturing);
1079
1114
  return;
1080
1115
  }
1116
+ clearActivePageCacheState(tabId, expectedUrl);
1081
1117
  await clearCacheAvailableIndicator(tabId);
1082
1118
  return;
1083
1119
  }
1084
- clearActivePageCacheDiagnostic(tabId, expectedUrl);
1120
+ setActivePageCacheState(tabId, expectedUrl, "cached");
1085
1121
  await setCacheAvailableIndicator(tabId, expectedUrl);
1086
1122
  } catch {
1087
1123
  await clearCacheAvailableIndicator(tabId);
@@ -1115,6 +1151,7 @@
1115
1151
  const port = state.port;
1116
1152
  if (!port) {
1117
1153
  connectNative();
1154
+ setActivePageCacheState(tab.id, url, "checking", "native host reconnecting");
1118
1155
  void setCacheWaitingIndicator(tab.id, url, "native host reconnecting");
1119
1156
  return;
1120
1157
  }
@@ -1167,7 +1204,7 @@
1167
1204
  }
1168
1205
  function isEligibleActivePageCacheTab(tab) {
1169
1206
  const url = activePageCacheUrl(tab);
1170
- return typeof tab.id === "number" && Boolean(url) && tab.incognito !== true && !browserState.incognitoTabIds.has(tab.id) && (typeof tab.windowId !== "number" || !browserState.incognitoWindowIds.has(tab.windowId)) && tab.discarded !== true && tab.status !== "loading" && isScriptableUrl(url);
1207
+ return typeof tab.id === "number" && Boolean(url) && tab.incognito !== true && !browserState.incognitoTabIds.has(tab.id) && (typeof tab.windowId !== "number" || !browserState.incognitoWindowIds.has(tab.windowId)) && tab.discarded !== true && isScriptableUrl(url);
1171
1208
  }
1172
1209
  function clearPendingActivePageCacheCapture() {
1173
1210
  if (activePageCache.timer) {
@@ -1206,6 +1243,17 @@
1206
1243
  }
1207
1244
  }, ACTIVE_PAGE_CACHE_QUIESCENT_DELAY_MS);
1208
1245
  }
1246
+ function scheduleFirstActivePageCacheRetry(pending, delayMs) {
1247
+ clearPendingActivePageCacheCapture();
1248
+ activePageCache.pending = pending;
1249
+ activePageCache.timer = setTimeout(() => {
1250
+ activePageCache.timer = null;
1251
+ const retry = activePageCache.pending;
1252
+ if (retry) {
1253
+ void captureFirstSettledActivePageCache(retry);
1254
+ }
1255
+ }, delayMs);
1256
+ }
1209
1257
  function scheduleActivePageCacheCapture(tab, reason) {
1210
1258
  if (!isEligibleActivePageCacheTab(tab)) {
1211
1259
  if (typeof tab.id === "number") {
@@ -1217,23 +1265,25 @@
1217
1265
  }
1218
1266
  const url = activePageCacheUrl(tab);
1219
1267
  const key = activePageCacheKey(tab, url);
1220
- clearActivePageCacheDiagnostic(tab.id, url);
1221
- void setCacheWaitingIndicator(tab.id, url, "checking status");
1268
+ if (tab.status === "loading") {
1269
+ setActivePageCacheState(tab.id, url, "loading");
1270
+ void setCacheWaitingIndicator(tab.id, url, ACTIVE_PAGE_CACHE_STATE_DETAILS.loading);
1271
+ if (activePageCache.pending?.key !== key) {
1272
+ scheduleFirstActivePageCacheRetry({ tabId: tab.id, reason, key, attempts: 0 }, ACTIVE_PAGE_CACHE_LOADING_RETRY_MS);
1273
+ }
1274
+ return;
1275
+ }
1276
+ setActivePageCacheState(tab.id, url, "checking");
1277
+ void setCacheWaitingIndicator(tab.id, url, ACTIVE_PAGE_CACHE_STATE_DETAILS.checking);
1222
1278
  requestPageCacheStatus(tab, reason);
1223
1279
  scheduleQuiescentActivePageCacheCapture(tab, reason, key);
1224
1280
  if (activePageCache.inFlightKeys.has(key) || activePageCache.lastCapturedKey === key) {
1225
1281
  return;
1226
1282
  }
1227
- clearPendingActivePageCacheCapture();
1228
- activePageCache.pending = { tab, reason, key };
1229
- activePageCache.timer = setTimeout(() => {
1230
- activePageCache.timer = null;
1231
- const pending = activePageCache.pending;
1232
- activePageCache.pending = null;
1233
- if (pending) {
1234
- void captureActivePageCache(pending.tab, pending.reason, pending.key);
1235
- }
1236
- }, ACTIVE_PAGE_CACHE_DEBOUNCE_MS);
1283
+ if (activePageCache.pending?.key === key) {
1284
+ return;
1285
+ }
1286
+ scheduleFirstActivePageCacheRetry({ tabId: tab.id, reason, key, attempts: 0 }, 0);
1237
1287
  }
1238
1288
  async function scheduleActivePageCacheCaptureForTab(tabId, reason) {
1239
1289
  try {
@@ -1268,6 +1318,83 @@
1268
1318
  return null;
1269
1319
  }
1270
1320
  }
1321
+ async function getPageCacheOpenTabs() {
1322
+ const tabs = await chrome.tabs.query({});
1323
+ return tabs.filter((tab) => typeof tab.id === "number").filter((tab) => tab.incognito !== true).map((tab) => ({ tabId: tab.id, url: activePageCacheUrl(tab) })).filter((tab) => tab.url.length > 0 && isScriptableUrl(tab.url));
1324
+ }
1325
+ async function captureFirstSettledActivePageCache(pending) {
1326
+ if (activePageCache.inFlightKeys.has(pending.key) || activePageCache.lastCapturedKey === pending.key) {
1327
+ if (activePageCache.pending?.key === pending.key) {
1328
+ activePageCache.pending = null;
1329
+ }
1330
+ return;
1331
+ }
1332
+ const tab = await currentActivePageCacheTab(pending.tabId, pending.key);
1333
+ if (!tab) {
1334
+ if (activePageCache.pending?.key === pending.key) {
1335
+ activePageCache.pending = null;
1336
+ }
1337
+ return;
1338
+ }
1339
+ const tabUrl = activePageCacheUrl(tab);
1340
+ if (tab.status === "loading") {
1341
+ setActivePageCacheState(tab.id, tabUrl, "loading");
1342
+ void setCacheWaitingIndicator(tab.id, tabUrl, ACTIVE_PAGE_CACHE_STATE_DETAILS.loading);
1343
+ if (pending.attempts < ACTIVE_PAGE_CACHE_FIRST_LOADING_MAX_ATTEMPTS) {
1344
+ scheduleFirstActivePageCacheRetry(
1345
+ { ...pending, attempts: pending.attempts + 1 },
1346
+ ACTIVE_PAGE_CACHE_LOADING_RETRY_MS
1347
+ );
1348
+ } else {
1349
+ failFirstActivePageCacheCapture(pending, tab, tabUrl, "loading attempts exhausted");
1350
+ }
1351
+ return;
1352
+ }
1353
+ setActivePageCacheState(tab.id, tabUrl, "settling");
1354
+ void setCacheWaitingIndicator(tab.id, tabUrl, ACTIVE_PAGE_CACHE_STATE_DETAILS.settling);
1355
+ const probe = await content.probePageQuiescence(
1356
+ tab.id,
1357
+ ACTIVE_PAGE_CACHE_FIRST_QUIESCENT_TIMEOUT_MS,
1358
+ ACTIVE_PAGE_CACHE_FIRST_QUIESCENT_SAMPLE_MS
1359
+ );
1360
+ if (activePageCache.pending?.key !== pending.key) {
1361
+ return;
1362
+ }
1363
+ if (!probe.quiet) {
1364
+ if (probe.error || probe.documentReadyState === null) {
1365
+ failFirstActivePageCacheCapture(
1366
+ pending,
1367
+ tab,
1368
+ tabUrl,
1369
+ probe.reason || "page settle probe failed"
1370
+ );
1371
+ return;
1372
+ }
1373
+ const loading = probe.documentReadyState !== "interactive" && probe.documentReadyState !== "complete";
1374
+ setActivePageCacheState(tab.id, tabUrl, loading ? "loading" : "settling");
1375
+ void setCacheWaitingIndicator(
1376
+ tab.id,
1377
+ tabUrl,
1378
+ loading ? ACTIVE_PAGE_CACHE_STATE_DETAILS.loading : ACTIVE_PAGE_CACHE_STATE_DETAILS.settling
1379
+ );
1380
+ if (pending.attempts < ACTIVE_PAGE_CACHE_FIRST_SETTLING_MAX_ATTEMPTS) {
1381
+ scheduleFirstActivePageCacheRetry(
1382
+ { ...pending, attempts: pending.attempts + 1 },
1383
+ loading ? ACTIVE_PAGE_CACHE_LOADING_RETRY_MS : ACTIVE_PAGE_CACHE_SETTLING_RETRY_MS
1384
+ );
1385
+ } else {
1386
+ failFirstActivePageCacheCapture(
1387
+ pending,
1388
+ tab,
1389
+ tabUrl,
1390
+ loading ? "loading attempts exhausted" : "settling attempts exhausted"
1391
+ );
1392
+ }
1393
+ return;
1394
+ }
1395
+ clearPendingFirstActivePageCacheCapture(pending.key);
1396
+ await captureActivePageCache(tab, pending.reason, pending.key);
1397
+ }
1271
1398
  async function rescheduleQuiescentActivePageCacheCapture(pending) {
1272
1399
  if (pending.attempts >= 2 || isQuiescentCaptureCoolingDown(pending.key)) {
1273
1400
  return;
@@ -1294,6 +1421,9 @@
1294
1421
  if (!tab) {
1295
1422
  return;
1296
1423
  }
1424
+ const tabUrl = activePageCacheUrl(tab);
1425
+ setActivePageCacheState(tab.id, tabUrl, "settling");
1426
+ void setCacheWaitingIndicator(tab.id, tabUrl, ACTIVE_PAGE_CACHE_STATE_DETAILS.settling);
1297
1427
  const probe = await content.probePageQuiescence(
1298
1428
  tab.id,
1299
1429
  ACTIVE_PAGE_CACHE_QUIESCENT_TIMEOUT_MS,
@@ -1314,7 +1444,7 @@
1314
1444
  connectNative();
1315
1445
  return false;
1316
1446
  }
1317
- if (!isEligibleActivePageCacheTab(tab) || activePageCache.inFlightKeys.has(key)) {
1447
+ if (!isEligibleActivePageCacheTab(tab) || tab.status === "loading" || activePageCache.inFlightKeys.has(key)) {
1318
1448
  return false;
1319
1449
  }
1320
1450
  activePageCache.inFlightKeys.add(key);
@@ -1323,6 +1453,15 @@
1323
1453
  if (!captureTab) {
1324
1454
  return false;
1325
1455
  }
1456
+ const captureUrl = activePageCacheUrl(captureTab);
1457
+ if (captureTab.status === "loading") {
1458
+ setActivePageCacheState(captureTab.id, captureUrl, "loading");
1459
+ void setCacheWaitingIndicator(captureTab.id, captureUrl, ACTIVE_PAGE_CACHE_STATE_DETAILS.loading);
1460
+ scheduleFirstActivePageCacheRetry({ tabId: captureTab.id, reason, key, attempts: 0 }, ACTIVE_PAGE_CACHE_LOADING_RETRY_MS);
1461
+ return false;
1462
+ }
1463
+ setActivePageCacheState(captureTab.id, captureUrl, "capturing");
1464
+ void setCacheWaitingIndicator(captureTab.id, captureUrl, ACTIVE_PAGE_CACHE_STATE_DETAILS.capturing);
1326
1465
  const extraction = await content.extractPageHtml(captureTab.id, ACTIVE_PAGE_CACHE_TIMEOUT_MS, ACTIVE_PAGE_CACHE_MAX_HTML_CHARS);
1327
1466
  if (extraction.status !== "READ" || typeof extraction.html !== "string" || extraction.html.length === 0) {
1328
1467
  log("active page cache extraction not readable", {
@@ -1338,12 +1477,16 @@
1338
1477
  });
1339
1478
  if (extraction.status === "NOT_LOADED") {
1340
1479
  const url = activePageCacheUrl(captureTab);
1341
- setActivePageCacheDiagnostic(captureTab.id, url, "waiting", "page still loading");
1342
- void setCacheWaitingIndicator(captureTab.id, url, "page still loading");
1480
+ setActivePageCacheState(captureTab.id, url, "loading");
1481
+ void setCacheWaitingIndicator(captureTab.id, url, ACTIVE_PAGE_CACHE_STATE_DETAILS.loading);
1482
+ scheduleFirstActivePageCacheRetry(
1483
+ { tabId: captureTab.id, reason, key, attempts: 0 },
1484
+ ACTIVE_PAGE_CACHE_LOADING_RETRY_MS
1485
+ );
1343
1486
  } else {
1344
1487
  const detail = typeof extraction.status === "string" ? extraction.status.toLowerCase().replace(/_/g, " ") : "capture failed";
1345
1488
  const url = activePageCacheUrl(captureTab);
1346
- setActivePageCacheDiagnostic(captureTab.id, url, "error", detail);
1489
+ setActivePageCacheState(captureTab.id, url, "error", detail);
1347
1490
  void setCacheErrorIndicator(captureTab.id, url, detail);
1348
1491
  }
1349
1492
  void requestPageCacheStatusForTab(captureTab.id, `${reason}:capture-failed`);
@@ -1357,6 +1500,7 @@
1357
1500
  if (!port) {
1358
1501
  return false;
1359
1502
  }
1503
+ const openTabs = await getPageCacheOpenTabs();
1360
1504
  const id = nextActivePageCacheId();
1361
1505
  trackPageCacheStatusRequest(id, verifiedTab.id, activePageCacheUrl(verifiedTab));
1362
1506
  port.postMessage({
@@ -1366,6 +1510,7 @@
1366
1510
  data: {
1367
1511
  reason,
1368
1512
  capturedAt: Date.now(),
1513
+ openTabs,
1369
1514
  tab: {
1370
1515
  tabId: verifiedTab.id,
1371
1516
  windowId: verifiedTab.windowId,
@@ -1389,7 +1534,7 @@
1389
1534
  } catch (error) {
1390
1535
  log("active page cache capture failed", { tabId: tab.id, reason, error });
1391
1536
  const url = activePageCacheUrl(tab);
1392
- setActivePageCacheDiagnostic(tab.id, url, "error", "capture exception");
1537
+ setActivePageCacheState(tab.id, url, "error", "capture exception");
1393
1538
  void setCacheErrorIndicator(tab.id, url, "capture exception");
1394
1539
  void requestPageCacheStatusForTab(tab.id, `${reason}:capture-error`);
1395
1540
  return false;
@@ -1514,7 +1659,7 @@
1514
1659
  changeInfo
1515
1660
  });
1516
1661
  if ("url" in changeInfo || "status" in changeInfo || "discarded" in changeInfo) {
1517
- clearActivePageCacheDiagnosticsForTab(tabId);
1662
+ clearActivePageCacheStatesForTab(tabId);
1518
1663
  void clearCacheAvailableIndicator(tabId);
1519
1664
  }
1520
1665
  if (tab.active && ("url" in changeInfo || "status" in changeInfo || "discarded" in changeInfo)) {
@@ -1554,7 +1699,7 @@
1554
1699
  activePageCache.statusRequests.delete(requestId);
1555
1700
  }
1556
1701
  });
1557
- clearActivePageCacheDiagnosticsForTab(tabId);
1702
+ clearActivePageCacheStatesForTab(tabId);
1558
1703
  void clearCacheAvailableIndicator(tabId);
1559
1704
  });
1560
1705
  chrome.tabs?.onActivated?.addListener((activeInfo) => {
@@ -22,5 +22,5 @@
22
22
  "action": {
23
23
  "default_title": "Tab Control"
24
24
  },
25
- "version_name": "0.6.0-rc.11"
25
+ "version_name": "0.6.0-rc.12"
26
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tabctl",
3
- "version": "0.6.0-rc.11",
3
+ "version": "0.6.0-rc.12",
4
4
  "description": "CLI tool to manage and analyze browser tabs",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -53,7 +53,7 @@
53
53
  "typescript": "^5.4.5"
54
54
  },
55
55
  "optionalDependencies": {
56
- "tabctl-win32-x64": "0.6.0-rc.11"
56
+ "tabctl-win32-x64": "0.6.0-rc.12"
57
57
  },
58
58
  "dependencies": {
59
59
  "normalize-url": "^8.1.1"