@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.
- 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 +392 -169
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +60 -14
- package/dist/src/server/index.d.ts +60 -14
- package/dist/src/server/index.js +392 -171
- package/dist/src/server/index.js.map +1 -1
- package/package.json +6 -6
|
@@ -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
|
-
/**
|
|
42
|
-
|
|
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
|
-
|
|
694
|
-
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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 —
|
|
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
|
-
#
|
|
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
|
-
|
|
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:
|
|
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.#
|
|
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
|
|
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)
|
|
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:
|
|
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
|
|
1383
|
+
const lease = await this.#serve(holder, signal);
|
|
1338
1384
|
signal.throwIfAborted();
|
|
1339
|
-
if (this.#leases.get(holder)
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
1452
|
+
let lease;
|
|
1405
1453
|
try {
|
|
1406
|
-
|
|
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 =
|
|
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)
|
|
1438
|
-
await this.#race(this.#ping(
|
|
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)
|
|
1442
|
-
const loss = this.#
|
|
1443
|
-
throw new _src_core.BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url),
|
|
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,
|
|
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,
|
|
1495
|
+
return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
|
|
1448
1496
|
}
|
|
1449
|
-
#annotate(holder, value,
|
|
1497
|
+
#annotate(holder, value, lease) {
|
|
1450
1498
|
const notes = [];
|
|
1451
1499
|
for (const lost of this.#notices.get(holder) ?? []) {
|
|
1452
|
-
if (lost ===
|
|
1453
|
-
const loss = this.#
|
|
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 =
|
|
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
|
|
1491
|
-
if (
|
|
1492
|
-
return
|
|
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
|
-
|
|
1550
|
+
token.release();
|
|
1497
1551
|
throw this.#ended();
|
|
1498
1552
|
}
|
|
1499
|
-
if (this.#
|
|
1500
|
-
|
|
1501
|
-
|
|
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.#
|
|
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 =
|
|
1582
|
+
const tools = generation.toolset.tools;
|
|
1506
1583
|
const mirrored = {
|
|
1507
|
-
|
|
1508
|
-
added: this.#mirror.bind(this,
|
|
1509
|
-
removed: this.#withdraw.bind(this,
|
|
1510
|
-
cleared: this.#clear.bind(this,
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
1534
|
-
if (this.#leases.get(holder) !==
|
|
1535
|
-
return
|
|
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,
|
|
1572
|
-
if (token.value !== slot
|
|
1573
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
1605
|
-
|
|
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.#
|
|
1693
|
+
const crash = this.#crash.bind(this, slot, generation, page);
|
|
1608
1694
|
page.emitter.on("crash", crash);
|
|
1609
|
-
this.#
|
|
1695
|
+
this.#observed.set(generation, {
|
|
1610
1696
|
...watch,
|
|
1611
1697
|
crashes: new Map([...watch.crashes, [page, crash]])
|
|
1612
1698
|
});
|
|
1613
1699
|
}
|
|
1614
|
-
#
|
|
1615
|
-
|
|
1616
|
-
|
|
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.#
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
1780
|
-
if (this.#leases.get(this.#shared) !==
|
|
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(
|
|
1787
|
-
if (this.#leases.get(this.#shared) !==
|
|
1788
|
-
const current =
|
|
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(
|
|
1984
|
+
else this.#mirror(lease, current);
|
|
1791
1985
|
}
|
|
1792
|
-
#clear(
|
|
1793
|
-
for (const tool of tools) this.#withdraw(
|
|
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.
|
|
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
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
this.#
|
|
2270
|
-
|
|
2271
|
-
|
|
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
|
-
|
|
2568
|
-
|
|
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
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
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;
|