@orkestrel/browser 0.0.24 → 0.0.25
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.
- package/dist/bin/main.js +10 -2
- package/dist/bin/main.js.map +1 -1
- package/dist/src/core/index.cjs +42 -13
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +13 -4
- package/dist/src/core/index.d.ts +13 -4
- package/dist/src/core/index.js +42 -13
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +373 -167
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +54 -14
- package/dist/src/server/index.d.ts +54 -14
- package/dist/src/server/index.js +373 -169
- package/dist/src/server/index.js.map +1 -1
- package/package.json +5 -5
package/dist/src/server/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
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, isCDPConnectionError, isCDPTimeoutError, normalizeBrowserJourneyReason, validateBrowserContextOptions, validateBrowserJourney, validateBrowserRun, validateBrowserStorePage } from "../core/index.js";
|
|
2
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";
|
|
@@ -37,8 +37,18 @@ var BROWSER_CDP_VERSION_PATH = "/json/version";
|
|
|
37
37
|
* other target category Chromium reports — `'/json/list'`.
|
|
38
38
|
*/
|
|
39
39
|
var BROWSER_CDP_LIST_PATH = "/json/list";
|
|
40
|
-
/**
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Lists the flags always passed to a launched browser process, alongside the caller's own.
|
|
42
|
+
*
|
|
43
|
+
* @remarks
|
|
44
|
+
* Disables account sync so extension installs cannot reset URL loader factories during page imports.
|
|
45
|
+
* Edge can still sign a fresh Windows profile in to the operating-system account.
|
|
46
|
+
*/
|
|
47
|
+
var BROWSER_LAUNCH_ARGS = Object.freeze([
|
|
48
|
+
"--no-first-run",
|
|
49
|
+
"--no-default-browser-check",
|
|
50
|
+
"--disable-sync"
|
|
51
|
+
]);
|
|
42
52
|
/**
|
|
43
53
|
* Names the flag that enables headless mode on a launched browser process, `'--headless=new'`.
|
|
44
54
|
*/
|
|
@@ -218,6 +228,10 @@ var BROWSER_SERVER_UNAVAILABLE = "BROWSER_SERVER_UNAVAILABLE";
|
|
|
218
228
|
var BROWSER_SERVER_LAUNCH = "BROWSER_SERVER_LAUNCH";
|
|
219
229
|
/** Sets the default number of warm browsers to 1. */
|
|
220
230
|
var BROWSER_SERVER_POOL_SIZE = 1;
|
|
231
|
+
/** Sets the default context capacity per browser, including the shared holder. */
|
|
232
|
+
var BROWSER_SERVER_CONTEXTS = 2;
|
|
233
|
+
/** Limits context capacity to the largest measured per-browser count. */
|
|
234
|
+
var BROWSER_SERVER_CONTEXTS_LIMIT = 4;
|
|
221
235
|
/** Limits the number of warm browsers to 3. */
|
|
222
236
|
var BROWSER_SERVER_POOL_LIMIT = 3;
|
|
223
237
|
/** Permits one failed refill before the next failure spends the bound. */
|
|
@@ -1041,7 +1055,7 @@ var FileBrowserWriter = class {
|
|
|
1041
1055
|
};
|
|
1042
1056
|
//#endregion
|
|
1043
1057
|
//#region package.json
|
|
1044
|
-
var version = "0.0.
|
|
1058
|
+
var version = "0.0.25";
|
|
1045
1059
|
//#endregion
|
|
1046
1060
|
//#region src/server/BrowserMCPServer.ts
|
|
1047
1061
|
/**
|
|
@@ -1057,17 +1071,18 @@ var version = "0.0.24";
|
|
|
1057
1071
|
* tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
|
|
1058
1072
|
* removed when the toolset withdraws the tool, so the server's tool list changes and a subscribed
|
|
1059
1073
|
* client is notified. The server also advertises `acquire`, `execute`, `tools`, and `destroy` with
|
|
1060
|
-
* `BROWSER_SERVER_COPY`. Startup warms the pool and
|
|
1074
|
+
* `BROWSER_SERVER_COPY`. Startup warms the pool and assigns a prepared context to the shared holder. The legacy
|
|
1061
1075
|
* handshake and browser calls await that setup. Named holders each own a lease, a pending grant,
|
|
1062
1076
|
* and loss notices; concurrent calls on a holder share its replacement grant. Ending a holder
|
|
1063
|
-
*
|
|
1077
|
+
* closes its context and releases its lease, and admission counts that holder until disposal settles.
|
|
1064
1078
|
* A launch creates `ROOT/.profiles/PID-UUID/` exclusively, connects a browser that never attaches to an
|
|
1065
1079
|
* existing endpoint, opens one page in an isolated context, and constructs the page toolset with
|
|
1066
1080
|
* that context and the file journey and run stores under the root, on a manager the toolset owns.
|
|
1067
1081
|
* A dispatcher forwards its arguments and the call's context, signal included, to the toolset's
|
|
1068
1082
|
* manager and returns that manager's value, or rejects with its failure message; a text value
|
|
1069
1083
|
* reaches the client as one text block rather than as its JSON text.
|
|
1070
|
-
* A disconnect
|
|
1084
|
+
* A disconnect retires its browser and every hosted generation. A current-page crash retires only
|
|
1085
|
+
* that holder's context. A failed call receives a liveness ping;
|
|
1071
1086
|
* loss makes its outcome unresolved, while a known success remains successful. Pending losses
|
|
1072
1087
|
* prefix the next outcome on a successor, or a refusal, and calls are never repeated.
|
|
1073
1088
|
* Concurrent checks share one ping per slot. Cancelling a call ends only its wait; the ping keeps
|
|
@@ -1075,7 +1090,8 @@ var version = "0.0.24";
|
|
|
1075
1090
|
*
|
|
1076
1091
|
* `start()` serves stdio and destroys the server at the end of its input, on `SIGINT`, and on
|
|
1077
1092
|
* `SIGTERM`. `destroy()` stops reading requests and removes every dispatcher, then destroys every
|
|
1078
|
-
* slot through the pool —
|
|
1093
|
+
* slot through the pool — each toolset, which aborts its active replay, then its context and downloads,
|
|
1094
|
+
* then its browser and its
|
|
1079
1095
|
* profile — and rechecks the profile folders this server made or kept.
|
|
1080
1096
|
*
|
|
1081
1097
|
* Chromium on Linux refuses to start as the root user unless its sandbox is disabled, so a server
|
|
@@ -1110,8 +1126,16 @@ var BrowserMCPServer = class {
|
|
|
1110
1126
|
#folders = /* @__PURE__ */ new Set();
|
|
1111
1127
|
#faults = /* @__PURE__ */ new Set();
|
|
1112
1128
|
#losses = /* @__PURE__ */ new WeakMap();
|
|
1129
|
+
#departures = /* @__PURE__ */ new WeakMap();
|
|
1113
1130
|
#pings = /* @__PURE__ */ new WeakMap();
|
|
1114
1131
|
#watches = /* @__PURE__ */ new Map();
|
|
1132
|
+
#observed = /* @__PURE__ */ new Map();
|
|
1133
|
+
#prepared = /* @__PURE__ */ new Map();
|
|
1134
|
+
#owned = /* @__PURE__ */ new Map();
|
|
1135
|
+
#building = /* @__PURE__ */ new Map();
|
|
1136
|
+
#cleaning = /* @__PURE__ */ new WeakMap();
|
|
1137
|
+
#endings = /* @__PURE__ */ new WeakMap();
|
|
1138
|
+
#retentions = /* @__PURE__ */ new Map();
|
|
1115
1139
|
#shared = {
|
|
1116
1140
|
id: "shared",
|
|
1117
1141
|
purpose: "Shared browser",
|
|
@@ -1125,7 +1149,7 @@ var BrowserMCPServer = class {
|
|
|
1125
1149
|
#drains = /* @__PURE__ */ new Map();
|
|
1126
1150
|
#readers = /* @__PURE__ */ new Map();
|
|
1127
1151
|
#writers = /* @__PURE__ */ new Set();
|
|
1128
|
-
#
|
|
1152
|
+
#admission;
|
|
1129
1153
|
#reference;
|
|
1130
1154
|
#references = 0;
|
|
1131
1155
|
#failure;
|
|
@@ -1140,17 +1164,20 @@ var BrowserMCPServer = class {
|
|
|
1140
1164
|
*/
|
|
1141
1165
|
constructor(options) {
|
|
1142
1166
|
const size = options?.pool?.size ?? 1;
|
|
1167
|
+
const contexts = options?.pool?.contexts ?? 2;
|
|
1143
1168
|
if (!isInteger(size) || size < 1 || size > 3) throw new BrowserError(`pool.size must be an integer from 1 through 3`, BROWSER_SERVER_OPTIONS);
|
|
1144
|
-
|
|
1169
|
+
if (!isInteger(contexts) || contexts < 1 || contexts > 4) throw new BrowserError(`pool.contexts must be an integer from 1 through 4`, BROWSER_SERVER_OPTIONS);
|
|
1170
|
+
this.#admission = size * contexts;
|
|
1145
1171
|
this.#log = options?.log ?? process.stderr;
|
|
1146
1172
|
this.#reference = this.#issue.bind(this);
|
|
1147
1173
|
this.#pool = createPool({
|
|
1148
1174
|
create: this.#warm.bind(this),
|
|
1149
|
-
destroy:
|
|
1175
|
+
destroy: this.#destroyRecord.bind(this),
|
|
1150
1176
|
validate: this.#validate.bind(this),
|
|
1151
1177
|
watch: this.#watch.bind(this),
|
|
1152
1178
|
error: this.#fault.bind(this),
|
|
1153
1179
|
min: size,
|
|
1180
|
+
capacity: contexts,
|
|
1154
1181
|
restarts: 1
|
|
1155
1182
|
});
|
|
1156
1183
|
this.#root = resolve(options?.root ?? "tmp/browsers");
|
|
@@ -1233,6 +1260,7 @@ var BrowserMCPServer = class {
|
|
|
1233
1260
|
this.#transport.stop();
|
|
1234
1261
|
for (const tool of this.#tools.tools()) this.#tools.remove(tool.name);
|
|
1235
1262
|
this.#unmirror();
|
|
1263
|
+
for (const generation of this.#observed.keys()) this.#untrack(generation);
|
|
1236
1264
|
this.#leases.clear();
|
|
1237
1265
|
for (const holder of this.#holders.values()) holder.abort.abort();
|
|
1238
1266
|
this.#abort.abort();
|
|
@@ -1241,6 +1269,7 @@ var BrowserMCPServer = class {
|
|
|
1241
1269
|
});
|
|
1242
1270
|
await this.#starting?.catch(() => void 0);
|
|
1243
1271
|
await barrier;
|
|
1272
|
+
await Promise.allSettled(this.#grants.values());
|
|
1244
1273
|
await Promise.allSettled(this.#disposals.values());
|
|
1245
1274
|
this.#holders.clear();
|
|
1246
1275
|
await this.#sweeping;
|
|
@@ -1292,7 +1321,7 @@ var BrowserMCPServer = class {
|
|
|
1292
1321
|
if (this.#closing !== void 0) throw this.#ended();
|
|
1293
1322
|
const purpose = args["purpose"];
|
|
1294
1323
|
if (!isString(purpose) || purpose.trim().length === 0) throw new BrowserError("purpose must describe the work", "BROWSER_TOOLSET_ARGUMENT");
|
|
1295
|
-
if (this.#holders.size >= this.#
|
|
1324
|
+
if (this.#holders.size >= this.#admission) 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
1325
|
const holder = {
|
|
1297
1326
|
id: randomUUID(),
|
|
1298
1327
|
purpose,
|
|
@@ -1301,13 +1330,13 @@ var BrowserMCPServer = class {
|
|
|
1301
1330
|
this.#holders.set(holder.id, holder);
|
|
1302
1331
|
const listener = addAbortListener(context.signal, () => holder.abort.abort(context.signal.reason));
|
|
1303
1332
|
try {
|
|
1304
|
-
const
|
|
1333
|
+
const lease = await this.#serve(holder, context.signal);
|
|
1305
1334
|
context.signal.throwIfAborted();
|
|
1306
1335
|
if (holder.abort.signal.aborted || this.#holders.get(holder.id) !== holder) throw this.#ended();
|
|
1307
|
-
if (this.#leases.get(holder)
|
|
1336
|
+
if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
|
|
1308
1337
|
const catalog = {
|
|
1309
1338
|
holder: holder.id,
|
|
1310
|
-
tools:
|
|
1339
|
+
tools: lease.generation.toolset.tools.definitions()
|
|
1311
1340
|
};
|
|
1312
1341
|
return JSON.stringify(catalog);
|
|
1313
1342
|
} catch (error) {
|
|
@@ -1333,12 +1362,12 @@ var BrowserMCPServer = class {
|
|
|
1333
1362
|
const holder = this.#holder(args["holder"]);
|
|
1334
1363
|
const signal = AbortSignal.any([context.signal, holder.abort.signal]);
|
|
1335
1364
|
try {
|
|
1336
|
-
const
|
|
1365
|
+
const lease = await this.#serve(holder, signal);
|
|
1337
1366
|
signal.throwIfAborted();
|
|
1338
|
-
if (this.#leases.get(holder)
|
|
1367
|
+
if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
|
|
1339
1368
|
const catalog = {
|
|
1340
1369
|
holder: holder.id,
|
|
1341
|
-
tools:
|
|
1370
|
+
tools: lease.generation.toolset.tools.definitions()
|
|
1342
1371
|
};
|
|
1343
1372
|
return this.#annotate(holder, JSON.stringify(catalog));
|
|
1344
1373
|
} catch (error) {
|
|
@@ -1357,17 +1386,19 @@ var BrowserMCPServer = class {
|
|
|
1357
1386
|
const pending = this.#disposals.get(holder.id);
|
|
1358
1387
|
if (pending !== void 0) return pending;
|
|
1359
1388
|
holder.abort.abort();
|
|
1360
|
-
const
|
|
1389
|
+
const lease = this.#leases.get(holder);
|
|
1361
1390
|
this.#leases.delete(holder);
|
|
1391
|
+
if (lease !== void 0) this.#untrack(lease.generation);
|
|
1362
1392
|
const disposal = Promise.resolve().then(async () => {
|
|
1363
1393
|
try {
|
|
1364
1394
|
await this.#grants.get(holder)?.catch(() => void 0);
|
|
1365
|
-
await
|
|
1395
|
+
if (lease !== void 0) await this.#release(holder, lease);
|
|
1366
1396
|
await Promise.all(this.#drains.get(holder) ?? []);
|
|
1397
|
+
await Promise.all([...this.#notices.get(holder) ?? []].filter((lost) => this.#losses.has(lost.token.value)).map((lost) => this.#endings.get(lost.token.value)?.promise));
|
|
1367
1398
|
} catch (error) {
|
|
1368
1399
|
this.#faults.add(error);
|
|
1369
1400
|
} finally {
|
|
1370
|
-
if (this.#holders.get(holder.id) === holder) this.#holders.delete(holder.id);
|
|
1401
|
+
if (this.#holders.get(holder.id) === holder && !this.#retentions.has(holder)) this.#holders.delete(holder.id);
|
|
1371
1402
|
this.#notices.delete(holder);
|
|
1372
1403
|
this.#drains.delete(holder);
|
|
1373
1404
|
}
|
|
@@ -1400,9 +1431,9 @@ var BrowserMCPServer = class {
|
|
|
1400
1431
|
}
|
|
1401
1432
|
async #perform(holder, name, args, context) {
|
|
1402
1433
|
const signal = holder === this.#shared ? context.signal : AbortSignal.any([context.signal, holder.abort.signal]);
|
|
1403
|
-
let
|
|
1434
|
+
let lease;
|
|
1404
1435
|
try {
|
|
1405
|
-
|
|
1436
|
+
lease = await this.#serve(holder, signal);
|
|
1406
1437
|
} catch (error) {
|
|
1407
1438
|
context.signal.throwIfAborted();
|
|
1408
1439
|
if (holder !== this.#shared && holder.abort.signal.aborted) throw new BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
|
|
@@ -1411,7 +1442,7 @@ var BrowserMCPServer = class {
|
|
|
1411
1442
|
}
|
|
1412
1443
|
let result;
|
|
1413
1444
|
try {
|
|
1414
|
-
const execution =
|
|
1445
|
+
const execution = lease.generation.toolset.tools.execute({
|
|
1415
1446
|
id: name,
|
|
1416
1447
|
name,
|
|
1417
1448
|
arguments: args
|
|
@@ -1433,23 +1464,23 @@ var BrowserMCPServer = class {
|
|
|
1433
1464
|
context.signal.throwIfAborted();
|
|
1434
1465
|
if (holder !== this.#shared && holder.abort.signal.aborted) throw new BrowserError(BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call", BROWSER_SERVER_UNRESOLVED);
|
|
1435
1466
|
if (!result.success) {
|
|
1436
|
-
if (this.#leases.get(holder)
|
|
1437
|
-
await this.#race(this.#ping(
|
|
1467
|
+
if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
|
|
1468
|
+
await this.#race(this.#ping(lease.token.value), context.signal);
|
|
1438
1469
|
} catch {}
|
|
1439
1470
|
context.signal.throwIfAborted();
|
|
1440
|
-
if (this.#leases.get(holder)
|
|
1441
|
-
const loss = this.#
|
|
1442
|
-
throw new BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url),
|
|
1471
|
+
if (this.#leases.get(holder) !== lease) {
|
|
1472
|
+
const loss = this.#departures.get(lease);
|
|
1473
|
+
throw new BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease), BROWSER_SERVER_UNRESOLVED);
|
|
1443
1474
|
}
|
|
1444
|
-
throw new BrowserError(this.#annotate(holder, result.error,
|
|
1475
|
+
throw new BrowserError(this.#annotate(holder, result.error, lease));
|
|
1445
1476
|
}
|
|
1446
|
-
return typeof result.value === "string" ? this.#annotate(holder, result.value,
|
|
1477
|
+
return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
|
|
1447
1478
|
}
|
|
1448
|
-
#annotate(holder, value,
|
|
1479
|
+
#annotate(holder, value, lease) {
|
|
1449
1480
|
const notes = [];
|
|
1450
1481
|
for (const lost of this.#notices.get(holder) ?? []) {
|
|
1451
|
-
if (lost ===
|
|
1452
|
-
const loss = this.#
|
|
1482
|
+
if (lost === lease) continue;
|
|
1483
|
+
const loss = this.#departures.get(lost);
|
|
1453
1484
|
notes.push(describeBrowserServerLoss(BROWSER_SERVER_CRASH, loss?.cause, loss?.url));
|
|
1454
1485
|
this.#notices.get(holder)?.delete(lost);
|
|
1455
1486
|
}
|
|
@@ -1480,41 +1511,70 @@ var BrowserMCPServer = class {
|
|
|
1480
1511
|
if (held !== void 0) return held;
|
|
1481
1512
|
let granting = this.#grants.get(holder);
|
|
1482
1513
|
if (granting === void 0) {
|
|
1483
|
-
granting =
|
|
1514
|
+
granting = Promise.resolve().then(async () => {
|
|
1515
|
+
await Promise.allSettled(this.#drains.get(holder) ?? []);
|
|
1516
|
+
if (this.#retentions.has(holder)) throw this.#unavailable(this.#retentions.get(holder));
|
|
1517
|
+
const token = await this.#pool.acquire(holder.abort.signal);
|
|
1518
|
+
return this.#hold(holder, token);
|
|
1519
|
+
});
|
|
1484
1520
|
this.#grants.set(holder, granting);
|
|
1485
1521
|
granting.finally(() => {
|
|
1486
1522
|
if (this.#grants.get(holder) === granting) this.#grants.delete(holder);
|
|
1487
1523
|
}).catch(() => void 0);
|
|
1488
1524
|
}
|
|
1489
|
-
const
|
|
1490
|
-
if (
|
|
1491
|
-
return
|
|
1525
|
+
const lease = await this.#race(granting, signal);
|
|
1526
|
+
if (lease === void 0) throw this.#ended();
|
|
1527
|
+
return lease;
|
|
1492
1528
|
}
|
|
1493
1529
|
async #hold(holder, token) {
|
|
1530
|
+
const slot = token.value;
|
|
1494
1531
|
if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted) {
|
|
1495
|
-
|
|
1532
|
+
token.release();
|
|
1533
|
+
throw this.#ended();
|
|
1534
|
+
}
|
|
1535
|
+
if (!this.#prepared.has(slot)) await this.#ping(slot);
|
|
1536
|
+
if (this.#closing !== void 0 || holder.abort.signal.aborted) {
|
|
1537
|
+
token.release();
|
|
1496
1538
|
throw this.#ended();
|
|
1497
1539
|
}
|
|
1498
|
-
if (this.#losses.has(
|
|
1499
|
-
|
|
1500
|
-
throw this.#unavailable(this.#losses.get(
|
|
1540
|
+
if (this.#losses.has(slot)) {
|
|
1541
|
+
token.release();
|
|
1542
|
+
throw this.#unavailable(this.#losses.get(slot)?.cause);
|
|
1501
1543
|
}
|
|
1502
|
-
this.#
|
|
1544
|
+
let generation = this.#prepared.get(slot);
|
|
1545
|
+
this.#prepared.delete(slot);
|
|
1546
|
+
try {
|
|
1547
|
+
generation ??= await this.#build(slot, token);
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
if (this.#losses.has(slot)) await this.#endings.get(slot)?.promise;
|
|
1550
|
+
else token.release();
|
|
1551
|
+
throw this.#unavailable(error);
|
|
1552
|
+
}
|
|
1553
|
+
const lease = {
|
|
1554
|
+
token,
|
|
1555
|
+
generation
|
|
1556
|
+
};
|
|
1557
|
+
if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted || this.#losses.has(slot)) {
|
|
1558
|
+
await this.#release(holder, lease);
|
|
1559
|
+
throw this.#ended();
|
|
1560
|
+
}
|
|
1561
|
+
this.#leases.set(holder, lease);
|
|
1562
|
+
this.#observe(slot, generation);
|
|
1503
1563
|
if (holder === this.#shared) {
|
|
1504
|
-
const tools =
|
|
1564
|
+
const tools = generation.toolset.tools;
|
|
1505
1565
|
const mirrored = {
|
|
1506
|
-
|
|
1507
|
-
added: this.#mirror.bind(this,
|
|
1508
|
-
removed: this.#withdraw.bind(this,
|
|
1509
|
-
cleared: this.#clear.bind(this,
|
|
1566
|
+
lease,
|
|
1567
|
+
added: this.#mirror.bind(this, lease),
|
|
1568
|
+
removed: this.#withdraw.bind(this, lease),
|
|
1569
|
+
cleared: this.#clear.bind(this, lease)
|
|
1510
1570
|
};
|
|
1511
1571
|
this.#mirrored = mirrored;
|
|
1512
|
-
for (const tool of tools.tools()) this.#mirror(
|
|
1572
|
+
for (const tool of tools.tools()) this.#mirror(lease, tool);
|
|
1513
1573
|
tools.emitter.on("add", mirrored.added);
|
|
1514
1574
|
tools.emitter.on("remove", mirrored.removed);
|
|
1515
1575
|
tools.emitter.on("clear", mirrored.cleared);
|
|
1516
1576
|
}
|
|
1517
|
-
return
|
|
1577
|
+
return lease;
|
|
1518
1578
|
}
|
|
1519
1579
|
async #serve(holder, signal) {
|
|
1520
1580
|
if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
|
|
@@ -1522,16 +1582,16 @@ var BrowserMCPServer = class {
|
|
|
1522
1582
|
if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
|
|
1523
1583
|
const held = this.#leases.get(holder);
|
|
1524
1584
|
if (held !== void 0) try {
|
|
1525
|
-
await this.#race(this.#ping(held.value), signal);
|
|
1526
|
-
if (this.#leases.get(holder) === held) return held
|
|
1585
|
+
await this.#race(this.#ping(held.token.value), signal);
|
|
1586
|
+
if (this.#leases.get(holder) === held) return held;
|
|
1527
1587
|
} catch {
|
|
1528
1588
|
signal.throwIfAborted();
|
|
1529
1589
|
if (holder.abort.signal.aborted) throw this.#ended();
|
|
1530
1590
|
}
|
|
1531
1591
|
try {
|
|
1532
|
-
const
|
|
1533
|
-
if (this.#leases.get(holder) !==
|
|
1534
|
-
return
|
|
1592
|
+
const lease = await this.#grant(holder, signal);
|
|
1593
|
+
if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
|
|
1594
|
+
return lease;
|
|
1535
1595
|
} catch (error) {
|
|
1536
1596
|
signal.throwIfAborted();
|
|
1537
1597
|
if (holder.abort.signal.aborted || isPoolError(error) && error.code === "destroyed") throw this.#ended();
|
|
@@ -1562,78 +1622,211 @@ var BrowserMCPServer = class {
|
|
|
1562
1622
|
}
|
|
1563
1623
|
#lose(slot, cause) {
|
|
1564
1624
|
if (this.#closing !== void 0 || this.#losses.has(slot)) return;
|
|
1565
|
-
this.#losses.set(slot, {
|
|
1566
|
-
cause,
|
|
1567
|
-
url: slot.toolset.view.url
|
|
1568
|
-
});
|
|
1625
|
+
this.#losses.set(slot, { cause });
|
|
1569
1626
|
this.#failure = cause;
|
|
1570
|
-
for (const [holder,
|
|
1571
|
-
if (token.value !== slot
|
|
1572
|
-
|
|
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);
|
|
1627
|
+
for (const [holder, lease] of this.#leases) {
|
|
1628
|
+
if (lease.token.value !== slot) continue;
|
|
1629
|
+
this.#detach(holder, lease, cause);
|
|
1582
1630
|
}
|
|
1631
|
+
const watch = this.#watches.get(slot);
|
|
1632
|
+
this.#unwatch(slot);
|
|
1633
|
+
watch?.resolve(cause);
|
|
1634
|
+
}
|
|
1635
|
+
#detach(holder, lease, cause) {
|
|
1636
|
+
if (this.#leases.get(holder) !== lease || this.#holders.get(holder.id) !== holder) return;
|
|
1637
|
+
this.#departures.set(lease, {
|
|
1638
|
+
cause,
|
|
1639
|
+
url: lease.generation.toolset.view.url
|
|
1640
|
+
});
|
|
1641
|
+
if (holder === this.#shared) this.#unmirror();
|
|
1642
|
+
this.#leases.delete(holder);
|
|
1643
|
+
this.#untrack(lease.generation);
|
|
1644
|
+
const notices = this.#notices.get(holder) ?? /* @__PURE__ */ new Set();
|
|
1645
|
+
notices.add(lease);
|
|
1646
|
+
this.#notices.set(holder, notices);
|
|
1583
1647
|
}
|
|
1584
1648
|
#watch(slot, signal) {
|
|
1585
1649
|
const loss = Promise.withResolvers();
|
|
1586
1650
|
const disconnect = this.#disconnect.bind(this, slot);
|
|
1587
|
-
const page = this.#track.bind(this, slot);
|
|
1588
1651
|
const subscription = addAbortListener(signal, this.#unwatch.bind(this, slot));
|
|
1589
1652
|
this.#watches.set(slot, {
|
|
1590
1653
|
resolve: loss.resolve,
|
|
1591
1654
|
disconnect,
|
|
1592
|
-
|
|
1593
|
-
subscription,
|
|
1594
|
-
crashes: /* @__PURE__ */ new Map()
|
|
1655
|
+
subscription
|
|
1595
1656
|
});
|
|
1596
1657
|
slot.browser.emitter.on("disconnect", disconnect);
|
|
1597
|
-
slot.context.emitter.on("page", page);
|
|
1598
|
-
for (const current of slot.context.pages()) this.#track(slot, current);
|
|
1599
1658
|
if (signal.aborted) this.#unwatch(slot);
|
|
1600
1659
|
else if (slot.browser.status !== "connected") this.#disconnect(slot);
|
|
1601
1660
|
return loss.promise;
|
|
1602
1661
|
}
|
|
1603
|
-
#
|
|
1604
|
-
|
|
1662
|
+
#observe(slot, generation) {
|
|
1663
|
+
if (this.#observed.has(generation)) return;
|
|
1664
|
+
const page = this.#track.bind(this, slot, generation);
|
|
1665
|
+
this.#observed.set(generation, {
|
|
1666
|
+
page,
|
|
1667
|
+
crashes: /* @__PURE__ */ new Map()
|
|
1668
|
+
});
|
|
1669
|
+
generation.context.emitter.on("page", page);
|
|
1670
|
+
for (const current of generation.context.pages()) this.#track(slot, generation, current);
|
|
1671
|
+
}
|
|
1672
|
+
#track(slot, generation, page) {
|
|
1673
|
+
const watch = this.#observed.get(generation);
|
|
1605
1674
|
if (watch === void 0 || watch.crashes.has(page)) return;
|
|
1606
|
-
const crash = this.#
|
|
1675
|
+
const crash = this.#crash.bind(this, slot, generation, page);
|
|
1607
1676
|
page.emitter.on("crash", crash);
|
|
1608
|
-
this.#
|
|
1677
|
+
this.#observed.set(generation, {
|
|
1609
1678
|
...watch,
|
|
1610
1679
|
crashes: new Map([...watch.crashes, [page, crash]])
|
|
1611
1680
|
});
|
|
1612
1681
|
}
|
|
1613
|
-
#
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1682
|
+
#crash(slot, generation, page) {
|
|
1683
|
+
if (this.#prepared.get(slot) === generation) {
|
|
1684
|
+
this.#prepared.delete(slot);
|
|
1685
|
+
this.#untrack(generation);
|
|
1686
|
+
this.#clean(slot, generation).catch(this.#fault.bind(this));
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
const held = [...this.#leases].find(([, lease]) => lease.generation === generation);
|
|
1690
|
+
if (held === void 0 || page !== generation.toolset.view) return;
|
|
1691
|
+
const [holder, lease] = held;
|
|
1692
|
+
this.#detach(holder, lease, /* @__PURE__ */ new Error("The current page renderer crashed"));
|
|
1693
|
+
const drain = this.#release(holder, lease);
|
|
1694
|
+
const drains = this.#drains.get(holder) ?? /* @__PURE__ */ new Set();
|
|
1695
|
+
drains.add(drain);
|
|
1696
|
+
this.#drains.set(holder, drains);
|
|
1697
|
+
drain.finally(() => drains.delete(drain)).catch(this.#fault.bind(this));
|
|
1698
|
+
}
|
|
1699
|
+
#untrack(generation) {
|
|
1700
|
+
const watch = this.#observed.get(generation);
|
|
1701
|
+
if (watch === void 0) return;
|
|
1702
|
+
this.#observed.delete(generation);
|
|
1703
|
+
generation.context.emitter.off("page", watch.page);
|
|
1704
|
+
for (const [page, crash] of watch.crashes) page.emitter.off("crash", crash);
|
|
1617
1705
|
}
|
|
1618
1706
|
#disconnect(slot) {
|
|
1619
|
-
this.#
|
|
1620
|
-
}
|
|
1621
|
-
#report(slot, cause) {
|
|
1622
|
-
const watch = this.#watches.get(slot);
|
|
1623
|
-
if (watch === void 0) return;
|
|
1624
|
-
this.#unwatch(slot);
|
|
1625
|
-
this.#lose(slot, cause);
|
|
1626
|
-
watch.resolve(cause);
|
|
1707
|
+
this.#lose(slot, /* @__PURE__ */ new Error(slot.browser.pid === void 0 ? "The browser process exited" : "The browser transport disconnected"));
|
|
1627
1708
|
}
|
|
1628
1709
|
#unwatch(slot) {
|
|
1629
1710
|
const watch = this.#watches.get(slot);
|
|
1630
1711
|
if (watch === void 0) return;
|
|
1631
1712
|
this.#watches.delete(slot);
|
|
1632
1713
|
slot.browser.emitter.off("disconnect", watch.disconnect);
|
|
1633
|
-
slot.context.emitter.off("page", watch.page);
|
|
1634
|
-
for (const [page, crash] of watch.crashes) page.emitter.off("crash", crash);
|
|
1635
1714
|
watch.subscription[Symbol.dispose]();
|
|
1636
1715
|
}
|
|
1716
|
+
async #release(holder, lease) {
|
|
1717
|
+
const slot = lease.token.value;
|
|
1718
|
+
const removed = await this.#clean(slot, lease.generation);
|
|
1719
|
+
const disposal = lease.generation.context.disposal;
|
|
1720
|
+
if (!removed) this.#retentions.set(holder, /* @__PURE__ */ new Error("The context downloads directory remains owned"));
|
|
1721
|
+
if (disposal?.confirmed !== true && !this.#losses.has(slot) && this.#closing === void 0) {
|
|
1722
|
+
if (![...this.#leases.values()].some((held) => held.token.value === slot && held !== lease) && (this.#building.get(slot)?.size ?? 0) === 0) lease.token.release();
|
|
1723
|
+
this.#lose(slot, disposal?.error ?? /* @__PURE__ */ new Error("The context disposal is unconfirmed"));
|
|
1724
|
+
await this.#endings.get(slot)?.promise;
|
|
1725
|
+
} else if (removed) lease.token.release();
|
|
1726
|
+
}
|
|
1727
|
+
#clean(slot, generation) {
|
|
1728
|
+
let cleaning = this.#cleaning.get(generation);
|
|
1729
|
+
if (cleaning !== void 0) return cleaning;
|
|
1730
|
+
cleaning = Promise.resolve().then(async () => {
|
|
1731
|
+
await generation.toolset.destroy().catch(this.#fault.bind(this));
|
|
1732
|
+
await generation.context.close().catch((error) => {
|
|
1733
|
+
if (!this.#losses.has(slot) && slot.browser.status === "connected") this.#fault(error);
|
|
1734
|
+
});
|
|
1735
|
+
try {
|
|
1736
|
+
await rm(generation.directory, {
|
|
1737
|
+
recursive: true,
|
|
1738
|
+
force: true
|
|
1739
|
+
});
|
|
1740
|
+
this.#owned.get(slot)?.delete(generation);
|
|
1741
|
+
return true;
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
this.#fault(error);
|
|
1744
|
+
return false;
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
this.#cleaning.set(generation, cleaning);
|
|
1748
|
+
return cleaning;
|
|
1749
|
+
}
|
|
1750
|
+
#build(slot, token) {
|
|
1751
|
+
const construction = this.#construct(slot, token);
|
|
1752
|
+
const building = this.#building.get(slot) ?? /* @__PURE__ */ new Set();
|
|
1753
|
+
building.add(construction);
|
|
1754
|
+
this.#building.set(slot, building);
|
|
1755
|
+
construction.finally(() => building.delete(construction)).catch(() => void 0);
|
|
1756
|
+
return construction;
|
|
1757
|
+
}
|
|
1758
|
+
async #construct(slot, token) {
|
|
1759
|
+
const directory = join(slot.profile, "contexts", randomUUID(), "downloads");
|
|
1760
|
+
let context;
|
|
1761
|
+
let toolset;
|
|
1762
|
+
try {
|
|
1763
|
+
await mkdir(directory, { recursive: true });
|
|
1764
|
+
let page;
|
|
1765
|
+
try {
|
|
1766
|
+
context = await slot.browser.isolate({
|
|
1767
|
+
reference: this.#reference,
|
|
1768
|
+
downloads: { path: directory },
|
|
1769
|
+
...this.#viewport === void 0 ? {} : { emulation: { viewport: this.#viewport } }
|
|
1770
|
+
});
|
|
1771
|
+
page = await context.create();
|
|
1772
|
+
} catch (error) {
|
|
1773
|
+
if (token !== void 0) {
|
|
1774
|
+
if (![...this.#leases.values()].some((lease) => lease.token.value === slot) && (this.#building.get(slot)?.size ?? 0) === 1) token.release();
|
|
1775
|
+
this.#lose(slot, error);
|
|
1776
|
+
}
|
|
1777
|
+
throw error;
|
|
1778
|
+
}
|
|
1779
|
+
toolset = createBrowserToolset(page, {
|
|
1780
|
+
context,
|
|
1781
|
+
journeys: {
|
|
1782
|
+
store: createFileBrowserJourneyStore({ root: this.#root }),
|
|
1783
|
+
runs: createFileBrowserRunStore({ root: this.#root }),
|
|
1784
|
+
readonly: this.#readonly
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
await toolset.start();
|
|
1788
|
+
const generation = {
|
|
1789
|
+
context,
|
|
1790
|
+
toolset,
|
|
1791
|
+
directory
|
|
1792
|
+
};
|
|
1793
|
+
const owned = this.#owned.get(slot) ?? /* @__PURE__ */ new Set();
|
|
1794
|
+
owned.add(generation);
|
|
1795
|
+
this.#owned.set(slot, owned);
|
|
1796
|
+
return generation;
|
|
1797
|
+
} catch (error) {
|
|
1798
|
+
await toolset?.destroy().catch(this.#fault.bind(this));
|
|
1799
|
+
await context?.close().catch((failure) => {
|
|
1800
|
+
if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !isCDPConnectionError(failure)) this.#fault(failure);
|
|
1801
|
+
});
|
|
1802
|
+
await rm(directory, {
|
|
1803
|
+
recursive: true,
|
|
1804
|
+
force: true
|
|
1805
|
+
}).catch(this.#fault.bind(this));
|
|
1806
|
+
throw error;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
async #destroyRecord(slot) {
|
|
1810
|
+
if (isCDPTimeoutError(this.#losses.get(slot)?.cause) && slot.browser.pid !== void 0) try {
|
|
1811
|
+
process.kill(slot.browser.pid, "SIGKILL");
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
if (!isError(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
|
|
1814
|
+
}
|
|
1815
|
+
try {
|
|
1816
|
+
await Promise.allSettled(this.#building.get(slot) ?? []);
|
|
1817
|
+
const prepared = this.#prepared.get(slot);
|
|
1818
|
+
if (prepared !== void 0) this.#untrack(prepared);
|
|
1819
|
+
this.#prepared.delete(slot);
|
|
1820
|
+
await Promise.all([...this.#owned.get(slot) ?? []].map((generation) => this.#clean(slot, generation)));
|
|
1821
|
+
await this.#destroySlot(slot.profile, slot.browser);
|
|
1822
|
+
this.#owned.delete(slot);
|
|
1823
|
+
this.#building.delete(slot);
|
|
1824
|
+
this.#endings.get(slot)?.resolve();
|
|
1825
|
+
} catch (error) {
|
|
1826
|
+
this.#endings.get(slot)?.reject(error);
|
|
1827
|
+
throw error;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1637
1830
|
#fault(error) {
|
|
1638
1831
|
this.#faults.add(error);
|
|
1639
1832
|
}
|
|
@@ -1642,7 +1835,7 @@ var BrowserMCPServer = class {
|
|
|
1642
1835
|
if (this.#stranded !== void 0) throw new BrowserError(isError(this.#stranded) ? this.#stranded.message : String(this.#stranded), BROWSER_SERVER_TEARDOWN, isBrowserError(this.#stranded) ? this.#stranded.context : void 0);
|
|
1643
1836
|
const profile = join(this.#root, ".profiles", formatBrowserLockEntry(process.pid, randomUUID()));
|
|
1644
1837
|
let browser;
|
|
1645
|
-
let
|
|
1838
|
+
let slot;
|
|
1646
1839
|
try {
|
|
1647
1840
|
await mkdir(profile);
|
|
1648
1841
|
this.#folders.add(profile);
|
|
@@ -1664,42 +1857,26 @@ var BrowserMCPServer = class {
|
|
|
1664
1857
|
}));
|
|
1665
1858
|
await rename(`${record}.tmp`, record);
|
|
1666
1859
|
}
|
|
1667
|
-
|
|
1668
|
-
reference: this.#reference,
|
|
1669
|
-
downloads: { path: join(profile, "downloads") },
|
|
1670
|
-
...this.#viewport === void 0 ? {} : { emulation: { viewport: this.#viewport } }
|
|
1671
|
-
});
|
|
1672
|
-
const page = await context.create();
|
|
1673
|
-
toolset = createBrowserToolset(page, {
|
|
1674
|
-
context,
|
|
1675
|
-
journeys: {
|
|
1676
|
-
store: createFileBrowserJourneyStore({ root: this.#root }),
|
|
1677
|
-
runs: createFileBrowserRunStore({ root: this.#root }),
|
|
1678
|
-
readonly: this.#readonly
|
|
1679
|
-
}
|
|
1680
|
-
});
|
|
1681
|
-
await toolset.start();
|
|
1682
|
-
return {
|
|
1860
|
+
slot = {
|
|
1683
1861
|
browser,
|
|
1684
|
-
profile
|
|
1685
|
-
context,
|
|
1686
|
-
toolset
|
|
1862
|
+
profile
|
|
1687
1863
|
};
|
|
1864
|
+
const ending = Promise.withResolvers();
|
|
1865
|
+
ending.promise.catch(() => void 0);
|
|
1866
|
+
this.#endings.set(slot, ending);
|
|
1867
|
+
const generation = await this.#build(slot);
|
|
1868
|
+
this.#prepared.set(slot, generation);
|
|
1869
|
+
this.#observe(slot, generation);
|
|
1870
|
+
return slot;
|
|
1688
1871
|
} catch (error) {
|
|
1689
|
-
if (this.#folders.has(profile)) await this.#destroySlot(profile, browser
|
|
1872
|
+
if (this.#folders.has(profile)) await (slot === void 0 ? this.#destroySlot(profile, browser) : this.#destroyRecord(slot)).catch((failure) => {
|
|
1690
1873
|
this.#stranded = failure;
|
|
1691
1874
|
});
|
|
1692
1875
|
if (this.#closing === void 0) this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_LAUNCH, error)}`);
|
|
1693
1876
|
throw error;
|
|
1694
1877
|
}
|
|
1695
1878
|
}
|
|
1696
|
-
async #destroySlot(profile, browser
|
|
1697
|
-
if (isCDPTimeoutError(cause) && browser?.pid !== void 0) try {
|
|
1698
|
-
process.kill(browser.pid, "SIGKILL");
|
|
1699
|
-
} catch (error) {
|
|
1700
|
-
if (!isError(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
|
|
1701
|
-
}
|
|
1702
|
-
await toolset?.destroy().catch((error) => this.#faults.add(error));
|
|
1879
|
+
async #destroySlot(profile, browser) {
|
|
1703
1880
|
try {
|
|
1704
1881
|
await browser?.destroy();
|
|
1705
1882
|
} catch (error) {
|
|
@@ -1775,27 +1952,27 @@ var BrowserMCPServer = class {
|
|
|
1775
1952
|
#issue() {
|
|
1776
1953
|
return `e${++this.#references}`;
|
|
1777
1954
|
}
|
|
1778
|
-
#mirror(
|
|
1779
|
-
if (this.#leases.get(this.#shared) !==
|
|
1955
|
+
#mirror(lease, tool) {
|
|
1956
|
+
if (this.#leases.get(this.#shared) !== lease || this.#vocabulary.has(tool.name)) return;
|
|
1780
1957
|
this.#tools.add(createTool({
|
|
1781
1958
|
...toolToDefinition(tool),
|
|
1782
1959
|
execute: this.#forward.bind(this, this.#shared, tool.name)
|
|
1783
1960
|
}));
|
|
1784
1961
|
}
|
|
1785
|
-
#withdraw(
|
|
1786
|
-
if (this.#leases.get(this.#shared) !==
|
|
1787
|
-
const current =
|
|
1962
|
+
#withdraw(lease, tool) {
|
|
1963
|
+
if (this.#leases.get(this.#shared) !== lease || this.#vocabulary.has(tool.name)) return;
|
|
1964
|
+
const current = lease.generation.toolset.tools.tool(tool.name);
|
|
1788
1965
|
if (current === void 0) this.#tools.remove(tool.name);
|
|
1789
|
-
else this.#mirror(
|
|
1966
|
+
else this.#mirror(lease, current);
|
|
1790
1967
|
}
|
|
1791
|
-
#clear(
|
|
1792
|
-
for (const tool of tools) this.#withdraw(
|
|
1968
|
+
#clear(lease, tools) {
|
|
1969
|
+
for (const tool of tools) this.#withdraw(lease, tool);
|
|
1793
1970
|
}
|
|
1794
1971
|
#unmirror() {
|
|
1795
1972
|
const mirrored = this.#mirrored;
|
|
1796
1973
|
if (mirrored === void 0) return;
|
|
1797
1974
|
this.#mirrored = void 0;
|
|
1798
|
-
const tools = mirrored.
|
|
1975
|
+
const tools = mirrored.lease.generation.toolset.tools;
|
|
1799
1976
|
for (const tool of tools.tools()) if (!this.#vocabulary.has(tool.name)) this.#tools.remove(tool.name);
|
|
1800
1977
|
tools.emitter.off("add", mirrored.added);
|
|
1801
1978
|
tools.emitter.off("remove", mirrored.removed);
|
|
@@ -2226,11 +2403,14 @@ var FileBrowserJourneyStore = class {
|
|
|
2226
2403
|
/**
|
|
2227
2404
|
* Persists runs and captures only in directories allocated by this instance.
|
|
2228
2405
|
* @remarks Requires an existing root. Reopened stores can read and delete saved runs.
|
|
2406
|
+
* Allocations of one journey share a process-local queue across instances; the filesystem lock
|
|
2407
|
+
* still refuses conflicting mutations and allocations from another process.
|
|
2229
2408
|
* @example
|
|
2230
2409
|
* const store = new FileBrowserRunStore({ root: directory })
|
|
2231
2410
|
* const slot = await store.open('check-ready')
|
|
2232
2411
|
*/
|
|
2233
|
-
var FileBrowserRunStore = class {
|
|
2412
|
+
var FileBrowserRunStore = class FileBrowserRunStore {
|
|
2413
|
+
static #allocations = /* @__PURE__ */ new Map();
|
|
2234
2414
|
#files;
|
|
2235
2415
|
#slots = /* @__PURE__ */ new WeakMap();
|
|
2236
2416
|
#directories = /* @__PURE__ */ new Set();
|
|
@@ -2258,16 +2438,27 @@ var FileBrowserRunStore = class {
|
|
|
2258
2438
|
async open(name, options) {
|
|
2259
2439
|
options?.signal?.throwIfAborted();
|
|
2260
2440
|
this.#files.validateName(name);
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
this.#
|
|
2269
|
-
|
|
2270
|
-
|
|
2441
|
+
const path = this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY);
|
|
2442
|
+
const previous = FileBrowserRunStore.#allocations.get(path);
|
|
2443
|
+
const allocation = Promise.withResolvers();
|
|
2444
|
+
FileBrowserRunStore.#allocations.set(path, allocation.promise);
|
|
2445
|
+
try {
|
|
2446
|
+
await previous;
|
|
2447
|
+
options?.signal?.throwIfAborted();
|
|
2448
|
+
return await this.#files.lock(path, async () => {
|
|
2449
|
+
const directory = await this.#files.allocate(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), generateBrowserRunId, options);
|
|
2450
|
+
const slot = Object.freeze({
|
|
2451
|
+
id: directory.id,
|
|
2452
|
+
directory: directory.path
|
|
2453
|
+
});
|
|
2454
|
+
this.#directories.add(directory.path);
|
|
2455
|
+
this.#slots.set(slot, directory.path);
|
|
2456
|
+
return slot;
|
|
2457
|
+
}, options);
|
|
2458
|
+
} finally {
|
|
2459
|
+
allocation.resolve();
|
|
2460
|
+
if (FileBrowserRunStore.#allocations.get(path) === allocation.promise) FileBrowserRunStore.#allocations.delete(path);
|
|
2461
|
+
}
|
|
2271
2462
|
}
|
|
2272
2463
|
async get(name, id, options) {
|
|
2273
2464
|
options?.signal?.throwIfAborted();
|
|
@@ -2477,6 +2668,7 @@ var Browser = class {
|
|
|
2477
2668
|
#servingPid;
|
|
2478
2669
|
#profile;
|
|
2479
2670
|
#contexts = [];
|
|
2671
|
+
#isolating = /* @__PURE__ */ new Set();
|
|
2480
2672
|
#destroyed = false;
|
|
2481
2673
|
#connecting = new BrowserTransition();
|
|
2482
2674
|
#disconnecting = new BrowserTransition();
|
|
@@ -2563,33 +2755,13 @@ var Browser = class {
|
|
|
2563
2755
|
return [...this.#contexts];
|
|
2564
2756
|
}
|
|
2565
2757
|
async isolate(options) {
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
|
|
2569
|
-
validateBrowserContextOptions(options);
|
|
2570
|
-
const params = { disposeOnDetach: false };
|
|
2571
|
-
if (options?.proxy !== void 0) {
|
|
2572
|
-
params["proxyServer"] = options.proxy.server;
|
|
2573
|
-
if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
|
|
2574
|
-
}
|
|
2575
|
-
if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
|
|
2576
|
-
const result = await client.send("Target.createBrowserContext", params);
|
|
2577
|
-
if (!isRecord(result) || !isString(result["browserContextId"])) throw new BrowserConnectionError("Failed to create isolated browser context");
|
|
2578
|
-
const id = result["browserContextId"];
|
|
2579
|
-
const context = new BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createBrowserWriter(), options?.emulation, options?.downloads, options);
|
|
2758
|
+
const attempt = this.#isolate(options);
|
|
2759
|
+
this.#isolating.add(attempt);
|
|
2580
2760
|
try {
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
downloadPath: options.downloads.path,
|
|
2585
|
-
eventsEnabled: true
|
|
2586
|
-
});
|
|
2587
|
-
} catch (error) {
|
|
2588
|
-
await context.close().catch(() => void 0);
|
|
2589
|
-
throw error;
|
|
2761
|
+
return await attempt;
|
|
2762
|
+
} finally {
|
|
2763
|
+
this.#isolating.delete(attempt);
|
|
2590
2764
|
}
|
|
2591
|
-
this.#registerContext(context);
|
|
2592
|
-
return context;
|
|
2593
2765
|
}
|
|
2594
2766
|
async create(options) {
|
|
2595
2767
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
@@ -2624,6 +2796,37 @@ var Browser = class {
|
|
|
2624
2796
|
this.#shutdown = shutdown;
|
|
2625
2797
|
return shutdown;
|
|
2626
2798
|
}
|
|
2799
|
+
async #isolate(options) {
|
|
2800
|
+
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
2801
|
+
const client = this.#client;
|
|
2802
|
+
if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
|
|
2803
|
+
validateBrowserContextOptions(options);
|
|
2804
|
+
const params = { disposeOnDetach: false };
|
|
2805
|
+
if (options?.proxy !== void 0) {
|
|
2806
|
+
params["proxyServer"] = options.proxy.server;
|
|
2807
|
+
if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
|
|
2808
|
+
}
|
|
2809
|
+
if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
|
|
2810
|
+
const result = await client.send("Target.createBrowserContext", params);
|
|
2811
|
+
if (!isRecord(result) || !isString(result["browserContextId"])) throw new BrowserConnectionError("Failed to create isolated browser context");
|
|
2812
|
+
const id = result["browserContextId"];
|
|
2813
|
+
const context = new BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createBrowserWriter(), options?.emulation, options?.downloads, options);
|
|
2814
|
+
try {
|
|
2815
|
+
if (this.#destroyed || this.#client !== client) throw new BrowserDestroyedError();
|
|
2816
|
+
if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
|
|
2817
|
+
behavior: options.downloads.named === true ? "allowAndName" : "allow",
|
|
2818
|
+
browserContextId: id,
|
|
2819
|
+
downloadPath: options.downloads.path,
|
|
2820
|
+
eventsEnabled: true
|
|
2821
|
+
});
|
|
2822
|
+
if (this.#destroyed || this.#client !== client) throw new BrowserDestroyedError();
|
|
2823
|
+
} catch (error) {
|
|
2824
|
+
await context.close().catch(() => void 0);
|
|
2825
|
+
throw error;
|
|
2826
|
+
}
|
|
2827
|
+
this.#registerContext(context);
|
|
2828
|
+
return context;
|
|
2829
|
+
}
|
|
2627
2830
|
async #establish() {
|
|
2628
2831
|
const disconnecting = this.#disconnecting.pending;
|
|
2629
2832
|
if (disconnecting !== void 0) await disconnecting;
|
|
@@ -3097,6 +3300,7 @@ var Browser = class {
|
|
|
3097
3300
|
}
|
|
3098
3301
|
}
|
|
3099
3302
|
async #settle() {
|
|
3303
|
+
await Promise.allSettled(this.#isolating);
|
|
3100
3304
|
await this.#connecting.pending?.catch(() => void 0);
|
|
3101
3305
|
await this.#disconnecting.pending?.catch(() => void 0);
|
|
3102
3306
|
await this.#settleExit();
|
|
@@ -3285,6 +3489,6 @@ var Browser = class {
|
|
|
3285
3489
|
}
|
|
3286
3490
|
};
|
|
3287
3491
|
//#endregion
|
|
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 };
|
|
3492
|
+
export { BROWSER_CDP_LIST_PATH, BROWSER_CDP_PROTOCOL, BROWSER_CDP_VERSION_PATH, BROWSER_DEFAULT_CDP_PORT, BROWSER_DEFAULT_HOST, BROWSER_DEVTOOLS_PATTERN, BROWSER_DRAIN_INTERVAL_MS, BROWSER_ENGINE_HINTS, BROWSER_ENV_PATH_KEYS, BROWSER_EXECUTABLE_NAMES, BROWSER_EXECUTABLE_PATHS, BROWSER_FILE_STORE_LIMIT, BROWSER_HEADLESS_ARG, BROWSER_JOURNEY_LOCK_ATTEMPTS, BROWSER_JOURNEY_LOCK_DIRECTORY, BROWSER_JOURNEY_REVISION_FILE, BROWSER_JOURNEY_SNAPSHOT_FILE, BROWSER_KILL_GRACE_MS, BROWSER_LAUNCH_ARGS, BROWSER_PORT_PROBE_TIMEOUT_MS, BROWSER_PROCESS_EXIT_CAUSE, BROWSER_PROFILE_PREFIX, BROWSER_RUN_DIRECTORY, BROWSER_RUN_FILE, BROWSER_SERVER_BUSY, BROWSER_SERVER_CONTEXTS, BROWSER_SERVER_CONTEXTS_LIMIT, BROWSER_SERVER_COPY, BROWSER_SERVER_CRASH, BROWSER_SERVER_EXHAUSTED, BROWSER_SERVER_HOLDER, BROWSER_SERVER_LAUNCH, BROWSER_SERVER_OPTIONS, BROWSER_SERVER_POOL_LIMIT, BROWSER_SERVER_POOL_SIZE, BROWSER_SERVER_RECORD, BROWSER_SERVER_RESTARTS, BROWSER_SERVER_SWEEP, BROWSER_SERVER_TEARDOWN, BROWSER_SERVER_UNAVAILABLE, BROWSER_SERVER_UNRESOLVED, BROWSER_STORE_CACHE_DIRS, BROWSER_STORE_DEFAULT_DIRS, BROWSER_STORE_ENV_KEY, BROWSER_STORE_GLOBS, BROWSER_STORE_LINK_NAME, BROWSER_TRANSPORT_LOSS_CAUSE, BROWSER_TRANSPORT_LOSS_DEFER_MS, BROWSER_WINDOWS_ROOT_FALLBACKS, BROWSER_WINDOWS_SUFFIXES, Browser, 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 };
|
|
3289
3493
|
|
|
3290
3494
|
//# sourceMappingURL=index.js.map
|