@orkestrel/browser 0.0.24 → 0.0.26

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.
@@ -38,8 +38,21 @@ var BROWSER_CDP_VERSION_PATH = "/json/version";
38
38
  * other target category Chromium reports — `'/json/list'`.
39
39
  */
40
40
  var BROWSER_CDP_LIST_PATH = "/json/list";
41
- /** Lists the flags always passed to a launched browser process, alongside the caller's own. */
42
- var BROWSER_LAUNCH_ARGS = Object.freeze(["--no-first-run", "--no-default-browser-check"]);
41
+ /**
42
+ * Lists the flags always passed to a launched browser process, alongside the caller's own.
43
+ *
44
+ * @remarks
45
+ * Disables account sync so extension installs cannot reset URL loader factories during page imports.
46
+ * Disables Edge's implicit sign-in so fresh automation profiles carry no Microsoft account.
47
+ * Chromium ignores the unknown Edge feature name on Chrome and Chromium.
48
+ * The launcher merges disabled features with the caller's into one switch.
49
+ */
50
+ var BROWSER_LAUNCH_ARGS = Object.freeze([
51
+ "--no-first-run",
52
+ "--no-default-browser-check",
53
+ "--disable-sync",
54
+ "--disable-features=msImplicitSignin"
55
+ ]);
43
56
  /**
44
57
  * Names the flag that enables headless mode on a launched browser process, `'--headless=new'`.
45
58
  */
@@ -219,6 +232,10 @@ var BROWSER_SERVER_UNAVAILABLE = "BROWSER_SERVER_UNAVAILABLE";
219
232
  var BROWSER_SERVER_LAUNCH = "BROWSER_SERVER_LAUNCH";
220
233
  /** Sets the default number of warm browsers to 1. */
221
234
  var BROWSER_SERVER_POOL_SIZE = 1;
235
+ /** Sets the default context capacity per browser, including the shared holder. */
236
+ var BROWSER_SERVER_CONTEXTS = 2;
237
+ /** Limits context capacity to the largest measured per-browser count. */
238
+ var BROWSER_SERVER_CONTEXTS_LIMIT = 4;
222
239
  /** Limits the number of warm browsers to 3. */
223
240
  var BROWSER_SERVER_POOL_LIMIT = 3;
224
241
  /** Permits one failed refill before the next failure spends the bound. */
@@ -673,6 +690,10 @@ function findStorePaths(base, platform) {
673
690
  * Launches a browser process with raw-CDP debugging flags.
674
691
  *
675
692
  * @remarks
693
+ * Merges every caller and library `--disable-features` value into one switch because Chromium
694
+ * reads only the last occurrence. Keeps `msImplicitSignin` disabled alongside caller features;
695
+ * Chrome and Chromium ignore this Edge feature name. Preserves the caller's other arguments.
696
+ *
676
697
  * POSIX launches own an isolated process group so lifecycle teardown can
677
698
  * signal and await every Chromium subprocess without affecting the caller.
678
699
  * Windows launches are not detached and own no group, so teardown there
@@ -690,8 +711,18 @@ function findStorePaths(base, platform) {
690
711
  */
691
712
  function launchBrowserProcess(executable, port, headless, profile, extra) {
692
713
  const args = [];
693
- if (extra !== void 0) args.push(...extra);
694
- args.push(`--remote-debugging-port=${port ?? 0}`, ...BROWSER_LAUNCH_ARGS);
714
+ const features = /* @__PURE__ */ new Set();
715
+ for (const arg of [
716
+ ...extra ?? [],
717
+ `--remote-debugging-port=${port ?? 0}`,
718
+ ...BROWSER_LAUNCH_ARGS
719
+ ]) {
720
+ if (arg === "--disable-features") continue;
721
+ if (arg.startsWith("--disable-features=")) {
722
+ for (const feature of arg.slice(19).split(",")) if (feature.trim() !== "") features.add(feature.trim());
723
+ } else args.push(arg);
724
+ }
725
+ args.push(`--disable-features=${[...features].join(",")}`);
695
726
  if (headless) args.push(BROWSER_HEADLESS_ARG);
696
727
  if (profile !== void 0) args.push(`--user-data-dir=${profile}`);
697
728
  return (0, node_child_process.spawn)(executable, args, {
@@ -1042,7 +1073,7 @@ var FileBrowserWriter = class {
1042
1073
  };
1043
1074
  //#endregion
1044
1075
  //#region package.json
1045
- var version = "0.0.24";
1076
+ var version = "0.0.26";
1046
1077
  //#endregion
1047
1078
  //#region src/server/BrowserMCPServer.ts
1048
1079
  /**
@@ -1058,17 +1089,18 @@ var version = "0.0.24";
1058
1089
  * tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
1059
1090
  * removed when the toolset withdraws the tool, so the server's tool list changes and a subscribed
1060
1091
  * client is notified. The server also advertises `acquire`, `execute`, `tools`, and `destroy` with
1061
- * `BROWSER_SERVER_COPY`. Startup warms the pool and leases the shared browser. The legacy
1092
+ * `BROWSER_SERVER_COPY`. Startup warms the pool and assigns a prepared context to the shared holder. The legacy
1062
1093
  * handshake and browser calls await that setup. Named holders each own a lease, a pending grant,
1063
1094
  * and loss notices; concurrent calls on a holder share its replacement grant. Ending a holder
1064
- * destroys its token, and admission counts that holder until disposal settles.
1095
+ * closes its context and releases its lease, and admission counts that holder until disposal settles.
1065
1096
  * A launch creates `ROOT/.profiles/PID-UUID/` exclusively, connects a browser that never attaches to an
1066
1097
  * existing endpoint, opens one page in an isolated context, and constructs the page toolset with
1067
1098
  * that context and the file journey and run stores under the root, on a manager the toolset owns.
1068
1099
  * A dispatcher forwards its arguments and the call's context, signal included, to the toolset's
1069
1100
  * manager and returns that manager's value, or rejects with its failure message; a text value
1070
1101
  * reaches the client as one text block rather than as its JSON text.
1071
- * A disconnect or current-page crash retires its slot. A failed call receives a liveness ping;
1102
+ * A disconnect retires its browser and every hosted generation. A current-page crash retires only
1103
+ * that holder's context. A failed call receives a liveness ping;
1072
1104
  * loss makes its outcome unresolved, while a known success remains successful. Pending losses
1073
1105
  * prefix the next outcome on a successor, or a refusal, and calls are never repeated.
1074
1106
  * Concurrent checks share one ping per slot. Cancelling a call ends only its wait; the ping keeps
@@ -1076,7 +1108,8 @@ var version = "0.0.24";
1076
1108
  *
1077
1109
  * `start()` serves stdio and destroys the server at the end of its input, on `SIGINT`, and on
1078
1110
  * `SIGTERM`. `destroy()` stops reading requests and removes every dispatcher, then destroys every
1079
- * slot through the pool — its toolset, which aborts the active replay, then its browser, then its
1111
+ * slot through the pool — each toolset, which aborts its active replay, then its context and downloads,
1112
+ * then its browser and its
1080
1113
  * profile — and rechecks the profile folders this server made or kept.
1081
1114
  *
1082
1115
  * Chromium on Linux refuses to start as the root user unless its sandbox is disabled, so a server
@@ -1111,8 +1144,16 @@ var BrowserMCPServer = class {
1111
1144
  #folders = /* @__PURE__ */ new Set();
1112
1145
  #faults = /* @__PURE__ */ new Set();
1113
1146
  #losses = /* @__PURE__ */ new WeakMap();
1147
+ #departures = /* @__PURE__ */ new WeakMap();
1114
1148
  #pings = /* @__PURE__ */ new WeakMap();
1115
1149
  #watches = /* @__PURE__ */ new Map();
1150
+ #observed = /* @__PURE__ */ new Map();
1151
+ #prepared = /* @__PURE__ */ new Map();
1152
+ #owned = /* @__PURE__ */ new Map();
1153
+ #building = /* @__PURE__ */ new Map();
1154
+ #cleaning = /* @__PURE__ */ new WeakMap();
1155
+ #endings = /* @__PURE__ */ new WeakMap();
1156
+ #retentions = /* @__PURE__ */ new Map();
1116
1157
  #shared = {
1117
1158
  id: "shared",
1118
1159
  purpose: "Shared browser",
@@ -1126,7 +1167,7 @@ var BrowserMCPServer = class {
1126
1167
  #drains = /* @__PURE__ */ new Map();
1127
1168
  #readers = /* @__PURE__ */ new Map();
1128
1169
  #writers = /* @__PURE__ */ new Set();
1129
- #size;
1170
+ #admission;
1130
1171
  #reference;
1131
1172
  #references = 0;
1132
1173
  #failure;
@@ -1141,17 +1182,20 @@ var BrowserMCPServer = class {
1141
1182
  */
1142
1183
  constructor(options) {
1143
1184
  const size = options?.pool?.size ?? 1;
1185
+ const contexts = options?.pool?.contexts ?? 2;
1144
1186
  if (!(0, _orkestrel_contract.isInteger)(size) || size < 1 || size > 3) throw new _src_core.BrowserError(`pool.size must be an integer from 1 through 3`, BROWSER_SERVER_OPTIONS);
1145
- this.#size = size;
1187
+ if (!(0, _orkestrel_contract.isInteger)(contexts) || contexts < 1 || contexts > 4) throw new _src_core.BrowserError(`pool.contexts must be an integer from 1 through 4`, BROWSER_SERVER_OPTIONS);
1188
+ this.#admission = size * contexts;
1146
1189
  this.#log = options?.log ?? process.stderr;
1147
1190
  this.#reference = this.#issue.bind(this);
1148
1191
  this.#pool = (0, _orkestrel_pool.createPool)({
1149
1192
  create: this.#warm.bind(this),
1150
- destroy: (slot) => this.#destroySlot(slot.profile, slot.browser, slot.toolset, this.#losses.get(slot)?.cause),
1193
+ destroy: this.#destroyRecord.bind(this),
1151
1194
  validate: this.#validate.bind(this),
1152
1195
  watch: this.#watch.bind(this),
1153
1196
  error: this.#fault.bind(this),
1154
1197
  min: size,
1198
+ capacity: contexts,
1155
1199
  restarts: 1
1156
1200
  });
1157
1201
  this.#root = (0, node_path.resolve)(options?.root ?? "tmp/browsers");
@@ -1234,6 +1278,7 @@ var BrowserMCPServer = class {
1234
1278
  this.#transport.stop();
1235
1279
  for (const tool of this.#tools.tools()) this.#tools.remove(tool.name);
1236
1280
  this.#unmirror();
1281
+ for (const generation of this.#observed.keys()) this.#untrack(generation);
1237
1282
  this.#leases.clear();
1238
1283
  for (const holder of this.#holders.values()) holder.abort.abort();
1239
1284
  this.#abort.abort();
@@ -1242,6 +1287,7 @@ var BrowserMCPServer = class {
1242
1287
  });
1243
1288
  await this.#starting?.catch(() => void 0);
1244
1289
  await barrier;
1290
+ await Promise.allSettled(this.#grants.values());
1245
1291
  await Promise.allSettled(this.#disposals.values());
1246
1292
  this.#holders.clear();
1247
1293
  await this.#sweeping;
@@ -1293,7 +1339,7 @@ var BrowserMCPServer = class {
1293
1339
  if (this.#closing !== void 0) throw this.#ended();
1294
1340
  const purpose = args["purpose"];
1295
1341
  if (!(0, _orkestrel_contract.isString)(purpose) || purpose.trim().length === 0) throw new _src_core.BrowserError("purpose must describe the work", "BROWSER_TOOLSET_ARGUMENT");
1296
- if (this.#holders.size >= this.#size) throw new _src_core.BrowserError(`${BROWSER_SERVER_BUSY}: ${[...this.#holders.values()].map((holder) => `${holder.id} (${holder.purpose})`).join(", ")}. Call destroy for a holder no longer needed or call the named tools to share the shared browser.`, BROWSER_SERVER_BUSY);
1342
+ if (this.#holders.size >= this.#admission) throw new _src_core.BrowserError(`${BROWSER_SERVER_BUSY}: ${[...this.#holders.values()].map((holder) => `${holder.id} (${holder.purpose})`).join(", ")}. Call destroy for a holder no longer needed or call the named tools to share the shared browser.`, BROWSER_SERVER_BUSY);
1297
1343
  const holder = {
1298
1344
  id: (0, node_crypto.randomUUID)(),
1299
1345
  purpose,
@@ -1302,13 +1348,13 @@ var BrowserMCPServer = class {
1302
1348
  this.#holders.set(holder.id, holder);
1303
1349
  const listener = (0, node_events.addAbortListener)(context.signal, () => holder.abort.abort(context.signal.reason));
1304
1350
  try {
1305
- const slot = await this.#serve(holder, context.signal);
1351
+ const lease = await this.#serve(holder, context.signal);
1306
1352
  context.signal.throwIfAborted();
1307
1353
  if (holder.abort.signal.aborted || this.#holders.get(holder.id) !== holder) throw this.#ended();
1308
- if (this.#leases.get(holder)?.value !== slot) throw this.#unavailable(this.#losses.get(slot)?.cause);
1354
+ if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
1309
1355
  const catalog = {
1310
1356
  holder: holder.id,
1311
- tools: slot.toolset.tools.definitions()
1357
+ tools: lease.generation.toolset.tools.definitions()
1312
1358
  };
1313
1359
  return JSON.stringify(catalog);
1314
1360
  } catch (error) {
@@ -1334,12 +1380,12 @@ var BrowserMCPServer = class {
1334
1380
  const holder = this.#holder(args["holder"]);
1335
1381
  const signal = AbortSignal.any([context.signal, holder.abort.signal]);
1336
1382
  try {
1337
- const slot = await this.#serve(holder, signal);
1383
+ const lease = await this.#serve(holder, signal);
1338
1384
  signal.throwIfAborted();
1339
- if (this.#leases.get(holder)?.value !== slot) throw this.#unavailable(this.#losses.get(slot)?.cause);
1385
+ if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
1340
1386
  const catalog = {
1341
1387
  holder: holder.id,
1342
- tools: slot.toolset.tools.definitions()
1388
+ tools: lease.generation.toolset.tools.definitions()
1343
1389
  };
1344
1390
  return this.#annotate(holder, JSON.stringify(catalog));
1345
1391
  } catch (error) {
@@ -1358,17 +1404,19 @@ var BrowserMCPServer = class {
1358
1404
  const pending = this.#disposals.get(holder.id);
1359
1405
  if (pending !== void 0) return pending;
1360
1406
  holder.abort.abort();
1361
- const token = this.#leases.get(holder);
1407
+ const lease = this.#leases.get(holder);
1362
1408
  this.#leases.delete(holder);
1409
+ if (lease !== void 0) this.#untrack(lease.generation);
1363
1410
  const disposal = Promise.resolve().then(async () => {
1364
1411
  try {
1365
1412
  await this.#grants.get(holder)?.catch(() => void 0);
1366
- await token?.destroy();
1413
+ if (lease !== void 0) await this.#release(holder, lease);
1367
1414
  await Promise.all(this.#drains.get(holder) ?? []);
1415
+ await Promise.all([...this.#notices.get(holder) ?? []].filter((lost) => this.#losses.has(lost.token.value)).map((lost) => this.#endings.get(lost.token.value)?.promise));
1368
1416
  } catch (error) {
1369
1417
  this.#faults.add(error);
1370
1418
  } finally {
1371
- if (this.#holders.get(holder.id) === holder) this.#holders.delete(holder.id);
1419
+ if (this.#holders.get(holder.id) === holder && !this.#retentions.has(holder)) this.#holders.delete(holder.id);
1372
1420
  this.#notices.delete(holder);
1373
1421
  this.#drains.delete(holder);
1374
1422
  }
@@ -1401,9 +1449,9 @@ var BrowserMCPServer = class {
1401
1449
  }
1402
1450
  async #perform(holder, name, args, context) {
1403
1451
  const signal = holder === this.#shared ? context.signal : AbortSignal.any([context.signal, holder.abort.signal]);
1404
- let slot;
1452
+ let lease;
1405
1453
  try {
1406
- slot = await this.#serve(holder, signal);
1454
+ lease = await this.#serve(holder, signal);
1407
1455
  } catch (error) {
1408
1456
  context.signal.throwIfAborted();
1409
1457
  if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
@@ -1412,7 +1460,7 @@ var BrowserMCPServer = class {
1412
1460
  }
1413
1461
  let result;
1414
1462
  try {
1415
- const execution = slot.toolset.tools.execute({
1463
+ const execution = lease.generation.toolset.tools.execute({
1416
1464
  id: name,
1417
1465
  name,
1418
1466
  arguments: args
@@ -1434,23 +1482,23 @@ var BrowserMCPServer = class {
1434
1482
  context.signal.throwIfAborted();
1435
1483
  if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call", BROWSER_SERVER_UNRESOLVED);
1436
1484
  if (!result.success) {
1437
- if (this.#leases.get(holder)?.value === slot && !context.signal.aborted) try {
1438
- await this.#race(this.#ping(slot), context.signal);
1485
+ if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
1486
+ await this.#race(this.#ping(lease.token.value), context.signal);
1439
1487
  } catch {}
1440
1488
  context.signal.throwIfAborted();
1441
- if (this.#leases.get(holder)?.value !== slot) {
1442
- const loss = this.#losses.get(slot);
1443
- throw new _src_core.BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), slot), BROWSER_SERVER_UNRESOLVED);
1489
+ if (this.#leases.get(holder) !== lease) {
1490
+ const loss = this.#departures.get(lease);
1491
+ throw new _src_core.BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease), BROWSER_SERVER_UNRESOLVED);
1444
1492
  }
1445
- throw new _src_core.BrowserError(this.#annotate(holder, result.error, slot));
1493
+ throw new _src_core.BrowserError(this.#annotate(holder, result.error, lease));
1446
1494
  }
1447
- return typeof result.value === "string" ? this.#annotate(holder, result.value, slot) : result.value;
1495
+ return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
1448
1496
  }
1449
- #annotate(holder, value, slot) {
1497
+ #annotate(holder, value, lease) {
1450
1498
  const notes = [];
1451
1499
  for (const lost of this.#notices.get(holder) ?? []) {
1452
- if (lost === slot) continue;
1453
- const loss = this.#losses.get(lost);
1500
+ if (lost === lease) continue;
1501
+ const loss = this.#departures.get(lost);
1454
1502
  notes.push(describeBrowserServerLoss(BROWSER_SERVER_CRASH, loss?.cause, loss?.url));
1455
1503
  this.#notices.get(holder)?.delete(lost);
1456
1504
  }
@@ -1481,41 +1529,70 @@ var BrowserMCPServer = class {
1481
1529
  if (held !== void 0) return held;
1482
1530
  let granting = this.#grants.get(holder);
1483
1531
  if (granting === void 0) {
1484
- granting = this.#pool.acquire(holder.abort.signal).then(this.#hold.bind(this, holder));
1532
+ granting = Promise.resolve().then(async () => {
1533
+ await Promise.allSettled(this.#drains.get(holder) ?? []);
1534
+ if (this.#retentions.has(holder)) throw this.#unavailable(this.#retentions.get(holder));
1535
+ const token = await this.#pool.acquire(holder.abort.signal);
1536
+ return this.#hold(holder, token);
1537
+ });
1485
1538
  this.#grants.set(holder, granting);
1486
1539
  granting.finally(() => {
1487
1540
  if (this.#grants.get(holder) === granting) this.#grants.delete(holder);
1488
1541
  }).catch(() => void 0);
1489
1542
  }
1490
- const token = await this.#race(granting, signal);
1491
- if (token === void 0) throw this.#ended();
1492
- return token;
1543
+ const lease = await this.#race(granting, signal);
1544
+ if (lease === void 0) throw this.#ended();
1545
+ return lease;
1493
1546
  }
1494
1547
  async #hold(holder, token) {
1548
+ const slot = token.value;
1495
1549
  if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted) {
1496
- await token.destroy();
1550
+ token.release();
1497
1551
  throw this.#ended();
1498
1552
  }
1499
- if (this.#losses.has(token.value)) {
1500
- await token.destroy().catch(() => void 0);
1501
- throw this.#unavailable(this.#losses.get(token.value)?.cause);
1553
+ if (!this.#prepared.has(slot)) await this.#ping(slot);
1554
+ if (this.#closing !== void 0 || holder.abort.signal.aborted) {
1555
+ token.release();
1556
+ throw this.#ended();
1502
1557
  }
1503
- this.#leases.set(holder, token);
1558
+ if (this.#losses.has(slot)) {
1559
+ token.release();
1560
+ throw this.#unavailable(this.#losses.get(slot)?.cause);
1561
+ }
1562
+ let generation = this.#prepared.get(slot);
1563
+ this.#prepared.delete(slot);
1564
+ try {
1565
+ generation ??= await this.#build(slot, token);
1566
+ } catch (error) {
1567
+ if (this.#losses.has(slot)) await this.#endings.get(slot)?.promise;
1568
+ else token.release();
1569
+ throw this.#unavailable(error);
1570
+ }
1571
+ const lease = {
1572
+ token,
1573
+ generation
1574
+ };
1575
+ if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted || this.#losses.has(slot)) {
1576
+ await this.#release(holder, lease);
1577
+ throw this.#ended();
1578
+ }
1579
+ this.#leases.set(holder, lease);
1580
+ this.#observe(slot, generation);
1504
1581
  if (holder === this.#shared) {
1505
- const tools = token.value.toolset.tools;
1582
+ const tools = generation.toolset.tools;
1506
1583
  const mirrored = {
1507
- token,
1508
- added: this.#mirror.bind(this, token),
1509
- removed: this.#withdraw.bind(this, token),
1510
- cleared: this.#clear.bind(this, token)
1584
+ lease,
1585
+ added: this.#mirror.bind(this, lease),
1586
+ removed: this.#withdraw.bind(this, lease),
1587
+ cleared: this.#clear.bind(this, lease)
1511
1588
  };
1512
1589
  this.#mirrored = mirrored;
1513
- for (const tool of tools.tools()) this.#mirror(token, tool);
1590
+ for (const tool of tools.tools()) this.#mirror(lease, tool);
1514
1591
  tools.emitter.on("add", mirrored.added);
1515
1592
  tools.emitter.on("remove", mirrored.removed);
1516
1593
  tools.emitter.on("clear", mirrored.cleared);
1517
1594
  }
1518
- return token;
1595
+ return lease;
1519
1596
  }
1520
1597
  async #serve(holder, signal) {
1521
1598
  if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
@@ -1523,16 +1600,16 @@ var BrowserMCPServer = class {
1523
1600
  if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
1524
1601
  const held = this.#leases.get(holder);
1525
1602
  if (held !== void 0) try {
1526
- await this.#race(this.#ping(held.value), signal);
1527
- if (this.#leases.get(holder) === held) return held.value;
1603
+ await this.#race(this.#ping(held.token.value), signal);
1604
+ if (this.#leases.get(holder) === held) return held;
1528
1605
  } catch {
1529
1606
  signal.throwIfAborted();
1530
1607
  if (holder.abort.signal.aborted) throw this.#ended();
1531
1608
  }
1532
1609
  try {
1533
- const token = await this.#grant(holder, signal);
1534
- if (this.#leases.get(holder) !== token) throw this.#unavailable(this.#losses.get(token.value)?.cause);
1535
- return token.value;
1610
+ const lease = await this.#grant(holder, signal);
1611
+ if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
1612
+ return lease;
1536
1613
  } catch (error) {
1537
1614
  signal.throwIfAborted();
1538
1615
  if (holder.abort.signal.aborted || (0, _orkestrel_pool.isPoolError)(error) && error.code === "destroyed") throw this.#ended();
@@ -1563,78 +1640,211 @@ var BrowserMCPServer = class {
1563
1640
  }
1564
1641
  #lose(slot, cause) {
1565
1642
  if (this.#closing !== void 0 || this.#losses.has(slot)) return;
1566
- this.#losses.set(slot, {
1567
- cause,
1568
- url: slot.toolset.view.url
1569
- });
1643
+ this.#losses.set(slot, { cause });
1570
1644
  this.#failure = cause;
1571
- for (const [holder, token] of this.#leases) {
1572
- if (token.value !== slot || this.#holders.get(holder.id) !== holder) continue;
1573
- if (holder === this.#shared) this.#unmirror();
1574
- this.#leases.delete(holder);
1575
- const notices = this.#notices.get(holder) ?? /* @__PURE__ */ new Set();
1576
- notices.add(slot);
1577
- this.#notices.set(holder, notices);
1578
- const drain = token.destroy();
1579
- const drains = this.#drains.get(holder) ?? /* @__PURE__ */ new Set();
1580
- drains.add(drain);
1581
- this.#drains.set(holder, drains);
1582
- drain.finally(() => drains.delete(drain)).catch(() => void 0);
1645
+ for (const [holder, lease] of this.#leases) {
1646
+ if (lease.token.value !== slot) continue;
1647
+ this.#detach(holder, lease, cause);
1583
1648
  }
1649
+ const watch = this.#watches.get(slot);
1650
+ this.#unwatch(slot);
1651
+ watch?.resolve(cause);
1652
+ }
1653
+ #detach(holder, lease, cause) {
1654
+ if (this.#leases.get(holder) !== lease || this.#holders.get(holder.id) !== holder) return;
1655
+ this.#departures.set(lease, {
1656
+ cause,
1657
+ url: lease.generation.toolset.view.url
1658
+ });
1659
+ if (holder === this.#shared) this.#unmirror();
1660
+ this.#leases.delete(holder);
1661
+ this.#untrack(lease.generation);
1662
+ const notices = this.#notices.get(holder) ?? /* @__PURE__ */ new Set();
1663
+ notices.add(lease);
1664
+ this.#notices.set(holder, notices);
1584
1665
  }
1585
1666
  #watch(slot, signal) {
1586
1667
  const loss = Promise.withResolvers();
1587
1668
  const disconnect = this.#disconnect.bind(this, slot);
1588
- const page = this.#track.bind(this, slot);
1589
1669
  const subscription = (0, node_events.addAbortListener)(signal, this.#unwatch.bind(this, slot));
1590
1670
  this.#watches.set(slot, {
1591
1671
  resolve: loss.resolve,
1592
1672
  disconnect,
1593
- page,
1594
- subscription,
1595
- crashes: /* @__PURE__ */ new Map()
1673
+ subscription
1596
1674
  });
1597
1675
  slot.browser.emitter.on("disconnect", disconnect);
1598
- slot.context.emitter.on("page", page);
1599
- for (const current of slot.context.pages()) this.#track(slot, current);
1600
1676
  if (signal.aborted) this.#unwatch(slot);
1601
1677
  else if (slot.browser.status !== "connected") this.#disconnect(slot);
1602
1678
  return loss.promise;
1603
1679
  }
1604
- #track(slot, page) {
1605
- const watch = this.#watches.get(slot);
1680
+ #observe(slot, generation) {
1681
+ if (this.#observed.has(generation)) return;
1682
+ const page = this.#track.bind(this, slot, generation);
1683
+ this.#observed.set(generation, {
1684
+ page,
1685
+ crashes: /* @__PURE__ */ new Map()
1686
+ });
1687
+ generation.context.emitter.on("page", page);
1688
+ for (const current of generation.context.pages()) this.#track(slot, generation, current);
1689
+ }
1690
+ #track(slot, generation, page) {
1691
+ const watch = this.#observed.get(generation);
1606
1692
  if (watch === void 0 || watch.crashes.has(page)) return;
1607
- const crash = this.#createCrash(slot, page);
1693
+ const crash = this.#crash.bind(this, slot, generation, page);
1608
1694
  page.emitter.on("crash", crash);
1609
- this.#watches.set(slot, {
1695
+ this.#observed.set(generation, {
1610
1696
  ...watch,
1611
1697
  crashes: new Map([...watch.crashes, [page, crash]])
1612
1698
  });
1613
1699
  }
1614
- #createCrash(slot, page) {
1615
- return () => {
1616
- if (page === slot.toolset.view) this.#report(slot, /* @__PURE__ */ new Error("The current page renderer crashed"));
1617
- };
1700
+ #crash(slot, generation, page) {
1701
+ if (this.#prepared.get(slot) === generation) {
1702
+ this.#prepared.delete(slot);
1703
+ this.#untrack(generation);
1704
+ this.#clean(slot, generation).catch(this.#fault.bind(this));
1705
+ return;
1706
+ }
1707
+ const held = [...this.#leases].find(([, lease]) => lease.generation === generation);
1708
+ if (held === void 0 || page !== generation.toolset.view) return;
1709
+ const [holder, lease] = held;
1710
+ this.#detach(holder, lease, /* @__PURE__ */ new Error("The current page renderer crashed"));
1711
+ const drain = this.#release(holder, lease);
1712
+ const drains = this.#drains.get(holder) ?? /* @__PURE__ */ new Set();
1713
+ drains.add(drain);
1714
+ this.#drains.set(holder, drains);
1715
+ drain.finally(() => drains.delete(drain)).catch(this.#fault.bind(this));
1716
+ }
1717
+ #untrack(generation) {
1718
+ const watch = this.#observed.get(generation);
1719
+ if (watch === void 0) return;
1720
+ this.#observed.delete(generation);
1721
+ generation.context.emitter.off("page", watch.page);
1722
+ for (const [page, crash] of watch.crashes) page.emitter.off("crash", crash);
1618
1723
  }
1619
1724
  #disconnect(slot) {
1620
- this.#report(slot, /* @__PURE__ */ new Error(slot.browser.pid === void 0 ? "The browser process exited" : "The browser transport disconnected"));
1621
- }
1622
- #report(slot, cause) {
1623
- const watch = this.#watches.get(slot);
1624
- if (watch === void 0) return;
1625
- this.#unwatch(slot);
1626
- this.#lose(slot, cause);
1627
- watch.resolve(cause);
1725
+ this.#lose(slot, /* @__PURE__ */ new Error(slot.browser.pid === void 0 ? "The browser process exited" : "The browser transport disconnected"));
1628
1726
  }
1629
1727
  #unwatch(slot) {
1630
1728
  const watch = this.#watches.get(slot);
1631
1729
  if (watch === void 0) return;
1632
1730
  this.#watches.delete(slot);
1633
1731
  slot.browser.emitter.off("disconnect", watch.disconnect);
1634
- slot.context.emitter.off("page", watch.page);
1635
- for (const [page, crash] of watch.crashes) page.emitter.off("crash", crash);
1636
1732
  watch.subscription[Symbol.dispose]();
1637
1733
  }
1734
+ async #release(holder, lease) {
1735
+ const slot = lease.token.value;
1736
+ const removed = await this.#clean(slot, lease.generation);
1737
+ const disposal = lease.generation.context.disposal;
1738
+ if (!removed) this.#retentions.set(holder, /* @__PURE__ */ new Error("The context downloads directory remains owned"));
1739
+ if (disposal?.confirmed !== true && !this.#losses.has(slot) && this.#closing === void 0) {
1740
+ if (![...this.#leases.values()].some((held) => held.token.value === slot && held !== lease) && (this.#building.get(slot)?.size ?? 0) === 0) lease.token.release();
1741
+ this.#lose(slot, disposal?.error ?? /* @__PURE__ */ new Error("The context disposal is unconfirmed"));
1742
+ await this.#endings.get(slot)?.promise;
1743
+ } else if (removed) lease.token.release();
1744
+ }
1745
+ #clean(slot, generation) {
1746
+ let cleaning = this.#cleaning.get(generation);
1747
+ if (cleaning !== void 0) return cleaning;
1748
+ cleaning = Promise.resolve().then(async () => {
1749
+ await generation.toolset.destroy().catch(this.#fault.bind(this));
1750
+ await generation.context.close().catch((error) => {
1751
+ if (!this.#losses.has(slot) && slot.browser.status === "connected") this.#fault(error);
1752
+ });
1753
+ try {
1754
+ await (0, node_fs_promises.rm)(generation.directory, {
1755
+ recursive: true,
1756
+ force: true
1757
+ });
1758
+ this.#owned.get(slot)?.delete(generation);
1759
+ return true;
1760
+ } catch (error) {
1761
+ this.#fault(error);
1762
+ return false;
1763
+ }
1764
+ });
1765
+ this.#cleaning.set(generation, cleaning);
1766
+ return cleaning;
1767
+ }
1768
+ #build(slot, token) {
1769
+ const construction = this.#construct(slot, token);
1770
+ const building = this.#building.get(slot) ?? /* @__PURE__ */ new Set();
1771
+ building.add(construction);
1772
+ this.#building.set(slot, building);
1773
+ construction.finally(() => building.delete(construction)).catch(() => void 0);
1774
+ return construction;
1775
+ }
1776
+ async #construct(slot, token) {
1777
+ const directory = (0, node_path.join)(slot.profile, "contexts", (0, node_crypto.randomUUID)(), "downloads");
1778
+ let context;
1779
+ let toolset;
1780
+ try {
1781
+ await (0, node_fs_promises.mkdir)(directory, { recursive: true });
1782
+ let page;
1783
+ try {
1784
+ context = await slot.browser.isolate({
1785
+ reference: this.#reference,
1786
+ downloads: { path: directory },
1787
+ ...this.#viewport === void 0 ? {} : { emulation: { viewport: this.#viewport } }
1788
+ });
1789
+ page = await context.create();
1790
+ } catch (error) {
1791
+ if (token !== void 0) {
1792
+ if (![...this.#leases.values()].some((lease) => lease.token.value === slot) && (this.#building.get(slot)?.size ?? 0) === 1) token.release();
1793
+ this.#lose(slot, error);
1794
+ }
1795
+ throw error;
1796
+ }
1797
+ toolset = (0, _src_core.createBrowserToolset)(page, {
1798
+ context,
1799
+ journeys: {
1800
+ store: createFileBrowserJourneyStore({ root: this.#root }),
1801
+ runs: createFileBrowserRunStore({ root: this.#root }),
1802
+ readonly: this.#readonly
1803
+ }
1804
+ });
1805
+ await toolset.start();
1806
+ const generation = {
1807
+ context,
1808
+ toolset,
1809
+ directory
1810
+ };
1811
+ const owned = this.#owned.get(slot) ?? /* @__PURE__ */ new Set();
1812
+ owned.add(generation);
1813
+ this.#owned.set(slot, owned);
1814
+ return generation;
1815
+ } catch (error) {
1816
+ await toolset?.destroy().catch(this.#fault.bind(this));
1817
+ await context?.close().catch((failure) => {
1818
+ if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !(0, _src_core.isCDPConnectionError)(failure)) this.#fault(failure);
1819
+ });
1820
+ await (0, node_fs_promises.rm)(directory, {
1821
+ recursive: true,
1822
+ force: true
1823
+ }).catch(this.#fault.bind(this));
1824
+ throw error;
1825
+ }
1826
+ }
1827
+ async #destroyRecord(slot) {
1828
+ if ((0, _src_core.isCDPTimeoutError)(this.#losses.get(slot)?.cause) && slot.browser.pid !== void 0) try {
1829
+ process.kill(slot.browser.pid, "SIGKILL");
1830
+ } catch (error) {
1831
+ if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
1832
+ }
1833
+ try {
1834
+ await Promise.allSettled(this.#building.get(slot) ?? []);
1835
+ const prepared = this.#prepared.get(slot);
1836
+ if (prepared !== void 0) this.#untrack(prepared);
1837
+ this.#prepared.delete(slot);
1838
+ await Promise.all([...this.#owned.get(slot) ?? []].map((generation) => this.#clean(slot, generation)));
1839
+ await this.#destroySlot(slot.profile, slot.browser);
1840
+ this.#owned.delete(slot);
1841
+ this.#building.delete(slot);
1842
+ this.#endings.get(slot)?.resolve();
1843
+ } catch (error) {
1844
+ this.#endings.get(slot)?.reject(error);
1845
+ throw error;
1846
+ }
1847
+ }
1638
1848
  #fault(error) {
1639
1849
  this.#faults.add(error);
1640
1850
  }
@@ -1643,7 +1853,7 @@ var BrowserMCPServer = class {
1643
1853
  if (this.#stranded !== void 0) throw new _src_core.BrowserError((0, _orkestrel_contract.isError)(this.#stranded) ? this.#stranded.message : String(this.#stranded), BROWSER_SERVER_TEARDOWN, (0, _src_core.isBrowserError)(this.#stranded) ? this.#stranded.context : void 0);
1644
1854
  const profile = (0, node_path.join)(this.#root, ".profiles", formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)()));
1645
1855
  let browser;
1646
- let toolset;
1856
+ let slot;
1647
1857
  try {
1648
1858
  await (0, node_fs_promises.mkdir)(profile);
1649
1859
  this.#folders.add(profile);
@@ -1665,42 +1875,26 @@ var BrowserMCPServer = class {
1665
1875
  }));
1666
1876
  await (0, node_fs_promises.rename)(`${record}.tmp`, record);
1667
1877
  }
1668
- const context = await browser.isolate({
1669
- reference: this.#reference,
1670
- downloads: { path: (0, node_path.join)(profile, "downloads") },
1671
- ...this.#viewport === void 0 ? {} : { emulation: { viewport: this.#viewport } }
1672
- });
1673
- const page = await context.create();
1674
- toolset = (0, _src_core.createBrowserToolset)(page, {
1675
- context,
1676
- journeys: {
1677
- store: createFileBrowserJourneyStore({ root: this.#root }),
1678
- runs: createFileBrowserRunStore({ root: this.#root }),
1679
- readonly: this.#readonly
1680
- }
1681
- });
1682
- await toolset.start();
1683
- return {
1878
+ slot = {
1684
1879
  browser,
1685
- profile,
1686
- context,
1687
- toolset
1880
+ profile
1688
1881
  };
1882
+ const ending = Promise.withResolvers();
1883
+ ending.promise.catch(() => void 0);
1884
+ this.#endings.set(slot, ending);
1885
+ const generation = await this.#build(slot);
1886
+ this.#prepared.set(slot, generation);
1887
+ this.#observe(slot, generation);
1888
+ return slot;
1689
1889
  } catch (error) {
1690
- if (this.#folders.has(profile)) await this.#destroySlot(profile, browser, toolset).catch((failure) => {
1890
+ if (this.#folders.has(profile)) await (slot === void 0 ? this.#destroySlot(profile, browser) : this.#destroyRecord(slot)).catch((failure) => {
1691
1891
  this.#stranded = failure;
1692
1892
  });
1693
1893
  if (this.#closing === void 0) this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_LAUNCH, error)}`);
1694
1894
  throw error;
1695
1895
  }
1696
1896
  }
1697
- async #destroySlot(profile, browser, toolset, cause) {
1698
- if ((0, _src_core.isCDPTimeoutError)(cause) && browser?.pid !== void 0) try {
1699
- process.kill(browser.pid, "SIGKILL");
1700
- } catch (error) {
1701
- if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
1702
- }
1703
- await toolset?.destroy().catch((error) => this.#faults.add(error));
1897
+ async #destroySlot(profile, browser) {
1704
1898
  try {
1705
1899
  await browser?.destroy();
1706
1900
  } catch (error) {
@@ -1776,27 +1970,27 @@ var BrowserMCPServer = class {
1776
1970
  #issue() {
1777
1971
  return `e${++this.#references}`;
1778
1972
  }
1779
- #mirror(token, tool) {
1780
- if (this.#leases.get(this.#shared) !== token || this.#vocabulary.has(tool.name)) return;
1973
+ #mirror(lease, tool) {
1974
+ if (this.#leases.get(this.#shared) !== lease || this.#vocabulary.has(tool.name)) return;
1781
1975
  this.#tools.add((0, _orkestrel_tool.createTool)({
1782
1976
  ...(0, _orkestrel_tool.toolToDefinition)(tool),
1783
1977
  execute: this.#forward.bind(this, this.#shared, tool.name)
1784
1978
  }));
1785
1979
  }
1786
- #withdraw(token, tool) {
1787
- if (this.#leases.get(this.#shared) !== token || this.#vocabulary.has(tool.name)) return;
1788
- const current = token.value.toolset.tools.tool(tool.name);
1980
+ #withdraw(lease, tool) {
1981
+ if (this.#leases.get(this.#shared) !== lease || this.#vocabulary.has(tool.name)) return;
1982
+ const current = lease.generation.toolset.tools.tool(tool.name);
1789
1983
  if (current === void 0) this.#tools.remove(tool.name);
1790
- else this.#mirror(token, current);
1984
+ else this.#mirror(lease, current);
1791
1985
  }
1792
- #clear(token, tools) {
1793
- for (const tool of tools) this.#withdraw(token, tool);
1986
+ #clear(lease, tools) {
1987
+ for (const tool of tools) this.#withdraw(lease, tool);
1794
1988
  }
1795
1989
  #unmirror() {
1796
1990
  const mirrored = this.#mirrored;
1797
1991
  if (mirrored === void 0) return;
1798
1992
  this.#mirrored = void 0;
1799
- const tools = mirrored.token.value.toolset.tools;
1993
+ const tools = mirrored.lease.generation.toolset.tools;
1800
1994
  for (const tool of tools.tools()) if (!this.#vocabulary.has(tool.name)) this.#tools.remove(tool.name);
1801
1995
  tools.emitter.off("add", mirrored.added);
1802
1996
  tools.emitter.off("remove", mirrored.removed);
@@ -2227,11 +2421,14 @@ var FileBrowserJourneyStore = class {
2227
2421
  /**
2228
2422
  * Persists runs and captures only in directories allocated by this instance.
2229
2423
  * @remarks Requires an existing root. Reopened stores can read and delete saved runs.
2424
+ * Allocations of one journey share a process-local queue across instances; the filesystem lock
2425
+ * still refuses conflicting mutations and allocations from another process.
2230
2426
  * @example
2231
2427
  * const store = new FileBrowserRunStore({ root: directory })
2232
2428
  * const slot = await store.open('check-ready')
2233
2429
  */
2234
- var FileBrowserRunStore = class {
2430
+ var FileBrowserRunStore = class FileBrowserRunStore {
2431
+ static #allocations = /* @__PURE__ */ new Map();
2235
2432
  #files;
2236
2433
  #slots = /* @__PURE__ */ new WeakMap();
2237
2434
  #directories = /* @__PURE__ */ new Set();
@@ -2259,16 +2456,27 @@ var FileBrowserRunStore = class {
2259
2456
  async open(name, options) {
2260
2457
  options?.signal?.throwIfAborted();
2261
2458
  this.#files.validateName(name);
2262
- return this.#files.lock(this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY), async () => {
2263
- const directory = await this.#files.allocate(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), _src_core.generateBrowserRunId, options);
2264
- const slot = Object.freeze({
2265
- id: directory.id,
2266
- directory: directory.path
2267
- });
2268
- this.#directories.add(directory.path);
2269
- this.#slots.set(slot, directory.path);
2270
- return slot;
2271
- }, options);
2459
+ const path = this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY);
2460
+ const previous = FileBrowserRunStore.#allocations.get(path);
2461
+ const allocation = Promise.withResolvers();
2462
+ FileBrowserRunStore.#allocations.set(path, allocation.promise);
2463
+ try {
2464
+ await previous;
2465
+ options?.signal?.throwIfAborted();
2466
+ return await this.#files.lock(path, async () => {
2467
+ const directory = await this.#files.allocate(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), _src_core.generateBrowserRunId, options);
2468
+ const slot = Object.freeze({
2469
+ id: directory.id,
2470
+ directory: directory.path
2471
+ });
2472
+ this.#directories.add(directory.path);
2473
+ this.#slots.set(slot, directory.path);
2474
+ return slot;
2475
+ }, options);
2476
+ } finally {
2477
+ allocation.resolve();
2478
+ if (FileBrowserRunStore.#allocations.get(path) === allocation.promise) FileBrowserRunStore.#allocations.delete(path);
2479
+ }
2272
2480
  }
2273
2481
  async get(name, id, options) {
2274
2482
  options?.signal?.throwIfAborted();
@@ -2478,6 +2686,7 @@ var Browser = class {
2478
2686
  #servingPid;
2479
2687
  #profile;
2480
2688
  #contexts = [];
2689
+ #isolating = /* @__PURE__ */ new Set();
2481
2690
  #destroyed = false;
2482
2691
  #connecting = new _src_core.BrowserTransition();
2483
2692
  #disconnecting = new _src_core.BrowserTransition();
@@ -2564,33 +2773,13 @@ var Browser = class {
2564
2773
  return [...this.#contexts];
2565
2774
  }
2566
2775
  async isolate(options) {
2567
- if (this.#destroyed) throw new BrowserDestroyedError();
2568
- const client = this.#client;
2569
- if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
2570
- (0, _src_core.validateBrowserContextOptions)(options);
2571
- const params = { disposeOnDetach: false };
2572
- if (options?.proxy !== void 0) {
2573
- params["proxyServer"] = options.proxy.server;
2574
- if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
2575
- }
2576
- if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
2577
- const result = await client.send("Target.createBrowserContext", params);
2578
- if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new _src_core.BrowserConnectionError("Failed to create isolated browser context");
2579
- const id = result["browserContextId"];
2580
- const context = new _src_core.BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createBrowserWriter(), options?.emulation, options?.downloads, options);
2776
+ const attempt = this.#isolate(options);
2777
+ this.#isolating.add(attempt);
2581
2778
  try {
2582
- if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
2583
- behavior: options.downloads.named === true ? "allowAndName" : "allow",
2584
- browserContextId: id,
2585
- downloadPath: options.downloads.path,
2586
- eventsEnabled: true
2587
- });
2588
- } catch (error) {
2589
- await context.close().catch(() => void 0);
2590
- throw error;
2779
+ return await attempt;
2780
+ } finally {
2781
+ this.#isolating.delete(attempt);
2591
2782
  }
2592
- this.#registerContext(context);
2593
- return context;
2594
2783
  }
2595
2784
  async create(options) {
2596
2785
  if (this.#destroyed) throw new BrowserDestroyedError();
@@ -2625,6 +2814,37 @@ var Browser = class {
2625
2814
  this.#shutdown = shutdown;
2626
2815
  return shutdown;
2627
2816
  }
2817
+ async #isolate(options) {
2818
+ if (this.#destroyed) throw new BrowserDestroyedError();
2819
+ const client = this.#client;
2820
+ if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
2821
+ (0, _src_core.validateBrowserContextOptions)(options);
2822
+ const params = { disposeOnDetach: false };
2823
+ if (options?.proxy !== void 0) {
2824
+ params["proxyServer"] = options.proxy.server;
2825
+ if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
2826
+ }
2827
+ if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
2828
+ const result = await client.send("Target.createBrowserContext", params);
2829
+ if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new _src_core.BrowserConnectionError("Failed to create isolated browser context");
2830
+ const id = result["browserContextId"];
2831
+ const context = new _src_core.BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createBrowserWriter(), options?.emulation, options?.downloads, options);
2832
+ try {
2833
+ if (this.#destroyed || this.#client !== client) throw new BrowserDestroyedError();
2834
+ if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
2835
+ behavior: options.downloads.named === true ? "allowAndName" : "allow",
2836
+ browserContextId: id,
2837
+ downloadPath: options.downloads.path,
2838
+ eventsEnabled: true
2839
+ });
2840
+ if (this.#destroyed || this.#client !== client) throw new BrowserDestroyedError();
2841
+ } catch (error) {
2842
+ await context.close().catch(() => void 0);
2843
+ throw error;
2844
+ }
2845
+ this.#registerContext(context);
2846
+ return context;
2847
+ }
2628
2848
  async #establish() {
2629
2849
  const disconnecting = this.#disconnecting.pending;
2630
2850
  if (disconnecting !== void 0) await disconnecting;
@@ -3098,6 +3318,7 @@ var Browser = class {
3098
3318
  }
3099
3319
  }
3100
3320
  async #settle() {
3321
+ await Promise.allSettled(this.#isolating);
3101
3322
  await this.#connecting.pending?.catch(() => void 0);
3102
3323
  await this.#disconnecting.pending?.catch(() => void 0);
3103
3324
  await this.#settleExit();
@@ -3311,6 +3532,8 @@ exports.BROWSER_PROFILE_PREFIX = BROWSER_PROFILE_PREFIX;
3311
3532
  exports.BROWSER_RUN_DIRECTORY = BROWSER_RUN_DIRECTORY;
3312
3533
  exports.BROWSER_RUN_FILE = BROWSER_RUN_FILE;
3313
3534
  exports.BROWSER_SERVER_BUSY = BROWSER_SERVER_BUSY;
3535
+ exports.BROWSER_SERVER_CONTEXTS = BROWSER_SERVER_CONTEXTS;
3536
+ exports.BROWSER_SERVER_CONTEXTS_LIMIT = BROWSER_SERVER_CONTEXTS_LIMIT;
3314
3537
  exports.BROWSER_SERVER_COPY = BROWSER_SERVER_COPY;
3315
3538
  exports.BROWSER_SERVER_CRASH = BROWSER_SERVER_CRASH;
3316
3539
  exports.BROWSER_SERVER_EXHAUSTED = BROWSER_SERVER_EXHAUSTED;