@orkestrel/browser 0.0.13 → 0.0.15

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.
@@ -14,35 +14,35 @@ let _orkestrel_emitter = require("@orkestrel/emitter");
14
14
  let _orkestrel_websocket = require("@orkestrel/websocket");
15
15
  let node_events = require("node:events");
16
16
  //#region src/server/constants.ts
17
- /** Default CDP port probed for an existing browser and used for launches. */
17
+ /** Sets the default CDP port probed for an existing browser and used for launches. */
18
18
  var BROWSER_DEFAULT_CDP_PORT = 9222;
19
- /** Default host probed for an existing browser and used for launches (avoids `localhost` resolving to `::1` when Chromium binds `127.0.0.1`). */
19
+ /** Sets the default host probed for an existing browser and used for launches (avoids `localhost` resolving to `::1` when Chromium binds `127.0.0.1`). */
20
20
  var BROWSER_DEFAULT_HOST = "127.0.0.1";
21
- /** Protocol prefix for CDP discovery requests. */
21
+ /** Names the protocol prefix for CDP discovery requests. */
22
22
  var BROWSER_CDP_PROTOCOL = "http";
23
- /** Path appended to the CDP host to fetch version metadata (endpoint discovery). */
23
+ /** Names the path appended to the CDP host to fetch version metadata (endpoint discovery). */
24
24
  var BROWSER_CDP_VERSION_PATH = "/json/version";
25
- /** Path appended to the CDP host to list open targets (pages, workers, etc). */
25
+ /** Names the path appended to the CDP host to list open targets (pages, workers, etc). */
26
26
  var BROWSER_CDP_LIST_PATH = "/json/list";
27
- /** Flags always passed to a launched browser process, alongside the caller's own. */
27
+ /** Lists the flags always passed to a launched browser process, alongside the caller's own. */
28
28
  var BROWSER_LAUNCH_ARGS = Object.freeze(["--no-first-run", "--no-default-browser-check"]);
29
- /** Flag enabling headless mode on a launched browser process. */
29
+ /** Names the flag that enables headless mode on a launched browser process. */
30
30
  var BROWSER_HEADLESS_ARG = "--headless=new";
31
- /** Prefix for isolated browser profiles created beneath the operating-system temp directory. */
31
+ /** Names the prefix for isolated browser profiles created beneath the operating-system temp directory. */
32
32
  var BROWSER_PROFILE_PREFIX = "orkestrel-browser-";
33
- /** Bound for each launched-process exit window during TERM-to-KILL teardown. */
33
+ /** Bounds each launched-process exit window during TERM-to-KILL teardown. */
34
34
  var BROWSER_KILL_GRACE_MS = 3e3;
35
- /** Bound for the `discover: false` port-occupancy probe before launching — short, since it only needs to detect an already-listening CDP endpoint, not perform full discovery. */
35
+ /** Bounds the `discover: false` port-occupancy probe before launching — short, because it only needs to detect an already-listening CDP endpoint, not perform full discovery. */
36
36
  var BROWSER_PORT_PROBE_TIMEOUT_MS = 200;
37
- /** Brief defer applied once when a transport loss is observed on an owned process, giving a near-simultaneous process-exit event (which libuv may reap slightly later than the socket close) first say over the diagnosis. */
37
+ /** Defers once, briefly, when a transport loss is observed on an owned process, giving a near-simultaneous process-exit event (which libuv may reap slightly later than the socket close) first say over the diagnosis. */
38
38
  var BROWSER_TRANSPORT_LOSS_DEFER_MS = 50;
39
- /** Machine-readable error-context cause for an owned browser process exiting. */
39
+ /** Names the machine-readable error-context cause for an owned browser process exiting. */
40
40
  var BROWSER_PROCESS_EXIT_CAUSE = "process-exit";
41
- /** Machine-readable error-context cause for a CDP transport disconnecting while its browser remains alive. */
41
+ /** Names the machine-readable error-context cause for a CDP transport disconnecting while its browser remains alive. */
42
42
  var BROWSER_TRANSPORT_LOSS_CAUSE = "transport-loss";
43
- /** Environment variables checked (in order) for an explicit browser executable path override. */
43
+ /** Lists the environment variables checked (in order) for an explicit browser executable path override. */
44
44
  var BROWSER_ENV_PATH_KEYS = Object.freeze(["PLAYWRIGHT_EXECUTABLE_PATH", "CHROME_PATH"]);
45
- /** Well-known Chrome/Chromium/Edge executable paths with no platform-specific root, keyed by `process.platform`. */
45
+ /** Lists the well-known Chrome/Chromium/Edge executable paths with no platform-specific root, keyed by `process.platform`. */
46
46
  var BROWSER_EXECUTABLE_PATHS = Object.freeze({
47
47
  linux: Object.freeze([
48
48
  "/usr/bin/google-chrome",
@@ -62,18 +62,18 @@ var BROWSER_EXECUTABLE_PATHS = Object.freeze({
62
62
  ]),
63
63
  win32: Object.freeze([])
64
64
  });
65
- /** Windows install-root-relative suffixes for Chrome/Edge/Chromium, joined against each candidate root (`PROGRAMFILES`, `PROGRAMFILES(X86)`, `LOCALAPPDATA`). */
65
+ /** Lists the Windows install-root-relative suffixes for Chrome/Edge/Chromium, joined against each candidate root (`PROGRAMFILES`, `PROGRAMFILES(X86)`, `LOCALAPPDATA`). */
66
66
  var BROWSER_WINDOWS_SUFFIXES = Object.freeze([
67
67
  "Google\\Chrome\\Application\\chrome.exe",
68
68
  "Microsoft\\Edge\\Application\\msedge.exe",
69
69
  "Chromium\\Application\\chrome.exe"
70
70
  ]);
71
- /** Fallback Windows install roots used when the corresponding environment variable is absent. */
71
+ /** Lists the fallback Windows install roots used when the corresponding environment variable is absent. */
72
72
  var BROWSER_WINDOWS_ROOT_FALLBACKS = Object.freeze({
73
73
  PROGRAMFILES: "C:\\Program Files",
74
74
  "PROGRAMFILES(X86)": "C:\\Program Files (x86)"
75
75
  });
76
- /** Command names probed on PATH when no well-known executable path exists. */
76
+ /** Lists the command names probed on PATH when no well-known executable path exists. */
77
77
  var BROWSER_EXECUTABLE_NAMES = Object.freeze([
78
78
  "google-chrome",
79
79
  "google-chrome-stable",
@@ -83,19 +83,19 @@ var BROWSER_EXECUTABLE_NAMES = Object.freeze([
83
83
  "chromium-browser",
84
84
  "chrome"
85
85
  ]);
86
- /** Environment variable naming an additional Playwright browser store base directory. */
86
+ /** Names the environment variable that carries an additional Playwright browser store base directory. */
87
87
  var BROWSER_STORE_ENV_KEY = "PLAYWRIGHT_BROWSERS_PATH";
88
- /** Well-known Playwright browser store base directories checked in addition to `PLAYWRIGHT_BROWSERS_PATH`. */
88
+ /** Lists the well-known Playwright browser store base directories checked in addition to `PLAYWRIGHT_BROWSERS_PATH`. */
89
89
  var BROWSER_STORE_DEFAULT_DIRS = Object.freeze(["/opt/pw-browsers"]);
90
- /** Per-OS default Playwright browser cache directory, relative to the home directory (win32 uses `LOCALAPPDATA` directly). */
90
+ /** Names the per-OS default Playwright browser cache directory, relative to the home directory (win32 uses `LOCALAPPDATA` directly). */
91
91
  var BROWSER_STORE_CACHE_DIRS = Object.freeze({
92
92
  linux: ".cache/ms-playwright",
93
93
  darwin: "Library/Caches/ms-playwright"
94
94
  });
95
- /** Name of the top-level Chromium symlink/binary Playwright maintains inside a browser store base. */
95
+ /** Names the top-level Chromium symlink/binary Playwright maintains inside a browser store base. */
96
96
  var BROWSER_STORE_LINK_NAME = "chromium";
97
97
  /**
98
- * Case-insensitive substrings identifying an executable path/name's browser
98
+ * Lists the case-insensitive substrings identifying an executable path/name's browser
99
99
  * engine, checked by `parseBrowserEngine` in the order `edge` → `chromium` → `chrome`.
100
100
  */
101
101
  var BROWSER_ENGINE_HINTS = Object.freeze({
@@ -119,7 +119,7 @@ var BROWSER_ENGINE_HINTS = Object.freeze({
119
119
  "chrome"
120
120
  ])
121
121
  });
122
- /** Glob pattern (relative to a store base) matching a versioned Chromium binary, keyed by `process.platform`. */
122
+ /** Names the glob pattern (relative to a store base) matching a versioned Chromium binary, keyed by `process.platform`. */
123
123
  var BROWSER_STORE_GLOBS = Object.freeze({
124
124
  linux: "chromium-*/chrome-linux*/chrome",
125
125
  darwin: "chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium",
@@ -128,7 +128,7 @@ var BROWSER_STORE_GLOBS = Object.freeze({
128
128
  //#endregion
129
129
  //#region src/server/errors.ts
130
130
  /**
131
- * A CDP connection, discovery, or launch attempt failed.
131
+ * Reports that a CDP connection, discovery, or launch attempt failed.
132
132
  */
133
133
  var BrowserConnectionError = class extends _src_core.BrowserError {
134
134
  constructor(message, context) {
@@ -137,7 +137,7 @@ var BrowserConnectionError = class extends _src_core.BrowserError {
137
137
  }
138
138
  };
139
139
  /**
140
- * An operation requiring an active connection was attempted while disconnected.
140
+ * Reports that an operation requiring an active connection was attempted while disconnected.
141
141
  */
142
142
  var BrowserNotConnectedError = class extends _src_core.BrowserError {
143
143
  constructor(context) {
@@ -146,7 +146,7 @@ var BrowserNotConnectedError = class extends _src_core.BrowserError {
146
146
  }
147
147
  };
148
148
  /**
149
- * An operation was attempted after the Browser was destroyed.
149
+ * Reports that an operation was attempted after the Browser was destroyed.
150
150
  */
151
151
  var BrowserDestroyedError = class extends _src_core.BrowserError {
152
152
  constructor(context) {
@@ -155,28 +155,28 @@ var BrowserDestroyedError = class extends _src_core.BrowserError {
155
155
  }
156
156
  };
157
157
  /**
158
- * Narrow an unknown value to BrowserConnectionError.
158
+ * Narrows an unknown value to BrowserConnectionError.
159
159
  *
160
160
  * @param value - Value to check
161
- * @returns True when value is a BrowserConnectionError instance
161
+ * @returns True if value is a BrowserConnectionError instance; false otherwise
162
162
  */
163
163
  function isBrowserConnectionError(value) {
164
164
  return (0, _orkestrel_contract.isInstance)(value, BrowserConnectionError);
165
165
  }
166
166
  /**
167
- * Narrow an unknown value to BrowserNotConnectedError.
167
+ * Narrows an unknown value to BrowserNotConnectedError.
168
168
  *
169
169
  * @param value - Value to check
170
- * @returns True when value is a BrowserNotConnectedError instance
170
+ * @returns True if value is a BrowserNotConnectedError instance; false otherwise
171
171
  */
172
172
  function isBrowserNotConnectedError(value) {
173
173
  return (0, _orkestrel_contract.isInstance)(value, BrowserNotConnectedError);
174
174
  }
175
175
  /**
176
- * Narrow an unknown value to BrowserDestroyedError.
176
+ * Narrows an unknown value to BrowserDestroyedError.
177
177
  *
178
178
  * @param value - Value to check
179
- * @returns True when value is a BrowserDestroyedError instance
179
+ * @returns True if value is a BrowserDestroyedError instance; false otherwise
180
180
  */
181
181
  function isBrowserDestroyedError(value) {
182
182
  return (0, _orkestrel_contract.isInstance)(value, BrowserDestroyedError);
@@ -184,7 +184,7 @@ function isBrowserDestroyedError(value) {
184
184
  //#endregion
185
185
  //#region src/server/helpers.ts
186
186
  /**
187
- * Enumerate every Chrome/Chromium/Edge executable discoverable on this
187
+ * Enumerates every Chrome/Chromium/Edge executable discoverable on this
188
188
  * machine, deduplicated by normalized absolute path.
189
189
  *
190
190
  * @remarks
@@ -198,7 +198,7 @@ function isBrowserDestroyedError(value) {
198
198
  * and the per-OS Playwright cache directory) — the top-level `chromium`
199
199
  * link/binary, then the highest-revision `chromium-*` install found
200
200
  *
201
- * Each candidate is classified into a {@link BrowserEngine} via
201
+ * Each candidate is classified into a {@link BrowserEngine} through
202
202
  * `parseBrowserEngine` (unclassifiable candidates default to `'chromium'`),
203
203
  * and `options.engine` (when given) narrows the result to that engine.
204
204
  *
@@ -209,13 +209,13 @@ function findSystemBrowsers(options) {
209
209
  const platform = process.platform;
210
210
  const env = options?.env ?? process.env;
211
211
  const candidates = [];
212
- candidates.push(...findAllEnvOverrides(env));
213
- const paths = options?.paths ?? defaultInstallPaths(platform, env);
214
- candidates.push(...findAllInstallPaths(paths));
212
+ candidates.push(...findEnvOverrides(env));
213
+ const paths = options?.paths ?? buildInstallPaths(platform, env);
214
+ candidates.push(...findInstallPaths(paths));
215
215
  const names = options?.names ?? BROWSER_EXECUTABLE_NAMES;
216
- candidates.push(...probeAllPathNames(names, platform));
217
- const stores = options?.stores ?? defaultStoreBases(env, platform);
218
- for (const store of stores) candidates.push(...findAllInStore(store, platform));
216
+ candidates.push(...probePathNames(names, platform));
217
+ const stores = options?.stores ?? buildStoreBases(env, platform);
218
+ for (const store of stores) candidates.push(...findStorePaths(store, platform));
219
219
  const seen = /* @__PURE__ */ new Set();
220
220
  const browsers = [];
221
221
  for (const executable of candidates) {
@@ -232,7 +232,7 @@ function findSystemBrowsers(options) {
232
232
  return browsers;
233
233
  }
234
234
  /**
235
- * Locate a Chrome/Chromium/Edge executable on this machine — the first entry
235
+ * Locates a Chrome/Chromium/Edge executable on this machine — the first entry
236
236
  * of {@link findSystemBrowsers}.
237
237
  *
238
238
  * @param options - Overrides for the candidate sources; see {@link SystemBrowserOptions}
@@ -242,7 +242,7 @@ function findSystemBrowser(options) {
242
242
  return findSystemBrowsers(options)[0];
243
243
  }
244
244
  /**
245
- * Classify an executable path/name into a {@link BrowserEngine} by
245
+ * Classifies an executable path/name into a {@link BrowserEngine} by
246
246
  * case-insensitive hint, checked in the order edge → chromium → chrome.
247
247
  *
248
248
  * @param executable - Executable path or command name to classify
@@ -256,19 +256,19 @@ function parseBrowserEngine(executable) {
256
256
  "chrome"
257
257
  ]) if (BROWSER_ENGINE_HINTS[engine].some((hint) => lower.includes(hint))) return engine;
258
258
  }
259
- /** Normalize an executable path for cross-source deduplication (case-insensitive on Windows). */
259
+ /** Normalizes an executable path for cross-source deduplication (case-insensitive on Windows). */
260
260
  function normalizeExecutablePath(path, platform) {
261
261
  const normalized = platform === "win32" ? node_path.win32.normalize(path) : node_path.posix.normalize(path);
262
262
  return platform === "win32" ? normalized.toLowerCase() : normalized;
263
263
  }
264
- /** Classify a `/json/version` `Browser` string into a {@link BrowserEngine} (`Edg/` → edge, `Chrome/` → chrome, else chromium). */
264
+ /** Classifies a `/json/version` `Browser` string into a {@link BrowserEngine} (`Edg/` → edge, `Chrome/` → chrome, else chromium). */
265
265
  function browserToEngine(browser) {
266
266
  if (browser?.includes("Edg/") === true) return "edge";
267
267
  if (browser?.includes("Chrome/") === true) return "chrome";
268
268
  return "chromium";
269
269
  }
270
270
  /**
271
- * Resolve a persistent caller profile or create an isolated temporary one.
271
+ * Resolves a persistent caller profile or creates an isolated temporary one.
272
272
  *
273
273
  * @param path - Optional caller-owned persistent user-data directory
274
274
  * @returns The resolved profile and whether the library owns its lifecycle
@@ -284,7 +284,7 @@ async function createBrowserProfile(path) {
284
284
  };
285
285
  }
286
286
  /**
287
- * Remove a library-owned isolated browser profile.
287
+ * Removes a library-owned isolated browser profile.
288
288
  *
289
289
  * @remarks
290
290
  * The path must remain a direct child of the operating-system temp directory
@@ -297,7 +297,7 @@ async function removeBrowserProfile(profile) {
297
297
  if (!profile.temporary) return;
298
298
  const path = (0, node_path.resolve)(profile.path);
299
299
  const temporary = (0, node_path.resolve)((0, node_os.tmpdir)());
300
- if ((0, node_path.dirname)(path) !== temporary || !(0, node_path.basename)(path).startsWith("orkestrel-browser-")) throw new Error("Refusing to remove an unsafe browser profile path");
300
+ if ((0, node_path.dirname)(path) !== temporary || !(0, node_path.basename)(path).startsWith("orkestrel-browser-")) throw new _src_core.BrowserError("Refusing to remove an unsafe browser profile path", void 0, { path });
301
301
  await (0, node_fs_promises.rm)(path, {
302
302
  recursive: true,
303
303
  force: true,
@@ -305,12 +305,8 @@ async function removeBrowserProfile(profile) {
305
305
  retryDelay: 100
306
306
  });
307
307
  }
308
- /** Check the env-override keys (`PLAYWRIGHT_EXECUTABLE_PATH`, `CHROME_PATH`) in order for an existing file. */
309
- function findEnvOverride(env) {
310
- return findAllEnvOverrides(env)[0];
311
- }
312
- /** Check the env-override keys (`PLAYWRIGHT_EXECUTABLE_PATH`, `CHROME_PATH`) in order, returning every one that exists. */
313
- function findAllEnvOverrides(env) {
308
+ /** Checks the env-override keys (`PLAYWRIGHT_EXECUTABLE_PATH`, `CHROME_PATH`) in order and returns every one that exists. */
309
+ function findEnvOverrides(env) {
314
310
  const found = [];
315
311
  for (const key of BROWSER_ENV_PATH_KEYS) {
316
312
  const value = env[key];
@@ -318,49 +314,64 @@ function findAllEnvOverrides(env) {
318
314
  }
319
315
  return found;
320
316
  }
321
- /** Build the default well-known install-path candidates for a platform, deriving Windows roots from env vars. */
322
- function defaultInstallPaths(platform, env) {
317
+ /** Builds the default well-known install-path candidates for a platform, deriving Windows roots from env vars. */
318
+ function buildInstallPaths(platform, env) {
323
319
  if (platform !== "win32") return BROWSER_EXECUTABLE_PATHS[platform] ?? [];
324
- const roots = windowsRoots(env);
320
+ const roots = buildWindowsRoots(env);
325
321
  const paths = [];
326
322
  for (const root of roots) for (const suffix of BROWSER_WINDOWS_SUFFIXES) paths.push(node_path.win32.join(root, suffix));
327
323
  return paths;
328
324
  }
329
- /** Derive Windows install roots from env vars, falling back to well-known literals when absent. */
330
- function windowsRoots(env) {
325
+ /** Derives Windows install roots from env vars, falling back to well-known literals when absent. */
326
+ function buildWindowsRoots(env) {
331
327
  return [
332
328
  env["PROGRAMFILES"] ?? BROWSER_WINDOWS_ROOT_FALLBACKS["PROGRAMFILES"],
333
329
  env["PROGRAMFILES(X86)"] ?? BROWSER_WINDOWS_ROOT_FALLBACKS["PROGRAMFILES(X86)"],
334
330
  env["LOCALAPPDATA"]
335
331
  ].filter((root) => root !== void 0);
336
332
  }
337
- /** Return the first candidate path that exists on disk. */
338
- function findInstallPath(paths) {
339
- return findAllInstallPaths(paths)[0];
340
- }
341
- /** Return every candidate path that exists on disk, in the given order. */
342
- function findAllInstallPaths(paths) {
333
+ /** Returns every candidate path that exists on disk, in the given order. */
334
+ function findInstallPaths(paths) {
343
335
  return paths.filter((path) => (0, node_fs.existsSync)(path));
344
336
  }
345
- /** Probe PATH (`which`/`where`) for the first resolvable command name. */
337
+ /** Probes PATH (`which`/`where`) for every resolvable command name, in the given order. */
346
338
  function probePathNames(names, platform) {
347
- return probeAllPathNames(names, platform)[0];
348
- }
349
- /** Probe PATH (`which`/`where`) for every resolvable command name, in the given order. */
350
- function probeAllPathNames(names, platform) {
351
339
  const finder = platform === "win32" ? "where" : "which";
352
340
  const found = [];
353
341
  for (const name of names) {
354
342
  const result = (0, node_child_process.spawnSync)(finder, [name], { stdio: "pipe" });
355
343
  if (result.status === 0) {
356
- const output = result.stdout.toString("utf-8").trim().split("\n")[0];
357
- if (output !== void 0 && output.length > 0) found.push(output);
344
+ const executable = readFirstLine(result.stdout.toString("utf-8"));
345
+ if (executable !== void 0) found.push(executable);
358
346
  }
359
347
  }
360
348
  return found;
361
349
  }
362
- /** Build the default Playwright browser store base directories to search for a managed Chromium. */
363
- function defaultStoreBases(env, platform) {
350
+ /**
351
+ * Returns the first non-empty line of a command's output, without its
352
+ * surrounding whitespace.
353
+ *
354
+ * @remarks
355
+ * `where` reports every PATH match on its own CRLF-terminated line, so a
356
+ * carriage return left on the first line would travel into `existsSync` and
357
+ * `spawn` as part of the executable path.
358
+ *
359
+ * @param output - Raw decoded standard output of a command
360
+ * @returns The first line carrying text, or undefined when the output has none
361
+ *
362
+ * @example
363
+ * ```ts
364
+ * readFirstLine('C:\\bin\\chrome.exe\r\nC:\\other\\chrome.exe\r\n') // 'C:\\bin\\chrome.exe'
365
+ * ```
366
+ */
367
+ function readFirstLine(output) {
368
+ for (const line of output.split(/\r?\n/)) {
369
+ const text = line.trim();
370
+ if (text.length > 0) return text;
371
+ }
372
+ }
373
+ /** Builds the default Playwright browser store base directories to search for a managed Chromium. */
374
+ function buildStoreBases(env, platform) {
364
375
  const bases = [];
365
376
  const envBase = env[BROWSER_STORE_ENV_KEY];
366
377
  if (envBase !== void 0 && envBase.length > 0) bases.push(envBase);
@@ -375,12 +386,8 @@ function defaultStoreBases(env, platform) {
375
386
  }
376
387
  return bases;
377
388
  }
378
- /** Search one store base for the top-level `chromium` link, else the highest-revision `chromium-*` install. */
379
- function findInStore(base, platform) {
380
- return findAllInStore(base, platform)[0];
381
- }
382
- /** Search one store base for the top-level `chromium` link and every `chromium-*` install, highest revision first. */
383
- function findAllInStore(base, platform) {
389
+ /** Searches one store base for the top-level `chromium` link and every `chromium-*` install, highest revision first. */
390
+ function findStorePaths(base, platform) {
384
391
  const joiner = platform === "win32" ? node_path.win32.join : node_path.posix.join;
385
392
  const found = [];
386
393
  const linkPath = joiner(base, BROWSER_STORE_LINK_NAME);
@@ -394,11 +401,16 @@ function findAllInStore(base, platform) {
394
401
  return found;
395
402
  }
396
403
  /**
397
- * Launch a browser process with raw-CDP debugging flags.
404
+ * Launches a browser process with raw-CDP debugging flags.
398
405
  *
399
406
  * @remarks
400
407
  * POSIX launches own an isolated process group so lifecycle teardown can
401
408
  * signal and await every Chromium subprocess without affecting the caller.
409
+ * Windows launches are not detached and own no group, so teardown there
410
+ * signals one process by identifier: the spawned process, or the process the
411
+ * launcher handed the endpoint to when that spawned process re-executed the
412
+ * browser and exited. Terminating a Chromium browser process takes its own
413
+ * subprocesses with it, so that single signal drains the tree.
402
414
  *
403
415
  * @param executable - Absolute path to the browser executable
404
416
  * @param port - Port the browser exposes its CDP endpoint on
@@ -419,11 +431,12 @@ function launchBrowserProcess(executable, port, headless, profile, extra) {
419
431
  });
420
432
  }
421
433
  /**
422
- * Poll a browser's CDP version endpoint until it responds or the timeout elapses.
434
+ * Polls a browser's CDP version endpoint until it responds or the timeout elapses.
423
435
  *
424
436
  * @param port - Port the browser exposes its CDP endpoint on
425
437
  * @param timeout - Maximum time to wait in milliseconds
426
438
  * @param host - Host the browser exposes its CDP endpoint on (default `127.0.0.1`)
439
+ * @param signal - Optional external abort; an abort while waiting rethrows rather than resolving
427
440
  * @returns The browser's WebSocket debugger URL
428
441
  *
429
442
  * @throws When the endpoint does not become ready before the timeout
@@ -447,15 +460,33 @@ async function waitForCDPReady(port, timeout, host = BROWSER_DEFAULT_HOST, signa
447
460
  const delay = Math.min(_src_core.BROWSER_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now()));
448
461
  if (delay > 0) await (0, node_timers_promises.setTimeout)(delay, void 0, { signal });
449
462
  }
450
- throw new Error(`CDP endpoint on port ${port} did not become ready within ${timeout}ms`);
463
+ throw new BrowserConnectionError(`CDP endpoint on port ${port} did not become ready within ${timeout}ms`, {
464
+ port,
465
+ timeout
466
+ });
451
467
  }
452
468
  /**
453
- * Fetch the current CDP target list from a browser's `/json/list` endpoint.
469
+ * Fetches the current CDP target list from a browser's `/json/list` endpoint.
470
+ *
471
+ * @remarks
472
+ * The endpoint is a network boundary, so an unreachable host, a non-2xx
473
+ * response, and a body that is not a JSON array each come back as a failed
474
+ * `Result` carrying a coded `BrowserConnectionError`. An entry missing a
475
+ * required string field is skipped rather than failing the whole list.
454
476
  *
455
477
  * @param port - Port the browser exposes its CDP endpoint on
456
478
  * @param timeout - Request timeout in milliseconds
457
479
  * @param host - Host the browser exposes its CDP endpoint on (default `127.0.0.1`)
458
- * @returns Normalized CDP targets
480
+ * @returns The normalized CDP targets, or the fault that stopped the request
481
+ *
482
+ * @example
483
+ * ```ts
484
+ * import { fetchCDPTargets } from '@orkestrel/browser/server'
485
+ *
486
+ * const result = await fetchCDPTargets(9222, 2000)
487
+ * if (result.success) log(result.value.length)
488
+ * else log(result.error.code)
489
+ * ```
459
490
  */
460
491
  async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
461
492
  const url = `${BROWSER_CDP_PROTOCOL}://${host}:${port}${BROWSER_CDP_LIST_PATH}`;
@@ -463,23 +494,41 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
463
494
  const timer = setTimeout(() => controller.abort(), timeout);
464
495
  try {
465
496
  const response = await fetch(url, { signal: controller.signal });
466
- if (!response.ok) return [];
497
+ if (!response.ok) return {
498
+ success: false,
499
+ error: new BrowserConnectionError("CDP target list request was refused", {
500
+ url,
501
+ status: response.status
502
+ })
503
+ };
467
504
  const list = await response.json();
468
- if (!(0, _orkestrel_contract.isArray)(list)) return [];
505
+ if (!(0, _orkestrel_contract.isArray)(list)) return {
506
+ success: false,
507
+ error: new BrowserConnectionError("CDP target list is not a JSON array", { url })
508
+ };
469
509
  const targets = [];
470
510
  for (const entry of list) {
471
511
  if (!(0, _orkestrel_contract.isRecord)(entry)) continue;
472
512
  if (!(0, _orkestrel_contract.isString)(entry["id"]) || !(0, _orkestrel_contract.isString)(entry["type"]) || !(0, _orkestrel_contract.isString)(entry["title"]) || !(0, _orkestrel_contract.isString)(entry["url"])) continue;
473
513
  targets.push({
474
514
  id: entry["id"],
475
- type: entry["type"],
515
+ category: entry["type"],
476
516
  title: entry["title"],
477
517
  url: entry["url"]
478
518
  });
479
519
  }
480
- return targets;
481
- } catch {
482
- return [];
520
+ return {
521
+ success: true,
522
+ value: targets
523
+ };
524
+ } catch (error) {
525
+ return {
526
+ success: false,
527
+ error: new BrowserConnectionError("CDP target list is unreachable", {
528
+ url,
529
+ error
530
+ })
531
+ };
483
532
  } finally {
484
533
  clearTimeout(timer);
485
534
  }
@@ -487,27 +536,36 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
487
536
  //#endregion
488
537
  //#region src/server/transports/WebSocketCDPTransport.ts
489
538
  /**
490
- * `@orkestrel/websocket`-backed raw CDP text transport.
539
+ * Provides a raw CDP text transport backed by `@orkestrel/websocket`.
491
540
  *
492
541
  * @remarks
493
542
  * Performs and validates the RFC 6455 HTTP upgrade, then delegates frame
494
543
  * handling to `@orkestrel/websocket`. Concurrent and repeated `start()` /
495
544
  * `close()` calls share their active transition, and a later `start()` opens a
496
545
  * fresh socket after the prior one closes.
546
+ *
547
+ * @example
548
+ * ```ts
549
+ * import { WebSocketCDPTransport } from '@orkestrel/browser/server'
550
+ *
551
+ * const transport = new WebSocketCDPTransport({ url: 'ws://127.0.0.1:9222/devtools/browser/abc' })
552
+ * await transport.start()
553
+ * await transport.close()
554
+ * ```
497
555
  */
498
556
  var WebSocketCDPTransport = class {
499
557
  #emitter;
500
558
  #url;
501
559
  #timeout;
502
560
  #socket;
503
- #starting;
504
- #closing;
561
+ #starting = new _src_core.BrowserTransition();
562
+ #closing = new _src_core.BrowserTransition();
505
563
  #request;
506
564
  #resolve;
507
565
  #reject;
508
566
  #timer;
509
567
  #closeResolve;
510
- #onSocketClose = this.#handleSocketClose.bind(this);
568
+ #socketCloseHandler = this.#handleSocketClose.bind(this);
511
569
  constructor(options) {
512
570
  this.#emitter = new _orkestrel_emitter.Emitter({
513
571
  ...options.on !== void 0 ? { on: options.on } : {},
@@ -520,43 +578,27 @@ var WebSocketCDPTransport = class {
520
578
  return this.#emitter;
521
579
  }
522
580
  async start() {
523
- if (this.#closing !== void 0) await this.#closing;
581
+ const closing = this.#closing.pending;
582
+ if (closing !== void 0) await closing;
524
583
  if (this.#socket?.readyState === _orkestrel_websocket.WEBSOCKET_READY_OPEN) return;
525
- const active = this.#starting;
584
+ const active = this.#starting.pending;
526
585
  if (active !== void 0) {
527
586
  await active;
528
587
  return;
529
588
  }
530
589
  this.#socket?.destroy();
531
590
  this.#socket = void 0;
532
- const transition = this.#open();
533
- this.#starting = transition;
534
- try {
535
- await transition;
536
- } finally {
537
- if (this.#starting === transition) this.#starting = void 0;
538
- }
591
+ await this.#starting.execute(() => this.#start());
539
592
  }
540
593
  async send(data) {
541
594
  const socket = this.#socket;
542
- if (socket === void 0 || socket.readyState !== _orkestrel_websocket.WEBSOCKET_READY_OPEN) throw new Error("WebSocket CDP transport is not open");
595
+ if (socket === void 0 || socket.readyState !== _orkestrel_websocket.WEBSOCKET_READY_OPEN) throw new BrowserConnectionError("WebSocket CDP transport is not open", { url: this.#url });
543
596
  socket.send(data);
544
597
  }
545
598
  async close() {
546
- const active = this.#closing;
547
- if (active !== void 0) {
548
- await active;
549
- return;
550
- }
551
- const transition = this.#stop();
552
- this.#closing = transition;
553
- try {
554
- await transition;
555
- } finally {
556
- if (this.#closing === transition) this.#closing = void 0;
557
- }
599
+ await this.#closing.execute(() => this.#close());
558
600
  }
559
- async #open() {
601
+ async #start() {
560
602
  const parsed = (0, _orkestrel_contract.attempt)(() => new URL(this.#url));
561
603
  if (!parsed.success) throw new BrowserConnectionError(`WebSocket CDP URL is invalid: ${this.#url}`, {
562
604
  url: this.#url,
@@ -592,19 +634,19 @@ var WebSocketCDPTransport = class {
592
634
  this.#socket = socket;
593
635
  this.#bind(socket);
594
636
  }
595
- async #stop() {
637
+ async #close() {
596
638
  const request = this.#request;
597
639
  if (request !== void 0) {
598
640
  this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} was closed before it finished connecting`, { url: this.#url }));
599
641
  request.destroy();
600
642
  }
601
- await this.#starting?.catch(() => void 0);
643
+ await this.#starting.pending?.catch(() => void 0);
602
644
  const socket = this.#socket;
603
645
  this.#socket = void 0;
604
646
  if (socket === void 0 || socket.readyState === _orkestrel_websocket.WEBSOCKET_READY_CLOSED) return;
605
647
  const closed = Promise.withResolvers();
606
648
  this.#closeResolve = closed.resolve;
607
- socket.emitter.on("close", this.#onSocketClose);
649
+ socket.emitter.on("close", this.#socketCloseHandler);
608
650
  try {
609
651
  socket.close();
610
652
  if (socket.readyState !== _orkestrel_websocket.WEBSOCKET_READY_CLOSED) await closed.promise;
@@ -612,7 +654,7 @@ var WebSocketCDPTransport = class {
612
654
  socket.destroy();
613
655
  if (socket.readyState !== _orkestrel_websocket.WEBSOCKET_READY_CLOSED) await closed.promise;
614
656
  } finally {
615
- socket.emitter.off("close", this.#onSocketClose);
657
+ socket.emitter.off("close", this.#socketCloseHandler);
616
658
  this.#closeResolve = void 0;
617
659
  }
618
660
  }
@@ -687,16 +729,46 @@ var WebSocketCDPTransport = class {
687
729
  }
688
730
  };
689
731
  //#endregion
732
+ //#region src/server/writers/FileBrowserWriter.ts
733
+ /**
734
+ * Persists captured browser bytes to the filesystem through `node:fs/promises`.
735
+ *
736
+ * @remarks
737
+ * Creates every missing parent directory of the target path before writing, so
738
+ * a caller may name a nested destination that does not exist yet.
739
+ *
740
+ * @example
741
+ * ```ts
742
+ * import { FileBrowserWriter } from '@orkestrel/browser/server'
743
+ *
744
+ * const writer = new FileBrowserWriter()
745
+ * await writer.write('shots/hero.png', new Uint8Array([137, 80, 78, 71]))
746
+ * ```
747
+ */
748
+ var FileBrowserWriter = class {
749
+ /**
750
+ * Writes the captured bytes to a path, creating its parent directories.
751
+ *
752
+ * @param path - Destination file path
753
+ * @param data - Captured bytes to persist
754
+ * @returns A promise resolving once the bytes are on disk
755
+ */
756
+ async write(path, data) {
757
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(path), { recursive: true });
758
+ await (0, node_fs_promises.writeFile)(path, data);
759
+ }
760
+ };
761
+ //#endregion
690
762
  //#region src/server/factories.ts
691
763
  /**
692
- * Create a raw-CDP Browser façade.
764
+ * Creates a raw-CDP Browser façade.
693
765
  *
694
766
  * @param options - Connection, launch, and viewport configuration
695
767
  * @returns A {@link BrowserInterface}
696
768
  *
697
769
  * @example
698
770
  * ```ts
699
- * import { createBrowser } from '@src/server'
771
+ * import { createBrowser } from '@orkestrel/browser/server'
700
772
  *
701
773
  * const browser = createBrowser()
702
774
  * await browser.connect()
@@ -706,7 +778,7 @@ function createBrowser(options) {
706
778
  return new Browser(options);
707
779
  }
708
780
  /**
709
- * Create a Node `WebSocket`-backed CDP transport.
781
+ * Creates a Node `WebSocket`-backed CDP transport.
710
782
  *
711
783
  * @param options - The CDP WebSocket debugger URL (and optional timeout)
712
784
  * @returns A {@link CDPTransportInterface}
@@ -715,20 +787,27 @@ function createCDPTransport(options) {
715
787
  return new WebSocketCDPTransport(options);
716
788
  }
717
789
  /**
718
- * Create a filesystem-backed screenshot writer.
790
+ * Creates a filesystem-backed browser writer.
719
791
  *
720
- * @returns A {@link ScreenshotWriterInterface} that persists bytes via `node:fs/promises`
792
+ * @returns A {@link BrowserWriterInterface} that persists bytes through `node:fs/promises`
721
793
  */
722
- function createScreenshotWriter() {
723
- return { async write(path, data) {
724
- await (0, node_fs_promises.mkdir)((0, node_path.dirname)(path), { recursive: true });
725
- await (0, node_fs_promises.writeFile)(path, data);
726
- } };
794
+ function createBrowserWriter() {
795
+ return new FileBrowserWriter();
727
796
  }
728
797
  //#endregion
729
798
  //#region src/server/Browser.ts
730
799
  /**
731
800
  * Discovers, launches, connects to, and owns Chromium-family browser sessions.
801
+ *
802
+ * @example
803
+ * ```ts
804
+ * import { Browser } from '@orkestrel/browser/server'
805
+ *
806
+ * const browser = new Browser({ headless: true })
807
+ * await browser.connect()
808
+ * const page = await browser.create({ url: 'https://example.com' })
809
+ * await browser.destroy()
810
+ * ```
732
811
  */
733
812
  var Browser = class {
734
813
  #emitter;
@@ -743,17 +822,18 @@ var Browser = class {
743
822
  #endpoint;
744
823
  #client;
745
824
  #process;
825
+ #servingPid;
746
826
  #profile;
747
827
  #contexts = [];
748
828
  #destroyed = false;
749
- #connecting;
750
- #disconnecting;
829
+ #connecting = new _src_core.BrowserTransition();
830
+ #disconnecting = new _src_core.BrowserTransition();
751
831
  #exitCleanup;
752
832
  #shutdown;
753
833
  #transport;
754
- #onTransportClose = this.#handleTransportLoss.bind(this);
755
- #onTransportError = this.#handleTransportLoss.bind(this);
756
- #onProcessExit = this.#handleProcessExit.bind(this);
834
+ #transportCloseHandler = this.#handleTransportLoss.bind(this);
835
+ #transportErrorHandler = this.#handleTransportLoss.bind(this);
836
+ #processExitHandler = this.#handleProcessExit.bind(this);
757
837
  constructor(options) {
758
838
  this.#emitter = new _orkestrel_emitter.Emitter({
759
839
  ...options?.on !== void 0 ? { on: options.on } : {},
@@ -780,11 +860,8 @@ var Browser = class {
780
860
  get owned() {
781
861
  return this.#owned;
782
862
  }
783
- get connected() {
784
- return this.#status === "connected";
785
- }
786
863
  get pid() {
787
- return this.#process?.pid;
864
+ return this.#servingPid ?? this.#process?.pid;
788
865
  }
789
866
  async discover() {
790
867
  const result = await this.#discoverCDP();
@@ -793,19 +870,13 @@ var Browser = class {
793
870
  }
794
871
  async connect() {
795
872
  if (this.#destroyed) throw new BrowserDestroyedError();
796
- const active = this.#connecting;
873
+ const active = this.#connecting.pending;
797
874
  if (active !== void 0) {
798
875
  await active;
799
876
  return;
800
877
  }
801
878
  if (this.#status === "connected") return;
802
- const attempt = this.#establish();
803
- this.#connecting = attempt;
804
- try {
805
- await attempt;
806
- } finally {
807
- if (this.#connecting === attempt) this.#connecting = void 0;
808
- }
879
+ await this.#connecting.execute(() => this.#establish());
809
880
  }
810
881
  adopt() {
811
882
  if (this.#destroyed) throw new BrowserDestroyedError();
@@ -814,19 +885,13 @@ var Browser = class {
814
885
  }
815
886
  async disconnect() {
816
887
  if (this.#destroyed) return;
817
- const disconnecting = this.#disconnecting;
818
- const connecting = this.#connecting;
888
+ const disconnecting = this.#disconnecting.pending;
889
+ const connecting = this.#connecting.pending;
819
890
  if (disconnecting !== void 0) await disconnecting;
820
891
  if (connecting !== void 0) await connecting.catch(() => void 0);
821
892
  if (this.#destroyed) return;
822
893
  if (this.#status !== "connected" || this.#client === void 0) return;
823
- const attempt = this.#detach();
824
- this.#disconnecting = attempt;
825
- try {
826
- await attempt;
827
- } finally {
828
- if (this.#disconnecting === attempt) this.#disconnecting = void 0;
829
- }
894
+ await this.#disconnecting.execute(() => this.#detach());
830
895
  }
831
896
  context(index) {
832
897
  const i = index ?? 0;
@@ -850,7 +915,7 @@ var Browser = class {
850
915
  const result = await client.send("Target.createBrowserContext", params);
851
916
  if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new BrowserConnectionError("Failed to create isolated browser context");
852
917
  const id = result["browserContextId"];
853
- const context = new _src_core.BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createScreenshotWriter(), options?.emulation, options?.downloads);
918
+ const context = new _src_core.BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createBrowserWriter(), options?.emulation, options?.downloads, options);
854
919
  try {
855
920
  if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
856
921
  behavior: options.downloads.named === true ? "allowAndName" : "allow",
@@ -871,7 +936,7 @@ var Browser = class {
871
936
  if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
872
937
  let context = this.#contexts[0];
873
938
  if (context === void 0) {
874
- context = new _src_core.BrowserContext(client, void 0, this.#options.viewport, createScreenshotWriter());
939
+ context = new _src_core.BrowserContext(client, void 0, this.#options.viewport, createBrowserWriter());
875
940
  this.#registerContext(context);
876
941
  }
877
942
  const page = await context.create(options);
@@ -899,7 +964,8 @@ var Browser = class {
899
964
  return shutdown;
900
965
  }
901
966
  async #establish() {
902
- if (this.#disconnecting !== void 0) await this.#disconnecting;
967
+ const disconnecting = this.#disconnecting.pending;
968
+ if (disconnecting !== void 0) await disconnecting;
903
969
  await this.#settleExit();
904
970
  if (this.#destroyed) throw new BrowserDestroyedError();
905
971
  if (this.#status === "connected") return;
@@ -917,7 +983,7 @@ var Browser = class {
917
983
  }
918
984
  if (this.#options.cdp?.discover ?? true) {
919
985
  const discovery = await this.#raceAbort(this.#discoverCDP(this.#signal()));
920
- if (discovery.found && discovery.endpoint !== void 0) {
986
+ if (discovery.endpoint !== void 0) {
921
987
  this.#engine = browserToEngine(discovery.browser);
922
988
  await this.#connectCDP(discovery.endpoint);
923
989
  return;
@@ -980,6 +1046,7 @@ var Browser = class {
980
1046
  this.#unbindTransport();
981
1047
  this.#unbindProcess();
982
1048
  this.#process = void 0;
1049
+ this.#servingPid = void 0;
983
1050
  const cleanup = this.#cleanupExitedProcess(process, contexts, client);
984
1051
  this.#exitCleanup = cleanup;
985
1052
  cleanup.catch((error) => this.#emitter.emit("error", error));
@@ -997,27 +1064,19 @@ var Browser = class {
997
1064
  }
998
1065
  #handleTransportLoss() {
999
1066
  if (this.#destroyed || this.#status !== "connected") return;
1000
- const process = this.#process;
1001
- if (process !== void 0 && (process.exitCode !== null || process.signalCode !== null)) {
1002
- this.#handleProcessExit();
1067
+ if (this.#process === void 0) {
1068
+ this.#confirmTransportLoss();
1003
1069
  return;
1004
1070
  }
1005
- if (process !== void 0) {
1006
- setTimeout(() => this.#confirmTransportLoss(), 50);
1071
+ if (!this.#alive()) {
1072
+ this.#handleProcessExit();
1007
1073
  return;
1008
1074
  }
1009
- this.#confirmTransportLoss();
1075
+ setTimeout(() => this.#confirmTransportLoss(), 50);
1010
1076
  }
1011
1077
  #confirmTransportLoss() {
1012
1078
  if (this.#destroyed || this.#status !== "connected") return;
1013
- const process = this.#process;
1014
- if (process !== void 0 && (process.exitCode !== null || process.signalCode !== null)) {
1015
- this.#handleProcessExit();
1016
- return;
1017
- }
1018
- if (process?.pid !== void 0) try {
1019
- process.kill(0);
1020
- } catch {
1079
+ if (this.#process !== void 0 && !this.#alive()) {
1021
1080
  this.#handleProcessExit();
1022
1081
  return;
1023
1082
  }
@@ -1042,23 +1101,24 @@ var Browser = class {
1042
1101
  #bindTransport(transport) {
1043
1102
  this.#unbindTransport();
1044
1103
  this.#transport = transport;
1045
- transport.emitter.on("close", this.#onTransportClose);
1046
- transport.emitter.on("error", this.#onTransportError);
1104
+ transport.emitter.on("close", this.#transportCloseHandler);
1105
+ transport.emitter.on("error", this.#transportErrorHandler);
1047
1106
  }
1048
1107
  #unbindTransport() {
1049
1108
  const transport = this.#transport;
1050
1109
  if (transport === void 0) return;
1051
- transport.emitter.off("close", this.#onTransportClose);
1052
- transport.emitter.off("error", this.#onTransportError);
1110
+ transport.emitter.off("close", this.#transportCloseHandler);
1111
+ transport.emitter.off("error", this.#transportErrorHandler);
1053
1112
  this.#transport = void 0;
1054
1113
  }
1055
1114
  #bindProcess(process) {
1056
1115
  this.#unbindProcess();
1057
- process.once("exit", this.#onProcessExit);
1058
- if (process.exitCode !== null || process.signalCode !== null) queueMicrotask(this.#onProcessExit);
1116
+ if (this.#servingPid !== void 0) return;
1117
+ process.once("exit", this.#processExitHandler);
1118
+ if (process.exitCode !== null || process.signalCode !== null) queueMicrotask(this.#processExitHandler);
1059
1119
  }
1060
1120
  #unbindProcess() {
1061
- this.#process?.off("exit", this.#onProcessExit);
1121
+ this.#process?.off("exit", this.#processExitHandler);
1062
1122
  }
1063
1123
  #timeout() {
1064
1124
  return this.#options.timeout ?? _src_core.BROWSER_DEFAULT_TIMEOUT_MS;
@@ -1071,13 +1131,9 @@ var Browser = class {
1071
1131
  if (!response.ok) return this.#notFound();
1072
1132
  const info = await response.json();
1073
1133
  if (!(0, _orkestrel_contract.isRecord)(info)) return this.#notFound();
1074
- const endpoint = (0, _orkestrel_contract.isString)(info["webSocketDebuggerUrl"]) ? info["webSocketDebuggerUrl"] : void 0;
1075
- const browser = (0, _orkestrel_contract.isString)(info["Browser"]) ? info["Browser"] : void 0;
1076
1134
  return {
1077
- found: endpoint !== void 0,
1078
- endpoint,
1079
- browser,
1080
- connection: endpoint !== void 0 ? "cdp" : void 0
1135
+ endpoint: (0, _orkestrel_contract.isString)(info["webSocketDebuggerUrl"]) ? info["webSocketDebuggerUrl"] : void 0,
1136
+ browser: (0, _orkestrel_contract.isString)(info["Browser"]) ? info["Browser"] : void 0
1081
1137
  };
1082
1138
  } catch (error) {
1083
1139
  if (signal?.aborted === true) throw error;
@@ -1086,10 +1142,8 @@ var Browser = class {
1086
1142
  }
1087
1143
  #notFound() {
1088
1144
  return {
1089
- found: false,
1090
1145
  endpoint: void 0,
1091
- browser: void 0,
1092
- connection: void 0
1146
+ browser: void 0
1093
1147
  };
1094
1148
  }
1095
1149
  async #assertPortFree() {
@@ -1178,6 +1232,7 @@ var Browser = class {
1178
1232
  timeout: this.#timeout()
1179
1233
  });
1180
1234
  await this.#raceAbort(client.connect());
1235
+ await this.#takeEndpointOwner(process, client);
1181
1236
  const connection = this.#options.profile === void 0 ? "launch" : "persistent";
1182
1237
  this.#client = client;
1183
1238
  this.#endpoint = endpoint;
@@ -1196,12 +1251,49 @@ var Browser = class {
1196
1251
  await this.#terminate(process);
1197
1252
  await this.#releaseProfile();
1198
1253
  this.#process = void 0;
1254
+ this.#servingPid = void 0;
1199
1255
  this.#client = void 0;
1200
1256
  this.#endpoint = void 0;
1201
1257
  this.#owned = void 0;
1202
1258
  throw error;
1203
1259
  }
1204
1260
  }
1261
+ /**
1262
+ * Takes over the process serving the CDP endpoint when the spawned child is
1263
+ * no longer it.
1264
+ *
1265
+ * @remarks
1266
+ * A Windows launcher — Microsoft Edge — re-executes the browser with the
1267
+ * same `--remote-debugging-port` and exits 0 before the endpoint answers, so
1268
+ * the child this instance spawned parents the real tree without being part
1269
+ * of it. Chromium names the process behind the endpoint as the `browser`
1270
+ * entry of `SystemInfo.getProcessInfo`, which identifies it without a
1271
+ * platform branch. An endpoint that names no such process cannot be owned,
1272
+ * so the launch closes it and fails rather than leaking it.
1273
+ */
1274
+ async #takeEndpointOwner(process, client) {
1275
+ if (process.exitCode === null && process.signalCode === null) return;
1276
+ let result;
1277
+ try {
1278
+ result = await client.send("SystemInfo.getProcessInfo");
1279
+ } catch {
1280
+ result = void 0;
1281
+ }
1282
+ let pid;
1283
+ if ((0, _orkestrel_contract.isRecord)(result) && (0, _orkestrel_contract.isArray)(result["processInfo"])) for (const entry of result["processInfo"]) {
1284
+ if (!(0, _orkestrel_contract.isRecord)(entry) || entry["type"] !== "browser" || !(0, _orkestrel_contract.isInteger)(entry["id"])) continue;
1285
+ pid = entry["id"];
1286
+ break;
1287
+ }
1288
+ if (pid === void 0) {
1289
+ await this.#closeRemote(client);
1290
+ throw new BrowserConnectionError("The browser launcher exited without naming the process serving its CDP endpoint", {
1291
+ executable: this.#options.executable,
1292
+ pid: process.pid
1293
+ });
1294
+ }
1295
+ this.#servingPid = pid;
1296
+ }
1205
1297
  async #waitForLaunch(process, executable, args) {
1206
1298
  const context = {
1207
1299
  executable,
@@ -1213,6 +1305,7 @@ var Browser = class {
1213
1305
  const exited = (0, node_events.once)(process, "exit", { signal }).then((values) => {
1214
1306
  const code = (0, _orkestrel_contract.isInteger)(values[0]) ? values[0] : null;
1215
1307
  const exitSignal = (0, _orkestrel_contract.isString)(values[1]) ? values[1] : null;
1308
+ if (code === 0 && exitSignal === null) return ready;
1216
1309
  throw new BrowserConnectionError(this.#formatLaunchExit(code, exitSignal), context);
1217
1310
  });
1218
1311
  try {
@@ -1251,13 +1344,13 @@ var Browser = class {
1251
1344
  if (!(0, _orkestrel_contract.isRecord)(target) || !(0, _orkestrel_contract.isString)(target["targetId"]) || target["type"] !== "page" || !(0, _orkestrel_contract.isString)(target["title"]) || !(0, _orkestrel_contract.isString)(target["url"])) continue;
1252
1345
  pages.push({
1253
1346
  id: target["targetId"],
1254
- type: target["type"],
1347
+ category: target["type"],
1255
1348
  title: target["title"],
1256
1349
  url: target["url"]
1257
1350
  });
1258
1351
  }
1259
1352
  if (pages.length === 0 || this.#contexts.length > 0) return;
1260
- const context = new _src_core.BrowserContext(client, void 0, this.#options.viewport, createScreenshotWriter());
1353
+ const context = new _src_core.BrowserContext(client, void 0, this.#options.viewport, createBrowserWriter());
1261
1354
  await context.sync(pages);
1262
1355
  this.#registerContext(context);
1263
1356
  }
@@ -1275,6 +1368,7 @@ var Browser = class {
1275
1368
  this.#contexts = [];
1276
1369
  await this.#terminate(this.#process);
1277
1370
  this.#process = void 0;
1371
+ this.#servingPid = void 0;
1278
1372
  terminated = true;
1279
1373
  } finally {
1280
1374
  const contexts = this.#contexts;
@@ -1304,6 +1398,7 @@ var Browser = class {
1304
1398
  if (!await this.#waitForTerminationWithin(process, 3e3)) await this.#terminate(process);
1305
1399
  }
1306
1400
  this.#process = void 0;
1401
+ this.#servingPid = void 0;
1307
1402
  terminated = true;
1308
1403
  } finally {
1309
1404
  const contexts = this.#contexts;
@@ -1319,8 +1414,8 @@ var Browser = class {
1319
1414
  }
1320
1415
  }
1321
1416
  async #settle() {
1322
- await this.#connecting?.catch(() => void 0);
1323
- await this.#disconnecting?.catch(() => void 0);
1417
+ await this.#connecting.pending?.catch(() => void 0);
1418
+ await this.#disconnecting.pending?.catch(() => void 0);
1324
1419
  await this.#settleExit();
1325
1420
  }
1326
1421
  async #settleExit() {
@@ -1415,31 +1510,39 @@ var Browser = class {
1415
1510
  const pid = process.pid;
1416
1511
  return globalThis.process.platform !== "win32" && pid !== void 0 && pid > 0;
1417
1512
  }
1418
- #inspectProcessGroup(process) {
1419
- if (!this.#hasProcessGroup(process)) return false;
1513
+ #alive() {
1514
+ const process = this.#process;
1515
+ if (process === void 0) return false;
1516
+ if (this.#servingPid !== void 0) return this.#signalProcess(process, 0) !== false;
1517
+ return process.exitCode === null && process.signalCode === null;
1518
+ }
1519
+ #inspectRemainder(process) {
1520
+ if (!this.#hasProcessGroup(process) && this.#servingPid === void 0) return false;
1420
1521
  return this.#signalProcess(process, 0);
1421
1522
  }
1422
1523
  #signalProcess(process, signal) {
1423
1524
  const pid = process.pid;
1525
+ const serving = this.#servingPid;
1424
1526
  try {
1425
1527
  if (this.#hasProcessGroup(process) && pid !== void 0) return globalThis.process.kill(-pid, signal);
1528
+ if (serving !== void 0) return globalThis.process.kill(serving, signal);
1426
1529
  return process.kill(signal);
1427
1530
  } catch (error) {
1428
1531
  const code = (0, _orkestrel_contract.isError)(error) && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : void 0;
1429
1532
  if (code === "ESRCH") return false;
1430
1533
  if (signal === 0) return code === "EPERM" ? true : void 0;
1431
1534
  throw new BrowserConnectionError("Failed to signal the browser process", {
1432
- pid,
1535
+ pid: serving ?? pid,
1433
1536
  signal,
1434
1537
  cause: error
1435
1538
  });
1436
1539
  }
1437
1540
  }
1438
- async #waitForProcessGroupWithin(process, timeout) {
1541
+ async #waitForRemainderWithin(process, timeout) {
1439
1542
  const deadline = performance.now() + timeout;
1440
1543
  let confirmed = false;
1441
1544
  while (true) {
1442
- const alive = this.#inspectProcessGroup(process);
1545
+ const alive = this.#inspectRemainder(process);
1443
1546
  if (alive === false) return true;
1444
1547
  if (alive === true) confirmed = true;
1445
1548
  const remaining = deadline - performance.now();
@@ -1449,17 +1552,17 @@ var Browser = class {
1449
1552
  }
1450
1553
  async #waitForTerminationWithin(process, timeout, final = false) {
1451
1554
  const exited = this.#waitForProcessWithin(process, timeout);
1452
- const drained = this.#waitForProcessGroupWithin(process, timeout);
1555
+ const drained = this.#waitForRemainderWithin(process, timeout);
1453
1556
  const results = await Promise.all([exited, drained]);
1454
1557
  return results[0] && (results[1] === true || final && results[1] === void 0);
1455
1558
  }
1456
1559
  async #terminate(process) {
1457
- if (process === void 0 || (process.exitCode !== null || process.signalCode !== null) && this.#inspectProcessGroup(process) === false) return;
1560
+ if (process === void 0 || (process.exitCode !== null || process.signalCode !== null) && this.#inspectRemainder(process) === false) return;
1458
1561
  if (this.#signalProcess(process, "SIGTERM") === false) return;
1459
1562
  if (await this.#waitForTerminationWithin(process, 3e3)) return;
1460
1563
  if (this.#signalProcess(process, "SIGKILL") === false) return;
1461
1564
  if (await this.#waitForTerminationWithin(process, 3e3, true)) return;
1462
- throw new BrowserConnectionError("Browser process did not exit after SIGKILL", { pid: process.pid });
1565
+ throw new BrowserConnectionError("Browser process did not exit after SIGKILL", { pid: this.#servingPid ?? process.pid });
1463
1566
  }
1464
1567
  #finish() {
1465
1568
  this.#unbindTransport();
@@ -1470,6 +1573,7 @@ var Browser = class {
1470
1573
  this.#endpoint = void 0;
1471
1574
  this.#client = void 0;
1472
1575
  this.#process = void 0;
1576
+ this.#servingPid = void 0;
1473
1577
  this.#exitCleanup = void 0;
1474
1578
  this.#contexts = [];
1475
1579
  this.#emitter.emit("destroy");
@@ -1505,21 +1609,20 @@ exports.Browser = Browser;
1505
1609
  exports.BrowserConnectionError = BrowserConnectionError;
1506
1610
  exports.BrowserDestroyedError = BrowserDestroyedError;
1507
1611
  exports.BrowserNotConnectedError = BrowserNotConnectedError;
1612
+ exports.FileBrowserWriter = FileBrowserWriter;
1508
1613
  exports.WebSocketCDPTransport = WebSocketCDPTransport;
1509
1614
  exports.browserToEngine = browserToEngine;
1615
+ exports.buildInstallPaths = buildInstallPaths;
1616
+ exports.buildStoreBases = buildStoreBases;
1617
+ exports.buildWindowsRoots = buildWindowsRoots;
1510
1618
  exports.createBrowser = createBrowser;
1511
1619
  exports.createBrowserProfile = createBrowserProfile;
1620
+ exports.createBrowserWriter = createBrowserWriter;
1512
1621
  exports.createCDPTransport = createCDPTransport;
1513
- exports.createScreenshotWriter = createScreenshotWriter;
1514
- exports.defaultInstallPaths = defaultInstallPaths;
1515
- exports.defaultStoreBases = defaultStoreBases;
1516
1622
  exports.fetchCDPTargets = fetchCDPTargets;
1517
- exports.findAllEnvOverrides = findAllEnvOverrides;
1518
- exports.findAllInStore = findAllInStore;
1519
- exports.findAllInstallPaths = findAllInstallPaths;
1520
- exports.findEnvOverride = findEnvOverride;
1521
- exports.findInStore = findInStore;
1522
- exports.findInstallPath = findInstallPath;
1623
+ exports.findEnvOverrides = findEnvOverrides;
1624
+ exports.findInstallPaths = findInstallPaths;
1625
+ exports.findStorePaths = findStorePaths;
1523
1626
  exports.findSystemBrowser = findSystemBrowser;
1524
1627
  exports.findSystemBrowsers = findSystemBrowsers;
1525
1628
  exports.isBrowserConnectionError = isBrowserConnectionError;
@@ -1528,10 +1631,9 @@ exports.isBrowserNotConnectedError = isBrowserNotConnectedError;
1528
1631
  exports.launchBrowserProcess = launchBrowserProcess;
1529
1632
  exports.normalizeExecutablePath = normalizeExecutablePath;
1530
1633
  exports.parseBrowserEngine = parseBrowserEngine;
1531
- exports.probeAllPathNames = probeAllPathNames;
1532
1634
  exports.probePathNames = probePathNames;
1635
+ exports.readFirstLine = readFirstLine;
1533
1636
  exports.removeBrowserProfile = removeBrowserProfile;
1534
1637
  exports.waitForCDPReady = waitForCDPReady;
1535
- exports.windowsRoots = windowsRoots;
1536
1638
 
1537
1639
  //# sourceMappingURL=index.cjs.map