@orkestrel/browser 0.0.23 → 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 +17 -3
- package/dist/bin/main.js.map +1 -1
- package/dist/src/core/index.cjs +99 -25
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +30 -13
- package/dist/src/core/index.d.ts +30 -13
- package/dist/src/core/index.js +99 -25
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +701 -187
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +107 -13
- package/dist/src/server/index.d.ts +107 -13
- package/dist/src/server/index.js +698 -190
- package/dist/src/server/index.js.map +1 -1
- package/package.json +6 -6
|
@@ -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. */
|
|
@@ -237,6 +251,84 @@ var BROWSER_SERVER_OPTIONS = "BROWSER_SERVER_OPTIONS";
|
|
|
237
251
|
var BROWSER_SERVER_CRASH = "BROWSER_SERVER_CRASH";
|
|
238
252
|
/** Names an interrupted call whose outcome is unknown. */
|
|
239
253
|
var BROWSER_SERVER_UNRESOLVED = "BROWSER_SERVER_UNRESOLVED";
|
|
254
|
+
/** Names the refusal when every browser is admitted to a holder. */
|
|
255
|
+
var BROWSER_SERVER_BUSY = "BROWSER_SERVER_BUSY";
|
|
256
|
+
/** Names the refusal for an unknown or ended holder. */
|
|
257
|
+
var BROWSER_SERVER_HOLDER = "BROWSER_SERVER_HOLDER";
|
|
258
|
+
/** Defines the holder tools independently of the shared browser's vocabulary. */
|
|
259
|
+
var BROWSER_SERVER_COPY = Object.freeze({
|
|
260
|
+
acquire: Object.freeze({
|
|
261
|
+
name: "acquire",
|
|
262
|
+
description: "Acquires an independent browser and returns its holder id and tool catalog. Destroy the holder when its work ends.",
|
|
263
|
+
parameters: Object.freeze({
|
|
264
|
+
type: "object",
|
|
265
|
+
properties: Object.freeze({ purpose: Object.freeze({
|
|
266
|
+
type: "string",
|
|
267
|
+
description: "The work this browser will serve."
|
|
268
|
+
}) }),
|
|
269
|
+
required: Object.freeze(["purpose"])
|
|
270
|
+
})
|
|
271
|
+
}),
|
|
272
|
+
execute: Object.freeze({
|
|
273
|
+
name: "execute",
|
|
274
|
+
description: "Runs a tool on the selected holder, with that tool’s arguments inside arguments. Get its available tools with tools.",
|
|
275
|
+
parameters: Object.freeze({
|
|
276
|
+
type: "object",
|
|
277
|
+
properties: Object.freeze({
|
|
278
|
+
holder: Object.freeze({
|
|
279
|
+
type: "string",
|
|
280
|
+
description: "The holder id returned by acquire."
|
|
281
|
+
}),
|
|
282
|
+
name: Object.freeze({
|
|
283
|
+
type: "string",
|
|
284
|
+
description: "The tool name from this holder’s catalog."
|
|
285
|
+
}),
|
|
286
|
+
arguments: Object.freeze({
|
|
287
|
+
type: "object",
|
|
288
|
+
description: "The arguments for the selected tool."
|
|
289
|
+
})
|
|
290
|
+
}),
|
|
291
|
+
required: Object.freeze([
|
|
292
|
+
"holder",
|
|
293
|
+
"name",
|
|
294
|
+
"arguments"
|
|
295
|
+
])
|
|
296
|
+
}),
|
|
297
|
+
annotations: Object.freeze({
|
|
298
|
+
pure: false,
|
|
299
|
+
untrusted: true
|
|
300
|
+
})
|
|
301
|
+
}),
|
|
302
|
+
tools: Object.freeze({
|
|
303
|
+
name: "tools",
|
|
304
|
+
description: "Lists the selected holder’s tools, including tools supplied by its page.",
|
|
305
|
+
parameters: Object.freeze({
|
|
306
|
+
type: "object",
|
|
307
|
+
properties: Object.freeze({ holder: Object.freeze({
|
|
308
|
+
type: "string",
|
|
309
|
+
description: "The holder id returned by acquire."
|
|
310
|
+
}) }),
|
|
311
|
+
required: Object.freeze(["holder"])
|
|
312
|
+
}),
|
|
313
|
+
annotations: Object.freeze({
|
|
314
|
+
pure: true,
|
|
315
|
+
untrusted: true
|
|
316
|
+
})
|
|
317
|
+
}),
|
|
318
|
+
destroy: Object.freeze({
|
|
319
|
+
name: "destroy",
|
|
320
|
+
description: "Ends the selected holder and destroys its browser. Calls still running on that holder report an unresolved outcome.",
|
|
321
|
+
parameters: Object.freeze({
|
|
322
|
+
type: "object",
|
|
323
|
+
properties: Object.freeze({ holder: Object.freeze({
|
|
324
|
+
type: "string",
|
|
325
|
+
description: "The holder id returned by acquire."
|
|
326
|
+
}) }),
|
|
327
|
+
required: Object.freeze(["holder"])
|
|
328
|
+
}),
|
|
329
|
+
annotations: Object.freeze({ pure: false })
|
|
330
|
+
})
|
|
331
|
+
});
|
|
240
332
|
//#endregion
|
|
241
333
|
//#region src/server/errors.ts
|
|
242
334
|
/**
|
|
@@ -280,6 +372,25 @@ function isBrowserDestroyedError(value) {
|
|
|
280
372
|
//#endregion
|
|
281
373
|
//#region src/server/helpers.ts
|
|
282
374
|
/**
|
|
375
|
+
* Parses positive integer viewport dimensions in WIDTHxHEIGHT form.
|
|
376
|
+
* @param value - The viewport environment variable, or undefined when unset
|
|
377
|
+
* @returns The viewport, or undefined when absent or malformed
|
|
378
|
+
* @example
|
|
379
|
+
* parseBrowserViewport('1280x720') // { width: 1280, height: 720 }
|
|
380
|
+
*/
|
|
381
|
+
function parseBrowserViewport(value) {
|
|
382
|
+
if (value === void 0) return void 0;
|
|
383
|
+
const match = /^([0-9]+)x([0-9]+)$/u.exec(value);
|
|
384
|
+
if (match === null || match[0] !== value) return void 0;
|
|
385
|
+
const width = (0, _orkestrel_contract.parseInteger)(match[1]);
|
|
386
|
+
const height = (0, _orkestrel_contract.parseInteger)(match[2]);
|
|
387
|
+
if (width === void 0 || height === void 0 || !Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0) return void 0;
|
|
388
|
+
return {
|
|
389
|
+
width,
|
|
390
|
+
height
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
283
394
|
* Probes whether a process has not been confirmed absent.
|
|
284
395
|
* @param pid - Process identifier to probe with signal zero
|
|
285
396
|
* @returns True if the host has not confirmed absence with ESRCH; false otherwise
|
|
@@ -945,7 +1056,7 @@ var FileBrowserWriter = class {
|
|
|
945
1056
|
};
|
|
946
1057
|
//#endregion
|
|
947
1058
|
//#region package.json
|
|
948
|
-
var version = "0.0.
|
|
1059
|
+
var version = "0.0.25";
|
|
949
1060
|
//#endregion
|
|
950
1061
|
//#region src/server/BrowserMCPServer.ts
|
|
951
1062
|
/**
|
|
@@ -960,21 +1071,28 @@ var version = "0.0.23";
|
|
|
960
1071
|
* absent. After the launch, a tool the toolset's manager adds under another name, such as a page
|
|
961
1072
|
* tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
|
|
962
1073
|
* removed when the toolset withdraws the tool, so the server's tool list changes and a subscribed
|
|
963
|
-
* client is notified.
|
|
964
|
-
*
|
|
1074
|
+
* client is notified. The server also advertises `acquire`, `execute`, `tools`, and `destroy` with
|
|
1075
|
+
* `BROWSER_SERVER_COPY`. Startup warms the pool and assigns a prepared context to the shared holder. The legacy
|
|
1076
|
+
* handshake and browser calls await that setup. Named holders each own a lease, a pending grant,
|
|
1077
|
+
* and loss notices; concurrent calls on a holder share its replacement grant. Ending a holder
|
|
1078
|
+
* closes its context and releases its lease, and admission counts that holder until disposal settles.
|
|
965
1079
|
* A launch creates `ROOT/.profiles/PID-UUID/` exclusively, connects a browser that never attaches to an
|
|
966
1080
|
* existing endpoint, opens one page in an isolated context, and constructs the page toolset with
|
|
967
1081
|
* that context and the file journey and run stores under the root, on a manager the toolset owns.
|
|
968
1082
|
* A dispatcher forwards its arguments and the call's context, signal included, to the toolset's
|
|
969
1083
|
* manager and returns that manager's value, or rejects with its failure message; a text value
|
|
970
1084
|
* reaches the client as one text block rather than as its JSON text.
|
|
971
|
-
* 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;
|
|
972
1087
|
* loss makes its outcome unresolved, while a known success remains successful. Pending losses
|
|
973
1088
|
* prefix the next outcome on a successor, or a refusal, and calls are never repeated.
|
|
1089
|
+
* Concurrent checks share one ping per slot. Cancelling a call ends only its wait; the ping keeps
|
|
1090
|
+
* its deadline and records loss for the next call.
|
|
974
1091
|
*
|
|
975
1092
|
* `start()` serves stdio and destroys the server at the end of its input, on `SIGINT`, and on
|
|
976
1093
|
* `SIGTERM`. `destroy()` stops reading requests and removes every dispatcher, then destroys every
|
|
977
|
-
* 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
|
|
978
1096
|
* profile — and rechecks the profile folders this server made or kept.
|
|
979
1097
|
*
|
|
980
1098
|
* Chromium on Linux refuses to start as the root user unless its sandbox is disabled, so a server
|
|
@@ -994,6 +1112,7 @@ var BrowserMCPServer = class {
|
|
|
994
1112
|
#root;
|
|
995
1113
|
#headless;
|
|
996
1114
|
#executable;
|
|
1115
|
+
#viewport;
|
|
997
1116
|
#readonly;
|
|
998
1117
|
#launcher;
|
|
999
1118
|
#input;
|
|
@@ -1002,22 +1121,40 @@ var BrowserMCPServer = class {
|
|
|
1002
1121
|
#abort = new AbortController();
|
|
1003
1122
|
#signal;
|
|
1004
1123
|
#vocabulary;
|
|
1005
|
-
#
|
|
1006
|
-
#removed;
|
|
1007
|
-
#cleared;
|
|
1124
|
+
#mirrored;
|
|
1008
1125
|
#pool;
|
|
1009
1126
|
#log;
|
|
1010
1127
|
#folders = /* @__PURE__ */ new Set();
|
|
1011
1128
|
#faults = /* @__PURE__ */ new Set();
|
|
1012
1129
|
#losses = /* @__PURE__ */ new WeakMap();
|
|
1130
|
+
#departures = /* @__PURE__ */ new WeakMap();
|
|
1131
|
+
#pings = /* @__PURE__ */ new WeakMap();
|
|
1013
1132
|
#watches = /* @__PURE__ */ new Map();
|
|
1014
|
-
#
|
|
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();
|
|
1140
|
+
#shared = {
|
|
1141
|
+
id: "shared",
|
|
1142
|
+
purpose: "Shared browser",
|
|
1143
|
+
abort: this.#abort
|
|
1144
|
+
};
|
|
1145
|
+
#holders = /* @__PURE__ */ new Map([[this.#shared.id, this.#shared]]);
|
|
1146
|
+
#leases = /* @__PURE__ */ new Map();
|
|
1147
|
+
#grants = /* @__PURE__ */ new Map();
|
|
1148
|
+
#notices = /* @__PURE__ */ new Map();
|
|
1149
|
+
#disposals = /* @__PURE__ */ new Map();
|
|
1150
|
+
#drains = /* @__PURE__ */ new Map();
|
|
1151
|
+
#readers = /* @__PURE__ */ new Map();
|
|
1152
|
+
#writers = /* @__PURE__ */ new Set();
|
|
1153
|
+
#admission;
|
|
1015
1154
|
#reference;
|
|
1016
1155
|
#references = 0;
|
|
1017
1156
|
#failure;
|
|
1018
1157
|
#stranded;
|
|
1019
|
-
#lease;
|
|
1020
|
-
#granting;
|
|
1021
1158
|
#starting;
|
|
1022
1159
|
#sweeping;
|
|
1023
1160
|
#closing;
|
|
@@ -1028,30 +1165,51 @@ var BrowserMCPServer = class {
|
|
|
1028
1165
|
*/
|
|
1029
1166
|
constructor(options) {
|
|
1030
1167
|
const size = options?.pool?.size ?? 1;
|
|
1168
|
+
const contexts = options?.pool?.contexts ?? 2;
|
|
1031
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);
|
|
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;
|
|
1032
1172
|
this.#log = options?.log ?? process.stderr;
|
|
1033
1173
|
this.#reference = this.#issue.bind(this);
|
|
1034
1174
|
this.#pool = (0, _orkestrel_pool.createPool)({
|
|
1035
1175
|
create: this.#warm.bind(this),
|
|
1036
|
-
destroy:
|
|
1176
|
+
destroy: this.#destroyRecord.bind(this),
|
|
1037
1177
|
validate: this.#validate.bind(this),
|
|
1038
1178
|
watch: this.#watch.bind(this),
|
|
1039
1179
|
error: this.#fault.bind(this),
|
|
1040
1180
|
min: size,
|
|
1181
|
+
capacity: contexts,
|
|
1041
1182
|
restarts: 1
|
|
1042
1183
|
});
|
|
1043
1184
|
this.#root = (0, node_path.resolve)(options?.root ?? "tmp/browsers");
|
|
1044
1185
|
this.#headless = options?.headless ?? true;
|
|
1045
1186
|
this.#executable = options?.executable;
|
|
1187
|
+
this.#viewport = options?.viewport;
|
|
1046
1188
|
this.#readonly = options?.readonly ?? false;
|
|
1047
1189
|
this.#launcher = options?.launch ?? createBrowser;
|
|
1048
1190
|
this.#input = options?.stdio?.input ?? process.stdin;
|
|
1049
1191
|
this.#tools = (0, _orkestrel_tool.createToolManager)();
|
|
1050
1192
|
const names = [..._src_core.BROWSER_TOOL_NAMES, ..._src_core.BROWSER_JOURNEY_TOOL_NAMES];
|
|
1051
|
-
this.#vocabulary = new Set(names);
|
|
1193
|
+
this.#vocabulary = /* @__PURE__ */ new Set([...names, ...Object.keys(BROWSER_SERVER_COPY)]);
|
|
1052
1194
|
for (const name of names) this.#tools.add((0, _orkestrel_tool.createTool)({
|
|
1053
1195
|
..._src_core.BROWSER_TOOL_COPY[name],
|
|
1054
|
-
execute: this.#forward.bind(this, name)
|
|
1196
|
+
execute: this.#forward.bind(this, this.#shared, name)
|
|
1197
|
+
}));
|
|
1198
|
+
this.#tools.add((0, _orkestrel_tool.createTool)({
|
|
1199
|
+
...BROWSER_SERVER_COPY.acquire,
|
|
1200
|
+
execute: this.#acquire.bind(this)
|
|
1201
|
+
}));
|
|
1202
|
+
this.#tools.add((0, _orkestrel_tool.createTool)({
|
|
1203
|
+
...BROWSER_SERVER_COPY.execute,
|
|
1204
|
+
execute: this.#dispatch.bind(this)
|
|
1205
|
+
}));
|
|
1206
|
+
this.#tools.add((0, _orkestrel_tool.createTool)({
|
|
1207
|
+
...BROWSER_SERVER_COPY.tools,
|
|
1208
|
+
execute: this.#catalog.bind(this)
|
|
1209
|
+
}));
|
|
1210
|
+
this.#tools.add((0, _orkestrel_tool.createTool)({
|
|
1211
|
+
...BROWSER_SERVER_COPY.destroy,
|
|
1212
|
+
execute: this.#dispose.bind(this)
|
|
1055
1213
|
}));
|
|
1056
1214
|
const server = (0, _orkestrel_mcp.createMCPServer)({
|
|
1057
1215
|
identity: {
|
|
@@ -1067,9 +1225,6 @@ var BrowserMCPServer = class {
|
|
|
1067
1225
|
...options?.stdio?.output === void 0 ? {} : { output: options.stdio.output }
|
|
1068
1226
|
});
|
|
1069
1227
|
this.#signal = this.#end.bind(this);
|
|
1070
|
-
this.#added = this.#mirror.bind(this);
|
|
1071
|
-
this.#removed = this.#withdraw.bind(this);
|
|
1072
|
-
this.#cleared = this.#clear.bind(this);
|
|
1073
1228
|
}
|
|
1074
1229
|
async start() {
|
|
1075
1230
|
if (this.#closing !== void 0) throw this.#ended();
|
|
@@ -1090,7 +1245,7 @@ var BrowserMCPServer = class {
|
|
|
1090
1245
|
if (this.#closing !== void 0) return;
|
|
1091
1246
|
this.#sweeping = this.#sweep();
|
|
1092
1247
|
const warming = this.#pool.start().then(void 0, (error) => error);
|
|
1093
|
-
await this.#grant(this.#abort.signal);
|
|
1248
|
+
await this.#grant(this.#shared, this.#abort.signal);
|
|
1094
1249
|
if (this.#closing !== void 0) return;
|
|
1095
1250
|
warming.then(this.#exhaust.bind(this));
|
|
1096
1251
|
} catch (error) {
|
|
@@ -1106,13 +1261,18 @@ var BrowserMCPServer = class {
|
|
|
1106
1261
|
this.#transport.stop();
|
|
1107
1262
|
for (const tool of this.#tools.tools()) this.#tools.remove(tool.name);
|
|
1108
1263
|
this.#unmirror();
|
|
1109
|
-
this.#
|
|
1264
|
+
for (const generation of this.#observed.keys()) this.#untrack(generation);
|
|
1265
|
+
this.#leases.clear();
|
|
1266
|
+
for (const holder of this.#holders.values()) holder.abort.abort();
|
|
1110
1267
|
this.#abort.abort();
|
|
1111
1268
|
const barrier = this.#pool.destroy().catch((error) => {
|
|
1112
1269
|
for (const failure of (0, _orkestrel_pool.isPoolError)(error) ? error.context?.failures ?? [error] : [error]) this.#faults.add(failure);
|
|
1113
1270
|
});
|
|
1114
1271
|
await this.#starting?.catch(() => void 0);
|
|
1115
1272
|
await barrier;
|
|
1273
|
+
await Promise.allSettled(this.#grants.values());
|
|
1274
|
+
await Promise.allSettled(this.#disposals.values());
|
|
1275
|
+
this.#holders.clear();
|
|
1116
1276
|
await this.#sweeping;
|
|
1117
1277
|
if (this.#stranded !== void 0) this.#faults.add(this.#stranded);
|
|
1118
1278
|
for (const folder of this.#folders) {
|
|
@@ -1157,22 +1317,143 @@ var BrowserMCPServer = class {
|
|
|
1157
1317
|
process.exitCode = 1;
|
|
1158
1318
|
this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_TEARDOWN, error)}`);
|
|
1159
1319
|
}
|
|
1160
|
-
async #
|
|
1161
|
-
|
|
1320
|
+
async #acquire(args, context) {
|
|
1321
|
+
context.signal.throwIfAborted();
|
|
1322
|
+
if (this.#closing !== void 0) throw this.#ended();
|
|
1323
|
+
const purpose = args["purpose"];
|
|
1324
|
+
if (!(0, _orkestrel_contract.isString)(purpose) || purpose.trim().length === 0) throw new _src_core.BrowserError("purpose must describe the work", "BROWSER_TOOLSET_ARGUMENT");
|
|
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);
|
|
1326
|
+
const holder = {
|
|
1327
|
+
id: (0, node_crypto.randomUUID)(),
|
|
1328
|
+
purpose,
|
|
1329
|
+
abort: new AbortController()
|
|
1330
|
+
};
|
|
1331
|
+
this.#holders.set(holder.id, holder);
|
|
1332
|
+
const listener = (0, node_events.addAbortListener)(context.signal, () => holder.abort.abort(context.signal.reason));
|
|
1333
|
+
try {
|
|
1334
|
+
const lease = await this.#serve(holder, context.signal);
|
|
1335
|
+
context.signal.throwIfAborted();
|
|
1336
|
+
if (holder.abort.signal.aborted || this.#holders.get(holder.id) !== holder) throw this.#ended();
|
|
1337
|
+
if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
|
|
1338
|
+
const catalog = {
|
|
1339
|
+
holder: holder.id,
|
|
1340
|
+
tools: lease.generation.toolset.tools.definitions()
|
|
1341
|
+
};
|
|
1342
|
+
return JSON.stringify(catalog);
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
await this.#retire(holder).catch(this.#fault.bind(this));
|
|
1345
|
+
throw error;
|
|
1346
|
+
} finally {
|
|
1347
|
+
listener[Symbol.dispose]();
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
#holder(value) {
|
|
1351
|
+
const holder = (0, _orkestrel_contract.isString)(value) ? this.#holders.get(value) : void 0;
|
|
1352
|
+
if (holder === void 0 || holder === this.#shared || holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_HOLDER}: unknown or ended holder`, BROWSER_SERVER_HOLDER);
|
|
1353
|
+
return holder;
|
|
1354
|
+
}
|
|
1355
|
+
async #dispatch(args, context) {
|
|
1356
|
+
const holder = this.#holder(args["holder"]);
|
|
1357
|
+
const name = args["name"];
|
|
1358
|
+
const parameters = args["arguments"];
|
|
1359
|
+
if (!(0, _orkestrel_contract.isString)(name) || !(0, _orkestrel_contract.isRecord)(parameters)) throw new _src_core.BrowserError("execute requires name and arguments", "BROWSER_TOOLSET_ARGUMENT");
|
|
1360
|
+
return this.#forward(holder, name, parameters, context);
|
|
1361
|
+
}
|
|
1362
|
+
async #catalog(args, context) {
|
|
1363
|
+
const holder = this.#holder(args["holder"]);
|
|
1364
|
+
const signal = AbortSignal.any([context.signal, holder.abort.signal]);
|
|
1365
|
+
try {
|
|
1366
|
+
const lease = await this.#serve(holder, signal);
|
|
1367
|
+
signal.throwIfAborted();
|
|
1368
|
+
if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
|
|
1369
|
+
const catalog = {
|
|
1370
|
+
holder: holder.id,
|
|
1371
|
+
tools: lease.generation.toolset.tools.definitions()
|
|
1372
|
+
};
|
|
1373
|
+
return this.#annotate(holder, JSON.stringify(catalog));
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
context.signal.throwIfAborted();
|
|
1376
|
+
if (holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
|
|
1377
|
+
if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`), error.code);
|
|
1378
|
+
throw error;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
async #dispose(args) {
|
|
1382
|
+
const id = args["holder"];
|
|
1383
|
+
await (((0, _orkestrel_contract.isString)(id) ? this.#disposals.get(id) : void 0) ?? this.#retire(this.#holder(id)));
|
|
1384
|
+
return "Holder destroyed.";
|
|
1385
|
+
}
|
|
1386
|
+
#retire(holder) {
|
|
1387
|
+
const pending = this.#disposals.get(holder.id);
|
|
1388
|
+
if (pending !== void 0) return pending;
|
|
1389
|
+
holder.abort.abort();
|
|
1390
|
+
const lease = this.#leases.get(holder);
|
|
1391
|
+
this.#leases.delete(holder);
|
|
1392
|
+
if (lease !== void 0) this.#untrack(lease.generation);
|
|
1393
|
+
const disposal = Promise.resolve().then(async () => {
|
|
1394
|
+
try {
|
|
1395
|
+
await this.#grants.get(holder)?.catch(() => void 0);
|
|
1396
|
+
if (lease !== void 0) await this.#release(holder, lease);
|
|
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));
|
|
1399
|
+
} catch (error) {
|
|
1400
|
+
this.#faults.add(error);
|
|
1401
|
+
} finally {
|
|
1402
|
+
if (this.#holders.get(holder.id) === holder && !this.#retentions.has(holder)) this.#holders.delete(holder.id);
|
|
1403
|
+
this.#notices.delete(holder);
|
|
1404
|
+
this.#drains.delete(holder);
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
this.#disposals.set(holder.id, disposal);
|
|
1408
|
+
return disposal;
|
|
1409
|
+
}
|
|
1410
|
+
async #forward(holder, name, args, context) {
|
|
1411
|
+
context.signal.throwIfAborted();
|
|
1412
|
+
const journey = args["journey"];
|
|
1413
|
+
if (name !== "replay" && name !== "forget" || !(0, _orkestrel_contract.isString)(journey)) return this.#perform(holder, name, args, context);
|
|
1414
|
+
if (this.#writers.has(journey) || name === "forget" && this.#readers.has(journey)) throw new _src_core.BrowserError(`BROWSER_JOURNEY_LOCKED: Journey ${journey} is locked; call ${name} again.`, "BROWSER_JOURNEY_LOCKED", { name: journey });
|
|
1415
|
+
const reader = Symbol(journey);
|
|
1416
|
+
if (name === "forget") this.#writers.add(journey);
|
|
1417
|
+
else {
|
|
1418
|
+
const readers = this.#readers.get(journey) ?? /* @__PURE__ */ new Set();
|
|
1419
|
+
readers.add(reader);
|
|
1420
|
+
this.#readers.set(journey, readers);
|
|
1421
|
+
}
|
|
1422
|
+
try {
|
|
1423
|
+
return await this.#perform(holder, name, args, context);
|
|
1424
|
+
} finally {
|
|
1425
|
+
if (name === "forget") this.#writers.delete(journey);
|
|
1426
|
+
else {
|
|
1427
|
+
const readers = this.#readers.get(journey);
|
|
1428
|
+
readers?.delete(reader);
|
|
1429
|
+
if (readers?.size === 0) this.#readers.delete(journey);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
async #perform(holder, name, args, context) {
|
|
1434
|
+
const signal = holder === this.#shared ? context.signal : AbortSignal.any([context.signal, holder.abort.signal]);
|
|
1435
|
+
let lease;
|
|
1162
1436
|
try {
|
|
1163
|
-
|
|
1437
|
+
lease = await this.#serve(holder, signal);
|
|
1164
1438
|
} catch (error) {
|
|
1165
1439
|
context.signal.throwIfAborted();
|
|
1166
|
-
if (
|
|
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);
|
|
1441
|
+
if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`), error.code);
|
|
1167
1442
|
throw error;
|
|
1168
1443
|
}
|
|
1169
1444
|
let result;
|
|
1170
1445
|
try {
|
|
1171
|
-
|
|
1446
|
+
const execution = lease.generation.toolset.tools.execute({
|
|
1172
1447
|
id: name,
|
|
1173
1448
|
name,
|
|
1174
1449
|
arguments: args
|
|
1175
|
-
},
|
|
1450
|
+
}, {
|
|
1451
|
+
...context,
|
|
1452
|
+
signal
|
|
1453
|
+
});
|
|
1454
|
+
const outcome = await (name === "replay" || name === "forget" ? execution : this.#race(execution, signal));
|
|
1455
|
+
if (outcome === void 0) throw this.#ended();
|
|
1456
|
+
result = outcome;
|
|
1176
1457
|
} catch (error) {
|
|
1177
1458
|
result = {
|
|
1178
1459
|
id: name,
|
|
@@ -1182,28 +1463,27 @@ var BrowserMCPServer = class {
|
|
|
1182
1463
|
};
|
|
1183
1464
|
}
|
|
1184
1465
|
context.signal.throwIfAborted();
|
|
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);
|
|
1185
1467
|
if (!result.success) {
|
|
1186
|
-
if (this.#
|
|
1187
|
-
await
|
|
1188
|
-
} catch
|
|
1189
|
-
if (!context.signal.aborted && !this.#abort.signal.aborted) this.#lose(slot, error);
|
|
1190
|
-
}
|
|
1468
|
+
if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
|
|
1469
|
+
await this.#race(this.#ping(lease.token.value), context.signal);
|
|
1470
|
+
} catch {}
|
|
1191
1471
|
context.signal.throwIfAborted();
|
|
1192
|
-
if (this.#
|
|
1193
|
-
const loss = this.#
|
|
1194
|
-
throw new _src_core.BrowserError(this.#annotate(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);
|
|
1195
1475
|
}
|
|
1196
|
-
throw new _src_core.BrowserError(this.#annotate(result.error,
|
|
1476
|
+
throw new _src_core.BrowserError(this.#annotate(holder, result.error, lease));
|
|
1197
1477
|
}
|
|
1198
|
-
return typeof result.value === "string" ? this.#annotate(result.value,
|
|
1478
|
+
return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
|
|
1199
1479
|
}
|
|
1200
|
-
#annotate(value,
|
|
1480
|
+
#annotate(holder, value, lease) {
|
|
1201
1481
|
const notes = [];
|
|
1202
|
-
for (const lost of this.#
|
|
1203
|
-
if (lost ===
|
|
1204
|
-
const loss = this.#
|
|
1482
|
+
for (const lost of this.#notices.get(holder) ?? []) {
|
|
1483
|
+
if (lost === lease) continue;
|
|
1484
|
+
const loss = this.#departures.get(lost);
|
|
1205
1485
|
notes.push(describeBrowserServerLoss(BROWSER_SERVER_CRASH, loss?.cause, loss?.url));
|
|
1206
|
-
this.#
|
|
1486
|
+
this.#notices.get(holder)?.delete(lost);
|
|
1207
1487
|
}
|
|
1208
1488
|
return [...notes, value].join("\n");
|
|
1209
1489
|
}
|
|
@@ -1227,52 +1507,95 @@ var BrowserMCPServer = class {
|
|
|
1227
1507
|
listener[Symbol.dispose]();
|
|
1228
1508
|
}
|
|
1229
1509
|
}
|
|
1230
|
-
async #grant(signal) {
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
if (
|
|
1235
|
-
|
|
1510
|
+
async #grant(holder, signal) {
|
|
1511
|
+
const held = this.#leases.get(holder);
|
|
1512
|
+
if (held !== void 0) return held;
|
|
1513
|
+
let granting = this.#grants.get(holder);
|
|
1514
|
+
if (granting === void 0) {
|
|
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
|
+
});
|
|
1521
|
+
this.#grants.set(holder, granting);
|
|
1522
|
+
granting.finally(() => {
|
|
1523
|
+
if (this.#grants.get(holder) === granting) this.#grants.delete(holder);
|
|
1524
|
+
}).catch(() => void 0);
|
|
1525
|
+
}
|
|
1526
|
+
const lease = await this.#race(granting, signal);
|
|
1527
|
+
if (lease === void 0) throw this.#ended();
|
|
1528
|
+
return lease;
|
|
1529
|
+
}
|
|
1530
|
+
async #hold(holder, token) {
|
|
1531
|
+
const slot = token.value;
|
|
1532
|
+
if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted) {
|
|
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();
|
|
1539
|
+
throw this.#ended();
|
|
1540
|
+
}
|
|
1541
|
+
if (this.#losses.has(slot)) {
|
|
1542
|
+
token.release();
|
|
1543
|
+
throw this.#unavailable(this.#losses.get(slot)?.cause);
|
|
1544
|
+
}
|
|
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);
|
|
1564
|
+
if (holder === this.#shared) {
|
|
1565
|
+
const tools = generation.toolset.tools;
|
|
1566
|
+
const mirrored = {
|
|
1567
|
+
lease,
|
|
1568
|
+
added: this.#mirror.bind(this, lease),
|
|
1569
|
+
removed: this.#withdraw.bind(this, lease),
|
|
1570
|
+
cleared: this.#clear.bind(this, lease)
|
|
1571
|
+
};
|
|
1572
|
+
this.#mirrored = mirrored;
|
|
1573
|
+
for (const tool of tools.tools()) this.#mirror(lease, tool);
|
|
1574
|
+
tools.emitter.on("add", mirrored.added);
|
|
1575
|
+
tools.emitter.on("remove", mirrored.removed);
|
|
1576
|
+
tools.emitter.on("clear", mirrored.cleared);
|
|
1577
|
+
}
|
|
1578
|
+
return lease;
|
|
1236
1579
|
}
|
|
1237
|
-
#
|
|
1238
|
-
this.#
|
|
1239
|
-
if (this.#closing !== void 0) throw this.#ended();
|
|
1240
|
-
if (this.#losses.has(token.value)) {
|
|
1241
|
-
token.destroy().catch(() => void 0);
|
|
1242
|
-
throw this.#unavailable(this.#losses.get(token.value)?.cause);
|
|
1243
|
-
}
|
|
1244
|
-
this.#lease = token;
|
|
1245
|
-
const tools = token.value.toolset.tools;
|
|
1246
|
-
for (const tool of tools.tools()) this.#mirror(tool);
|
|
1247
|
-
tools.emitter.on("add", this.#added);
|
|
1248
|
-
tools.emitter.on("remove", this.#removed);
|
|
1249
|
-
tools.emitter.on("clear", this.#cleared);
|
|
1250
|
-
return token;
|
|
1251
|
-
}
|
|
1252
|
-
#refuse(error) {
|
|
1253
|
-
this.#granting = void 0;
|
|
1254
|
-
throw error;
|
|
1255
|
-
}
|
|
1256
|
-
async #serve(signal) {
|
|
1257
|
-
if (this.#closing !== void 0) throw this.#ended();
|
|
1580
|
+
async #serve(holder, signal) {
|
|
1581
|
+
if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
|
|
1258
1582
|
await this.#race(this.#starting, signal);
|
|
1259
|
-
if (this.#closing !== void 0) throw this.#ended();
|
|
1260
|
-
const held = this.#
|
|
1583
|
+
if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
|
|
1584
|
+
const held = this.#leases.get(holder);
|
|
1261
1585
|
if (held !== void 0) try {
|
|
1262
|
-
await held.value
|
|
1263
|
-
if (this.#
|
|
1264
|
-
} catch
|
|
1586
|
+
await this.#race(this.#ping(held.token.value), signal);
|
|
1587
|
+
if (this.#leases.get(holder) === held) return held;
|
|
1588
|
+
} catch {
|
|
1265
1589
|
signal.throwIfAborted();
|
|
1266
|
-
if (
|
|
1267
|
-
this.#lose(held.value, error);
|
|
1590
|
+
if (holder.abort.signal.aborted) throw this.#ended();
|
|
1268
1591
|
}
|
|
1269
1592
|
try {
|
|
1270
|
-
const
|
|
1271
|
-
if (this.#
|
|
1272
|
-
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;
|
|
1273
1596
|
} catch (error) {
|
|
1274
1597
|
signal.throwIfAborted();
|
|
1275
|
-
if (
|
|
1598
|
+
if (holder.abort.signal.aborted || (0, _orkestrel_pool.isPoolError)(error) && error.code === "destroyed") throw this.#ended();
|
|
1276
1599
|
if ((0, _orkestrel_pool.isPoolError)(error) && (error.code === "create" || error.code === "cleanup")) {
|
|
1277
1600
|
this.#exhaust(error);
|
|
1278
1601
|
throw this.#unavailable(error.cause ?? this.#failure);
|
|
@@ -1280,6 +1603,15 @@ var BrowserMCPServer = class {
|
|
|
1280
1603
|
throw error;
|
|
1281
1604
|
}
|
|
1282
1605
|
}
|
|
1606
|
+
#ping(slot) {
|
|
1607
|
+
const pending = this.#pings.get(slot);
|
|
1608
|
+
if (pending !== void 0) return pending;
|
|
1609
|
+
const ping = slot.browser.ping({ signal: this.#abort.signal }).catch((error) => {
|
|
1610
|
+
if (!this.#abort.signal.aborted) this.#lose(slot, error);
|
|
1611
|
+
}).finally(() => this.#pings.delete(slot));
|
|
1612
|
+
this.#pings.set(slot, ping);
|
|
1613
|
+
return ping;
|
|
1614
|
+
}
|
|
1283
1615
|
async #validate(slot) {
|
|
1284
1616
|
try {
|
|
1285
1617
|
await slot.browser.ping({ signal: this.#abort.signal });
|
|
@@ -1291,71 +1623,211 @@ var BrowserMCPServer = class {
|
|
|
1291
1623
|
}
|
|
1292
1624
|
#lose(slot, cause) {
|
|
1293
1625
|
if (this.#closing !== void 0 || this.#losses.has(slot)) return;
|
|
1294
|
-
this.#losses.set(slot, {
|
|
1626
|
+
this.#losses.set(slot, { cause });
|
|
1627
|
+
this.#failure = cause;
|
|
1628
|
+
for (const [holder, lease] of this.#leases) {
|
|
1629
|
+
if (lease.token.value !== slot) continue;
|
|
1630
|
+
this.#detach(holder, lease, cause);
|
|
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, {
|
|
1295
1639
|
cause,
|
|
1296
|
-
url:
|
|
1640
|
+
url: lease.generation.toolset.view.url
|
|
1297
1641
|
});
|
|
1298
|
-
this.#
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
this.#
|
|
1302
|
-
|
|
1303
|
-
this.#
|
|
1304
|
-
token.destroy().catch(() => void 0);
|
|
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);
|
|
1305
1648
|
}
|
|
1306
1649
|
#watch(slot, signal) {
|
|
1307
1650
|
const loss = Promise.withResolvers();
|
|
1308
1651
|
const disconnect = this.#disconnect.bind(this, slot);
|
|
1309
|
-
const page = this.#track.bind(this, slot);
|
|
1310
1652
|
const subscription = (0, node_events.addAbortListener)(signal, this.#unwatch.bind(this, slot));
|
|
1311
1653
|
this.#watches.set(slot, {
|
|
1312
1654
|
resolve: loss.resolve,
|
|
1313
1655
|
disconnect,
|
|
1314
|
-
|
|
1315
|
-
subscription,
|
|
1316
|
-
crashes: /* @__PURE__ */ new Map()
|
|
1656
|
+
subscription
|
|
1317
1657
|
});
|
|
1318
1658
|
slot.browser.emitter.on("disconnect", disconnect);
|
|
1319
|
-
slot.context.emitter.on("page", page);
|
|
1320
|
-
for (const current of slot.context.pages()) this.#track(slot, current);
|
|
1321
1659
|
if (signal.aborted) this.#unwatch(slot);
|
|
1322
1660
|
else if (slot.browser.status !== "connected") this.#disconnect(slot);
|
|
1323
1661
|
return loss.promise;
|
|
1324
1662
|
}
|
|
1325
|
-
#
|
|
1326
|
-
|
|
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);
|
|
1327
1675
|
if (watch === void 0 || watch.crashes.has(page)) return;
|
|
1328
|
-
const crash = this.#
|
|
1676
|
+
const crash = this.#crash.bind(this, slot, generation, page);
|
|
1329
1677
|
page.emitter.on("crash", crash);
|
|
1330
|
-
this.#
|
|
1678
|
+
this.#observed.set(generation, {
|
|
1331
1679
|
...watch,
|
|
1332
1680
|
crashes: new Map([...watch.crashes, [page, crash]])
|
|
1333
1681
|
});
|
|
1334
1682
|
}
|
|
1335
|
-
#
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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);
|
|
1339
1706
|
}
|
|
1340
1707
|
#disconnect(slot) {
|
|
1341
|
-
this.#
|
|
1342
|
-
}
|
|
1343
|
-
#report(slot, cause) {
|
|
1344
|
-
const watch = this.#watches.get(slot);
|
|
1345
|
-
if (watch === void 0) return;
|
|
1346
|
-
this.#unwatch(slot);
|
|
1347
|
-
this.#lose(slot, cause);
|
|
1348
|
-
watch.resolve(cause);
|
|
1708
|
+
this.#lose(slot, /* @__PURE__ */ new Error(slot.browser.pid === void 0 ? "The browser process exited" : "The browser transport disconnected"));
|
|
1349
1709
|
}
|
|
1350
1710
|
#unwatch(slot) {
|
|
1351
1711
|
const watch = this.#watches.get(slot);
|
|
1352
1712
|
if (watch === void 0) return;
|
|
1353
1713
|
this.#watches.delete(slot);
|
|
1354
1714
|
slot.browser.emitter.off("disconnect", watch.disconnect);
|
|
1355
|
-
slot.context.emitter.off("page", watch.page);
|
|
1356
|
-
for (const [page, crash] of watch.crashes) page.emitter.off("crash", crash);
|
|
1357
1715
|
watch.subscription[Symbol.dispose]();
|
|
1358
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
|
+
}
|
|
1359
1831
|
#fault(error) {
|
|
1360
1832
|
this.#faults.add(error);
|
|
1361
1833
|
}
|
|
@@ -1364,7 +1836,7 @@ var BrowserMCPServer = class {
|
|
|
1364
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);
|
|
1365
1837
|
const profile = (0, node_path.join)(this.#root, ".profiles", formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)()));
|
|
1366
1838
|
let browser;
|
|
1367
|
-
let
|
|
1839
|
+
let slot;
|
|
1368
1840
|
try {
|
|
1369
1841
|
await (0, node_fs_promises.mkdir)(profile);
|
|
1370
1842
|
this.#folders.add(profile);
|
|
@@ -1374,6 +1846,7 @@ var BrowserMCPServer = class {
|
|
|
1374
1846
|
cdp: { discover: false },
|
|
1375
1847
|
signal: this.#abort.signal,
|
|
1376
1848
|
...this.#executable === void 0 ? {} : { executable: this.#executable },
|
|
1849
|
+
...this.#viewport === void 0 ? {} : { viewport: this.#viewport },
|
|
1377
1850
|
...process.platform === "linux" && process.getuid?.() === 0 ? { args: ["--no-sandbox"] } : {}
|
|
1378
1851
|
});
|
|
1379
1852
|
await browser.connect();
|
|
@@ -1385,38 +1858,26 @@ var BrowserMCPServer = class {
|
|
|
1385
1858
|
}));
|
|
1386
1859
|
await (0, node_fs_promises.rename)(`${record}.tmp`, record);
|
|
1387
1860
|
}
|
|
1388
|
-
|
|
1389
|
-
const page = await context.create();
|
|
1390
|
-
toolset = (0, _src_core.createBrowserToolset)(page, {
|
|
1391
|
-
context,
|
|
1392
|
-
journeys: {
|
|
1393
|
-
store: createFileBrowserJourneyStore({ root: this.#root }),
|
|
1394
|
-
runs: createFileBrowserRunStore({ root: this.#root }),
|
|
1395
|
-
readonly: this.#readonly
|
|
1396
|
-
}
|
|
1397
|
-
});
|
|
1398
|
-
await toolset.start();
|
|
1399
|
-
return {
|
|
1861
|
+
slot = {
|
|
1400
1862
|
browser,
|
|
1401
|
-
profile
|
|
1402
|
-
context,
|
|
1403
|
-
toolset
|
|
1863
|
+
profile
|
|
1404
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;
|
|
1405
1872
|
} catch (error) {
|
|
1406
|
-
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) => {
|
|
1407
1874
|
this.#stranded = failure;
|
|
1408
1875
|
});
|
|
1409
1876
|
if (this.#closing === void 0) this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_LAUNCH, error)}`);
|
|
1410
1877
|
throw error;
|
|
1411
1878
|
}
|
|
1412
1879
|
}
|
|
1413
|
-
async #destroySlot(profile, browser
|
|
1414
|
-
if ((0, _src_core.isCDPTimeoutError)(cause) && browser?.pid !== void 0) try {
|
|
1415
|
-
process.kill(browser.pid, "SIGKILL");
|
|
1416
|
-
} catch (error) {
|
|
1417
|
-
if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
|
|
1418
|
-
}
|
|
1419
|
-
await toolset?.destroy().catch((error) => this.#faults.add(error));
|
|
1880
|
+
async #destroySlot(profile, browser) {
|
|
1420
1881
|
try {
|
|
1421
1882
|
await browser?.destroy();
|
|
1422
1883
|
} catch (error) {
|
|
@@ -1492,29 +1953,31 @@ var BrowserMCPServer = class {
|
|
|
1492
1953
|
#issue() {
|
|
1493
1954
|
return `e${++this.#references}`;
|
|
1494
1955
|
}
|
|
1495
|
-
#mirror(tool) {
|
|
1496
|
-
if (this.#vocabulary.has(tool.name)) return;
|
|
1956
|
+
#mirror(lease, tool) {
|
|
1957
|
+
if (this.#leases.get(this.#shared) !== lease || this.#vocabulary.has(tool.name)) return;
|
|
1497
1958
|
this.#tools.add((0, _orkestrel_tool.createTool)({
|
|
1498
1959
|
...(0, _orkestrel_tool.toolToDefinition)(tool),
|
|
1499
|
-
execute: this.#forward.bind(this, tool.name)
|
|
1960
|
+
execute: this.#forward.bind(this, this.#shared, tool.name)
|
|
1500
1961
|
}));
|
|
1501
1962
|
}
|
|
1502
|
-
#withdraw(tool) {
|
|
1503
|
-
if (this.#vocabulary.has(tool.name)) return;
|
|
1504
|
-
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);
|
|
1505
1966
|
if (current === void 0) this.#tools.remove(tool.name);
|
|
1506
|
-
else this.#mirror(current);
|
|
1967
|
+
else this.#mirror(lease, current);
|
|
1507
1968
|
}
|
|
1508
|
-
#clear(tools) {
|
|
1509
|
-
for (const tool of tools) this.#withdraw(tool);
|
|
1969
|
+
#clear(lease, tools) {
|
|
1970
|
+
for (const tool of tools) this.#withdraw(lease, tool);
|
|
1510
1971
|
}
|
|
1511
1972
|
#unmirror() {
|
|
1512
|
-
const mirrored = this.#
|
|
1973
|
+
const mirrored = this.#mirrored;
|
|
1513
1974
|
if (mirrored === void 0) return;
|
|
1514
|
-
|
|
1515
|
-
mirrored.
|
|
1516
|
-
|
|
1517
|
-
|
|
1975
|
+
this.#mirrored = void 0;
|
|
1976
|
+
const tools = mirrored.lease.generation.toolset.tools;
|
|
1977
|
+
for (const tool of tools.tools()) if (!this.#vocabulary.has(tool.name)) this.#tools.remove(tool.name);
|
|
1978
|
+
tools.emitter.off("add", mirrored.added);
|
|
1979
|
+
tools.emitter.off("remove", mirrored.removed);
|
|
1980
|
+
tools.emitter.off("clear", mirrored.cleared);
|
|
1518
1981
|
}
|
|
1519
1982
|
#ended() {
|
|
1520
1983
|
return new _src_core.BrowserError("the browser session ended", "BROWSER_TOOLSET_ENDED");
|
|
@@ -1941,30 +2404,62 @@ var FileBrowserJourneyStore = class {
|
|
|
1941
2404
|
/**
|
|
1942
2405
|
* Persists runs and captures only in directories allocated by this instance.
|
|
1943
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.
|
|
1944
2409
|
* @example
|
|
1945
2410
|
* const store = new FileBrowserRunStore({ root: directory })
|
|
1946
2411
|
* const slot = await store.open('check-ready')
|
|
1947
2412
|
*/
|
|
1948
|
-
var FileBrowserRunStore = class {
|
|
2413
|
+
var FileBrowserRunStore = class FileBrowserRunStore {
|
|
2414
|
+
static #allocations = /* @__PURE__ */ new Map();
|
|
1949
2415
|
#files;
|
|
1950
2416
|
#slots = /* @__PURE__ */ new WeakMap();
|
|
1951
2417
|
#directories = /* @__PURE__ */ new Set();
|
|
1952
2418
|
constructor(options) {
|
|
1953
2419
|
this.#files = new FileBrowserStore(options);
|
|
1954
2420
|
}
|
|
2421
|
+
async snapshot(bytes, options) {
|
|
2422
|
+
options?.signal?.throwIfAborted();
|
|
2423
|
+
const owned = new Uint8Array(bytes);
|
|
2424
|
+
const directory = await this.#files.allocate(this.#files.resolvePath(BROWSER_RUN_DIRECTORY), _src_core.generateBrowserRunId, options);
|
|
2425
|
+
const path = this.#files.resolvePath(directory.path, "view.png");
|
|
2426
|
+
try {
|
|
2427
|
+
await this.#files.check(path, options);
|
|
2428
|
+
options?.signal?.throwIfAborted();
|
|
2429
|
+
await (0, node_fs_promises.writeFile)(path, owned, {
|
|
2430
|
+
flag: "wx",
|
|
2431
|
+
signal: options?.signal
|
|
2432
|
+
});
|
|
2433
|
+
return path;
|
|
2434
|
+
} catch (error) {
|
|
2435
|
+
options?.signal?.throwIfAborted();
|
|
2436
|
+
throw this.#files.translateError(path, error);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
1955
2439
|
async open(name, options) {
|
|
1956
2440
|
options?.signal?.throwIfAborted();
|
|
1957
2441
|
this.#files.validateName(name);
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
this.#
|
|
1966
|
-
|
|
1967
|
-
|
|
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
|
+
}
|
|
1968
2463
|
}
|
|
1969
2464
|
async get(name, id, options) {
|
|
1970
2465
|
options?.signal?.throwIfAborted();
|
|
@@ -2174,6 +2669,7 @@ var Browser = class {
|
|
|
2174
2669
|
#servingPid;
|
|
2175
2670
|
#profile;
|
|
2176
2671
|
#contexts = [];
|
|
2672
|
+
#isolating = /* @__PURE__ */ new Set();
|
|
2177
2673
|
#destroyed = false;
|
|
2178
2674
|
#connecting = new _src_core.BrowserTransition();
|
|
2179
2675
|
#disconnecting = new _src_core.BrowserTransition();
|
|
@@ -2260,33 +2756,13 @@ var Browser = class {
|
|
|
2260
2756
|
return [...this.#contexts];
|
|
2261
2757
|
}
|
|
2262
2758
|
async isolate(options) {
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
|
|
2266
|
-
(0, _src_core.validateBrowserContextOptions)(options);
|
|
2267
|
-
const params = { disposeOnDetach: false };
|
|
2268
|
-
if (options?.proxy !== void 0) {
|
|
2269
|
-
params["proxyServer"] = options.proxy.server;
|
|
2270
|
-
if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
|
|
2271
|
-
}
|
|
2272
|
-
if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
|
|
2273
|
-
const result = await client.send("Target.createBrowserContext", params);
|
|
2274
|
-
if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new _src_core.BrowserConnectionError("Failed to create isolated browser context");
|
|
2275
|
-
const id = result["browserContextId"];
|
|
2276
|
-
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);
|
|
2277
2761
|
try {
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
downloadPath: options.downloads.path,
|
|
2282
|
-
eventsEnabled: true
|
|
2283
|
-
});
|
|
2284
|
-
} catch (error) {
|
|
2285
|
-
await context.close().catch(() => void 0);
|
|
2286
|
-
throw error;
|
|
2762
|
+
return await attempt;
|
|
2763
|
+
} finally {
|
|
2764
|
+
this.#isolating.delete(attempt);
|
|
2287
2765
|
}
|
|
2288
|
-
this.#registerContext(context);
|
|
2289
|
-
return context;
|
|
2290
2766
|
}
|
|
2291
2767
|
async create(options) {
|
|
2292
2768
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
@@ -2321,6 +2797,37 @@ var Browser = class {
|
|
|
2321
2797
|
this.#shutdown = shutdown;
|
|
2322
2798
|
return shutdown;
|
|
2323
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
|
+
}
|
|
2324
2831
|
async #establish() {
|
|
2325
2832
|
const disconnecting = this.#disconnecting.pending;
|
|
2326
2833
|
if (disconnecting !== void 0) await disconnecting;
|
|
@@ -2794,6 +3301,7 @@ var Browser = class {
|
|
|
2794
3301
|
}
|
|
2795
3302
|
}
|
|
2796
3303
|
async #settle() {
|
|
3304
|
+
await Promise.allSettled(this.#isolating);
|
|
2797
3305
|
await this.#connecting.pending?.catch(() => void 0);
|
|
2798
3306
|
await this.#disconnecting.pending?.catch(() => void 0);
|
|
2799
3307
|
await this.#settleExit();
|
|
@@ -3006,8 +3514,13 @@ exports.BROWSER_PROCESS_EXIT_CAUSE = BROWSER_PROCESS_EXIT_CAUSE;
|
|
|
3006
3514
|
exports.BROWSER_PROFILE_PREFIX = BROWSER_PROFILE_PREFIX;
|
|
3007
3515
|
exports.BROWSER_RUN_DIRECTORY = BROWSER_RUN_DIRECTORY;
|
|
3008
3516
|
exports.BROWSER_RUN_FILE = BROWSER_RUN_FILE;
|
|
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;
|
|
3520
|
+
exports.BROWSER_SERVER_COPY = BROWSER_SERVER_COPY;
|
|
3009
3521
|
exports.BROWSER_SERVER_CRASH = BROWSER_SERVER_CRASH;
|
|
3010
3522
|
exports.BROWSER_SERVER_EXHAUSTED = BROWSER_SERVER_EXHAUSTED;
|
|
3523
|
+
exports.BROWSER_SERVER_HOLDER = BROWSER_SERVER_HOLDER;
|
|
3011
3524
|
exports.BROWSER_SERVER_LAUNCH = BROWSER_SERVER_LAUNCH;
|
|
3012
3525
|
exports.BROWSER_SERVER_OPTIONS = BROWSER_SERVER_OPTIONS;
|
|
3013
3526
|
exports.BROWSER_SERVER_POOL_LIMIT = BROWSER_SERVER_POOL_LIMIT;
|
|
@@ -3061,6 +3574,7 @@ exports.normalizeExecutablePath = normalizeExecutablePath;
|
|
|
3061
3574
|
exports.parseBrowserEngine = parseBrowserEngine;
|
|
3062
3575
|
exports.parseBrowserLockEntry = parseBrowserLockEntry;
|
|
3063
3576
|
exports.parseBrowserProfileRecord = parseBrowserProfileRecord;
|
|
3577
|
+
exports.parseBrowserViewport = parseBrowserViewport;
|
|
3064
3578
|
exports.probePathNames = probePathNames;
|
|
3065
3579
|
exports.probeProcess = probeProcess;
|
|
3066
3580
|
exports.readBrowserEndpoint = readBrowserEndpoint;
|