@orkestrel/browser 0.0.23 → 0.0.24

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,5 +1,5 @@
1
1
  import { BROWSER_DEFAULT_TIMEOUT_MS, BROWSER_JOURNEY_FORMAT_VERSION, BROWSER_JOURNEY_NAME_PATTERN, BROWSER_JOURNEY_TOOL_NAMES, BROWSER_RUN_ID_PATTERN, BROWSER_TOOL_COPY, BROWSER_TOOL_NAMES, BrowserConnectionError, BrowserContext, BrowserError, BrowserTransition, CDPClient, createBrowserToolset, createCDPClient, generateBrowserRunId, isBrowserConnectionError, isBrowserError, isCDPTimeoutError, normalizeBrowserJourneyReason, validateBrowserContextOptions, validateBrowserJourney, validateBrowserRun, validateBrowserStorePage } from "../core/index.js";
2
- import { attempt, isArray, isError, isInstance, isInteger, isNumber, isRecord, isString, parseJSON } from "@orkestrel/contract";
2
+ import { attempt, isArray, isError, isInstance, isInteger, isNumber, isRecord, isString, parseInteger, parseJSON } from "@orkestrel/contract";
3
3
  import { existsSync, globSync, realpathSync } from "node:fs";
4
4
  import { lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, rmdir, unlink, writeFile } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
@@ -236,6 +236,84 @@ var BROWSER_SERVER_OPTIONS = "BROWSER_SERVER_OPTIONS";
236
236
  var BROWSER_SERVER_CRASH = "BROWSER_SERVER_CRASH";
237
237
  /** Names an interrupted call whose outcome is unknown. */
238
238
  var BROWSER_SERVER_UNRESOLVED = "BROWSER_SERVER_UNRESOLVED";
239
+ /** Names the refusal when every browser is admitted to a holder. */
240
+ var BROWSER_SERVER_BUSY = "BROWSER_SERVER_BUSY";
241
+ /** Names the refusal for an unknown or ended holder. */
242
+ var BROWSER_SERVER_HOLDER = "BROWSER_SERVER_HOLDER";
243
+ /** Defines the holder tools independently of the shared browser's vocabulary. */
244
+ var BROWSER_SERVER_COPY = Object.freeze({
245
+ acquire: Object.freeze({
246
+ name: "acquire",
247
+ description: "Acquires an independent browser and returns its holder id and tool catalog. Destroy the holder when its work ends.",
248
+ parameters: Object.freeze({
249
+ type: "object",
250
+ properties: Object.freeze({ purpose: Object.freeze({
251
+ type: "string",
252
+ description: "The work this browser will serve."
253
+ }) }),
254
+ required: Object.freeze(["purpose"])
255
+ })
256
+ }),
257
+ execute: Object.freeze({
258
+ name: "execute",
259
+ description: "Runs a tool on the selected holder, with that tool’s arguments inside arguments. Get its available tools with tools.",
260
+ parameters: Object.freeze({
261
+ type: "object",
262
+ properties: Object.freeze({
263
+ holder: Object.freeze({
264
+ type: "string",
265
+ description: "The holder id returned by acquire."
266
+ }),
267
+ name: Object.freeze({
268
+ type: "string",
269
+ description: "The tool name from this holder’s catalog."
270
+ }),
271
+ arguments: Object.freeze({
272
+ type: "object",
273
+ description: "The arguments for the selected tool."
274
+ })
275
+ }),
276
+ required: Object.freeze([
277
+ "holder",
278
+ "name",
279
+ "arguments"
280
+ ])
281
+ }),
282
+ annotations: Object.freeze({
283
+ pure: false,
284
+ untrusted: true
285
+ })
286
+ }),
287
+ tools: Object.freeze({
288
+ name: "tools",
289
+ description: "Lists the selected holder’s tools, including tools supplied by its page.",
290
+ parameters: Object.freeze({
291
+ type: "object",
292
+ properties: Object.freeze({ holder: Object.freeze({
293
+ type: "string",
294
+ description: "The holder id returned by acquire."
295
+ }) }),
296
+ required: Object.freeze(["holder"])
297
+ }),
298
+ annotations: Object.freeze({
299
+ pure: true,
300
+ untrusted: true
301
+ })
302
+ }),
303
+ destroy: Object.freeze({
304
+ name: "destroy",
305
+ description: "Ends the selected holder and destroys its browser. Calls still running on that holder report an unresolved outcome.",
306
+ parameters: Object.freeze({
307
+ type: "object",
308
+ properties: Object.freeze({ holder: Object.freeze({
309
+ type: "string",
310
+ description: "The holder id returned by acquire."
311
+ }) }),
312
+ required: Object.freeze(["holder"])
313
+ }),
314
+ annotations: Object.freeze({ pure: false })
315
+ })
316
+ });
239
317
  //#endregion
240
318
  //#region src/server/errors.ts
241
319
  /**
@@ -279,6 +357,25 @@ function isBrowserDestroyedError(value) {
279
357
  //#endregion
280
358
  //#region src/server/helpers.ts
281
359
  /**
360
+ * Parses positive integer viewport dimensions in WIDTHxHEIGHT form.
361
+ * @param value - The viewport environment variable, or undefined when unset
362
+ * @returns The viewport, or undefined when absent or malformed
363
+ * @example
364
+ * parseBrowserViewport('1280x720') // { width: 1280, height: 720 }
365
+ */
366
+ function parseBrowserViewport(value) {
367
+ if (value === void 0) return void 0;
368
+ const match = /^([0-9]+)x([0-9]+)$/u.exec(value);
369
+ if (match === null || match[0] !== value) return void 0;
370
+ const width = parseInteger(match[1]);
371
+ const height = parseInteger(match[2]);
372
+ if (width === void 0 || height === void 0 || !Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0) return void 0;
373
+ return {
374
+ width,
375
+ height
376
+ };
377
+ }
378
+ /**
282
379
  * Probes whether a process has not been confirmed absent.
283
380
  * @param pid - Process identifier to probe with signal zero
284
381
  * @returns True if the host has not confirmed absence with ESRCH; false otherwise
@@ -944,7 +1041,7 @@ var FileBrowserWriter = class {
944
1041
  };
945
1042
  //#endregion
946
1043
  //#region package.json
947
- var version = "0.0.23";
1044
+ var version = "0.0.24";
948
1045
  //#endregion
949
1046
  //#region src/server/BrowserMCPServer.ts
950
1047
  /**
@@ -959,8 +1056,11 @@ var version = "0.0.23";
959
1056
  * absent. After the launch, a tool the toolset's manager adds under another name, such as a page
960
1057
  * tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
961
1058
  * removed when the toolset withdraws the tool, so the server's tool list changes and a subscribed
962
- * client is notified. Startup warms the pool and leases one validated browser. The legacy
963
- * handshake and every tool call await that setup; concurrent callers share a replacement lease.
1059
+ * client is notified. The server also advertises `acquire`, `execute`, `tools`, and `destroy` with
1060
+ * `BROWSER_SERVER_COPY`. Startup warms the pool and leases the shared browser. The legacy
1061
+ * handshake and browser calls await that setup. Named holders each own a lease, a pending grant,
1062
+ * and loss notices; concurrent calls on a holder share its replacement grant. Ending a holder
1063
+ * destroys its token, and admission counts that holder until disposal settles.
964
1064
  * A launch creates `ROOT/.profiles/PID-UUID/` exclusively, connects a browser that never attaches to an
965
1065
  * existing endpoint, opens one page in an isolated context, and constructs the page toolset with
966
1066
  * that context and the file journey and run stores under the root, on a manager the toolset owns.
@@ -970,6 +1070,8 @@ var version = "0.0.23";
970
1070
  * A disconnect or current-page crash retires its slot. A failed call receives a liveness ping;
971
1071
  * loss makes its outcome unresolved, while a known success remains successful. Pending losses
972
1072
  * prefix the next outcome on a successor, or a refusal, and calls are never repeated.
1073
+ * Concurrent checks share one ping per slot. Cancelling a call ends only its wait; the ping keeps
1074
+ * its deadline and records loss for the next call.
973
1075
  *
974
1076
  * `start()` serves stdio and destroys the server at the end of its input, on `SIGINT`, and on
975
1077
  * `SIGTERM`. `destroy()` stops reading requests and removes every dispatcher, then destroys every
@@ -993,6 +1095,7 @@ var BrowserMCPServer = class {
993
1095
  #root;
994
1096
  #headless;
995
1097
  #executable;
1098
+ #viewport;
996
1099
  #readonly;
997
1100
  #launcher;
998
1101
  #input;
@@ -1001,22 +1104,32 @@ var BrowserMCPServer = class {
1001
1104
  #abort = new AbortController();
1002
1105
  #signal;
1003
1106
  #vocabulary;
1004
- #added;
1005
- #removed;
1006
- #cleared;
1107
+ #mirrored;
1007
1108
  #pool;
1008
1109
  #log;
1009
1110
  #folders = /* @__PURE__ */ new Set();
1010
1111
  #faults = /* @__PURE__ */ new Set();
1011
1112
  #losses = /* @__PURE__ */ new WeakMap();
1113
+ #pings = /* @__PURE__ */ new WeakMap();
1012
1114
  #watches = /* @__PURE__ */ new Map();
1013
- #notice = /* @__PURE__ */ new Set();
1115
+ #shared = {
1116
+ id: "shared",
1117
+ purpose: "Shared browser",
1118
+ abort: this.#abort
1119
+ };
1120
+ #holders = /* @__PURE__ */ new Map([[this.#shared.id, this.#shared]]);
1121
+ #leases = /* @__PURE__ */ new Map();
1122
+ #grants = /* @__PURE__ */ new Map();
1123
+ #notices = /* @__PURE__ */ new Map();
1124
+ #disposals = /* @__PURE__ */ new Map();
1125
+ #drains = /* @__PURE__ */ new Map();
1126
+ #readers = /* @__PURE__ */ new Map();
1127
+ #writers = /* @__PURE__ */ new Set();
1128
+ #size;
1014
1129
  #reference;
1015
1130
  #references = 0;
1016
1131
  #failure;
1017
1132
  #stranded;
1018
- #lease;
1019
- #granting;
1020
1133
  #starting;
1021
1134
  #sweeping;
1022
1135
  #closing;
@@ -1028,6 +1141,7 @@ var BrowserMCPServer = class {
1028
1141
  constructor(options) {
1029
1142
  const size = options?.pool?.size ?? 1;
1030
1143
  if (!isInteger(size) || size < 1 || size > 3) throw new BrowserError(`pool.size must be an integer from 1 through 3`, BROWSER_SERVER_OPTIONS);
1144
+ this.#size = size;
1031
1145
  this.#log = options?.log ?? process.stderr;
1032
1146
  this.#reference = this.#issue.bind(this);
1033
1147
  this.#pool = createPool({
@@ -1042,15 +1156,32 @@ var BrowserMCPServer = class {
1042
1156
  this.#root = resolve(options?.root ?? "tmp/browsers");
1043
1157
  this.#headless = options?.headless ?? true;
1044
1158
  this.#executable = options?.executable;
1159
+ this.#viewport = options?.viewport;
1045
1160
  this.#readonly = options?.readonly ?? false;
1046
1161
  this.#launcher = options?.launch ?? createBrowser;
1047
1162
  this.#input = options?.stdio?.input ?? process.stdin;
1048
1163
  this.#tools = createToolManager();
1049
1164
  const names = [...BROWSER_TOOL_NAMES, ...BROWSER_JOURNEY_TOOL_NAMES];
1050
- this.#vocabulary = new Set(names);
1165
+ this.#vocabulary = /* @__PURE__ */ new Set([...names, ...Object.keys(BROWSER_SERVER_COPY)]);
1051
1166
  for (const name of names) this.#tools.add(createTool({
1052
1167
  ...BROWSER_TOOL_COPY[name],
1053
- execute: this.#forward.bind(this, name)
1168
+ execute: this.#forward.bind(this, this.#shared, name)
1169
+ }));
1170
+ this.#tools.add(createTool({
1171
+ ...BROWSER_SERVER_COPY.acquire,
1172
+ execute: this.#acquire.bind(this)
1173
+ }));
1174
+ this.#tools.add(createTool({
1175
+ ...BROWSER_SERVER_COPY.execute,
1176
+ execute: this.#dispatch.bind(this)
1177
+ }));
1178
+ this.#tools.add(createTool({
1179
+ ...BROWSER_SERVER_COPY.tools,
1180
+ execute: this.#catalog.bind(this)
1181
+ }));
1182
+ this.#tools.add(createTool({
1183
+ ...BROWSER_SERVER_COPY.destroy,
1184
+ execute: this.#dispose.bind(this)
1054
1185
  }));
1055
1186
  const server = createMCPServer({
1056
1187
  identity: {
@@ -1066,9 +1197,6 @@ var BrowserMCPServer = class {
1066
1197
  ...options?.stdio?.output === void 0 ? {} : { output: options.stdio.output }
1067
1198
  });
1068
1199
  this.#signal = this.#end.bind(this);
1069
- this.#added = this.#mirror.bind(this);
1070
- this.#removed = this.#withdraw.bind(this);
1071
- this.#cleared = this.#clear.bind(this);
1072
1200
  }
1073
1201
  async start() {
1074
1202
  if (this.#closing !== void 0) throw this.#ended();
@@ -1089,7 +1217,7 @@ var BrowserMCPServer = class {
1089
1217
  if (this.#closing !== void 0) return;
1090
1218
  this.#sweeping = this.#sweep();
1091
1219
  const warming = this.#pool.start().then(void 0, (error) => error);
1092
- await this.#grant(this.#abort.signal);
1220
+ await this.#grant(this.#shared, this.#abort.signal);
1093
1221
  if (this.#closing !== void 0) return;
1094
1222
  warming.then(this.#exhaust.bind(this));
1095
1223
  } catch (error) {
@@ -1105,13 +1233,16 @@ var BrowserMCPServer = class {
1105
1233
  this.#transport.stop();
1106
1234
  for (const tool of this.#tools.tools()) this.#tools.remove(tool.name);
1107
1235
  this.#unmirror();
1108
- this.#lease = void 0;
1236
+ this.#leases.clear();
1237
+ for (const holder of this.#holders.values()) holder.abort.abort();
1109
1238
  this.#abort.abort();
1110
1239
  const barrier = this.#pool.destroy().catch((error) => {
1111
1240
  for (const failure of isPoolError(error) ? error.context?.failures ?? [error] : [error]) this.#faults.add(failure);
1112
1241
  });
1113
1242
  await this.#starting?.catch(() => void 0);
1114
1243
  await barrier;
1244
+ await Promise.allSettled(this.#disposals.values());
1245
+ this.#holders.clear();
1115
1246
  await this.#sweeping;
1116
1247
  if (this.#stranded !== void 0) this.#faults.add(this.#stranded);
1117
1248
  for (const folder of this.#folders) {
@@ -1156,22 +1287,141 @@ var BrowserMCPServer = class {
1156
1287
  process.exitCode = 1;
1157
1288
  this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_TEARDOWN, error)}`);
1158
1289
  }
1159
- async #forward(name, args, context) {
1290
+ async #acquire(args, context) {
1291
+ context.signal.throwIfAborted();
1292
+ if (this.#closing !== void 0) throw this.#ended();
1293
+ const purpose = args["purpose"];
1294
+ if (!isString(purpose) || purpose.trim().length === 0) throw new BrowserError("purpose must describe the work", "BROWSER_TOOLSET_ARGUMENT");
1295
+ if (this.#holders.size >= this.#size) throw new 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);
1296
+ const holder = {
1297
+ id: randomUUID(),
1298
+ purpose,
1299
+ abort: new AbortController()
1300
+ };
1301
+ this.#holders.set(holder.id, holder);
1302
+ const listener = addAbortListener(context.signal, () => holder.abort.abort(context.signal.reason));
1303
+ try {
1304
+ const slot = await this.#serve(holder, context.signal);
1305
+ context.signal.throwIfAborted();
1306
+ if (holder.abort.signal.aborted || this.#holders.get(holder.id) !== holder) throw this.#ended();
1307
+ if (this.#leases.get(holder)?.value !== slot) throw this.#unavailable(this.#losses.get(slot)?.cause);
1308
+ const catalog = {
1309
+ holder: holder.id,
1310
+ tools: slot.toolset.tools.definitions()
1311
+ };
1312
+ return JSON.stringify(catalog);
1313
+ } catch (error) {
1314
+ await this.#retire(holder).catch(this.#fault.bind(this));
1315
+ throw error;
1316
+ } finally {
1317
+ listener[Symbol.dispose]();
1318
+ }
1319
+ }
1320
+ #holder(value) {
1321
+ const holder = isString(value) ? this.#holders.get(value) : void 0;
1322
+ if (holder === void 0 || holder === this.#shared || holder.abort.signal.aborted) throw new BrowserError(`${BROWSER_SERVER_HOLDER}: unknown or ended holder`, BROWSER_SERVER_HOLDER);
1323
+ return holder;
1324
+ }
1325
+ async #dispatch(args, context) {
1326
+ const holder = this.#holder(args["holder"]);
1327
+ const name = args["name"];
1328
+ const parameters = args["arguments"];
1329
+ if (!isString(name) || !isRecord(parameters)) throw new BrowserError("execute requires name and arguments", "BROWSER_TOOLSET_ARGUMENT");
1330
+ return this.#forward(holder, name, parameters, context);
1331
+ }
1332
+ async #catalog(args, context) {
1333
+ const holder = this.#holder(args["holder"]);
1334
+ const signal = AbortSignal.any([context.signal, holder.abort.signal]);
1335
+ try {
1336
+ const slot = await this.#serve(holder, signal);
1337
+ signal.throwIfAborted();
1338
+ if (this.#leases.get(holder)?.value !== slot) throw this.#unavailable(this.#losses.get(slot)?.cause);
1339
+ const catalog = {
1340
+ holder: holder.id,
1341
+ tools: slot.toolset.tools.definitions()
1342
+ };
1343
+ return this.#annotate(holder, JSON.stringify(catalog));
1344
+ } catch (error) {
1345
+ context.signal.throwIfAborted();
1346
+ if (holder.abort.signal.aborted) throw new BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
1347
+ if (isBrowserError(error)) throw new BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`), error.code);
1348
+ throw error;
1349
+ }
1350
+ }
1351
+ async #dispose(args) {
1352
+ const id = args["holder"];
1353
+ await ((isString(id) ? this.#disposals.get(id) : void 0) ?? this.#retire(this.#holder(id)));
1354
+ return "Holder destroyed.";
1355
+ }
1356
+ #retire(holder) {
1357
+ const pending = this.#disposals.get(holder.id);
1358
+ if (pending !== void 0) return pending;
1359
+ holder.abort.abort();
1360
+ const token = this.#leases.get(holder);
1361
+ this.#leases.delete(holder);
1362
+ const disposal = Promise.resolve().then(async () => {
1363
+ try {
1364
+ await this.#grants.get(holder)?.catch(() => void 0);
1365
+ await token?.destroy();
1366
+ await Promise.all(this.#drains.get(holder) ?? []);
1367
+ } catch (error) {
1368
+ this.#faults.add(error);
1369
+ } finally {
1370
+ if (this.#holders.get(holder.id) === holder) this.#holders.delete(holder.id);
1371
+ this.#notices.delete(holder);
1372
+ this.#drains.delete(holder);
1373
+ }
1374
+ });
1375
+ this.#disposals.set(holder.id, disposal);
1376
+ return disposal;
1377
+ }
1378
+ async #forward(holder, name, args, context) {
1379
+ context.signal.throwIfAborted();
1380
+ const journey = args["journey"];
1381
+ if (name !== "replay" && name !== "forget" || !isString(journey)) return this.#perform(holder, name, args, context);
1382
+ if (this.#writers.has(journey) || name === "forget" && this.#readers.has(journey)) throw new BrowserError(`BROWSER_JOURNEY_LOCKED: Journey ${journey} is locked; call ${name} again.`, "BROWSER_JOURNEY_LOCKED", { name: journey });
1383
+ const reader = Symbol(journey);
1384
+ if (name === "forget") this.#writers.add(journey);
1385
+ else {
1386
+ const readers = this.#readers.get(journey) ?? /* @__PURE__ */ new Set();
1387
+ readers.add(reader);
1388
+ this.#readers.set(journey, readers);
1389
+ }
1390
+ try {
1391
+ return await this.#perform(holder, name, args, context);
1392
+ } finally {
1393
+ if (name === "forget") this.#writers.delete(journey);
1394
+ else {
1395
+ const readers = this.#readers.get(journey);
1396
+ readers?.delete(reader);
1397
+ if (readers?.size === 0) this.#readers.delete(journey);
1398
+ }
1399
+ }
1400
+ }
1401
+ async #perform(holder, name, args, context) {
1402
+ const signal = holder === this.#shared ? context.signal : AbortSignal.any([context.signal, holder.abort.signal]);
1160
1403
  let slot;
1161
1404
  try {
1162
- slot = await this.#serve(context.signal);
1405
+ slot = await this.#serve(holder, signal);
1163
1406
  } catch (error) {
1164
1407
  context.signal.throwIfAborted();
1165
- if (isBrowserError(error)) throw new BrowserError(this.#annotate(`${error.code}: ${error.message}`), error.code);
1408
+ if (holder !== this.#shared && holder.abort.signal.aborted) throw new BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
1409
+ if (isBrowserError(error)) throw new BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`), error.code);
1166
1410
  throw error;
1167
1411
  }
1168
1412
  let result;
1169
1413
  try {
1170
- result = await slot.toolset.tools.execute({
1414
+ const execution = slot.toolset.tools.execute({
1171
1415
  id: name,
1172
1416
  name,
1173
1417
  arguments: args
1174
- }, context);
1418
+ }, {
1419
+ ...context,
1420
+ signal
1421
+ });
1422
+ const outcome = await (name === "replay" || name === "forget" ? execution : this.#race(execution, signal));
1423
+ if (outcome === void 0) throw this.#ended();
1424
+ result = outcome;
1175
1425
  } catch (error) {
1176
1426
  result = {
1177
1427
  id: name,
@@ -1181,28 +1431,27 @@ var BrowserMCPServer = class {
1181
1431
  };
1182
1432
  }
1183
1433
  context.signal.throwIfAborted();
1434
+ if (holder !== this.#shared && holder.abort.signal.aborted) throw new BrowserError(BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call", BROWSER_SERVER_UNRESOLVED);
1184
1435
  if (!result.success) {
1185
- if (this.#lease?.value === slot && !context.signal.aborted) try {
1186
- await slot.browser.ping({ signal: AbortSignal.any([context.signal, this.#abort.signal]) });
1187
- } catch (error) {
1188
- if (!context.signal.aborted && !this.#abort.signal.aborted) this.#lose(slot, error);
1189
- }
1436
+ if (this.#leases.get(holder)?.value === slot && !context.signal.aborted) try {
1437
+ await this.#race(this.#ping(slot), context.signal);
1438
+ } catch {}
1190
1439
  context.signal.throwIfAborted();
1191
- if (this.#lease?.value !== slot) {
1440
+ if (this.#leases.get(holder)?.value !== slot) {
1192
1441
  const loss = this.#losses.get(slot);
1193
- throw new BrowserError(this.#annotate(describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), slot), BROWSER_SERVER_UNRESOLVED);
1442
+ throw new BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), slot), BROWSER_SERVER_UNRESOLVED);
1194
1443
  }
1195
- throw new BrowserError(this.#annotate(result.error, slot));
1444
+ throw new BrowserError(this.#annotate(holder, result.error, slot));
1196
1445
  }
1197
- return typeof result.value === "string" ? this.#annotate(result.value, slot) : result.value;
1446
+ return typeof result.value === "string" ? this.#annotate(holder, result.value, slot) : result.value;
1198
1447
  }
1199
- #annotate(value, slot) {
1448
+ #annotate(holder, value, slot) {
1200
1449
  const notes = [];
1201
- for (const lost of this.#notice) {
1450
+ for (const lost of this.#notices.get(holder) ?? []) {
1202
1451
  if (lost === slot) continue;
1203
1452
  const loss = this.#losses.get(lost);
1204
1453
  notes.push(describeBrowserServerLoss(BROWSER_SERVER_CRASH, loss?.cause, loss?.url));
1205
- this.#notice.delete(lost);
1454
+ this.#notices.get(holder)?.delete(lost);
1206
1455
  }
1207
1456
  return [...notes, value].join("\n");
1208
1457
  }
@@ -1226,52 +1475,66 @@ var BrowserMCPServer = class {
1226
1475
  listener[Symbol.dispose]();
1227
1476
  }
1228
1477
  }
1229
- async #grant(signal) {
1230
- if (this.#lease !== void 0) return this.#lease;
1231
- this.#granting ??= this.#pool.acquire(this.#abort.signal).then(this.#hold.bind(this), this.#refuse.bind(this));
1232
- const token = await this.#race(this.#granting, signal);
1478
+ async #grant(holder, signal) {
1479
+ const held = this.#leases.get(holder);
1480
+ if (held !== void 0) return held;
1481
+ let granting = this.#grants.get(holder);
1482
+ if (granting === void 0) {
1483
+ granting = this.#pool.acquire(holder.abort.signal).then(this.#hold.bind(this, holder));
1484
+ this.#grants.set(holder, granting);
1485
+ granting.finally(() => {
1486
+ if (this.#grants.get(holder) === granting) this.#grants.delete(holder);
1487
+ }).catch(() => void 0);
1488
+ }
1489
+ const token = await this.#race(granting, signal);
1233
1490
  if (token === void 0) throw this.#ended();
1234
1491
  return token;
1235
1492
  }
1236
- #hold(token) {
1237
- this.#granting = void 0;
1238
- if (this.#closing !== void 0) throw this.#ended();
1493
+ async #hold(holder, token) {
1494
+ if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted) {
1495
+ await token.destroy();
1496
+ throw this.#ended();
1497
+ }
1239
1498
  if (this.#losses.has(token.value)) {
1240
- token.destroy().catch(() => void 0);
1499
+ await token.destroy().catch(() => void 0);
1241
1500
  throw this.#unavailable(this.#losses.get(token.value)?.cause);
1242
1501
  }
1243
- this.#lease = token;
1244
- const tools = token.value.toolset.tools;
1245
- for (const tool of tools.tools()) this.#mirror(tool);
1246
- tools.emitter.on("add", this.#added);
1247
- tools.emitter.on("remove", this.#removed);
1248
- tools.emitter.on("clear", this.#cleared);
1502
+ this.#leases.set(holder, token);
1503
+ if (holder === this.#shared) {
1504
+ const tools = token.value.toolset.tools;
1505
+ const mirrored = {
1506
+ token,
1507
+ added: this.#mirror.bind(this, token),
1508
+ removed: this.#withdraw.bind(this, token),
1509
+ cleared: this.#clear.bind(this, token)
1510
+ };
1511
+ this.#mirrored = mirrored;
1512
+ for (const tool of tools.tools()) this.#mirror(token, tool);
1513
+ tools.emitter.on("add", mirrored.added);
1514
+ tools.emitter.on("remove", mirrored.removed);
1515
+ tools.emitter.on("clear", mirrored.cleared);
1516
+ }
1249
1517
  return token;
1250
1518
  }
1251
- #refuse(error) {
1252
- this.#granting = void 0;
1253
- throw error;
1254
- }
1255
- async #serve(signal) {
1256
- if (this.#closing !== void 0) throw this.#ended();
1519
+ async #serve(holder, signal) {
1520
+ if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
1257
1521
  await this.#race(this.#starting, signal);
1258
- if (this.#closing !== void 0) throw this.#ended();
1259
- const held = this.#lease;
1522
+ if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
1523
+ const held = this.#leases.get(holder);
1260
1524
  if (held !== void 0) try {
1261
- await held.value.browser.ping({ signal: AbortSignal.any([signal, this.#abort.signal]) });
1262
- if (this.#lease === held) return held.value;
1263
- } catch (error) {
1525
+ await this.#race(this.#ping(held.value), signal);
1526
+ if (this.#leases.get(holder) === held) return held.value;
1527
+ } catch {
1264
1528
  signal.throwIfAborted();
1265
- if (this.#abort.signal.aborted) throw this.#ended();
1266
- this.#lose(held.value, error);
1529
+ if (holder.abort.signal.aborted) throw this.#ended();
1267
1530
  }
1268
1531
  try {
1269
- const token = await this.#grant(signal);
1270
- if (this.#lease !== token) throw this.#unavailable(this.#losses.get(token.value)?.cause);
1532
+ const token = await this.#grant(holder, signal);
1533
+ if (this.#leases.get(holder) !== token) throw this.#unavailable(this.#losses.get(token.value)?.cause);
1271
1534
  return token.value;
1272
1535
  } catch (error) {
1273
1536
  signal.throwIfAborted();
1274
- if (this.#abort.signal.aborted || isPoolError(error) && error.code === "destroyed") throw this.#ended();
1537
+ if (holder.abort.signal.aborted || isPoolError(error) && error.code === "destroyed") throw this.#ended();
1275
1538
  if (isPoolError(error) && (error.code === "create" || error.code === "cleanup")) {
1276
1539
  this.#exhaust(error);
1277
1540
  throw this.#unavailable(error.cause ?? this.#failure);
@@ -1279,6 +1542,15 @@ var BrowserMCPServer = class {
1279
1542
  throw error;
1280
1543
  }
1281
1544
  }
1545
+ #ping(slot) {
1546
+ const pending = this.#pings.get(slot);
1547
+ if (pending !== void 0) return pending;
1548
+ const ping = slot.browser.ping({ signal: this.#abort.signal }).catch((error) => {
1549
+ if (!this.#abort.signal.aborted) this.#lose(slot, error);
1550
+ }).finally(() => this.#pings.delete(slot));
1551
+ this.#pings.set(slot, ping);
1552
+ return ping;
1553
+ }
1282
1554
  async #validate(slot) {
1283
1555
  try {
1284
1556
  await slot.browser.ping({ signal: this.#abort.signal });
@@ -1295,12 +1567,19 @@ var BrowserMCPServer = class {
1295
1567
  url: slot.toolset.view.url
1296
1568
  });
1297
1569
  this.#failure = cause;
1298
- if (this.#lease?.value !== slot) return;
1299
- const token = this.#lease;
1300
- this.#unmirror();
1301
- this.#lease = void 0;
1302
- this.#notice.add(slot);
1303
- token.destroy().catch(() => void 0);
1570
+ for (const [holder, token] of this.#leases) {
1571
+ if (token.value !== slot || this.#holders.get(holder.id) !== holder) continue;
1572
+ if (holder === this.#shared) this.#unmirror();
1573
+ this.#leases.delete(holder);
1574
+ const notices = this.#notices.get(holder) ?? /* @__PURE__ */ new Set();
1575
+ notices.add(slot);
1576
+ this.#notices.set(holder, notices);
1577
+ const drain = token.destroy();
1578
+ const drains = this.#drains.get(holder) ?? /* @__PURE__ */ new Set();
1579
+ drains.add(drain);
1580
+ this.#drains.set(holder, drains);
1581
+ drain.finally(() => drains.delete(drain)).catch(() => void 0);
1582
+ }
1304
1583
  }
1305
1584
  #watch(slot, signal) {
1306
1585
  const loss = Promise.withResolvers();
@@ -1373,6 +1652,7 @@ var BrowserMCPServer = class {
1373
1652
  cdp: { discover: false },
1374
1653
  signal: this.#abort.signal,
1375
1654
  ...this.#executable === void 0 ? {} : { executable: this.#executable },
1655
+ ...this.#viewport === void 0 ? {} : { viewport: this.#viewport },
1376
1656
  ...process.platform === "linux" && process.getuid?.() === 0 ? { args: ["--no-sandbox"] } : {}
1377
1657
  });
1378
1658
  await browser.connect();
@@ -1384,7 +1664,11 @@ var BrowserMCPServer = class {
1384
1664
  }));
1385
1665
  await rename(`${record}.tmp`, record);
1386
1666
  }
1387
- const context = await browser.isolate({ reference: this.#reference });
1667
+ const context = await browser.isolate({
1668
+ reference: this.#reference,
1669
+ downloads: { path: join(profile, "downloads") },
1670
+ ...this.#viewport === void 0 ? {} : { emulation: { viewport: this.#viewport } }
1671
+ });
1388
1672
  const page = await context.create();
1389
1673
  toolset = createBrowserToolset(page, {
1390
1674
  context,
@@ -1491,29 +1775,31 @@ var BrowserMCPServer = class {
1491
1775
  #issue() {
1492
1776
  return `e${++this.#references}`;
1493
1777
  }
1494
- #mirror(tool) {
1495
- if (this.#vocabulary.has(tool.name)) return;
1778
+ #mirror(token, tool) {
1779
+ if (this.#leases.get(this.#shared) !== token || this.#vocabulary.has(tool.name)) return;
1496
1780
  this.#tools.add(createTool({
1497
1781
  ...toolToDefinition(tool),
1498
- execute: this.#forward.bind(this, tool.name)
1782
+ execute: this.#forward.bind(this, this.#shared, tool.name)
1499
1783
  }));
1500
1784
  }
1501
- #withdraw(tool) {
1502
- if (this.#vocabulary.has(tool.name)) return;
1503
- const current = this.#lease?.value.toolset.tools.tool(tool.name);
1785
+ #withdraw(token, tool) {
1786
+ if (this.#leases.get(this.#shared) !== token || this.#vocabulary.has(tool.name)) return;
1787
+ const current = token.value.toolset.tools.tool(tool.name);
1504
1788
  if (current === void 0) this.#tools.remove(tool.name);
1505
- else this.#mirror(current);
1789
+ else this.#mirror(token, current);
1506
1790
  }
1507
- #clear(tools) {
1508
- for (const tool of tools) this.#withdraw(tool);
1791
+ #clear(token, tools) {
1792
+ for (const tool of tools) this.#withdraw(token, tool);
1509
1793
  }
1510
1794
  #unmirror() {
1511
- const mirrored = this.#lease?.value.toolset.tools;
1795
+ const mirrored = this.#mirrored;
1512
1796
  if (mirrored === void 0) return;
1513
- for (const tool of mirrored.tools()) if (!this.#vocabulary.has(tool.name)) this.#tools.remove(tool.name);
1514
- mirrored.emitter.off("add", this.#added);
1515
- mirrored.emitter.off("remove", this.#removed);
1516
- mirrored.emitter.off("clear", this.#cleared);
1797
+ this.#mirrored = void 0;
1798
+ const tools = mirrored.token.value.toolset.tools;
1799
+ for (const tool of tools.tools()) if (!this.#vocabulary.has(tool.name)) this.#tools.remove(tool.name);
1800
+ tools.emitter.off("add", mirrored.added);
1801
+ tools.emitter.off("remove", mirrored.removed);
1802
+ tools.emitter.off("clear", mirrored.cleared);
1517
1803
  }
1518
1804
  #ended() {
1519
1805
  return new BrowserError("the browser session ended", "BROWSER_TOOLSET_ENDED");
@@ -1951,6 +2237,24 @@ var FileBrowserRunStore = class {
1951
2237
  constructor(options) {
1952
2238
  this.#files = new FileBrowserStore(options);
1953
2239
  }
2240
+ async snapshot(bytes, options) {
2241
+ options?.signal?.throwIfAborted();
2242
+ const owned = new Uint8Array(bytes);
2243
+ const directory = await this.#files.allocate(this.#files.resolvePath(BROWSER_RUN_DIRECTORY), generateBrowserRunId, options);
2244
+ const path = this.#files.resolvePath(directory.path, "view.png");
2245
+ try {
2246
+ await this.#files.check(path, options);
2247
+ options?.signal?.throwIfAborted();
2248
+ await writeFile(path, owned, {
2249
+ flag: "wx",
2250
+ signal: options?.signal
2251
+ });
2252
+ return path;
2253
+ } catch (error) {
2254
+ options?.signal?.throwIfAborted();
2255
+ throw this.#files.translateError(path, error);
2256
+ }
2257
+ }
1954
2258
  async open(name, options) {
1955
2259
  options?.signal?.throwIfAborted();
1956
2260
  this.#files.validateName(name);
@@ -2981,6 +3285,6 @@ var Browser = class {
2981
3285
  }
2982
3286
  };
2983
3287
  //#endregion
2984
- export { BROWSER_CDP_LIST_PATH, BROWSER_CDP_PROTOCOL, BROWSER_CDP_VERSION_PATH, BROWSER_DEFAULT_CDP_PORT, BROWSER_DEFAULT_HOST, BROWSER_DEVTOOLS_PATTERN, BROWSER_DRAIN_INTERVAL_MS, BROWSER_ENGINE_HINTS, BROWSER_ENV_PATH_KEYS, BROWSER_EXECUTABLE_NAMES, BROWSER_EXECUTABLE_PATHS, BROWSER_FILE_STORE_LIMIT, BROWSER_HEADLESS_ARG, BROWSER_JOURNEY_LOCK_ATTEMPTS, BROWSER_JOURNEY_LOCK_DIRECTORY, BROWSER_JOURNEY_REVISION_FILE, BROWSER_JOURNEY_SNAPSHOT_FILE, BROWSER_KILL_GRACE_MS, BROWSER_LAUNCH_ARGS, BROWSER_PORT_PROBE_TIMEOUT_MS, BROWSER_PROCESS_EXIT_CAUSE, BROWSER_PROFILE_PREFIX, BROWSER_RUN_DIRECTORY, BROWSER_RUN_FILE, BROWSER_SERVER_CRASH, BROWSER_SERVER_EXHAUSTED, BROWSER_SERVER_LAUNCH, BROWSER_SERVER_OPTIONS, BROWSER_SERVER_POOL_LIMIT, BROWSER_SERVER_POOL_SIZE, BROWSER_SERVER_RECORD, BROWSER_SERVER_RESTARTS, BROWSER_SERVER_SWEEP, BROWSER_SERVER_TEARDOWN, BROWSER_SERVER_UNAVAILABLE, BROWSER_SERVER_UNRESOLVED, BROWSER_STORE_CACHE_DIRS, BROWSER_STORE_DEFAULT_DIRS, BROWSER_STORE_ENV_KEY, BROWSER_STORE_GLOBS, BROWSER_STORE_LINK_NAME, BROWSER_TRANSPORT_LOSS_CAUSE, BROWSER_TRANSPORT_LOSS_DEFER_MS, BROWSER_WINDOWS_ROOT_FALLBACKS, BROWSER_WINDOWS_SUFFIXES, Browser, BrowserDestroyedError, BrowserMCPServer, BrowserNotConnectedError, FileBrowserJourneyStore, FileBrowserRunStore, FileBrowserWriter, WebSocketCDPTransport, browserToEngine, buildInstallPaths, buildStoreBases, buildWindowsRoots, createBrowser, createBrowserMCPServer, createBrowserProfile, createBrowserWriter, createCDPTransport, createFileBrowserJourneyStore, createFileBrowserRunStore, describeBrowserServerLoss, fetchCDPTargets, findEnvOverrides, findInstallPaths, findStorePaths, findSystemBrowser, findSystemBrowsers, formatBrowserLockEntry, isBrowserDestroyedError, isBrowserNotConnectedError, launchBrowserProcess, normalizeExecutablePath, parseBrowserEngine, parseBrowserLockEntry, parseBrowserProfileRecord, probePathNames, probeProcess, readBrowserEndpoint, readFirstLine, removeBrowserProfile };
3288
+ export { BROWSER_CDP_LIST_PATH, BROWSER_CDP_PROTOCOL, BROWSER_CDP_VERSION_PATH, BROWSER_DEFAULT_CDP_PORT, BROWSER_DEFAULT_HOST, BROWSER_DEVTOOLS_PATTERN, BROWSER_DRAIN_INTERVAL_MS, BROWSER_ENGINE_HINTS, BROWSER_ENV_PATH_KEYS, BROWSER_EXECUTABLE_NAMES, BROWSER_EXECUTABLE_PATHS, BROWSER_FILE_STORE_LIMIT, BROWSER_HEADLESS_ARG, BROWSER_JOURNEY_LOCK_ATTEMPTS, BROWSER_JOURNEY_LOCK_DIRECTORY, BROWSER_JOURNEY_REVISION_FILE, BROWSER_JOURNEY_SNAPSHOT_FILE, BROWSER_KILL_GRACE_MS, BROWSER_LAUNCH_ARGS, BROWSER_PORT_PROBE_TIMEOUT_MS, BROWSER_PROCESS_EXIT_CAUSE, BROWSER_PROFILE_PREFIX, BROWSER_RUN_DIRECTORY, BROWSER_RUN_FILE, BROWSER_SERVER_BUSY, BROWSER_SERVER_COPY, BROWSER_SERVER_CRASH, BROWSER_SERVER_EXHAUSTED, BROWSER_SERVER_HOLDER, BROWSER_SERVER_LAUNCH, BROWSER_SERVER_OPTIONS, BROWSER_SERVER_POOL_LIMIT, BROWSER_SERVER_POOL_SIZE, BROWSER_SERVER_RECORD, BROWSER_SERVER_RESTARTS, BROWSER_SERVER_SWEEP, BROWSER_SERVER_TEARDOWN, BROWSER_SERVER_UNAVAILABLE, BROWSER_SERVER_UNRESOLVED, BROWSER_STORE_CACHE_DIRS, BROWSER_STORE_DEFAULT_DIRS, BROWSER_STORE_ENV_KEY, BROWSER_STORE_GLOBS, BROWSER_STORE_LINK_NAME, BROWSER_TRANSPORT_LOSS_CAUSE, BROWSER_TRANSPORT_LOSS_DEFER_MS, BROWSER_WINDOWS_ROOT_FALLBACKS, BROWSER_WINDOWS_SUFFIXES, Browser, BrowserDestroyedError, BrowserMCPServer, BrowserNotConnectedError, FileBrowserJourneyStore, FileBrowserRunStore, FileBrowserWriter, WebSocketCDPTransport, browserToEngine, buildInstallPaths, buildStoreBases, buildWindowsRoots, createBrowser, createBrowserMCPServer, createBrowserProfile, createBrowserWriter, createCDPTransport, createFileBrowserJourneyStore, createFileBrowserRunStore, describeBrowserServerLoss, fetchCDPTargets, findEnvOverrides, findInstallPaths, findStorePaths, findSystemBrowser, findSystemBrowsers, formatBrowserLockEntry, isBrowserDestroyedError, isBrowserNotConnectedError, launchBrowserProcess, normalizeExecutablePath, parseBrowserEngine, parseBrowserLockEntry, parseBrowserProfileRecord, parseBrowserViewport, probePathNames, probeProcess, readBrowserEndpoint, readFirstLine, removeBrowserProfile };
2985
3289
 
2986
3290
  //# sourceMappingURL=index.js.map