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