@orkestrel/browser 0.0.28 → 0.0.30

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.
@@ -12,11 +12,12 @@ let node_https = require("node:https");
12
12
  let _orkestrel_emitter = require("@orkestrel/emitter");
13
13
  let _orkestrel_websocket = require("@orkestrel/websocket");
14
14
  let node_events = require("node:events");
15
+ let _orkestrel_process_server = require("@orkestrel/process/server");
15
16
  let _orkestrel_mcp = require("@orkestrel/mcp");
16
17
  let _orkestrel_pool = require("@orkestrel/pool");
17
18
  let _orkestrel_mcp_server = require("@orkestrel/mcp/server");
18
19
  let _orkestrel_tool = require("@orkestrel/tool");
19
- //#region src/server/constants.ts
20
+ // src/server/constants.ts
20
21
  /**
21
22
  * Sets the default CDP port probed for an existing browser and used for launches, `9222`.
22
23
  */
@@ -332,8 +333,43 @@ var BROWSER_SERVER_COPY = Object.freeze({
332
333
  annotations: Object.freeze({ pure: false })
333
334
  })
334
335
  });
335
- //#endregion
336
- //#region src/server/helpers.ts
336
+ // src/server/BrowserEndpoint.ts
337
+ /** Reads the announced CDP endpoint while continuing to drain the browser's standard error. */
338
+ var BrowserEndpoint = class {
339
+ #completion = Promise.withResolvers();
340
+ #pending = "";
341
+ #endpoint;
342
+ constructor(stream, signal) {
343
+ if (signal.aborted) this.#completion.reject(signal.reason);
344
+ signal.addEventListener("abort", () => this.#completion.reject(signal.reason), { once: true });
345
+ stream.on("error", this.#completion.reject);
346
+ stream.setEncoding("utf8");
347
+ stream.on("data", this.#handleChunk.bind(this));
348
+ stream.on("close", this.#handleClose.bind(this));
349
+ }
350
+ get endpoint() {
351
+ return this.#completion.promise;
352
+ }
353
+ #handleChunk(chunk) {
354
+ if (this.#endpoint !== void 0 || !(0, _orkestrel_contract.isString)(chunk)) return;
355
+ const lines = (this.#pending + chunk).split(/\r\n|\n/);
356
+ this.#pending = lines.pop() ?? "";
357
+ for (const line of lines) {
358
+ const endpoint = BROWSER_DEVTOOLS_PATTERN.exec(line)?.[1];
359
+ if (endpoint === void 0) continue;
360
+ this.#endpoint = endpoint;
361
+ this.#pending = "";
362
+ this.#completion.resolve(endpoint);
363
+ return;
364
+ }
365
+ }
366
+ #handleClose() {
367
+ const endpoint = this.#endpoint === void 0 ? BROWSER_DEVTOOLS_PATTERN.exec(this.#pending)?.[1] : void 0;
368
+ if (endpoint !== void 0) this.#completion.resolve(endpoint);
369
+ else this.#completion.reject(new _src_core.BrowserError("CONNECTION", "Browser closed its standard error before reporting a CDP endpoint"));
370
+ }
371
+ };
372
+ // src/server/helpers.ts
337
373
  /**
338
374
  * Parses positive integer viewport dimensions in WIDTHxHEIGHT form.
339
375
  * @param value - The viewport environment variable, or undefined when unset
@@ -354,21 +390,6 @@ function parseBrowserViewport(value) {
354
390
  };
355
391
  }
356
392
  /**
357
- * Probes whether a process has not been confirmed absent.
358
- * @param pid - Process identifier to probe with signal zero
359
- * @returns True if the host has not confirmed absence with ESRCH; false otherwise
360
- * @example
361
- * probeProcess(process.pid) // true
362
- */
363
- function probeProcess(pid) {
364
- try {
365
- process.kill(pid, 0);
366
- return true;
367
- } catch (error) {
368
- return !((0, _orkestrel_contract.isError)(error) && "code" in error && error.code === "ESRCH");
369
- }
370
- }
371
- /**
372
393
  * Parses a profile record naming a positive process identifier and a loopback DevTools endpoint.
373
394
  * @param text - JSON record read from a browse profile
374
395
  * @returns The record, or undefined when its shape or endpoint is refused
@@ -700,32 +721,7 @@ function launchBrowserProcess(executable, port, headless, profile, extra) {
700
721
  * @throws When the stream closes without the line, or the signal aborts first
701
722
  */
702
723
  function readBrowserEndpoint(stream, signal) {
703
- const read = Promise.withResolvers();
704
- let pending = "";
705
- let found = false;
706
- if (signal.aborted) read.reject(signal.reason);
707
- signal.addEventListener("abort", () => read.reject(signal.reason), { once: true });
708
- stream.on("error", read.reject);
709
- stream.setEncoding("utf8");
710
- stream.on("data", (chunk) => {
711
- if (found || !(0, _orkestrel_contract.isString)(chunk)) return;
712
- const lines = (pending + chunk).split(/\r\n|\n/);
713
- pending = lines.pop() ?? "";
714
- for (const line of lines) {
715
- const endpoint = BROWSER_DEVTOOLS_PATTERN.exec(line)?.[1];
716
- if (endpoint === void 0) continue;
717
- found = true;
718
- pending = "";
719
- read.resolve(endpoint);
720
- return;
721
- }
722
- });
723
- stream.on("close", () => {
724
- const endpoint = found ? void 0 : BROWSER_DEVTOOLS_PATTERN.exec(pending)?.[1];
725
- if (endpoint !== void 0) read.resolve(endpoint);
726
- else read.reject(new _src_core.BrowserError("CONNECTION", "Browser closed its standard error before reporting a CDP endpoint"));
727
- });
728
- return read.promise;
724
+ return new BrowserEndpoint(stream, signal).endpoint;
729
725
  }
730
726
  /**
731
727
  * Fetches the current CDP target list from a browser's `/json/list` endpoint, as a `Result`
@@ -796,8 +792,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
796
792
  clearTimeout(timer);
797
793
  }
798
794
  }
799
- //#endregion
800
- //#region src/server/transports/WebSocketCDPTransport.ts
795
+ // src/server/transports/WebSocketCDPTransport.ts
801
796
  /**
802
797
  * Provides a raw CDP text transport backed by `@orkestrel/websocket`.
803
798
  *
@@ -991,8 +986,7 @@ var WebSocketCDPTransport = class {
991
986
  this.#closeResolve?.();
992
987
  }
993
988
  };
994
- //#endregion
995
- //#region src/server/writers/FileBrowserWriter.ts
989
+ // src/server/writers/FileBrowserWriter.ts
996
990
  /**
997
991
  * Persists captured browser bytes to the filesystem through `node:fs/promises`.
998
992
  *
@@ -1021,11 +1015,35 @@ var FileBrowserWriter = class {
1021
1015
  await (0, node_fs_promises.writeFile)(path, data);
1022
1016
  }
1023
1017
  };
1024
- //#endregion
1025
- //#region package.json
1026
- var version = "0.0.28";
1027
- //#endregion
1028
- //#region src/server/BrowserMCPServer.ts
1018
+ // src/server/BrowserProcessDrain.ts
1019
+ /** Waits for a process remainder whose host exposes no exit event. */
1020
+ var BrowserProcessDrain = class {
1021
+ #inspect;
1022
+ #deadline;
1023
+ #result;
1024
+ #confirmed = false;
1025
+ constructor(inspect, timeout) {
1026
+ this.#inspect = inspect;
1027
+ this.#deadline = performance.now() + timeout;
1028
+ this.#result = this.#wait();
1029
+ }
1030
+ get result() {
1031
+ return this.#result;
1032
+ }
1033
+ async #wait() {
1034
+ while (true) {
1035
+ const alive = this.#inspect();
1036
+ if (alive === false) return true;
1037
+ if (alive === true) this.#confirmed = true;
1038
+ const remaining = this.#deadline - performance.now();
1039
+ if (remaining <= 0) return this.#confirmed ? false : void 0;
1040
+ await new Promise((resolve) => setTimeout(resolve, Math.min(100, remaining)));
1041
+ }
1042
+ }
1043
+ };
1044
+ // package.json
1045
+ var version = "0.0.30";
1046
+ // src/server/BrowserMCPServer.ts
1029
1047
  /**
1030
1048
  * Implements `BrowserMCPServerInterface`: serves the browser vocabulary and the journey tools over
1031
1049
  * the Model Context Protocol on stdio, and warms Chromium at server start.
@@ -1245,7 +1263,7 @@ var BrowserMCPServer = class {
1245
1263
  for (const folder of this.#folders) {
1246
1264
  try {
1247
1265
  const record = parseBrowserProfileRecord(await (0, node_fs_promises.readFile)((0, node_path.join)(folder, BROWSER_SERVER_RECORD), "utf8"));
1248
- if (record === void 0 || probeProcess(record.pid)) continue;
1266
+ if (record === void 0 || (0, _orkestrel_process_server.inspectProcess)(record.pid) !== false) continue;
1249
1267
  } catch (error) {
1250
1268
  if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ENOENT") continue;
1251
1269
  }
@@ -1403,16 +1421,34 @@ var BrowserMCPServer = class {
1403
1421
  }
1404
1422
  async #perform(holder, name, args, context) {
1405
1423
  const signal = holder === this.#shared ? context.signal : AbortSignal.any([context.signal, holder.abort.signal]);
1406
- let lease;
1424
+ const lease = await this.#serveCall(holder, context, signal);
1425
+ const result = await this.#executeCall(lease, name, args, context, signal);
1426
+ context.signal.throwIfAborted();
1427
+ if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call");
1428
+ if (!result.success) {
1429
+ if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
1430
+ await this.#race(this.#ping(lease.token.value), context.signal);
1431
+ } catch {}
1432
+ context.signal.throwIfAborted();
1433
+ if (this.#leases.get(holder) !== lease) {
1434
+ const loss = this.#departures.get(lease);
1435
+ throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease));
1436
+ }
1437
+ throw new _src_core.BrowserError("PROTOCOL", (0, _src_core.boundBrowserText)(this.#annotate(holder, result.error, lease), _src_core.BROWSER_TOOL_LIMIT, _src_core.BROWSER_TOOL_CUT_FOOTER));
1438
+ }
1439
+ return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
1440
+ }
1441
+ async #serveCall(holder, context, signal) {
1407
1442
  try {
1408
- lease = await this.#serve(holder, signal);
1443
+ return await this.#serve(holder, signal);
1409
1444
  } catch (error) {
1410
1445
  context.signal.throwIfAborted();
1411
1446
  if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, `${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`);
1412
1447
  if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(error.code, this.#annotate(holder, `${error.code}: ${error.message}`));
1413
1448
  throw error;
1414
1449
  }
1415
- let result;
1450
+ }
1451
+ async #executeCall(lease, name, args, context, signal) {
1416
1452
  try {
1417
1453
  const execution = lease.generation.toolset.tools.execute({
1418
1454
  id: name,
@@ -1424,29 +1460,15 @@ var BrowserMCPServer = class {
1424
1460
  });
1425
1461
  const outcome = await (name === "replay" || name === "forget" ? execution : this.#race(execution, signal));
1426
1462
  if (outcome === void 0) throw this.#ended();
1427
- result = outcome;
1463
+ return outcome;
1428
1464
  } catch (error) {
1429
- result = {
1465
+ return {
1430
1466
  id: name,
1431
1467
  name,
1432
1468
  success: false,
1433
1469
  error: (0, _orkestrel_contract.isError)(error) ? error.message : String(error)
1434
1470
  };
1435
1471
  }
1436
- context.signal.throwIfAborted();
1437
- if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call");
1438
- if (!result.success) {
1439
- if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
1440
- await this.#race(this.#ping(lease.token.value), context.signal);
1441
- } catch {}
1442
- context.signal.throwIfAborted();
1443
- if (this.#leases.get(holder) !== lease) {
1444
- const loss = this.#departures.get(lease);
1445
- throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease));
1446
- }
1447
- throw new _src_core.BrowserError("PROTOCOL", (0, _src_core.boundBrowserText)(this.#annotate(holder, result.error, lease), _src_core.BROWSER_TOOL_LIMIT, _src_core.BROWSER_TOOL_CUT_FOOTER));
1448
- }
1449
- return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
1450
1472
  }
1451
1473
  #annotate(holder, value, lease) {
1452
1474
  const notes = [];
@@ -1485,23 +1507,35 @@ var BrowserMCPServer = class {
1485
1507
  async #grant(holder, signal) {
1486
1508
  const held = this.#leases.get(holder);
1487
1509
  if (held !== void 0) return held;
1488
- let granting = this.#grants.get(holder);
1489
- if (granting === void 0) {
1490
- granting = Promise.resolve().then(async () => {
1491
- await Promise.allSettled(this.#drains.get(holder) ?? []);
1492
- if (this.#retentions.has(holder)) throw this.#unavailable(this.#retentions.get(holder));
1493
- const token = await this.#pool.acquire(holder.abort.signal);
1494
- return this.#hold(holder, token);
1495
- });
1496
- this.#grants.set(holder, granting);
1497
- granting.finally(() => {
1498
- if (this.#grants.get(holder) === granting) this.#grants.delete(holder);
1499
- }).catch(() => void 0);
1500
- }
1510
+ const granting = this.#grants.get(holder) ?? this.#beginGrant(holder);
1501
1511
  const lease = await this.#race(granting, signal);
1502
1512
  if (lease === void 0) throw this.#ended();
1503
1513
  return lease;
1504
1514
  }
1515
+ async #prepareGeneration(slot, token) {
1516
+ const prepared = this.#prepared.get(slot);
1517
+ this.#prepared.delete(slot);
1518
+ try {
1519
+ return prepared ?? await this.#build(slot, token);
1520
+ } catch (error) {
1521
+ if (this.#losses.has(slot)) await this.#endings.get(slot)?.promise;
1522
+ else token.release();
1523
+ throw this.#unavailable(error);
1524
+ }
1525
+ }
1526
+ #beginGrant(holder) {
1527
+ const granting = Promise.resolve().then(async () => {
1528
+ await Promise.allSettled(this.#drains.get(holder) ?? []);
1529
+ if (this.#retentions.has(holder)) throw this.#unavailable(this.#retentions.get(holder));
1530
+ const token = await this.#pool.acquire(holder.abort.signal);
1531
+ return this.#hold(holder, token);
1532
+ });
1533
+ this.#grants.set(holder, granting);
1534
+ granting.finally(() => {
1535
+ if (this.#grants.get(holder) === granting) this.#grants.delete(holder);
1536
+ }).catch(() => void 0);
1537
+ return granting;
1538
+ }
1505
1539
  async #hold(holder, token) {
1506
1540
  const slot = token.value;
1507
1541
  if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted) {
@@ -1517,15 +1551,7 @@ var BrowserMCPServer = class {
1517
1551
  token.release();
1518
1552
  throw this.#unavailable(this.#losses.get(slot)?.cause);
1519
1553
  }
1520
- let generation = this.#prepared.get(slot);
1521
- this.#prepared.delete(slot);
1522
- try {
1523
- generation ??= await this.#build(slot, token);
1524
- } catch (error) {
1525
- if (this.#losses.has(slot)) await this.#endings.get(slot)?.promise;
1526
- else token.release();
1527
- throw this.#unavailable(error);
1528
- }
1554
+ const generation = await this.#prepareGeneration(slot, token);
1529
1555
  const lease = {
1530
1556
  token,
1531
1557
  generation
@@ -1701,9 +1727,9 @@ var BrowserMCPServer = class {
1701
1727
  } else if (removed) lease.token.release();
1702
1728
  }
1703
1729
  #clean(slot, generation) {
1704
- let cleaning = this.#cleaning.get(generation);
1705
- if (cleaning !== void 0) return cleaning;
1706
- cleaning = Promise.resolve().then(async () => {
1730
+ const pending = this.#cleaning.get(generation);
1731
+ if (pending !== void 0) return pending;
1732
+ const cleaning = Promise.resolve().then(async () => {
1707
1733
  await generation.toolset.destroy().catch(this.#fault.bind(this));
1708
1734
  await generation.context.close().catch((error) => {
1709
1735
  if (!this.#losses.has(slot) && slot.browser.status === "connected") this.#fault(error);
@@ -1733,26 +1759,11 @@ var BrowserMCPServer = class {
1733
1759
  }
1734
1760
  async #construct(slot, token) {
1735
1761
  const directory = (0, node_path.join)(slot.profile, "contexts", (0, node_crypto.randomUUID)(), "downloads");
1736
- let context;
1737
- let toolset;
1762
+ const cleanup = [];
1738
1763
  try {
1739
1764
  await (0, node_fs_promises.mkdir)(directory, { recursive: true });
1740
- let page;
1741
- try {
1742
- context = await slot.browser.isolate({
1743
- reference: this.#reference,
1744
- downloads: { path: directory },
1745
- ...this.#viewport === void 0 ? {} : { emulation: { viewport: this.#viewport } }
1746
- });
1747
- page = await context.create();
1748
- } catch (error) {
1749
- if (token !== void 0) {
1750
- if (![...this.#leases.values()].some((lease) => lease.token.value === slot) && (this.#building.get(slot)?.size ?? 0) === 1) token.release();
1751
- this.#lose(slot, error);
1752
- }
1753
- throw error;
1754
- }
1755
- toolset = (0, _src_core.createBrowserToolset)(page, {
1765
+ const { context, page } = await this.#openGeneration(slot, directory, cleanup, token);
1766
+ const toolset = (0, _src_core.createBrowserToolset)(page, {
1756
1767
  context,
1757
1768
  notes: this.#notice.bind(this, context),
1758
1769
  journeys: {
@@ -1761,6 +1772,9 @@ var BrowserMCPServer = class {
1761
1772
  readonly: this.#readonly
1762
1773
  }
1763
1774
  });
1775
+ cleanup.push(async () => {
1776
+ await toolset.destroy().catch(this.#fault.bind(this));
1777
+ });
1764
1778
  await toolset.start();
1765
1779
  const generation = {
1766
1780
  context,
@@ -1772,10 +1786,7 @@ var BrowserMCPServer = class {
1772
1786
  this.#owned.set(slot, owned);
1773
1787
  return generation;
1774
1788
  } catch (error) {
1775
- await toolset?.destroy().catch(this.#fault.bind(this));
1776
- await context?.close().catch((failure) => {
1777
- if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !((0, _src_core.isBrowserError)(failure) && failure.code === "DISCONNECTED")) this.#fault(failure);
1778
- });
1789
+ for (const release of cleanup.toReversed()) await release();
1779
1790
  await (0, node_fs_promises.rm)(directory, {
1780
1791
  recursive: true,
1781
1792
  force: true
@@ -1783,6 +1794,31 @@ var BrowserMCPServer = class {
1783
1794
  throw error;
1784
1795
  }
1785
1796
  }
1797
+ async #openGeneration(slot, directory, cleanup, token) {
1798
+ try {
1799
+ const context = await slot.browser.isolate({
1800
+ reference: this.#reference,
1801
+ downloads: { path: directory },
1802
+ ...this.#viewport === void 0 ? {} : { emulation: { viewport: this.#viewport } }
1803
+ });
1804
+ cleanup.push(this.#closeGenerationContext.bind(this, context, slot));
1805
+ return {
1806
+ context,
1807
+ page: await context.create()
1808
+ };
1809
+ } catch (error) {
1810
+ if (token !== void 0) {
1811
+ if (![...this.#leases.values()].some((lease) => lease.token.value === slot) && (this.#building.get(slot)?.size ?? 0) === 1) token.release();
1812
+ this.#lose(slot, error);
1813
+ }
1814
+ throw error;
1815
+ }
1816
+ }
1817
+ async #closeGenerationContext(context, slot) {
1818
+ await context.close().catch((failure) => {
1819
+ if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !((0, _src_core.isBrowserError)(failure) && failure.code === "DISCONNECTED")) this.#fault(failure);
1820
+ });
1821
+ }
1786
1822
  async #destroyRecord(slot) {
1787
1823
  const cause = this.#losses.get(slot)?.cause;
1788
1824
  if ((0, _src_core.isBrowserError)(cause) && cause.code === "TIMEOUT" && slot.browser.pid !== void 0) try {
@@ -1812,12 +1848,23 @@ var BrowserMCPServer = class {
1812
1848
  if (this.#closing !== void 0) throw this.#ended();
1813
1849
  if (this.#stranded !== void 0) throw new _src_core.BrowserError(BROWSER_SERVER_TEARDOWN, (0, _orkestrel_contract.isError)(this.#stranded) ? this.#stranded.message : String(this.#stranded), (0, _src_core.isBrowserError)(this.#stranded) ? this.#stranded.context : void 0);
1814
1850
  const profile = (0, node_path.join)(this.#root, ".profiles", formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)()));
1815
- let browser;
1816
- let slot;
1817
1851
  try {
1818
1852
  await (0, node_fs_promises.mkdir)(profile);
1819
1853
  this.#folders.add(profile);
1820
- browser = this.#launcher({
1854
+ const browser = await this.#launchWarm(profile);
1855
+ await this.#connectWarm(profile, browser);
1856
+ return await this.#prepareWarm({
1857
+ browser,
1858
+ profile
1859
+ });
1860
+ } catch (error) {
1861
+ if (this.#closing === void 0) this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_LAUNCH, error)}`);
1862
+ throw error;
1863
+ }
1864
+ }
1865
+ async #launchWarm(profile) {
1866
+ try {
1867
+ return this.#launcher({
1821
1868
  headless: this.#headless,
1822
1869
  profile,
1823
1870
  cdp: { discover: false },
@@ -1826,6 +1873,13 @@ var BrowserMCPServer = class {
1826
1873
  ...this.#viewport === void 0 ? {} : { viewport: this.#viewport },
1827
1874
  ...process.platform === "linux" && process.getuid?.() === 0 ? { args: ["--no-sandbox"] } : {}
1828
1875
  });
1876
+ } catch (error) {
1877
+ await this.#discardWarm(profile, void 0);
1878
+ throw error;
1879
+ }
1880
+ }
1881
+ async #connectWarm(profile, browser) {
1882
+ try {
1829
1883
  await browser.connect();
1830
1884
  if (browser.pid !== void 0 && browser.endpoint !== void 0) {
1831
1885
  const record = (0, node_path.join)(profile, BROWSER_SERVER_RECORD);
@@ -1835,10 +1889,13 @@ var BrowserMCPServer = class {
1835
1889
  }));
1836
1890
  await (0, node_fs_promises.rename)(`${record}.tmp`, record);
1837
1891
  }
1838
- slot = {
1839
- browser,
1840
- profile
1841
- };
1892
+ } catch (error) {
1893
+ await this.#discardWarm(profile, browser);
1894
+ throw error;
1895
+ }
1896
+ }
1897
+ async #prepareWarm(slot) {
1898
+ try {
1842
1899
  const ending = Promise.withResolvers();
1843
1900
  ending.promise.catch(() => void 0);
1844
1901
  this.#endings.set(slot, ending);
@@ -1847,13 +1904,15 @@ var BrowserMCPServer = class {
1847
1904
  this.#observe(slot, generation);
1848
1905
  return slot;
1849
1906
  } catch (error) {
1850
- if (this.#folders.has(profile)) await (slot === void 0 ? this.#destroySlot(profile, browser) : this.#destroyRecord(slot)).catch((failure) => {
1851
- this.#stranded = failure;
1852
- });
1853
- if (this.#closing === void 0) this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_LAUNCH, error)}`);
1907
+ await this.#discardWarm(slot.profile, slot.browser, slot);
1854
1908
  throw error;
1855
1909
  }
1856
1910
  }
1911
+ async #discardWarm(profile, browser, slot) {
1912
+ if (this.#folders.has(profile)) await (slot === void 0 ? this.#destroySlot(profile, browser) : this.#destroyRecord(slot)).catch((failure) => {
1913
+ this.#stranded = failure;
1914
+ });
1915
+ }
1857
1916
  async #destroySlot(profile, browser) {
1858
1917
  try {
1859
1918
  await browser?.destroy();
@@ -1876,13 +1935,13 @@ var BrowserMCPServer = class {
1876
1935
  for (const entry of await (0, node_fs_promises.readdir)(profiles)) {
1877
1936
  if (this.#closing !== void 0) return;
1878
1937
  const pid = parseBrowserLockEntry(entry);
1879
- if (pid === void 0 || pid === process.pid || probeProcess(pid)) continue;
1938
+ if (pid === void 0 || pid === process.pid || (0, _orkestrel_process_server.inspectProcess)(pid) !== false) continue;
1880
1939
  const folder = (0, node_path.join)(profiles, entry);
1881
1940
  this.#folders.add(folder);
1882
1941
  try {
1883
1942
  const record = parseBrowserProfileRecord(await (0, node_fs_promises.readFile)((0, node_path.join)(folder, BROWSER_SERVER_RECORD), "utf8"));
1884
1943
  if (record === void 0) continue;
1885
- if (probeProcess(record.pid)) {
1944
+ if ((0, _orkestrel_process_server.inspectProcess)(record.pid) !== false) {
1886
1945
  const transport = createWebSocketCDPTransport({ url: record.endpoint });
1887
1946
  const client = (0, _src_core.createCDPClient)({ transport });
1888
1947
  try {
@@ -1893,7 +1952,7 @@ var BrowserMCPServer = class {
1893
1952
  await transport.close();
1894
1953
  await client.close();
1895
1954
  }
1896
- if (probeProcess(record.pid)) continue;
1955
+ if ((0, _orkestrel_process_server.inspectProcess)(record.pid) !== false) continue;
1897
1956
  }
1898
1957
  } catch (error) {
1899
1958
  if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ENOENT") continue;
@@ -1960,8 +2019,7 @@ var BrowserMCPServer = class {
1960
2019
  return new _src_core.BrowserError("CLOSED", "the browser session ended", { subject: "toolset" });
1961
2020
  }
1962
2021
  };
1963
- //#endregion
1964
- //#region src/server/stores/FileBrowserStore.ts
2022
+ // src/server/stores/FileBrowserStore.ts
1965
2023
  /**
1966
2024
  * Shares confined filesystem operations between the journey and run stores.
1967
2025
  * @remarks Requires an existing root directory. Links present during a component check are refused;
@@ -2006,9 +2064,11 @@ var FileBrowserStore = class FileBrowserStore {
2006
2064
  */
2007
2065
  async check(path, options) {
2008
2066
  this.resolvePath(path);
2009
- let current = this.#root;
2010
2067
  const parts = (0, node_path.relative)(this.#root, path).split(node_path.sep).filter(Boolean);
2011
- for (let index = 0; index <= parts.length; index += 1) {
2068
+ return (await [void 0, ...parts].reduce(async (previous, part, index) => {
2069
+ const ancestor = await previous;
2070
+ if (!ancestor.exists) return ancestor;
2071
+ const current = part === void 0 ? ancestor.path : this.resolvePath(ancestor.path, part);
2012
2072
  options?.signal?.throwIfAborted();
2013
2073
  try {
2014
2074
  const status = await (0, node_fs_promises.lstat)(current);
@@ -2017,13 +2077,20 @@ var FileBrowserStore = class FileBrowserStore {
2017
2077
  if (index < parts.length && !status.isDirectory()) throw new _src_core.BrowserError("STORE_PATH", `Not a directory: ${current}`);
2018
2078
  } catch (error) {
2019
2079
  options?.signal?.throwIfAborted();
2020
- if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
2080
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return {
2081
+ path: current,
2082
+ exists: false
2083
+ };
2021
2084
  throw this.translateError(current, error);
2022
2085
  }
2023
- const part = parts[index];
2024
- if (part !== void 0) current = this.resolvePath(current, part);
2025
- }
2026
- return true;
2086
+ return {
2087
+ path: current,
2088
+ exists: true
2089
+ };
2090
+ }, Promise.resolve({
2091
+ path: this.#root,
2092
+ exists: true
2093
+ }))).exists;
2027
2094
  }
2028
2095
  /** Creates checked directories one component at a time. @param path - Directory @param options - Cancellation options */
2029
2096
  async createDirectory(path, options) {
@@ -2058,35 +2125,45 @@ var FileBrowserStore = class FileBrowserStore {
2058
2125
  /** Atomically replaces a file and cleans its owned temporary file on failure. @param path - Destination @param value - Bytes or UTF-8 @param options - Cancellation options */
2059
2126
  async write(path, value, options) {
2060
2127
  const temporary = this.resolvePath((0, node_path.dirname)(path), `.${(0, node_crypto.randomUUID)()}.tmp`);
2061
- let owned = false;
2128
+ const file = await this.#openTemporary(path, temporary, options);
2062
2129
  try {
2063
- await this.check(path, options);
2064
- await this.check(temporary, options);
2065
- options?.signal?.throwIfAborted();
2066
- const file = await (0, node_fs_promises.open)(temporary, "wx");
2067
- owned = true;
2130
+ await this.#commitTemporary(file, path, temporary, value, options);
2131
+ } catch (error) {
2068
2132
  try {
2069
2133
  options?.signal?.throwIfAborted();
2070
- await file.writeFile(value, { signal: options?.signal });
2134
+ throw this.translateError(path, error);
2071
2135
  } finally {
2072
- await file.close();
2136
+ await this.remove(temporary);
2073
2137
  }
2138
+ }
2139
+ }
2140
+ async #openTemporary(path, temporary, options) {
2141
+ try {
2074
2142
  await this.check(path, options);
2075
2143
  await this.check(temporary, options);
2076
2144
  options?.signal?.throwIfAborted();
2077
- try {
2078
- await (0, node_fs_promises.rename)(temporary, path);
2079
- } catch (error) {
2080
- const target = await (0, node_fs_promises.lstat)(path).catch(() => void 0);
2081
- if (error instanceof Error && "code" in error && (error.code === "EISDIR" || error.code === "EPERM") && target?.isDirectory()) throw new _src_core.BrowserError("STORE_FILE", `Cannot replace directory: ${path}`, { path });
2082
- throw error;
2083
- }
2084
- owned = false;
2145
+ return await (0, node_fs_promises.open)(temporary, "wx");
2085
2146
  } catch (error) {
2086
2147
  options?.signal?.throwIfAborted();
2087
2148
  throw this.translateError(path, error);
2149
+ }
2150
+ }
2151
+ async #commitTemporary(file, path, temporary, value, options) {
2152
+ try {
2153
+ options?.signal?.throwIfAborted();
2154
+ await file.writeFile(value, { signal: options?.signal });
2088
2155
  } finally {
2089
- if (owned) await this.remove(temporary);
2156
+ await file.close();
2157
+ }
2158
+ await this.check(path, options);
2159
+ await this.check(temporary, options);
2160
+ options?.signal?.throwIfAborted();
2161
+ try {
2162
+ await (0, node_fs_promises.rename)(temporary, path);
2163
+ } catch (error) {
2164
+ const target = await (0, node_fs_promises.lstat)(path).catch(() => void 0);
2165
+ if (error instanceof Error && "code" in error && (error.code === "EISDIR" || error.code === "EPERM") && target?.isDirectory()) throw new _src_core.BrowserError("STORE_FILE", `Cannot replace directory: ${path}`, { path });
2166
+ throw error;
2090
2167
  }
2091
2168
  }
2092
2169
  /** Removes a checked file, accepting absence. @param path - File @param options - Cancellation options */
@@ -2126,43 +2203,43 @@ var FileBrowserStore = class FileBrowserStore {
2126
2203
  await this.createDirectory((0, node_path.dirname)(path), options);
2127
2204
  const name = formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)());
2128
2205
  const entry = this.resolvePath(path, name);
2129
- for (let attempt = 0; attempt < 8; attempt += 1) {
2130
- await this.check(path, options);
2206
+ for (const lock of Array.from({ length: 8 }, () => path)) {
2207
+ await this.check(lock, options);
2131
2208
  options?.signal?.throwIfAborted();
2132
2209
  try {
2133
- await (0, node_fs_promises.mkdir)(path);
2210
+ await (0, node_fs_promises.mkdir)(lock);
2134
2211
  } catch (error) {
2135
- if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw this.translateError(path, error);
2136
- const entries = await this.#readLock(path, options);
2212
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw this.translateError(lock, error);
2213
+ const entries = await this.#readLock(lock, options);
2137
2214
  if (entries === void 0) continue;
2138
2215
  if (entries.length > 1) break;
2139
2216
  const holder = entries[0];
2140
2217
  if (holder !== void 0) {
2141
2218
  const pid = parseBrowserLockEntry(holder);
2142
- if (pid === void 0 || probeProcess(pid)) break;
2143
- if (!await this.#unlinkLock(this.resolvePath(path, holder))) continue;
2219
+ if (pid === void 0 || (0, _orkestrel_process_server.inspectProcess)(pid) !== false) break;
2220
+ if (!await this.#unlinkLock(this.resolvePath(lock, holder))) continue;
2144
2221
  }
2145
- await this.#removeLock(path);
2222
+ await this.#removeLock(lock);
2146
2223
  continue;
2147
2224
  }
2148
- let owned = false;
2225
+ const cleanup = [];
2149
2226
  try {
2150
2227
  await this.check(entry, options);
2151
2228
  try {
2152
2229
  const file = await (0, node_fs_promises.open)(entry, "wx");
2153
- owned = true;
2230
+ cleanup.push(this.#unlinkLock.bind(this, entry));
2154
2231
  await file.close();
2155
2232
  } catch (error) {
2156
2233
  if (error instanceof Error && "code" in error && error.code === "ENOENT") continue;
2157
2234
  throw this.translateError(entry, error);
2158
2235
  }
2159
- const entries = await this.#readLock(path, options);
2236
+ const entries = await this.#readLock(lock, options);
2160
2237
  if (entries?.length !== 1 || entries[0] !== name) break;
2161
2238
  options?.signal?.throwIfAborted();
2162
2239
  return await action();
2163
2240
  } finally {
2164
- if (owned) await this.#unlinkLock(entry);
2165
- await this.#removeLock(path);
2241
+ for (const release of cleanup) await release();
2242
+ await this.#removeLock(lock);
2166
2243
  }
2167
2244
  }
2168
2245
  throw new _src_core.BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
@@ -2186,27 +2263,18 @@ var FileBrowserStore = class FileBrowserStore {
2186
2263
  faults,
2187
2264
  truncated: false
2188
2265
  };
2189
- let names;
2190
- try {
2191
- options?.signal?.throwIfAborted();
2192
- names = (await (0, node_fs_promises.readdir)(path)).sort();
2193
- } catch (error) {
2194
- options?.signal?.throwIfAborted();
2195
- throw this.translateError(path, error);
2196
- }
2197
- let count = 0;
2266
+ const names = await this.#readNames(path, options);
2198
2267
  for (const name of names) {
2199
2268
  options?.signal?.throwIfAborted();
2200
2269
  try {
2201
2270
  const entry = await reader(name);
2202
2271
  if (entry === void 0) continue;
2203
- if (count++ < offset) continue;
2204
- if (entries.length === limit) return {
2205
- entries,
2272
+ entries.push(entry);
2273
+ if (entries.length > offset + limit) return {
2274
+ entries: entries.filter((_entry, index) => index >= offset && index < offset + limit),
2206
2275
  faults,
2207
2276
  truncated: true
2208
2277
  };
2209
- entries.push(entry);
2210
2278
  } catch (error) {
2211
2279
  options?.signal?.throwIfAborted();
2212
2280
  const reason = error instanceof _src_core.BrowserError ? error.context?.["reason"] : void 0;
@@ -2217,11 +2285,20 @@ var FileBrowserStore = class FileBrowserStore {
2217
2285
  }
2218
2286
  }
2219
2287
  return {
2220
- entries,
2288
+ entries: entries.filter((_entry, index) => index >= offset),
2221
2289
  faults,
2222
2290
  truncated: false
2223
2291
  };
2224
2292
  }
2293
+ async #readNames(path, options) {
2294
+ try {
2295
+ options?.signal?.throwIfAborted();
2296
+ return (await (0, node_fs_promises.readdir)(path)).sort();
2297
+ } catch (error) {
2298
+ options?.signal?.throwIfAborted();
2299
+ throw this.translateError(path, error);
2300
+ }
2301
+ }
2225
2302
  /**
2226
2303
  * Allocates a run directory exclusively, retrying colliding producer candidates.
2227
2304
  * @param parent - Checked parent directory
@@ -2301,8 +2378,7 @@ var FileBrowserStore = class FileBrowserStore {
2301
2378
  }
2302
2379
  }
2303
2380
  };
2304
- //#endregion
2305
- //#region src/server/stores/FileBrowserJourneyStore.ts
2381
+ // src/server/stores/FileBrowserJourneyStore.ts
2306
2382
  /**
2307
2383
  * Persists journeys with exclusive writes and revision counters retained across deletion.
2308
2384
  * @remarks Requires an existing root. A live holder refuses immediately; dead or empty locks are reclaimed within a bounded attempt count.
@@ -2396,8 +2472,7 @@ var FileBrowserJourneyStore = class {
2396
2472
  }, options);
2397
2473
  }
2398
2474
  };
2399
- //#endregion
2400
- //#region src/server/stores/FileBrowserRunStore.ts
2475
+ // src/server/stores/FileBrowserRunStore.ts
2401
2476
  /**
2402
2477
  * Persists runs and captures only in directories allocated by this instance.
2403
2478
  * @remarks Requires an existing root. Reopened stores can read and delete saved runs.
@@ -2554,8 +2629,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
2554
2629
  }
2555
2630
  }
2556
2631
  };
2557
- //#endregion
2558
- //#region src/server/factories.ts
2632
+ // src/server/factories.ts
2559
2633
  /**
2560
2634
  * Creates a raw-CDP `BrowserInterface` façade with discovery, connection, and lifecycle
2561
2635
  * management.
@@ -2635,8 +2709,7 @@ function createFileBrowserRunStore(options) {
2635
2709
  function createBrowserMCPServer(options) {
2636
2710
  return new BrowserMCPServer(options);
2637
2711
  }
2638
- //#endregion
2639
- //#region src/server/Browser.ts
2712
+ // src/server/Browser.ts
2640
2713
  /**
2641
2714
  * Discovers, launches, connects to, and owns Chromium-family browser sessions.
2642
2715
  *
@@ -2753,30 +2826,30 @@ var Browser = class {
2753
2826
  return [...this.#contexts];
2754
2827
  }
2755
2828
  async isolate(options) {
2756
- const attempt = this.#isolate(options);
2757
- this.#isolating.add(attempt);
2829
+ const pending = this.#isolate(options);
2830
+ this.#isolating.add(pending);
2758
2831
  try {
2759
- return await attempt;
2832
+ return await pending;
2760
2833
  } finally {
2761
- this.#isolating.delete(attempt);
2834
+ this.#isolating.delete(pending);
2762
2835
  }
2763
2836
  }
2764
2837
  async create(options) {
2765
2838
  if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2766
2839
  const client = this.#client;
2767
2840
  if (this.#status !== "connected" || client === void 0) throw new _src_core.BrowserError("DISCONNECTED", "Browser is not connected");
2768
- let context = this.#contexts[0];
2769
- if (context === void 0) {
2770
- context = new _src_core.BrowserContext(client, {
2771
- ...this.#options.viewport === void 0 ? {} : { viewport: this.#options.viewport },
2772
- writer: createFileBrowserWriter()
2773
- });
2774
- this.#registerContext(context);
2775
- }
2776
- const page = await context.create(options);
2841
+ const page = await (this.#contexts[0] ?? this.#defaultContext(client)).create(options);
2777
2842
  this.#emitter.emit("page", page);
2778
2843
  return page;
2779
2844
  }
2845
+ #defaultContext(client) {
2846
+ const context = new _src_core.BrowserContext(client, {
2847
+ ...this.#options.viewport === void 0 ? {} : { viewport: this.#options.viewport },
2848
+ writer: createFileBrowserWriter()
2849
+ });
2850
+ this.#registerContext(context);
2851
+ return context;
2852
+ }
2780
2853
  destroy() {
2781
2854
  const active = this.#shutdown;
2782
2855
  if (active !== void 0) return active;
@@ -3070,41 +3143,40 @@ var Browser = class {
3070
3143
  async #launch() {
3071
3144
  if (this.#process !== void 0) throw new _src_core.BrowserError("CONNECTION", "A browser process is already active on this instance");
3072
3145
  const requestedEngine = this.#options.browsers?.engine;
3073
- let executable = this.#options.executable;
3074
- let resolvedEngine;
3075
- if (executable !== void 0) resolvedEngine = parseBrowserEngine(executable) ?? "chromium";
3076
- else {
3077
- const found = findSystemBrowsers({
3078
- ...this.#options.browsers,
3079
- ...requestedEngine !== void 0 ? { engine: requestedEngine } : {}
3080
- })[0];
3081
- executable = found?.executable;
3082
- resolvedEngine = found?.engine;
3083
- }
3146
+ const supplied = this.#options.executable;
3147
+ const selected = supplied === void 0 ? findSystemBrowsers({
3148
+ ...this.#options.browsers,
3149
+ ...requestedEngine !== void 0 ? { engine: requestedEngine } : {}
3150
+ })[0] : {
3151
+ executable: supplied,
3152
+ engine: parseBrowserEngine(supplied) ?? "chromium"
3153
+ };
3154
+ const executable = selected?.executable;
3155
+ const resolvedEngine = selected?.engine;
3084
3156
  if (executable === void 0) throw new _src_core.BrowserError("CONNECTION", "No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine === void 0 ? void 0 : { engine: requestedEngine });
3085
3157
  this.#engine = resolvedEngine ?? "chromium";
3086
3158
  await this.#releaseProfile();
3087
3159
  const profile = await createBrowserProfile(this.#options.profile);
3088
3160
  this.#profile = profile;
3089
- let process;
3090
- try {
3091
- process = launchBrowserProcess(executable, this.#options.cdp?.port, this.#options.headless ?? true, profile.path, this.#options.args);
3092
- } catch (error) {
3161
+ const launched = (0, _orkestrel_contract.attempt)(() => launchBrowserProcess(executable, this.#options.cdp?.port, this.#options.headless ?? true, profile.path, this.#options.args));
3162
+ if (!launched.success) {
3093
3163
  await this.#releaseProfile();
3094
- throw error;
3164
+ throw launched.error;
3095
3165
  }
3166
+ const process = launched.value;
3096
3167
  this.#process = process;
3097
- let client;
3168
+ const cleanup = [];
3098
3169
  try {
3099
3170
  const endpoint = await this.#waitForLaunch(process, executable, this.#options.args);
3100
3171
  const transport = createWebSocketCDPTransport({
3101
3172
  url: endpoint,
3102
3173
  timeout: this.#timeout()
3103
3174
  });
3104
- client = new _src_core.CDPClient({
3175
+ const client = new _src_core.CDPClient({
3105
3176
  transport,
3106
3177
  timeout: this.#timeout()
3107
3178
  });
3179
+ cleanup.push(this.#closeClient.bind(this, client));
3108
3180
  await this.#raceAbort(client.connect());
3109
3181
  await this.#takeEndpointOwner(process, client);
3110
3182
  const connection = this.#options.profile === void 0 ? "launch" : "persistent";
@@ -3121,7 +3193,7 @@ var Browser = class {
3121
3193
  } catch (error) {
3122
3194
  this.#unbindTransport();
3123
3195
  this.#unbindProcess();
3124
- await this.#closeClient(client);
3196
+ for (const release of cleanup) await release();
3125
3197
  await this.#terminate(process);
3126
3198
  await this.#releaseProfile();
3127
3199
  this.#process = void 0;
@@ -3147,18 +3219,8 @@ var Browser = class {
3147
3219
  */
3148
3220
  async #takeEndpointOwner(process, client) {
3149
3221
  if (process.exitCode === null && process.signalCode === null) return;
3150
- let result;
3151
- try {
3152
- result = await client.send("SystemInfo.getProcessInfo");
3153
- } catch {
3154
- result = void 0;
3155
- }
3156
- let pid;
3157
- if ((0, _orkestrel_contract.isRecord)(result) && (0, _orkestrel_contract.isArray)(result["processInfo"])) for (const entry of result["processInfo"]) {
3158
- if (!(0, _orkestrel_contract.isRecord)(entry) || entry["type"] !== "browser" || !(0, _orkestrel_contract.isInteger)(entry["id"])) continue;
3159
- pid = entry["id"];
3160
- break;
3161
- }
3222
+ const result = await client.send("SystemInfo.getProcessInfo").catch(() => void 0);
3223
+ const pid = (0, _orkestrel_contract.isRecord)(result) && (0, _orkestrel_contract.isArray)(result["processInfo"]) ? Iterator.from(result["processInfo"]).map((entry) => (0, _orkestrel_contract.isRecord)(entry) && entry["type"] === "browser" && (0, _orkestrel_contract.isInteger)(entry["id"]) ? entry["id"] : void 0).find((id) => id !== void 0) : void 0;
3162
3224
  if (pid === void 0) {
3163
3225
  await this.#closeRemote(client);
3164
3226
  throw new _src_core.BrowserError("CONNECTION", "The browser launcher exited without naming the process serving its CDP endpoint", {
@@ -3226,12 +3288,7 @@ var Browser = class {
3226
3288
  async #syncContexts() {
3227
3289
  const client = this.#client;
3228
3290
  if (client === void 0) return;
3229
- let result;
3230
- try {
3231
- result = await client.send("Target.getTargets");
3232
- } catch {
3233
- return;
3234
- }
3291
+ const result = await client.send("Target.getTargets").catch(() => void 0);
3235
3292
  if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isArray)(result["targetInfos"])) return;
3236
3293
  const pages = [];
3237
3294
  for (const target of result["targetInfos"]) {
@@ -3252,63 +3309,63 @@ var Browser = class {
3252
3309
  this.#registerContext(context);
3253
3310
  }
3254
3311
  async #destroyResources() {
3255
- let terminated = false;
3256
3312
  try {
3257
- await this.#settle();
3258
- this.#unbindTransport();
3259
- this.#unbindProcess();
3260
- const client = this.#client;
3261
- if (this.#owned === true) {
3262
- await this.#closeContexts();
3263
- if (this.#process === void 0) await this.#closeRemote(client);
3264
- } else await this.#destroyContexts();
3265
- this.#contexts = [];
3266
- await this.#terminate(this.#process);
3267
- this.#process = void 0;
3268
- this.#servingPid = void 0;
3269
- terminated = true;
3270
- } finally {
3271
- const contexts = this.#contexts;
3272
- this.#contexts = [];
3273
- const client = this.#client;
3274
- this.#client = void 0;
3275
- try {
3276
- await this.#cleanupLocal(contexts, client);
3277
- if (terminated) await this.#releaseProfile();
3278
- } finally {
3279
- this.#finish();
3280
- }
3313
+ await this.#destroySession();
3314
+ } catch (error) {
3315
+ await this.#finishResources(false);
3316
+ throw error;
3281
3317
  }
3318
+ await this.#finishResources(true);
3319
+ }
3320
+ async #destroySession() {
3321
+ await this.#settle();
3322
+ this.#unbindTransport();
3323
+ this.#unbindProcess();
3324
+ const client = this.#client;
3325
+ if (this.#owned === true) {
3326
+ await this.#closeContexts();
3327
+ if (this.#process === void 0) await this.#closeRemote(client);
3328
+ } else await this.#destroyContexts();
3329
+ this.#contexts = [];
3330
+ await this.#terminate(this.#process);
3331
+ this.#process = void 0;
3332
+ this.#servingPid = void 0;
3282
3333
  }
3283
3334
  async #closeResources() {
3284
- let terminated = false;
3285
3335
  try {
3286
- await this.#settle();
3287
- this.#unbindTransport();
3288
- this.#unbindProcess();
3289
- const client = this.#client;
3290
- await this.#closeContexts();
3291
- this.#contexts = [];
3292
- await this.#closeRemote(client);
3293
- const process = this.#process;
3294
- if (process !== void 0) {
3295
- if (!await this.#waitForTerminationWithin(process, 3e3)) await this.#terminate(process);
3296
- await this.#closeProcessPipe(process);
3297
- }
3298
- this.#process = void 0;
3299
- this.#servingPid = void 0;
3300
- terminated = true;
3336
+ await this.#closeSession();
3337
+ } catch (error) {
3338
+ await this.#finishResources(false);
3339
+ throw error;
3340
+ }
3341
+ await this.#finishResources(true);
3342
+ }
3343
+ async #closeSession() {
3344
+ await this.#settle();
3345
+ this.#unbindTransport();
3346
+ this.#unbindProcess();
3347
+ const client = this.#client;
3348
+ await this.#closeContexts();
3349
+ this.#contexts = [];
3350
+ await this.#closeRemote(client);
3351
+ const process = this.#process;
3352
+ if (process !== void 0) {
3353
+ if (!await this.#waitForTerminationWithin(process, 3e3)) await this.#terminate(process);
3354
+ await this.#closeProcessPipe(process);
3355
+ }
3356
+ this.#process = void 0;
3357
+ this.#servingPid = void 0;
3358
+ }
3359
+ async #finishResources(release) {
3360
+ const contexts = this.#contexts;
3361
+ this.#contexts = [];
3362
+ const client = this.#client;
3363
+ this.#client = void 0;
3364
+ try {
3365
+ await this.#cleanupLocal(contexts, client);
3366
+ if (release) await this.#releaseProfile();
3301
3367
  } finally {
3302
- const contexts = this.#contexts;
3303
- this.#contexts = [];
3304
- const client = this.#client;
3305
- this.#client = void 0;
3306
- try {
3307
- await this.#cleanupLocal(contexts, client);
3308
- if (terminated) await this.#releaseProfile();
3309
- } finally {
3310
- this.#finish();
3311
- }
3368
+ this.#finish();
3312
3369
  }
3313
3370
  }
3314
3371
  async #settle() {
@@ -3324,29 +3381,29 @@ var Browser = class {
3324
3381
  if (this.#exitCleanup === cleanup) this.#exitCleanup = void 0;
3325
3382
  }
3326
3383
  async #closeRemote(client) {
3327
- let remote = client;
3328
- let temporary = false;
3329
- if (remote === void 0 && this.#owned === true && this.#endpoint !== void 0) {
3330
- const transport = createWebSocketCDPTransport({
3331
- url: this.#endpoint,
3332
- timeout: this.#timeout()
3333
- });
3334
- remote = new _src_core.CDPClient({
3335
- transport,
3336
- timeout: this.#timeout()
3337
- });
3338
- try {
3339
- await remote.connect();
3340
- temporary = true;
3341
- } catch {
3342
- await this.#closeClient(remote);
3343
- return;
3344
- }
3345
- }
3384
+ const remote = client ?? await this.#openRemote();
3346
3385
  if (remote !== void 0) try {
3347
3386
  await remote.send("Browser.close");
3348
3387
  } catch {}
3349
- if (temporary) await this.#closeClient(remote);
3388
+ if (client === void 0 && remote !== void 0) await this.#closeClient(remote);
3389
+ }
3390
+ async #openRemote() {
3391
+ if (this.#owned !== true || this.#endpoint === void 0) return void 0;
3392
+ const transport = createWebSocketCDPTransport({
3393
+ url: this.#endpoint,
3394
+ timeout: this.#timeout()
3395
+ });
3396
+ const remote = new _src_core.CDPClient({
3397
+ transport,
3398
+ timeout: this.#timeout()
3399
+ });
3400
+ try {
3401
+ await remote.connect();
3402
+ return remote;
3403
+ } catch {
3404
+ await this.#closeClient(remote);
3405
+ return;
3406
+ }
3350
3407
  }
3351
3408
  async #closeContexts() {
3352
3409
  for (const context of [...this.#contexts]) try {
@@ -3381,14 +3438,12 @@ var Browser = class {
3381
3438
  if (profile !== void 0) await removeBrowserProfile(profile);
3382
3439
  }
3383
3440
  async #cleanupExitedProcess(process, contexts, client) {
3384
- let terminated = false;
3385
3441
  try {
3386
3442
  await this.#terminate(process);
3387
- terminated = true;
3388
3443
  } finally {
3389
3444
  await this.#cleanupLocal(contexts, client);
3390
3445
  }
3391
- if (terminated) await this.#releaseProfile();
3446
+ await this.#releaseProfile();
3392
3447
  }
3393
3448
  async #cleanupLocal(contexts, client) {
3394
3449
  await this.#destroyContextList(contexts);
@@ -3438,31 +3493,9 @@ var Browser = class {
3438
3493
  });
3439
3494
  }
3440
3495
  }
3441
- /**
3442
- * Waits within a bound for the rest of a terminated process group, or the process a
3443
- * launcher handed the endpoint to, to disappear.
3444
- *
3445
- * @remarks
3446
- * This is the one interval loop in the server. Node raises an exit event for the direct
3447
- * child only, so no event source reports the remainder of a POSIX process group or a
3448
- * handed-off process, and the drain probes on `BROWSER_DRAIN_INTERVAL_MS` until the
3449
- * remainder is gone or `timeout` elapses.
3450
- */
3451
- async #waitForRemainderWithin(process, timeout) {
3452
- const deadline = performance.now() + timeout;
3453
- let confirmed = false;
3454
- while (true) {
3455
- const alive = this.#inspectRemainder(process);
3456
- if (alive === false) return true;
3457
- if (alive === true) confirmed = true;
3458
- const remaining = deadline - performance.now();
3459
- if (remaining <= 0) return confirmed ? false : void 0;
3460
- await new Promise((resolve) => setTimeout(resolve, Math.min(100, remaining)));
3461
- }
3462
- }
3463
3496
  async #waitForTerminationWithin(process, timeout, final = false) {
3464
3497
  const exited = this.#waitForProcessWithin(process, timeout);
3465
- const drained = this.#waitForRemainderWithin(process, timeout);
3498
+ const drained = new BrowserProcessDrain(this.#inspectRemainder.bind(this, process), timeout).result;
3466
3499
  const results = await Promise.all([exited, drained]);
3467
3500
  return results[0] && (results[1] === true || final && results[1] === void 0);
3468
3501
  }
@@ -3502,7 +3535,6 @@ var Browser = class {
3502
3535
  this.#emitter.destroy();
3503
3536
  }
3504
3537
  };
3505
- //#endregion
3506
3538
  exports.BROWSER_CDP_LIST_PATH = BROWSER_CDP_LIST_PATH;
3507
3539
  exports.BROWSER_CDP_PROTOCOL = BROWSER_CDP_PROTOCOL;
3508
3540
  exports.BROWSER_CDP_VERSION_PATH = BROWSER_CDP_VERSION_PATH;
@@ -3554,7 +3586,9 @@ exports.BROWSER_TRANSPORT_LOSS_DEFER_MS = BROWSER_TRANSPORT_LOSS_DEFER_MS;
3554
3586
  exports.BROWSER_WINDOWS_ROOT_FALLBACKS = BROWSER_WINDOWS_ROOT_FALLBACKS;
3555
3587
  exports.BROWSER_WINDOWS_SUFFIXES = BROWSER_WINDOWS_SUFFIXES;
3556
3588
  exports.Browser = Browser;
3589
+ exports.BrowserEndpoint = BrowserEndpoint;
3557
3590
  exports.BrowserMCPServer = BrowserMCPServer;
3591
+ exports.BrowserProcessDrain = BrowserProcessDrain;
3558
3592
  exports.FileBrowserJourneyStore = FileBrowserJourneyStore;
3559
3593
  exports.FileBrowserRunStore = FileBrowserRunStore;
3560
3594
  exports.FileBrowserWriter = FileBrowserWriter;
@@ -3584,7 +3618,6 @@ exports.parseBrowserLockEntry = parseBrowserLockEntry;
3584
3618
  exports.parseBrowserProfileRecord = parseBrowserProfileRecord;
3585
3619
  exports.parseBrowserViewport = parseBrowserViewport;
3586
3620
  exports.probePathNames = probePathNames;
3587
- exports.probeProcess = probeProcess;
3588
3621
  exports.readBrowserEndpoint = readBrowserEndpoint;
3589
3622
  exports.readFirstLine = readFirstLine;
3590
3623
  exports.removeBrowserProfile = removeBrowserProfile;