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