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