@saptools/cf-inspector 0.6.2 → 0.7.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.
package/dist/index.js CHANGED
@@ -913,6 +913,56 @@ async function fetchInspectorVersion(host, port, timeoutMs) {
913
913
  }
914
914
  return { browser, protocolVersion };
915
915
  }
916
+ function startInspectorKeepalive(host, port, options = {}) {
917
+ const intervalMs = options.intervalMs ?? 1e4;
918
+ const probeTimeoutMs = options.probeTimeoutMs ?? 2e3;
919
+ const failureThreshold = options.failureThreshold ?? 3;
920
+ const probe = options.probe ?? (async () => await fetchInspectorVersion(host, port, probeTimeoutMs));
921
+ let cancelled = false;
922
+ let consecutiveFailures = 0;
923
+ let timer;
924
+ let rejectFailure;
925
+ const failure = new Promise((_resolve, reject) => {
926
+ rejectFailure = reject;
927
+ });
928
+ const schedule = () => {
929
+ if (cancelled) {
930
+ return;
931
+ }
932
+ timer = setTimeout(() => {
933
+ void runProbe();
934
+ }, intervalMs);
935
+ };
936
+ const runProbe = async () => {
937
+ try {
938
+ await probe();
939
+ consecutiveFailures = 0;
940
+ } catch (error) {
941
+ consecutiveFailures += 1;
942
+ if (consecutiveFailures >= failureThreshold) {
943
+ cancelled = true;
944
+ const detail = error instanceof Error ? error.message : String(error);
945
+ rejectFailure?.(new CfInspectorError(
946
+ "INSPECTOR_CONNECTION_FAILED",
947
+ `Inspector tunnel ${host}:${port.toString()} failed ${failureThreshold.toString()} consecutive keepalive probes and is no longer round-tripping. Retry the command after restarting or investigating the owning tunnel session.`,
948
+ detail
949
+ ));
950
+ return;
951
+ }
952
+ }
953
+ schedule();
954
+ };
955
+ schedule();
956
+ return {
957
+ failure,
958
+ cancel: () => {
959
+ cancelled = true;
960
+ if (timer !== void 0) {
961
+ clearTimeout(timer);
962
+ }
963
+ }
964
+ };
965
+ }
916
966
 
917
967
  // src/inspector/pause.ts
918
968
  init_types();
@@ -1035,10 +1085,16 @@ async function waitForLivePause(session, options, deadlineMs) {
1035
1085
  return toPauseEvent(params, receivedAtMs ?? performance2.now(), session.scripts);
1036
1086
  }
1037
1087
 
1088
+ // src/inspector/fanout.ts
1089
+ init_types();
1090
+ import { performance as performance3 } from "perf_hooks";
1091
+
1038
1092
  // src/inspector/runtime.ts
1039
1093
  init_types();
1040
1094
  async function resume(session) {
1041
1095
  await session.client.send("Debugger.resume");
1096
+ session.debuggerState.paused = false;
1097
+ delete session.debuggerState.currentPause;
1042
1098
  }
1043
1099
  async function setPauseOnExceptions(session, state) {
1044
1100
  await session.client.send("Debugger.setPauseOnExceptions", { state });
@@ -1154,8 +1210,264 @@ async function releaseObjectGroup(session, objectGroup) {
1154
1210
  await session.client.send("Runtime.releaseObjectGroup", { objectGroup });
1155
1211
  }
1156
1212
 
1213
+ // src/inspector/fanout.ts
1214
+ var DEFAULT_CLEANUP_TIMEOUT_MS = 2e3;
1215
+ var BreakpointFanout = class {
1216
+ records = /* @__PURE__ */ new Map();
1217
+ setupErrors = [];
1218
+ detach;
1219
+ detachRemoved;
1220
+ detachError;
1221
+ activeRace;
1222
+ pauseReasons = [];
1223
+ constructor(group, setupSession, pauseReasons = []) {
1224
+ this.pauseReasons = pauseReasons;
1225
+ this.detach = group.onSession((session) => {
1226
+ const record = { session, handles: [], setup: Promise.resolve() };
1227
+ this.records.set(session, record);
1228
+ record.setup = setupSession(session, (handle) => {
1229
+ this.trackHandle(session, handle);
1230
+ }).then((result) => {
1231
+ for (const handle of result.handles) {
1232
+ this.trackHandle(session, handle);
1233
+ }
1234
+ });
1235
+ const setup = record.setup;
1236
+ setup.catch((error) => {
1237
+ for (const reject of this.setupErrors) {
1238
+ reject(error);
1239
+ }
1240
+ });
1241
+ this.activeRace?.add(record);
1242
+ });
1243
+ this.detachRemoved = group.onSessionRemoved((session) => {
1244
+ this.records.delete(session);
1245
+ this.activeRace?.remove(session);
1246
+ });
1247
+ this.detachError = group.onError((error) => {
1248
+ for (const reject of this.setupErrors) {
1249
+ reject(error);
1250
+ }
1251
+ });
1252
+ }
1253
+ async ready() {
1254
+ await Promise.all([...this.records.values()].map((record) => record.setup));
1255
+ }
1256
+ trackHandle(session, handle) {
1257
+ const record = this.records.get(session);
1258
+ if (record !== void 0 && !record.handles.some((candidate) => candidate.breakpointId === handle.breakpointId)) {
1259
+ record.handles.push(handle);
1260
+ }
1261
+ }
1262
+ availableOutcomes() {
1263
+ return [...this.records.values()].map((record) => ({
1264
+ session: record.session,
1265
+ setup: { handles: record.handles }
1266
+ }));
1267
+ }
1268
+ async waitForFirst(timeoutMs, options = {}, signal) {
1269
+ if (this.activeRace !== void 0) {
1270
+ throw new CfInspectorError("INVALID_ARGUMENT", "A fan-out pause race is already active");
1271
+ }
1272
+ const race = new ActivePauseRace(timeoutMs, options, signal);
1273
+ this.pauseReasons = options.pauseReasons ?? [];
1274
+ this.activeRace = race;
1275
+ this.setupErrors.push(race.reject);
1276
+ for (const record of this.records.values()) {
1277
+ race.add(record);
1278
+ }
1279
+ try {
1280
+ const winner = await race.result;
1281
+ await race.stopAndSettle();
1282
+ await this.resumePausedLosers(winner.session);
1283
+ return winner;
1284
+ } finally {
1285
+ this.activeRace = void 0;
1286
+ const index = this.setupErrors.indexOf(race.reject);
1287
+ if (index >= 0) {
1288
+ this.setupErrors.splice(index, 1);
1289
+ }
1290
+ await race.stopAndSettle();
1291
+ }
1292
+ }
1293
+ async resumePaused(except) {
1294
+ return await this.resumePausedLosers(except, DEFAULT_CLEANUP_TIMEOUT_MS);
1295
+ }
1296
+ async cleanup(timeoutMs = DEFAULT_CLEANUP_TIMEOUT_MS, preservePaused) {
1297
+ this.detach();
1298
+ this.detachRemoved();
1299
+ this.detachError();
1300
+ const deadline = performance3.now() + timeoutMs;
1301
+ await settleWithin(Promise.allSettled([...this.records.values()].map(async (record) => {
1302
+ await record.setup;
1303
+ })), remaining(deadline));
1304
+ const breakpointEntries = [...this.records.values()].flatMap((record) => record.handles.map((handle) => ({
1305
+ session: record.session,
1306
+ breakpointId: handle.breakpointId
1307
+ })));
1308
+ let cleared = 0;
1309
+ const clearWork = Promise.allSettled(breakpointEntries.map(async (entry) => {
1310
+ await removeBreakpoint(entry.session, entry.breakpointId);
1311
+ cleared += 1;
1312
+ }));
1313
+ await settleWithin(clearWork, remaining(deadline));
1314
+ const resumed = await this.resumePausedLosers(preservePaused, remaining(deadline));
1315
+ return { attempted: breakpointEntries.length, cleared, resumed };
1316
+ }
1317
+ async resumePausedLosers(except, timeoutMs = DEFAULT_CLEANUP_TIMEOUT_MS) {
1318
+ let resumed = 0;
1319
+ await settleWithin(Promise.allSettled([...this.records.keys()].map(async (session) => {
1320
+ if (session === except || session.debuggerState.paused !== true || session.client.isClosed || !this.ownsCurrentPause(session)) {
1321
+ return;
1322
+ }
1323
+ await resume(session);
1324
+ session.debuggerState.paused = false;
1325
+ resumed += 1;
1326
+ })), timeoutMs);
1327
+ return resumed;
1328
+ }
1329
+ ownsCurrentPause(session) {
1330
+ const pause = session.debuggerState.currentPause;
1331
+ if (pause === void 0) {
1332
+ return false;
1333
+ }
1334
+ if (this.pauseReasons.includes(pause.reason)) {
1335
+ return true;
1336
+ }
1337
+ const record = this.records.get(session);
1338
+ const breakpointIds = new Set(record?.handles.map((handle) => handle.breakpointId) ?? []);
1339
+ return pause.hitBreakpoints.some((breakpointId) => breakpointIds.has(breakpointId));
1340
+ }
1341
+ };
1342
+ var ActivePauseRace = class {
1343
+ constructor(timeoutMs, options, signal) {
1344
+ this.options = options;
1345
+ this.externalSignal = signal;
1346
+ this.deadline = performance3.now() + timeoutMs;
1347
+ let resolveResult;
1348
+ let rejectResult;
1349
+ this.result = new Promise((resolve, reject) => {
1350
+ resolveResult = resolve;
1351
+ rejectResult = reject;
1352
+ });
1353
+ this.resolveResult = (winner) => {
1354
+ if (this.settled) {
1355
+ return;
1356
+ }
1357
+ this.settled = true;
1358
+ resolveResult?.(winner);
1359
+ };
1360
+ this.reject = (error) => {
1361
+ if (this.settled) {
1362
+ return;
1363
+ }
1364
+ this.settled = true;
1365
+ rejectResult?.(error);
1366
+ };
1367
+ this.timeout = setTimeout(() => {
1368
+ this.reject(this.terminalTimeoutError ?? new CfInspectorError(
1369
+ "BREAKPOINT_NOT_HIT",
1370
+ `Timed out waiting for a matching pause in any isolate after ${timeoutMs.toString()}ms`
1371
+ ));
1372
+ this.controller.abort();
1373
+ }, timeoutMs + 25);
1374
+ if (signal !== void 0) {
1375
+ if (signal.aborted) {
1376
+ this.reject(new CfInspectorError("ABORTED", "Aborted while waiting for an isolate pause"));
1377
+ } else {
1378
+ signal.addEventListener("abort", this.onExternalAbort, { once: true });
1379
+ }
1380
+ }
1381
+ }
1382
+ options;
1383
+ controller = new AbortController();
1384
+ waits = /* @__PURE__ */ new Set();
1385
+ settled = false;
1386
+ deadline;
1387
+ timeout;
1388
+ externalSignal;
1389
+ onExternalAbort = () => {
1390
+ this.reject(new CfInspectorError("ABORTED", "Aborted while waiting for an isolate pause"));
1391
+ this.controller.abort();
1392
+ };
1393
+ resolveResult;
1394
+ terminalTimeoutError;
1395
+ removedSessions = /* @__PURE__ */ new Set();
1396
+ reject;
1397
+ result;
1398
+ add(record) {
1399
+ if (this.settled) {
1400
+ return;
1401
+ }
1402
+ const wait = this.wait(record).finally(() => {
1403
+ this.waits.delete(wait);
1404
+ });
1405
+ this.waits.add(wait);
1406
+ }
1407
+ remove(session) {
1408
+ this.removedSessions.add(session);
1409
+ }
1410
+ async stopAndSettle() {
1411
+ clearTimeout(this.timeout);
1412
+ this.externalSignal?.removeEventListener("abort", this.onExternalAbort);
1413
+ this.controller.abort();
1414
+ await Promise.allSettled([...this.waits]);
1415
+ }
1416
+ async wait(record) {
1417
+ try {
1418
+ await record.setup;
1419
+ const remainingMs = Math.max(1, this.deadline - performance3.now());
1420
+ const pause = await waitForPause(record.session, {
1421
+ ...this.options,
1422
+ timeoutMs: remainingMs,
1423
+ breakpointIds: record.handles.map((handle) => handle.breakpointId),
1424
+ signal: this.controller.signal
1425
+ });
1426
+ record.session.debuggerState.paused = true;
1427
+ this.resolveResult({ session: record.session, pause });
1428
+ this.controller.abort();
1429
+ } catch (error) {
1430
+ if (this.removedSessions.has(record.session)) {
1431
+ return;
1432
+ }
1433
+ if (error instanceof CfInspectorError && error.code === "UNRELATED_PAUSE_TIMEOUT") {
1434
+ this.terminalTimeoutError = error;
1435
+ return;
1436
+ }
1437
+ if (isExpectedRaceStop(error)) {
1438
+ return;
1439
+ }
1440
+ this.reject(error);
1441
+ this.controller.abort();
1442
+ }
1443
+ }
1444
+ };
1445
+ function isExpectedRaceStop(error) {
1446
+ return error instanceof CfInspectorError && (error.code === "ABORTED" || error.code === "BREAKPOINT_NOT_HIT" || error.code === "UNRELATED_PAUSE_TIMEOUT");
1447
+ }
1448
+ function remaining(deadline) {
1449
+ return Math.max(0, deadline - performance3.now());
1450
+ }
1451
+ async function settleWithin(work, timeoutMs) {
1452
+ let timer;
1453
+ try {
1454
+ return await Promise.race([
1455
+ work,
1456
+ new Promise((resolve) => {
1457
+ timer = setTimeout(() => {
1458
+ resolve(null);
1459
+ }, timeoutMs);
1460
+ })
1461
+ ]);
1462
+ } finally {
1463
+ if (timer !== void 0) {
1464
+ clearTimeout(timer);
1465
+ }
1466
+ }
1467
+ }
1468
+
1157
1469
  // src/inspector/session.ts
1158
- import { performance as performance3 } from "perf_hooks";
1470
+ import { performance as performance4 } from "perf_hooks";
1159
1471
 
1160
1472
  // src/cdp/client.ts
1161
1473
  init_types();
@@ -1518,6 +1830,7 @@ init_types();
1518
1830
  var DEFAULT_CONNECT_TIMEOUT_MS = 5e3;
1519
1831
  var DEFAULT_HOST = "127.0.0.1";
1520
1832
  var PAUSE_BUFFER_LIMIT = 32;
1833
+ var WORKER_DISCOVERY_SETTLE_MS = 500;
1521
1834
  var NodeWorkerDiscovery = class {
1522
1835
  constructor(client) {
1523
1836
  this.client = client;
@@ -1526,12 +1839,18 @@ var NodeWorkerDiscovery = class {
1526
1839
  const worker = toInspectorWorkerTarget(raw);
1527
1840
  if (worker !== void 0) {
1528
1841
  this.workers.set(worker.sessionId, worker);
1842
+ for (const listener of this.attachedListeners) {
1843
+ listener(worker);
1844
+ }
1529
1845
  }
1530
1846
  }),
1531
1847
  client.on("NodeWorker.detachedFromWorker", (raw) => {
1532
1848
  const sessionId = readField(raw, "sessionId");
1533
1849
  if (typeof sessionId === "string") {
1534
1850
  this.workers.delete(sessionId);
1851
+ for (const listener of this.detachedListeners) {
1852
+ listener(sessionId);
1853
+ }
1535
1854
  }
1536
1855
  })
1537
1856
  ];
@@ -1541,6 +1860,8 @@ var NodeWorkerDiscovery = class {
1541
1860
  detachListeners;
1542
1861
  supported = false;
1543
1862
  disposed = false;
1863
+ attachedListeners = /* @__PURE__ */ new Set();
1864
+ detachedListeners = /* @__PURE__ */ new Set();
1544
1865
  async enable() {
1545
1866
  try {
1546
1867
  await this.client.send("NodeWorker.enable", { waitForDebuggerOnStart: false });
@@ -1554,6 +1875,45 @@ var NodeWorkerDiscovery = class {
1554
1875
  list() {
1555
1876
  return [...this.workers.values()].sort(compareWorkers);
1556
1877
  }
1878
+ onAttached(listener) {
1879
+ this.attachedListeners.add(listener);
1880
+ return () => {
1881
+ this.attachedListeners.delete(listener);
1882
+ };
1883
+ }
1884
+ onDetached(listener) {
1885
+ this.detachedListeners.add(listener);
1886
+ return () => {
1887
+ this.detachedListeners.delete(listener);
1888
+ };
1889
+ }
1890
+ async waitFor(predicate, timeoutMs = WORKER_DISCOVERY_SETTLE_MS) {
1891
+ const existing = predicate(this.list());
1892
+ if (existing !== void 0) {
1893
+ return existing;
1894
+ }
1895
+ return await new Promise((resolve) => {
1896
+ let settled = false;
1897
+ const finish = (worker) => {
1898
+ if (settled) {
1899
+ return;
1900
+ }
1901
+ settled = true;
1902
+ clearTimeout(timer);
1903
+ detach();
1904
+ resolve(worker);
1905
+ };
1906
+ const detach = this.onAttached(() => {
1907
+ const worker = predicate(this.list());
1908
+ if (worker !== void 0) {
1909
+ finish(worker);
1910
+ }
1911
+ });
1912
+ const timer = setTimeout(() => {
1913
+ finish();
1914
+ }, timeoutMs);
1915
+ });
1916
+ }
1557
1917
  async dispose() {
1558
1918
  if (this.disposed) {
1559
1919
  return;
@@ -1568,6 +1928,8 @@ var NodeWorkerDiscovery = class {
1568
1928
  for (const detach of this.detachListeners) {
1569
1929
  detach();
1570
1930
  }
1931
+ this.attachedListeners.clear();
1932
+ this.detachedListeners.clear();
1571
1933
  }
1572
1934
  };
1573
1935
  function isUnsupportedNodeWorkerDomain(error) {
@@ -1632,14 +1994,15 @@ async function connectInspector(options) {
1632
1994
  let workerDiscovery;
1633
1995
  try {
1634
1996
  workerDiscovery = await startNodeWorkerDiscovery(client);
1635
- if (options.workerIndex === void 0) {
1636
- const session = await initSession(client, target);
1997
+ if (options.workerIndex === void 0 && options.workerId === void 0) {
1998
+ const session = await initSession(client, target, { kind: "main" });
1637
1999
  return withWorkerMetadata(session, workerDiscovery, targetIndex, targets.length);
1638
2000
  }
1639
2001
  return await initWorkerSession(
1640
2002
  client,
1641
2003
  workerDiscovery,
1642
2004
  options.workerIndex,
2005
+ options.workerId,
1643
2006
  targetIndex,
1644
2007
  targets.length
1645
2008
  );
@@ -1649,25 +2012,207 @@ async function connectInspector(options) {
1649
2012
  throw err;
1650
2013
  }
1651
2014
  }
1652
- async function initWorkerSession(parent, discovery, workerIndex, targetIndex, targetCount) {
1653
- const workers = discovery.list();
2015
+ async function initWorkerSession(parent, discovery, workerIndex, workerId, targetIndex, targetCount) {
1654
2016
  if (!discovery.supported) {
1655
2017
  throw new CfInspectorError(
1656
2018
  "INSPECTOR_DISCOVERY_FAILED",
1657
2019
  "This runtime does not expose the NodeWorker CDP domain; --worker cannot be used. Run list-targets for available raw targets."
1658
2020
  );
1659
2021
  }
1660
- const worker = workers[workerIndex];
2022
+ const worker = await discovery.waitFor((workers) => workerId === void 0 ? workerIndex === void 0 ? void 0 : workers[workerIndex] : workers.find((candidate) => candidate.workerId === workerId));
1661
2023
  if (worker === void 0) {
2024
+ const workers = discovery.list();
2025
+ const selector = workerId === void 0 ? `index ${(workerIndex ?? 0).toString()}` : `workerId ${JSON.stringify(workerId)}`;
1662
2026
  throw new CfInspectorError(
1663
2027
  "INSPECTOR_DISCOVERY_FAILED",
1664
- `No NodeWorker sub-session at index ${workerIndex.toString()} (available: ${workers.length.toString()}). Ensure the worker is alive, then rerun list-targets.`
2028
+ `No NodeWorker sub-session with ${selector} is currently attached (available: ${workers.length.toString()}). Ensure the worker is alive, then rerun list-targets.`
1665
2029
  );
1666
2030
  }
1667
2031
  const client = await createNodeWorkerClient(parent, worker.sessionId);
1668
- const session = await initSession(client, workerToInspectorTarget(worker));
2032
+ const session = await initSession(client, workerToInspectorTarget(worker), {
2033
+ kind: "worker",
2034
+ workerId: worker.workerId
2035
+ });
1669
2036
  return withWorkerMetadata(session, discovery, targetIndex, targetCount, workerIndex, parent);
1670
2037
  }
2038
+ async function connectInspectorGroup(options) {
2039
+ const host = options.host ?? DEFAULT_HOST;
2040
+ const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
2041
+ const targets = await discoverInspectorTargets(host, options.port, connectTimeoutMs);
2042
+ const targetIndex = options.targetIndex ?? 0;
2043
+ const target = targets[targetIndex];
2044
+ if (target === void 0) {
2045
+ throw new CfInspectorError(
2046
+ "INSPECTOR_DISCOVERY_FAILED",
2047
+ `No inspector target at index ${targetIndex.toString()} on ${host}:${options.port.toString()} (available: ${targets.length.toString()})`
2048
+ );
2049
+ }
2050
+ const parent = await CdpClient.connect({ url: target.webSocketDebuggerUrl, connectTimeoutMs });
2051
+ let discovery;
2052
+ try {
2053
+ discovery = await startNodeWorkerDiscovery(parent);
2054
+ const main = await initSession(parent, target, { kind: "main" });
2055
+ const group = new DynamicInspectorSessionGroup(
2056
+ parent,
2057
+ discovery,
2058
+ main,
2059
+ targetIndex,
2060
+ targets.length
2061
+ );
2062
+ await group.initialize();
2063
+ return group;
2064
+ } catch (error) {
2065
+ await discovery?.dispose();
2066
+ parent.dispose();
2067
+ throw error;
2068
+ }
2069
+ }
2070
+ var DynamicInspectorSessionGroup = class {
2071
+ constructor(parent, discovery, main, targetIndex, targetCount) {
2072
+ this.parent = parent;
2073
+ this.discovery = discovery;
2074
+ this.targetIndex = targetIndex;
2075
+ this.targetCount = targetCount;
2076
+ this.workerDiscoverySupported = discovery.supported;
2077
+ this.sessions.set("main", main);
2078
+ this.detachDiscoveryListeners = [
2079
+ discovery.onAttached((worker) => {
2080
+ this.queueWorker(worker);
2081
+ }),
2082
+ discovery.onDetached((sessionId) => {
2083
+ void this.detachWorker(sessionId);
2084
+ })
2085
+ ];
2086
+ }
2087
+ parent;
2088
+ discovery;
2089
+ targetIndex;
2090
+ targetCount;
2091
+ sessions = /* @__PURE__ */ new Map();
2092
+ listeners = /* @__PURE__ */ new Set();
2093
+ removedListeners = /* @__PURE__ */ new Set();
2094
+ errorListeners = /* @__PURE__ */ new Set();
2095
+ pending = /* @__PURE__ */ new Set();
2096
+ detachedSessionIds = /* @__PURE__ */ new Set();
2097
+ detachDiscoveryListeners;
2098
+ initializationError;
2099
+ initializing = true;
2100
+ disposed = false;
2101
+ workerDiscoverySupported;
2102
+ async initialize() {
2103
+ for (const worker of this.discovery.list()) {
2104
+ this.queueWorker(worker);
2105
+ }
2106
+ await this.waitForPending();
2107
+ this.initializing = false;
2108
+ if (this.initializationError !== void 0) {
2109
+ throw this.initializationError;
2110
+ }
2111
+ }
2112
+ list() {
2113
+ return [...this.sessions.values()];
2114
+ }
2115
+ onSession(listener) {
2116
+ this.listeners.add(listener);
2117
+ for (const session of this.sessions.values()) {
2118
+ listener(session);
2119
+ }
2120
+ return () => {
2121
+ this.listeners.delete(listener);
2122
+ };
2123
+ }
2124
+ onSessionRemoved(listener) {
2125
+ this.removedListeners.add(listener);
2126
+ return () => {
2127
+ this.removedListeners.delete(listener);
2128
+ };
2129
+ }
2130
+ onError(listener) {
2131
+ this.errorListeners.add(listener);
2132
+ return () => {
2133
+ this.errorListeners.delete(listener);
2134
+ };
2135
+ }
2136
+ async dispose() {
2137
+ if (this.disposed) {
2138
+ return;
2139
+ }
2140
+ this.disposed = true;
2141
+ for (const detach of this.detachDiscoveryListeners) {
2142
+ detach();
2143
+ }
2144
+ await Promise.allSettled([...this.pending]);
2145
+ const sessions = [...this.list()].reverse();
2146
+ for (const session of sessions) {
2147
+ try {
2148
+ await session.dispose();
2149
+ } catch {
2150
+ }
2151
+ }
2152
+ this.sessions.clear();
2153
+ this.listeners.clear();
2154
+ this.removedListeners.clear();
2155
+ this.errorListeners.clear();
2156
+ await this.discovery.dispose();
2157
+ this.parent.dispose();
2158
+ }
2159
+ queueWorker(worker) {
2160
+ if (this.disposed || this.sessions.has(worker.sessionId)) {
2161
+ return;
2162
+ }
2163
+ const pending = this.attachWorker(worker).catch((error) => {
2164
+ const normalized = error instanceof Error ? error : new Error("Worker inspector attachment failed");
2165
+ if (this.initializing && this.initializationError === void 0) {
2166
+ this.initializationError = normalized;
2167
+ }
2168
+ for (const listener of this.errorListeners) {
2169
+ listener(normalized);
2170
+ }
2171
+ }).finally(() => {
2172
+ this.pending.delete(pending);
2173
+ });
2174
+ this.pending.add(pending);
2175
+ }
2176
+ async attachWorker(worker) {
2177
+ const client = await createNodeWorkerClient(this.parent, worker.sessionId);
2178
+ try {
2179
+ const session = await initSession(client, workerToInspectorTarget(worker), {
2180
+ kind: "worker",
2181
+ workerId: worker.workerId
2182
+ });
2183
+ if (this.disposed || this.detachedSessionIds.delete(worker.sessionId)) {
2184
+ await session.dispose();
2185
+ return;
2186
+ }
2187
+ this.sessions.set(worker.sessionId, session);
2188
+ for (const listener of this.listeners) {
2189
+ listener(session);
2190
+ }
2191
+ } catch (error) {
2192
+ client.dispose();
2193
+ if (!this.disposed) {
2194
+ throw error;
2195
+ }
2196
+ }
2197
+ }
2198
+ async detachWorker(sessionId) {
2199
+ const session = this.sessions.get(sessionId);
2200
+ if (session === void 0) {
2201
+ this.detachedSessionIds.add(sessionId);
2202
+ return;
2203
+ }
2204
+ this.sessions.delete(sessionId);
2205
+ for (const listener of this.removedListeners) {
2206
+ listener(session);
2207
+ }
2208
+ await session.dispose();
2209
+ }
2210
+ async waitForPending() {
2211
+ while (this.pending.size > 0) {
2212
+ await Promise.all([...this.pending]);
2213
+ }
2214
+ }
2215
+ };
1671
2216
  function withWorkerMetadata(session, discovery, targetIndex, targetCount, workerIndex, parent) {
1672
2217
  return {
1673
2218
  ...session,
@@ -1693,16 +2238,16 @@ function workerToInspectorTarget(worker) {
1693
2238
  webSocketDebuggerUrl: `node-worker://${worker.sessionId}`
1694
2239
  };
1695
2240
  }
1696
- async function initSession(client, target) {
2241
+ async function initSession(client, target, isolate) {
1697
2242
  const scripts = /* @__PURE__ */ new Map();
1698
2243
  registerScriptTracking(client, scripts);
1699
2244
  const pauseBuffer = [];
1700
2245
  const pauseWaitGate = { active: false };
1701
- const debuggerState = {};
2246
+ const debuggerState = { paused: false };
1702
2247
  registerPauseTracking(client, scripts, pauseBuffer, pauseWaitGate, debuggerState);
1703
2248
  await client.send("Runtime.enable");
1704
2249
  await client.send("Debugger.enable");
1705
- return createSession(client, target, scripts, pauseBuffer, pauseWaitGate, debuggerState);
2250
+ return createSession(client, target, isolate, scripts, pauseBuffer, pauseWaitGate, debuggerState);
1706
2251
  }
1707
2252
  function registerScriptTracking(client, scripts) {
1708
2253
  client.on("Debugger.scriptParsed", (raw) => {
@@ -1714,23 +2259,28 @@ function registerScriptTracking(client, scripts) {
1714
2259
  }
1715
2260
  function registerPauseTracking(client, scripts, pauseBuffer, pauseWaitGate, debuggerState) {
1716
2261
  client.on("Debugger.paused", (raw) => {
2262
+ const event = toPauseEvent(raw, performance4.now(), scripts);
2263
+ debuggerState.paused = true;
2264
+ debuggerState.currentPause = event;
1717
2265
  if (pauseWaitGate.active) {
1718
2266
  return;
1719
2267
  }
1720
- const event = toPauseEvent(raw, performance3.now(), scripts);
1721
2268
  if (pauseBuffer.length >= PAUSE_BUFFER_LIMIT) {
1722
2269
  pauseBuffer.shift();
1723
2270
  }
1724
2271
  pauseBuffer.push(event);
1725
2272
  });
1726
2273
  client.on("Debugger.resumed", () => {
1727
- debuggerState.lastResumedAtMs = performance3.now();
2274
+ debuggerState.paused = false;
2275
+ delete debuggerState.currentPause;
2276
+ debuggerState.lastResumedAtMs = performance4.now();
1728
2277
  });
1729
2278
  }
1730
- function createSession(client, target, scripts, pauseBuffer, pauseWaitGate, debuggerState) {
2279
+ function createSession(client, target, isolate, scripts, pauseBuffer, pauseWaitGate, debuggerState) {
1731
2280
  return {
1732
2281
  client,
1733
2282
  target,
2283
+ isolate,
1734
2284
  scripts,
1735
2285
  pauseBuffer,
1736
2286
  pauseWaitGate,
@@ -2766,6 +3316,8 @@ async function waitForStop(session, options, registerMaxEventsSignal) {
2766
3316
 
2767
3317
  // src/cf/tunnel.ts
2768
3318
  import { startDebugger } from "@saptools/cf-debugger";
3319
+ init_types();
3320
+ var REUSED_TUNNEL_GRACE_MS = 5e3;
2769
3321
  function targetOptions(target) {
2770
3322
  return {
2771
3323
  ...target.apiEndpoint === void 0 ? {} : { apiEndpoint: target.apiEndpoint },
@@ -2827,11 +3379,22 @@ async function openCfTunnel(target) {
2827
3379
  if (localPort === void 0) {
2828
3380
  throw error;
2829
3381
  }
3382
+ try {
3383
+ await fetchInspectorVersion("127.0.0.1", localPort, REUSED_TUNNEL_GRACE_MS);
3384
+ } catch (livenessError) {
3385
+ const detail = livenessError instanceof Error ? livenessError.message : String(livenessError);
3386
+ throw new CfInspectorError(
3387
+ "INSPECTOR_DISCOVERY_FAILED",
3388
+ `Another debugger session claims tunnel port ${localPort.toString()}, but its Node inspector did not respond within ${REUSED_TUNNEL_GRACE_MS.toString()}ms. The tunnel may be stale or still finishing setup; retry shortly, or inspect and stop the owning cf-debugger session before opening a fresh tunnel.`,
3389
+ detail
3390
+ );
3391
+ }
2830
3392
  target.onStatus?.("ready", `Reusing existing tunnel on port ${localPort.toString()}`);
2831
3393
  return { localPort, dispose: () => Promise.resolve() };
2832
3394
  }
2833
3395
  }
2834
3396
  export {
3397
+ BreakpointFanout,
2835
3398
  CfInspectorError,
2836
3399
  buildBreakpointUrlRegex,
2837
3400
  buildHitCountedCondition,
@@ -2839,6 +3402,7 @@ export {
2839
3402
  captureException,
2840
3403
  captureSnapshot,
2841
3404
  connectInspector,
3405
+ connectInspectorGroup,
2842
3406
  discoverInspectorTargets,
2843
3407
  evaluateGlobal,
2844
3408
  evaluateOnFrame,
@@ -2860,6 +3424,7 @@ export {
2860
3424
  setBreakpoint,
2861
3425
  setBreakpointAtLocation,
2862
3426
  setPauseOnExceptions,
3427
+ startInspectorKeepalive,
2863
3428
  stepInto,
2864
3429
  stepOut,
2865
3430
  stepOver,