@anvia/browser 1.0.7 → 1.0.10
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/MIGRATION.md +68 -0
- package/README.md +202 -26
- package/dist/automation-worker.d.ts +2 -0
- package/dist/automation-worker.js +298 -0
- package/dist/automation-worker.js.map +1 -0
- package/dist/index.d.ts +71 -21
- package/dist/index.js +2006 -572
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
package/dist/index.js
CHANGED
|
@@ -1,82 +1,680 @@
|
|
|
1
|
-
// src/docker-browser.ts
|
|
1
|
+
// src/docker-browser-client.ts
|
|
2
2
|
import { fileURLToPath } from "url";
|
|
3
3
|
|
|
4
|
-
// src/
|
|
5
|
-
import {
|
|
6
|
-
|
|
4
|
+
// src/automation-client.ts
|
|
5
|
+
import { fork } from "child_process";
|
|
6
|
+
|
|
7
|
+
// src/automation-protocol.ts
|
|
8
|
+
function isAutomationResponse(value) {
|
|
9
|
+
if (!isRecord(value)) return false;
|
|
10
|
+
switch (value.kind) {
|
|
11
|
+
case "event":
|
|
12
|
+
return value.event === "disconnected";
|
|
13
|
+
case "cancelled":
|
|
14
|
+
return isRequestId(value.id);
|
|
15
|
+
case "response":
|
|
16
|
+
if (!isRequestId(value.id) || typeof value.ok !== "boolean") return false;
|
|
17
|
+
return value.ok ? Object.hasOwn(value, "value") : isSerializedError(value.error);
|
|
18
|
+
default:
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function isSerializedError(value) {
|
|
23
|
+
return isRecord(value) && typeof value.name === "string" && typeof value.message === "string" && (value.stack === void 0 || typeof value.stack === "string") && (value.code === void 0 || typeof value.code === "string");
|
|
24
|
+
}
|
|
25
|
+
function isRequestId(value) {
|
|
26
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
27
|
+
}
|
|
28
|
+
function isRecord(value) {
|
|
29
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
7
31
|
|
|
8
32
|
// src/errors.ts
|
|
33
|
+
var defaults = {
|
|
34
|
+
action_timeout: { retryable: true, recovery: "retry" },
|
|
35
|
+
agent_action_busy: { retryable: true, recovery: "retry" },
|
|
36
|
+
cancelled: { retryable: true, recovery: "retry" },
|
|
37
|
+
connection_closed: { retryable: true, recovery: "reconnect" },
|
|
38
|
+
connection_timeout: { retryable: true, recovery: "reconnect" },
|
|
39
|
+
human_control_conflict: { retryable: true, recovery: "retry" },
|
|
40
|
+
human_controlled: { retryable: true, recovery: "retry" },
|
|
41
|
+
invalid_state: { retryable: false, recovery: "none" },
|
|
42
|
+
lifecycle_timeout: { retryable: true, recovery: "retry" },
|
|
43
|
+
navigation_blocked: { retryable: false, recovery: "none" },
|
|
44
|
+
not_ready: { retryable: true, recovery: "retry" },
|
|
45
|
+
readiness_timeout: { retryable: true, recovery: "retry" },
|
|
46
|
+
runtime_destroyed: { retryable: false, recovery: "recreate" },
|
|
47
|
+
startup_failed: { retryable: true, recovery: "recreate" },
|
|
48
|
+
tool_failed: { retryable: true, recovery: "retry" },
|
|
49
|
+
transport_failure: { retryable: true, recovery: "reconnect" }
|
|
50
|
+
};
|
|
9
51
|
var BrowserError = class extends Error {
|
|
10
|
-
constructor(message, code, options) {
|
|
52
|
+
constructor(message, code, options = {}) {
|
|
11
53
|
super(message, options);
|
|
12
54
|
this.code = code;
|
|
13
55
|
this.name = "BrowserError";
|
|
56
|
+
this.retryable = options.retryable ?? defaults[code].retryable;
|
|
57
|
+
this.recovery = options.recovery ?? defaults[code].recovery;
|
|
58
|
+
this.capability = options.capability;
|
|
59
|
+
this.phase = options.phase;
|
|
14
60
|
}
|
|
15
61
|
code;
|
|
62
|
+
retryable;
|
|
63
|
+
recovery;
|
|
64
|
+
capability;
|
|
65
|
+
phase;
|
|
66
|
+
};
|
|
67
|
+
function cancellationError(reason, phase) {
|
|
68
|
+
return new BrowserError("Browser operation was cancelled by the caller.", "cancelled", {
|
|
69
|
+
cause: reason,
|
|
70
|
+
phase
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/automation-client.ts
|
|
75
|
+
var cancellationCleanupTimeoutMs = 5e3;
|
|
76
|
+
var workerTerminationTimeoutMs = 3e3;
|
|
77
|
+
var maxWorkerStderrChars = 32768;
|
|
78
|
+
var AutomationWorkerClient = class _AutomationWorkerClient {
|
|
79
|
+
child;
|
|
80
|
+
pending = /* @__PURE__ */ new Map();
|
|
81
|
+
disconnectedListeners = /* @__PURE__ */ new Set();
|
|
82
|
+
nextId = 1;
|
|
83
|
+
isClosed = false;
|
|
84
|
+
disconnecting = false;
|
|
85
|
+
termination;
|
|
86
|
+
workerStderr = "";
|
|
87
|
+
onStderrData = (chunk) => {
|
|
88
|
+
this.workerStderr = `${this.workerStderr}${chunk}`.slice(-maxWorkerStderrChars);
|
|
89
|
+
};
|
|
90
|
+
onStderrError = () => {
|
|
91
|
+
};
|
|
92
|
+
onMessage = (message) => {
|
|
93
|
+
try {
|
|
94
|
+
this.handleMessage(message);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
this.handleExit(
|
|
97
|
+
new BrowserError(
|
|
98
|
+
"Browser automation worker sent an invalid response.",
|
|
99
|
+
"transport_failure",
|
|
100
|
+
{
|
|
101
|
+
cause: error,
|
|
102
|
+
phase: "automation-worker"
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
void this.terminate().catch(() => void 0);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
onError = (error) => {
|
|
110
|
+
this.handleExit(error);
|
|
111
|
+
void this.terminate().catch(() => void 0);
|
|
112
|
+
};
|
|
113
|
+
onExit = (code, signal) => {
|
|
114
|
+
const diagnostics = this.workerStderr.trim();
|
|
115
|
+
this.handleExit(
|
|
116
|
+
new Error(
|
|
117
|
+
`Browser automation worker exited: code=${code ?? "null"} signal=${signal ?? "null"}${diagnostics.length === 0 ? "" : `
|
|
118
|
+
${diagnostics}`}`
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
constructor(factory) {
|
|
123
|
+
this.child = factory();
|
|
124
|
+
this.child.stderr?.setEncoding("utf8");
|
|
125
|
+
this.child.stderr?.on("data", this.onStderrData);
|
|
126
|
+
this.child.stderr?.on("error", this.onStderrError);
|
|
127
|
+
this.child.on("message", this.onMessage);
|
|
128
|
+
this.child.on("error", this.onError);
|
|
129
|
+
this.child.once("exit", this.onExit);
|
|
130
|
+
}
|
|
131
|
+
static async connect(options) {
|
|
132
|
+
assertPositiveSafeInteger(options.timeoutMs, "timeoutMs");
|
|
133
|
+
if (options.abortSignal?.aborted) {
|
|
134
|
+
throw cancellationError(options.abortSignal.reason, "connect");
|
|
135
|
+
}
|
|
136
|
+
let client;
|
|
137
|
+
try {
|
|
138
|
+
client = new _AutomationWorkerClient(options.workerFactory ?? spawnAutomationWorker);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
throw new BrowserError(
|
|
141
|
+
"Unable to start the browser automation worker.",
|
|
142
|
+
"transport_failure",
|
|
143
|
+
{
|
|
144
|
+
cause: error,
|
|
145
|
+
phase: "connect",
|
|
146
|
+
recovery: "reconnect"
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const timeoutController = new AbortController();
|
|
151
|
+
const timer = setTimeout(
|
|
152
|
+
() => timeoutController.abort(
|
|
153
|
+
new BrowserError(
|
|
154
|
+
"Browser connection did not initialize before the timeout.",
|
|
155
|
+
"connection_timeout",
|
|
156
|
+
{ phase: "connect" }
|
|
157
|
+
)
|
|
158
|
+
),
|
|
159
|
+
options.timeoutMs
|
|
160
|
+
);
|
|
161
|
+
timer.unref?.();
|
|
162
|
+
const abortSignal = options.abortSignal === void 0 ? timeoutController.signal : AbortSignal.any([options.abortSignal, timeoutController.signal]);
|
|
163
|
+
try {
|
|
164
|
+
await client.command(
|
|
165
|
+
{
|
|
166
|
+
method: "connect",
|
|
167
|
+
params: { endpointUrl: options.endpointUrl, timeoutMs: options.timeoutMs }
|
|
168
|
+
},
|
|
169
|
+
{ abortSignal, phase: "connect" }
|
|
170
|
+
);
|
|
171
|
+
return client;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
await client.terminate();
|
|
174
|
+
if (options.abortSignal?.aborted) {
|
|
175
|
+
throw cancellationError(options.abortSignal.reason, "connect");
|
|
176
|
+
}
|
|
177
|
+
if (timeoutController.signal.aborted) throw timeoutController.signal.reason;
|
|
178
|
+
if (error instanceof Error && (error.name === "TimeoutError" || /timeout/i.test(error.message))) {
|
|
179
|
+
throw new BrowserError(
|
|
180
|
+
"Browser connection did not initialize before the timeout.",
|
|
181
|
+
"connection_timeout",
|
|
182
|
+
{ cause: error, phase: "connect" }
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
throw new BrowserError("Unable to initialize browser automation.", "transport_failure", {
|
|
186
|
+
cause: error,
|
|
187
|
+
phase: "connect"
|
|
188
|
+
});
|
|
189
|
+
} finally {
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
get closed() {
|
|
194
|
+
return this.isClosed || this.child.exitCode !== null || this.child.signalCode !== null;
|
|
195
|
+
}
|
|
196
|
+
command(command, options) {
|
|
197
|
+
if (this.closed) {
|
|
198
|
+
return Promise.reject(
|
|
199
|
+
new BrowserError("Browser automation connection is closed.", "connection_closed", {
|
|
200
|
+
phase: options.phase
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (options.abortSignal?.aborted) {
|
|
205
|
+
return Promise.reject(this.abortError(options.abortSignal, options.phase));
|
|
206
|
+
}
|
|
207
|
+
const id = this.nextId++;
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const pending = {
|
|
210
|
+
state: "active",
|
|
211
|
+
resolve: (value) => resolve(value),
|
|
212
|
+
reject
|
|
213
|
+
};
|
|
214
|
+
if (options.abortSignal !== void 0) {
|
|
215
|
+
pending.abortSignal = options.abortSignal;
|
|
216
|
+
pending.abort = () => this.cancelRequest(id, pending, options);
|
|
217
|
+
options.abortSignal.addEventListener("abort", pending.abort, { once: true });
|
|
218
|
+
}
|
|
219
|
+
this.pending.set(id, pending);
|
|
220
|
+
const request = { kind: "request", id, ...command };
|
|
221
|
+
try {
|
|
222
|
+
this.child.send(request, (error) => {
|
|
223
|
+
if (error == null) return;
|
|
224
|
+
this.rejectSend(options.phase, error);
|
|
225
|
+
});
|
|
226
|
+
} catch (error) {
|
|
227
|
+
this.rejectSend(options.phase, error);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
async disconnect(options = {}) {
|
|
232
|
+
if (this.termination !== void 0) return this.termination;
|
|
233
|
+
this.disconnecting = true;
|
|
234
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
235
|
+
const timeoutController = new AbortController();
|
|
236
|
+
const timer = setTimeout(
|
|
237
|
+
() => timeoutController.abort(
|
|
238
|
+
new BrowserError("Browser disconnect timed out.", "connection_timeout", {
|
|
239
|
+
phase: "disconnect"
|
|
240
|
+
})
|
|
241
|
+
),
|
|
242
|
+
timeoutMs
|
|
243
|
+
);
|
|
244
|
+
timer.unref?.();
|
|
245
|
+
const abortSignal = options.abortSignal === void 0 ? timeoutController.signal : AbortSignal.any([options.abortSignal, timeoutController.signal]);
|
|
246
|
+
try {
|
|
247
|
+
if (!this.closed) {
|
|
248
|
+
await this.command(
|
|
249
|
+
{ method: "disconnect", params: {} },
|
|
250
|
+
{ abortSignal, phase: "disconnect" }
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (options.abortSignal?.aborted) {
|
|
255
|
+
throw cancellationError(options.abortSignal.reason, "disconnect");
|
|
256
|
+
}
|
|
257
|
+
if (timeoutController.signal.aborted) throw timeoutController.signal.reason;
|
|
258
|
+
if (!this.closed) throw error;
|
|
259
|
+
} finally {
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
await this.terminate();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
onDisconnected(listener) {
|
|
265
|
+
if (this.closed) {
|
|
266
|
+
queueMicrotask(() => {
|
|
267
|
+
try {
|
|
268
|
+
listener();
|
|
269
|
+
} catch {
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
return () => void 0;
|
|
273
|
+
}
|
|
274
|
+
this.disconnectedListeners.add(listener);
|
|
275
|
+
return () => this.disconnectedListeners.delete(listener);
|
|
276
|
+
}
|
|
277
|
+
async terminate() {
|
|
278
|
+
if (this.termination !== void 0) return this.termination;
|
|
279
|
+
this.termination = this.performTerminate();
|
|
280
|
+
return this.termination;
|
|
281
|
+
}
|
|
282
|
+
async performTerminate() {
|
|
283
|
+
this.isClosed = true;
|
|
284
|
+
if (this.child.exitCode !== null || this.child.signalCode !== null) return;
|
|
285
|
+
const exited = new Promise((resolve) => this.child.once("exit", () => resolve()));
|
|
286
|
+
this.child.kill("SIGTERM");
|
|
287
|
+
let forceTimer;
|
|
288
|
+
await Promise.race([
|
|
289
|
+
exited,
|
|
290
|
+
new Promise((resolve) => {
|
|
291
|
+
forceTimer = setTimeout(() => {
|
|
292
|
+
this.child.kill("SIGKILL");
|
|
293
|
+
resolve();
|
|
294
|
+
}, workerTerminationTimeoutMs);
|
|
295
|
+
forceTimer.unref?.();
|
|
296
|
+
})
|
|
297
|
+
]);
|
|
298
|
+
if (forceTimer !== void 0) clearTimeout(forceTimer);
|
|
299
|
+
if (this.child.exitCode === null && this.child.signalCode === null) await exited;
|
|
300
|
+
}
|
|
301
|
+
cancelRequest(id, pending, options) {
|
|
302
|
+
if (pending.state !== "active") return;
|
|
303
|
+
pending.state = "cancelling";
|
|
304
|
+
pending.abortError = this.abortError(pending.abortSignal, options.phase);
|
|
305
|
+
if (options.cancelOperation === true && this.child.connected) {
|
|
306
|
+
try {
|
|
307
|
+
this.child.send({ kind: "cancel", id }, (error) => {
|
|
308
|
+
if (error !== null) void this.terminate().catch(() => void 0);
|
|
309
|
+
});
|
|
310
|
+
} catch {
|
|
311
|
+
void this.terminate().catch(() => void 0);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
pending.cleanupTimer = setTimeout(() => {
|
|
315
|
+
void this.terminate().catch(() => void 0);
|
|
316
|
+
}, cancellationCleanupTimeoutMs);
|
|
317
|
+
pending.cleanupTimer.unref?.();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
void this.terminate().catch(() => void 0);
|
|
321
|
+
}
|
|
322
|
+
abortError(abortSignal, phase) {
|
|
323
|
+
return abortSignal.reason instanceof BrowserError ? abortSignal.reason : cancellationError(abortSignal.reason, phase);
|
|
324
|
+
}
|
|
325
|
+
handleMessage(message) {
|
|
326
|
+
if (!isAutomationResponse(message)) {
|
|
327
|
+
throw new TypeError("Invalid browser automation worker response.");
|
|
328
|
+
}
|
|
329
|
+
if (message.kind === "event") {
|
|
330
|
+
if (!this.disconnecting) {
|
|
331
|
+
this.handleExit(
|
|
332
|
+
new BrowserError("Browser disconnected from CDP.", "connection_closed", {
|
|
333
|
+
phase: "automation-event"
|
|
334
|
+
})
|
|
335
|
+
);
|
|
336
|
+
void this.terminate().catch(() => void 0);
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const pending = this.pending.get(message.id);
|
|
341
|
+
if (pending === void 0) return;
|
|
342
|
+
if (message.kind === "cancelled") {
|
|
343
|
+
if (pending.state === "cancelling") {
|
|
344
|
+
this.settle(message.id, pending, () => pending.reject(pending.abortError));
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (pending.state === "cancelling") return;
|
|
349
|
+
this.settle(message.id, pending, () => {
|
|
350
|
+
if (message.ok) pending.resolve(message.value);
|
|
351
|
+
else pending.reject(deserializeError(message.error));
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
handleExit(cause) {
|
|
355
|
+
if (this.isClosed && this.pending.size === 0) {
|
|
356
|
+
this.cleanupChildListeners();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
this.isClosed = true;
|
|
360
|
+
const error = cause instanceof BrowserError ? cause : new BrowserError("Browser automation worker terminated.", "transport_failure", {
|
|
361
|
+
cause,
|
|
362
|
+
phase: "automation-worker"
|
|
363
|
+
});
|
|
364
|
+
for (const [id, pending] of this.pending) {
|
|
365
|
+
this.settle(
|
|
366
|
+
id,
|
|
367
|
+
pending,
|
|
368
|
+
() => pending.reject(pending.state === "cancelling" ? pending.abortError : error)
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
const listeners = [...this.disconnectedListeners];
|
|
372
|
+
this.disconnectedListeners.clear();
|
|
373
|
+
for (const listener of listeners) {
|
|
374
|
+
try {
|
|
375
|
+
listener();
|
|
376
|
+
} catch {
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
this.cleanupChildListeners();
|
|
380
|
+
}
|
|
381
|
+
cleanupChildListeners() {
|
|
382
|
+
this.child.off("message", this.onMessage);
|
|
383
|
+
this.child.off("exit", this.onExit);
|
|
384
|
+
this.child.stderr?.off("data", this.onStderrData);
|
|
385
|
+
}
|
|
386
|
+
rejectSend(phase, cause) {
|
|
387
|
+
this.handleExit(
|
|
388
|
+
new BrowserError("Unable to send browser automation command.", "transport_failure", {
|
|
389
|
+
cause,
|
|
390
|
+
phase
|
|
391
|
+
})
|
|
392
|
+
);
|
|
393
|
+
void this.terminate().catch(() => void 0);
|
|
394
|
+
}
|
|
395
|
+
settle(id, pending, callback) {
|
|
396
|
+
if (this.pending.get(id) !== pending) return;
|
|
397
|
+
this.pending.delete(id);
|
|
398
|
+
if (pending.cleanupTimer !== void 0) clearTimeout(pending.cleanupTimer);
|
|
399
|
+
if (pending.abort !== void 0) {
|
|
400
|
+
pending.abortSignal?.removeEventListener("abort", pending.abort);
|
|
401
|
+
}
|
|
402
|
+
callback();
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
function spawnAutomationWorker() {
|
|
406
|
+
return fork(automationWorkerUrl(), [], {
|
|
407
|
+
execArgv: [],
|
|
408
|
+
serialization: "advanced",
|
|
409
|
+
stdio: ["ignore", "ignore", "pipe", "ipc"]
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function automationWorkerUrl() {
|
|
413
|
+
return import.meta.url.endsWith("/src/automation-client.ts") ? new URL("../dist/automation-worker.js", import.meta.url) : new URL("./automation-worker.js", import.meta.url);
|
|
414
|
+
}
|
|
415
|
+
function deserializeError(value) {
|
|
416
|
+
const error = new Error(value.message);
|
|
417
|
+
error.name = value.name;
|
|
418
|
+
if (value.stack !== void 0) error.stack = value.stack;
|
|
419
|
+
if (value.code !== void 0) Object.assign(error, { code: value.code });
|
|
420
|
+
return error;
|
|
421
|
+
}
|
|
422
|
+
function assertPositiveSafeInteger(value, name) {
|
|
423
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > 2147483647) {
|
|
424
|
+
throw new RangeError(`${name} must be a positive integer no greater than 2147483647.`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/scheduler.ts
|
|
429
|
+
var ResourceScheduler = class {
|
|
430
|
+
resources = /* @__PURE__ */ new Map();
|
|
431
|
+
readyResources = [];
|
|
432
|
+
activeTasks = /* @__PURE__ */ new Set();
|
|
433
|
+
serial;
|
|
434
|
+
maxConcurrentResources;
|
|
435
|
+
maxQueuedActions;
|
|
436
|
+
activeResources = 0;
|
|
437
|
+
queuedActions = 0;
|
|
438
|
+
closedError;
|
|
439
|
+
constructor(options) {
|
|
440
|
+
this.serial = options.mode === "serial";
|
|
441
|
+
this.maxConcurrentResources = options.maxConcurrentResources;
|
|
442
|
+
this.maxQueuedActions = options.maxQueuedActions;
|
|
443
|
+
}
|
|
444
|
+
run(resourceKey, abortSignal, operation) {
|
|
445
|
+
if (this.closedError !== void 0) return Promise.reject(this.closedError);
|
|
446
|
+
if (abortSignal?.aborted) {
|
|
447
|
+
return Promise.reject(abortReason(abortSignal, "agent-action-queue"));
|
|
448
|
+
}
|
|
449
|
+
if (this.queuedActions >= this.maxQueuedActions) {
|
|
450
|
+
return Promise.reject(
|
|
451
|
+
new BrowserError("Browser action queue capacity was reached.", "agent_action_busy", {
|
|
452
|
+
phase: "agent-action-queue"
|
|
453
|
+
})
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
const key = this.serial ? "browser" : resourceKey;
|
|
457
|
+
let resource = this.resources.get(key);
|
|
458
|
+
if (resource === void 0) {
|
|
459
|
+
resource = { running: false, ready: false, tasks: [] };
|
|
460
|
+
this.resources.set(key, resource);
|
|
461
|
+
}
|
|
462
|
+
return new Promise((resolve, reject) => {
|
|
463
|
+
const task = {
|
|
464
|
+
controller: new AbortController(),
|
|
465
|
+
operation,
|
|
466
|
+
resolve,
|
|
467
|
+
reject,
|
|
468
|
+
...abortSignal === void 0 ? {} : { abortSignal }
|
|
469
|
+
};
|
|
470
|
+
if (abortSignal !== void 0) {
|
|
471
|
+
task.abort = () => {
|
|
472
|
+
const current = this.resources.get(key);
|
|
473
|
+
const index = current?.tasks.indexOf(task) ?? -1;
|
|
474
|
+
if (index === -1) return;
|
|
475
|
+
current?.tasks.splice(index, 1);
|
|
476
|
+
this.queuedActions -= 1;
|
|
477
|
+
abortSignal.removeEventListener("abort", task.abort);
|
|
478
|
+
reject(abortReason(abortSignal, "agent-action-queue"));
|
|
479
|
+
this.deleteIdleResource(key, current);
|
|
480
|
+
};
|
|
481
|
+
abortSignal.addEventListener("abort", task.abort, { once: true });
|
|
482
|
+
}
|
|
483
|
+
resource.tasks.push(task);
|
|
484
|
+
this.queuedActions += 1;
|
|
485
|
+
this.markReady(key, resource);
|
|
486
|
+
this.pump();
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
close(error = new BrowserError("Browser connection is closed.", "connection_closed")) {
|
|
490
|
+
if (this.closedError !== void 0) return;
|
|
491
|
+
this.closedError = error;
|
|
492
|
+
for (const task of this.activeTasks) task.controller.abort(error);
|
|
493
|
+
for (const [key, resource] of this.resources) {
|
|
494
|
+
for (const task of resource.tasks.splice(0)) {
|
|
495
|
+
this.queuedActions -= 1;
|
|
496
|
+
if (task.abort !== void 0) task.abortSignal?.removeEventListener("abort", task.abort);
|
|
497
|
+
task.reject(error);
|
|
498
|
+
}
|
|
499
|
+
resource.ready = false;
|
|
500
|
+
if (!resource.running) this.resources.delete(key);
|
|
501
|
+
}
|
|
502
|
+
this.readyResources.length = 0;
|
|
503
|
+
}
|
|
504
|
+
markReady(key, resource) {
|
|
505
|
+
if (resource.running || resource.ready || resource.tasks.length === 0) return;
|
|
506
|
+
resource.ready = true;
|
|
507
|
+
this.readyResources.push(key);
|
|
508
|
+
}
|
|
509
|
+
pump() {
|
|
510
|
+
while (this.closedError === void 0 && this.activeResources < this.maxConcurrentResources && this.readyResources.length > 0) {
|
|
511
|
+
const key = this.readyResources.shift();
|
|
512
|
+
if (key === void 0) return;
|
|
513
|
+
const resource = this.resources.get(key);
|
|
514
|
+
if (resource === void 0 || resource.running || resource.tasks.length === 0) continue;
|
|
515
|
+
resource.ready = false;
|
|
516
|
+
const task = resource.tasks.shift();
|
|
517
|
+
if (task === void 0) continue;
|
|
518
|
+
this.queuedActions -= 1;
|
|
519
|
+
if (task.abort !== void 0) task.abortSignal?.removeEventListener("abort", task.abort);
|
|
520
|
+
resource.running = true;
|
|
521
|
+
this.activeResources += 1;
|
|
522
|
+
this.activeTasks.add(task);
|
|
523
|
+
const effectiveSignal = task.abortSignal === void 0 ? task.controller.signal : AbortSignal.any([task.abortSignal, task.controller.signal]);
|
|
524
|
+
let operation;
|
|
525
|
+
try {
|
|
526
|
+
operation = task.operation(effectiveSignal);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
operation = Promise.reject(error);
|
|
529
|
+
}
|
|
530
|
+
void operation.then(task.resolve, task.reject).finally(() => {
|
|
531
|
+
this.activeTasks.delete(task);
|
|
532
|
+
resource.running = false;
|
|
533
|
+
this.activeResources -= 1;
|
|
534
|
+
this.markReady(key, resource);
|
|
535
|
+
this.deleteIdleResource(key, resource);
|
|
536
|
+
this.pump();
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
deleteIdleResource(key, resource) {
|
|
541
|
+
if (resource !== void 0 && !resource.running && resource.tasks.length === 0) {
|
|
542
|
+
this.resources.delete(key);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
var AsyncMutex = class {
|
|
547
|
+
waiters = [];
|
|
548
|
+
locked = false;
|
|
549
|
+
closedError;
|
|
550
|
+
async run(abortSignal, operation) {
|
|
551
|
+
const release = await this.acquire(abortSignal);
|
|
552
|
+
try {
|
|
553
|
+
return await operation();
|
|
554
|
+
} finally {
|
|
555
|
+
release();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
close(error = new BrowserError("Browser connection is closed.", "connection_closed")) {
|
|
559
|
+
if (this.closedError !== void 0) return;
|
|
560
|
+
this.closedError = error;
|
|
561
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
562
|
+
if (waiter.abort !== void 0) {
|
|
563
|
+
waiter.abortSignal?.removeEventListener("abort", waiter.abort);
|
|
564
|
+
}
|
|
565
|
+
waiter.reject(error);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
acquire(abortSignal) {
|
|
569
|
+
if (this.closedError !== void 0) return Promise.reject(this.closedError);
|
|
570
|
+
if (abortSignal?.aborted) {
|
|
571
|
+
return Promise.reject(abortReason(abortSignal, "browser-resource-lock"));
|
|
572
|
+
}
|
|
573
|
+
if (!this.locked) {
|
|
574
|
+
this.locked = true;
|
|
575
|
+
return Promise.resolve(this.releaseCallback());
|
|
576
|
+
}
|
|
577
|
+
return new Promise((resolve, reject) => {
|
|
578
|
+
const waiter = {
|
|
579
|
+
resolve,
|
|
580
|
+
reject,
|
|
581
|
+
...abortSignal === void 0 ? {} : { abortSignal }
|
|
582
|
+
};
|
|
583
|
+
if (abortSignal !== void 0) {
|
|
584
|
+
waiter.abort = () => {
|
|
585
|
+
const index = this.waiters.indexOf(waiter);
|
|
586
|
+
if (index === -1) return;
|
|
587
|
+
this.waiters.splice(index, 1);
|
|
588
|
+
reject(abortReason(abortSignal, "browser-resource-lock"));
|
|
589
|
+
};
|
|
590
|
+
abortSignal.addEventListener("abort", waiter.abort, { once: true });
|
|
591
|
+
}
|
|
592
|
+
this.waiters.push(waiter);
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
releaseCallback() {
|
|
596
|
+
let released = false;
|
|
597
|
+
return () => {
|
|
598
|
+
if (released) return;
|
|
599
|
+
released = true;
|
|
600
|
+
const waiter = this.waiters.shift();
|
|
601
|
+
if (waiter === void 0) {
|
|
602
|
+
this.locked = false;
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (waiter.abort !== void 0) {
|
|
606
|
+
waiter.abortSignal?.removeEventListener("abort", waiter.abort);
|
|
607
|
+
}
|
|
608
|
+
waiter.resolve(this.releaseCallback());
|
|
609
|
+
};
|
|
610
|
+
}
|
|
16
611
|
};
|
|
612
|
+
function abortReason(abortSignal, phase) {
|
|
613
|
+
return abortSignal.reason instanceof BrowserError ? abortSignal.reason : cancellationError(abortSignal.reason, phase);
|
|
614
|
+
}
|
|
17
615
|
|
|
18
616
|
// src/connection.ts
|
|
617
|
+
var defaultConnectionTimeoutMs = 3e4;
|
|
618
|
+
var defaultActionTimeoutMs = 1e4;
|
|
619
|
+
var defaultMaxConcurrentTabs = 8;
|
|
620
|
+
var defaultMaxQueuedActions = 1e3;
|
|
621
|
+
var maxClosedTabTombstones = 1e4;
|
|
622
|
+
var maxTimerMs = 2147483647;
|
|
19
623
|
async function connectPlaywrightBrowser(options) {
|
|
624
|
+
const timeoutMs = options.timeoutMs ?? defaultConnectionTimeoutMs;
|
|
625
|
+
assertPositiveSafeInteger2(timeoutMs, "timeoutMs");
|
|
20
626
|
options.abortSignal?.throwIfAborted();
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
options.abortSignal.
|
|
25
|
-
|
|
26
|
-
|
|
627
|
+
const backend = await AutomationWorkerClient.connect({
|
|
628
|
+
endpointUrl: options.endpointUrl,
|
|
629
|
+
timeoutMs,
|
|
630
|
+
...options.abortSignal === void 0 ? {} : { abortSignal: options.abortSignal },
|
|
631
|
+
...options.workerFactory === void 0 ? {} : { workerFactory: options.workerFactory }
|
|
632
|
+
});
|
|
633
|
+
return new PlaywrightBrowserConnectionImpl({
|
|
634
|
+
backend,
|
|
635
|
+
control: options.control,
|
|
636
|
+
scheduling: options.scheduling,
|
|
637
|
+
...options.onClosed === void 0 ? {} : { onClosed: options.onClosed }
|
|
638
|
+
});
|
|
27
639
|
}
|
|
28
640
|
var PlaywrightBrowserConnectionImpl = class {
|
|
29
|
-
|
|
641
|
+
backend;
|
|
30
642
|
control;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
643
|
+
scheduler;
|
|
644
|
+
compatibilityMutex = new AsyncMutex();
|
|
645
|
+
contextMutex = new AsyncMutex();
|
|
646
|
+
closingTabs = /* @__PURE__ */ new Set();
|
|
647
|
+
closedTabs = /* @__PURE__ */ new Set();
|
|
35
648
|
navigationPolicyKey;
|
|
36
649
|
navigationGuard = Promise.resolve();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
650
|
+
isClosed = false;
|
|
651
|
+
disconnectCompleted = false;
|
|
652
|
+
disconnectPromise;
|
|
653
|
+
removeDisconnectedListener;
|
|
654
|
+
onClosed;
|
|
655
|
+
constructor(options) {
|
|
656
|
+
this.backend = options.backend;
|
|
657
|
+
this.control = options.control;
|
|
658
|
+
this.onClosed = options.onClosed;
|
|
659
|
+
const scheduling = normalizeScheduling(options.scheduling);
|
|
660
|
+
this.scheduler = new ResourceScheduler(scheduling);
|
|
661
|
+
this.removeDisconnectedListener = this.backend.onDisconnected(() => this.markClosed());
|
|
44
662
|
}
|
|
45
663
|
get closed() {
|
|
46
|
-
return this.isClosed ||
|
|
47
|
-
}
|
|
48
|
-
async listTabs() {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
title: await page.title(),
|
|
58
|
-
url: page.url(),
|
|
59
|
-
selected: page === this.selected
|
|
60
|
-
})
|
|
61
|
-
)
|
|
664
|
+
return this.isClosed || this.backend.closed;
|
|
665
|
+
}
|
|
666
|
+
async listTabs(options = {}) {
|
|
667
|
+
assertOptionsObject(options);
|
|
668
|
+
return this.runContextAction(
|
|
669
|
+
options.abortSignal,
|
|
670
|
+
options.timeoutMs ?? defaultActionTimeoutMs,
|
|
671
|
+
"list-tabs",
|
|
672
|
+
(signal) => this.backend.command(
|
|
673
|
+
{ method: "listTabs", params: {} },
|
|
674
|
+
{ abortSignal: signal, cancelOperation: true, phase: "list-tabs" }
|
|
62
675
|
)
|
|
63
676
|
);
|
|
64
677
|
}
|
|
65
|
-
async runAction(abortSignal, operation) {
|
|
66
|
-
const previous = this.actionTail;
|
|
67
|
-
let release;
|
|
68
|
-
this.actionTail = new Promise((resolve) => {
|
|
69
|
-
release = resolve;
|
|
70
|
-
});
|
|
71
|
-
await previous;
|
|
72
|
-
try {
|
|
73
|
-
this.assertOpen();
|
|
74
|
-
await this.navigationGuard;
|
|
75
|
-
return await this.control.runAgentAction(() => this.withAbort(abortSignal, operation));
|
|
76
|
-
} finally {
|
|
77
|
-
release?.();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
678
|
setNavigationPolicy(policy) {
|
|
81
679
|
this.assertOpen();
|
|
82
680
|
const key = JSON.stringify(policy);
|
|
@@ -87,141 +685,438 @@ var PlaywrightBrowserConnectionImpl = class {
|
|
|
87
685
|
return;
|
|
88
686
|
}
|
|
89
687
|
this.navigationPolicyKey = key;
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
688
|
+
const installation = this.withActionTimeout(
|
|
689
|
+
void 0,
|
|
690
|
+
defaultActionTimeoutMs,
|
|
691
|
+
"navigation-policy",
|
|
692
|
+
(signal) => this.scheduler.run(
|
|
693
|
+
"browser-context",
|
|
694
|
+
signal,
|
|
695
|
+
(scheduledSignal) => this.contextMutex.run(
|
|
696
|
+
scheduledSignal,
|
|
697
|
+
() => this.control.runAgentAction(async () => {
|
|
698
|
+
try {
|
|
699
|
+
await this.backend.command(
|
|
700
|
+
{ method: "setNavigationPolicy", params: { policy } },
|
|
701
|
+
{
|
|
702
|
+
abortSignal: scheduledSignal,
|
|
703
|
+
cancelOperation: true,
|
|
704
|
+
phase: "navigation-policy"
|
|
705
|
+
}
|
|
706
|
+
);
|
|
707
|
+
} catch (error) {
|
|
708
|
+
throw normalizeAutomationError(error, "navigation-policy");
|
|
709
|
+
}
|
|
710
|
+
})
|
|
711
|
+
)
|
|
712
|
+
)
|
|
713
|
+
);
|
|
714
|
+
this.navigationGuard = installation.catch(async (error) => {
|
|
715
|
+
let cleanupError;
|
|
716
|
+
try {
|
|
717
|
+
await this.disconnect();
|
|
718
|
+
} catch (caught) {
|
|
719
|
+
cleanupError = caught;
|
|
720
|
+
}
|
|
721
|
+
throw new BrowserError(
|
|
722
|
+
"Unable to install the browser navigation policy.",
|
|
723
|
+
"transport_failure",
|
|
724
|
+
{
|
|
725
|
+
cause: combineCleanupError(error, cleanupError),
|
|
726
|
+
phase: "navigation-policy"
|
|
727
|
+
}
|
|
728
|
+
);
|
|
729
|
+
});
|
|
94
730
|
void this.navigationGuard.catch(() => void 0);
|
|
95
731
|
}
|
|
96
|
-
|
|
97
|
-
this.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
async
|
|
134
|
-
|
|
135
|
-
this.
|
|
136
|
-
|
|
732
|
+
async openTab(abortSignal, timeoutMs) {
|
|
733
|
+
return this.withActionTimeout(
|
|
734
|
+
abortSignal,
|
|
735
|
+
timeoutMs,
|
|
736
|
+
"open-tab",
|
|
737
|
+
(signal) => this.compatibilityMutex.run(
|
|
738
|
+
signal,
|
|
739
|
+
() => this.runContextActionScheduled(
|
|
740
|
+
signal,
|
|
741
|
+
"open-tab",
|
|
742
|
+
(scheduledSignal) => this.backend.command(
|
|
743
|
+
{ method: "openTab", params: {} },
|
|
744
|
+
{ abortSignal: scheduledSignal, cancelOperation: true, phase: "open-tab" }
|
|
745
|
+
)
|
|
746
|
+
)
|
|
747
|
+
)
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
async selectTab(tabId, abortSignal, timeoutMs) {
|
|
751
|
+
this.assertTabUsable(tabId);
|
|
752
|
+
return this.withActionTimeout(
|
|
753
|
+
abortSignal,
|
|
754
|
+
timeoutMs,
|
|
755
|
+
"select-tab",
|
|
756
|
+
(signal) => this.compatibilityMutex.run(
|
|
757
|
+
signal,
|
|
758
|
+
() => this.runContextActionScheduled(
|
|
759
|
+
signal,
|
|
760
|
+
"select-tab",
|
|
761
|
+
(scheduledSignal) => this.backend.command(
|
|
762
|
+
{ method: "selectTab", params: { tabId } },
|
|
763
|
+
{ abortSignal: scheduledSignal, cancelOperation: true, phase: "select-tab" }
|
|
764
|
+
)
|
|
765
|
+
)
|
|
766
|
+
)
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
async closeTab(tabId, abortSignal, timeoutMs) {
|
|
770
|
+
this.assertTabUsable(tabId);
|
|
771
|
+
this.closingTabs.add(tabId);
|
|
772
|
+
let closed = false;
|
|
773
|
+
try {
|
|
774
|
+
const result = await this.withActionTimeout(
|
|
775
|
+
abortSignal,
|
|
776
|
+
timeoutMs,
|
|
777
|
+
"close-tab",
|
|
778
|
+
(signal) => this.scheduler.run(`tab:${tabId}`, signal, (scheduledSignal) => {
|
|
779
|
+
return this.contextMutex.run(
|
|
780
|
+
scheduledSignal,
|
|
781
|
+
() => this.control.runAgentAction(async () => {
|
|
782
|
+
try {
|
|
783
|
+
return await this.backend.command(
|
|
784
|
+
{ method: "closeTab", params: { tabId } },
|
|
785
|
+
{ abortSignal: scheduledSignal, cancelOperation: true, phase: "close-tab" }
|
|
786
|
+
);
|
|
787
|
+
} catch (error) {
|
|
788
|
+
throw normalizeAutomationError(error, "close-tab");
|
|
789
|
+
}
|
|
790
|
+
})
|
|
791
|
+
);
|
|
792
|
+
})
|
|
793
|
+
);
|
|
794
|
+
closed = true;
|
|
795
|
+
return result;
|
|
796
|
+
} finally {
|
|
797
|
+
this.closingTabs.delete(tabId);
|
|
798
|
+
if (closed && !this.isClosed) this.rememberClosedTab(tabId);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
async runTabCommand(options) {
|
|
802
|
+
return this.withActionTimeout(
|
|
803
|
+
options.abortSignal,
|
|
804
|
+
options.timeoutMs,
|
|
805
|
+
options.phase,
|
|
806
|
+
async (signal) => {
|
|
807
|
+
if (options.tabId !== void 0) {
|
|
808
|
+
this.assertTabUsable(options.tabId);
|
|
809
|
+
return this.runScheduledTabCommand(options.tabId, options, signal);
|
|
810
|
+
}
|
|
811
|
+
return this.compatibilityMutex.run(signal, async () => {
|
|
812
|
+
const tabId = await this.selectedTabId(signal);
|
|
813
|
+
this.assertTabUsable(tabId);
|
|
814
|
+
return this.runScheduledTabCommand(tabId, options, signal);
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
async disconnect(options = {}) {
|
|
820
|
+
assertOptionsObject(options);
|
|
821
|
+
if (options.timeoutMs !== void 0) assertPositiveSafeInteger2(options.timeoutMs, "timeoutMs");
|
|
822
|
+
if (this.disconnectCompleted) return;
|
|
823
|
+
if (options.abortSignal?.aborted) {
|
|
824
|
+
throw cancellationError(options.abortSignal.reason, "disconnect");
|
|
825
|
+
}
|
|
826
|
+
if (this.disconnectPromise !== void 0) {
|
|
827
|
+
return waitForSharedDisconnect(this.disconnectPromise, options);
|
|
828
|
+
}
|
|
829
|
+
this.markClosed();
|
|
830
|
+
const promise = this.backend.disconnect(options);
|
|
831
|
+
this.disconnectPromise = promise;
|
|
832
|
+
try {
|
|
833
|
+
await promise;
|
|
834
|
+
this.disconnectCompleted = true;
|
|
835
|
+
} finally {
|
|
836
|
+
if (this.disconnectPromise === promise) this.disconnectPromise = void 0;
|
|
837
|
+
}
|
|
137
838
|
}
|
|
138
839
|
async [Symbol.asyncDispose]() {
|
|
139
840
|
await this.disconnect();
|
|
140
841
|
}
|
|
141
|
-
|
|
142
|
-
return this.
|
|
842
|
+
async runScheduledTabCommand(tabId, options, abortSignal) {
|
|
843
|
+
return this.scheduler.run(
|
|
844
|
+
`tab:${tabId}`,
|
|
845
|
+
abortSignal,
|
|
846
|
+
(scheduledSignal) => this.control.runAgentAction(async () => {
|
|
847
|
+
await waitForOwnedPromise(this.navigationGuard, scheduledSignal);
|
|
848
|
+
try {
|
|
849
|
+
return await this.backend.command(options.command(tabId), {
|
|
850
|
+
abortSignal: scheduledSignal,
|
|
851
|
+
cancelOperation: true,
|
|
852
|
+
phase: options.phase
|
|
853
|
+
});
|
|
854
|
+
} catch (error) {
|
|
855
|
+
throw normalizeAutomationError(error, options.phase);
|
|
856
|
+
}
|
|
857
|
+
})
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
async selectedTabId(abortSignal) {
|
|
861
|
+
const tabs = await this.runContextActionScheduled(
|
|
862
|
+
abortSignal,
|
|
863
|
+
"selected-tab-lookup",
|
|
864
|
+
(signal) => this.backend.command(
|
|
865
|
+
{ method: "listTabs", params: {} },
|
|
866
|
+
{ abortSignal: signal, cancelOperation: true, phase: "selected-tab-lookup" }
|
|
867
|
+
)
|
|
868
|
+
);
|
|
869
|
+
const selected = tabs.find((tab) => tab.selected) ?? tabs[0];
|
|
870
|
+
if (selected === void 0) {
|
|
871
|
+
throw new BrowserError("Browser has no open tabs.", "invalid_state", {
|
|
872
|
+
phase: "selected-tab-lookup"
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
return selected.id;
|
|
876
|
+
}
|
|
877
|
+
runContextAction(abortSignal, timeoutMs, phase, operation) {
|
|
878
|
+
return this.withActionTimeout(
|
|
879
|
+
abortSignal,
|
|
880
|
+
timeoutMs,
|
|
881
|
+
phase,
|
|
882
|
+
(signal) => this.runContextActionScheduled(signal, phase, operation)
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
runContextActionScheduled(abortSignal, phase, operation) {
|
|
886
|
+
return this.scheduler.run(
|
|
887
|
+
"browser-context",
|
|
888
|
+
abortSignal,
|
|
889
|
+
(scheduledSignal) => this.contextMutex.run(
|
|
890
|
+
scheduledSignal,
|
|
891
|
+
() => this.control.runAgentAction(async () => {
|
|
892
|
+
this.assertOpen();
|
|
893
|
+
await waitForOwnedPromise(this.navigationGuard, scheduledSignal);
|
|
894
|
+
try {
|
|
895
|
+
return await operation(scheduledSignal);
|
|
896
|
+
} catch (error) {
|
|
897
|
+
throw normalizeAutomationError(error, phase);
|
|
898
|
+
}
|
|
899
|
+
})
|
|
900
|
+
)
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
async withActionTimeout(abortSignal, timeoutMs, phase, operation) {
|
|
904
|
+
assertPositiveSafeInteger2(timeoutMs, "timeoutMs");
|
|
905
|
+
if (abortSignal?.aborted) throw cancellationError(abortSignal.reason, phase);
|
|
906
|
+
const timeoutController = new AbortController();
|
|
907
|
+
const timer = setTimeout(
|
|
908
|
+
() => timeoutController.abort(
|
|
909
|
+
new BrowserError("Browser action timed out.", "action_timeout", { phase })
|
|
910
|
+
),
|
|
911
|
+
timeoutMs
|
|
912
|
+
);
|
|
913
|
+
timer.unref?.();
|
|
914
|
+
const effectiveSignal = abortSignal === void 0 ? timeoutController.signal : AbortSignal.any([abortSignal, timeoutController.signal]);
|
|
915
|
+
try {
|
|
916
|
+
const value = await operation(effectiveSignal);
|
|
917
|
+
if (abortSignal?.aborted) throw cancellationError(abortSignal.reason, phase);
|
|
918
|
+
if (timeoutController.signal.aborted) throw timeoutController.signal.reason;
|
|
919
|
+
return value;
|
|
920
|
+
} catch (error) {
|
|
921
|
+
if (abortSignal?.aborted) throw cancellationError(abortSignal.reason, phase);
|
|
922
|
+
if (timeoutController.signal.aborted) throw timeoutController.signal.reason;
|
|
923
|
+
throw error;
|
|
924
|
+
} finally {
|
|
925
|
+
clearTimeout(timer);
|
|
926
|
+
}
|
|
143
927
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
throw new BrowserError(`Browser tab
|
|
928
|
+
assertTabUsable(tabId) {
|
|
929
|
+
this.assertOpen();
|
|
930
|
+
if (this.closingTabs.has(tabId) || this.closedTabs.has(tabId)) {
|
|
931
|
+
throw new BrowserError(`Browser tab is closing or closed: ${tabId}`, "invalid_state", {
|
|
932
|
+
phase: "tab-lookup"
|
|
933
|
+
});
|
|
148
934
|
}
|
|
149
|
-
|
|
935
|
+
}
|
|
936
|
+
rememberClosedTab(tabId) {
|
|
937
|
+
this.closedTabs.add(tabId);
|
|
938
|
+
if (this.closedTabs.size <= maxClosedTabTombstones) return;
|
|
939
|
+
const oldest = this.closedTabs.values().next().value;
|
|
940
|
+
if (oldest !== void 0) this.closedTabs.delete(oldest);
|
|
150
941
|
}
|
|
151
942
|
assertOpen() {
|
|
152
|
-
if (this.closed)
|
|
943
|
+
if (this.closed) {
|
|
944
|
+
throw new BrowserError("Browser connection is closed.", "connection_closed");
|
|
945
|
+
}
|
|
153
946
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
void this.disconnect().finally(() => finish(() => reject(abortSignal.reason)));
|
|
167
|
-
};
|
|
168
|
-
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
169
|
-
void operation().then(
|
|
170
|
-
(value) => finish(() => resolve(value)),
|
|
171
|
-
(error) => finish(() => reject(error))
|
|
172
|
-
);
|
|
173
|
-
});
|
|
947
|
+
markClosed() {
|
|
948
|
+
if (this.isClosed) return;
|
|
949
|
+
this.isClosed = true;
|
|
950
|
+
const error = new BrowserError("Browser connection is closed.", "connection_closed");
|
|
951
|
+
this.scheduler.close(error);
|
|
952
|
+
this.compatibilityMutex.close(error);
|
|
953
|
+
this.contextMutex.close(error);
|
|
954
|
+
this.closingTabs.clear();
|
|
955
|
+
this.closedTabs.clear();
|
|
956
|
+
this.removeDisconnectedListener?.();
|
|
957
|
+
this.removeDisconnectedListener = void 0;
|
|
958
|
+
this.onClosed?.(this);
|
|
174
959
|
}
|
|
175
960
|
};
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
961
|
+
function normalizeScheduling(value) {
|
|
962
|
+
if (value === void 0) {
|
|
963
|
+
return {
|
|
964
|
+
mode: "serial",
|
|
965
|
+
maxConcurrentResources: 1,
|
|
966
|
+
maxQueuedActions: defaultMaxQueuedActions
|
|
967
|
+
};
|
|
182
968
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
url = new URL(value);
|
|
189
|
-
} catch {
|
|
190
|
-
return false;
|
|
969
|
+
assertOptionsObject(value);
|
|
970
|
+
const maxQueuedActions = value.maxQueuedActions ?? defaultMaxQueuedActions;
|
|
971
|
+
assertPositiveSafeInteger2(maxQueuedActions, "scheduling.maxQueuedActions");
|
|
972
|
+
if (value.mode === "serial") {
|
|
973
|
+
return { mode: "serial", maxConcurrentResources: 1, maxQueuedActions };
|
|
191
974
|
}
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
975
|
+
if (value.mode !== "per-tab") throw new TypeError("Unsupported browser scheduling mode.");
|
|
976
|
+
const maxConcurrentResources = value.maxConcurrentTabs ?? defaultMaxConcurrentTabs;
|
|
977
|
+
assertPositiveSafeInteger2(maxConcurrentResources, "scheduling.maxConcurrentTabs");
|
|
978
|
+
return { mode: "per-tab", maxConcurrentResources, maxQueuedActions };
|
|
195
979
|
}
|
|
196
|
-
function
|
|
197
|
-
if (
|
|
198
|
-
|
|
980
|
+
function normalizeAutomationError(error, phase) {
|
|
981
|
+
if (error instanceof BrowserError) return error;
|
|
982
|
+
if (error instanceof Error && /ERR_BLOCKED_BY_CLIENT|blockedbyclient/i.test(error.message)) {
|
|
983
|
+
return new BrowserError("Browser navigation was rejected by policy.", "navigation_blocked", {
|
|
984
|
+
cause: error,
|
|
985
|
+
phase
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
989
|
+
return new BrowserError("Browser action timed out.", "action_timeout", {
|
|
990
|
+
cause: error,
|
|
991
|
+
phase
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
if (error instanceof Error && /tab does not exist|has no open tabs|target page.*closed|page has been closed/i.test(
|
|
995
|
+
error.message
|
|
996
|
+
)) {
|
|
997
|
+
return new BrowserError(error.message, "invalid_state", { cause: error, phase });
|
|
998
|
+
}
|
|
999
|
+
if (error instanceof Error && /browser.*closed|disconnected|connection.*closed|websocket.*closed/i.test(error.message)) {
|
|
1000
|
+
return new BrowserError("Browser automation connection closed.", "connection_closed", {
|
|
1001
|
+
cause: error,
|
|
1002
|
+
phase
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
return new BrowserError("Browser tool operation failed.", "tool_failed", {
|
|
1006
|
+
cause: error,
|
|
1007
|
+
phase
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
function assertPositiveSafeInteger2(value, name) {
|
|
1011
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maxTimerMs) {
|
|
1012
|
+
throw new RangeError(`${name} must be a positive integer no greater than ${maxTimerMs}.`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
function assertOptionsObject(value) {
|
|
1016
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1017
|
+
throw new TypeError("options must be an object.");
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
function combineCleanupError(error, cleanupError) {
|
|
1021
|
+
return cleanupError === void 0 ? error : new AggregateError([error, cleanupError], "Browser automation cleanup also failed.");
|
|
1022
|
+
}
|
|
1023
|
+
async function waitForOwnedPromise(promise, abortSignal) {
|
|
1024
|
+
abortSignal.throwIfAborted();
|
|
1025
|
+
return new Promise((resolve, reject) => {
|
|
1026
|
+
let settled = false;
|
|
1027
|
+
const finish = (callback) => {
|
|
1028
|
+
if (settled) return;
|
|
1029
|
+
settled = true;
|
|
1030
|
+
abortSignal.removeEventListener("abort", abort);
|
|
1031
|
+
callback();
|
|
1032
|
+
};
|
|
1033
|
+
const abort = () => finish(() => reject(abortSignal.reason));
|
|
1034
|
+
abortSignal.addEventListener("abort", abort, { once: true });
|
|
1035
|
+
void promise.then(
|
|
1036
|
+
(value) => finish(() => resolve(value)),
|
|
1037
|
+
(error) => finish(() => reject(error))
|
|
1038
|
+
);
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
async function waitForSharedDisconnect(promise, options) {
|
|
1042
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
1043
|
+
assertPositiveSafeInteger2(timeoutMs, "timeoutMs");
|
|
1044
|
+
if (options.abortSignal?.aborted) {
|
|
1045
|
+
throw cancellationError(options.abortSignal.reason, "disconnect");
|
|
1046
|
+
}
|
|
1047
|
+
const controller = new AbortController();
|
|
1048
|
+
const timer = setTimeout(
|
|
1049
|
+
() => controller.abort(
|
|
1050
|
+
new BrowserError("Browser disconnect timed out.", "connection_timeout", {
|
|
1051
|
+
phase: "disconnect"
|
|
1052
|
+
})
|
|
1053
|
+
),
|
|
1054
|
+
timeoutMs
|
|
1055
|
+
);
|
|
1056
|
+
timer.unref?.();
|
|
1057
|
+
const signal = options.abortSignal === void 0 ? controller.signal : AbortSignal.any([options.abortSignal, controller.signal]);
|
|
1058
|
+
try {
|
|
1059
|
+
await waitForOwnedPromise(promise, signal);
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
if (options.abortSignal?.aborted) {
|
|
1062
|
+
throw cancellationError(options.abortSignal.reason, "disconnect");
|
|
1063
|
+
}
|
|
1064
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
1065
|
+
throw error;
|
|
1066
|
+
} finally {
|
|
1067
|
+
clearTimeout(timer);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
function asConnection(connection) {
|
|
1071
|
+
if (!(connection instanceof PlaywrightBrowserConnectionImpl)) {
|
|
1072
|
+
throw new TypeError("connection must be created by DockerBrowser.connect().");
|
|
199
1073
|
}
|
|
200
1074
|
return connection;
|
|
201
1075
|
}
|
|
202
1076
|
|
|
203
1077
|
// src/control.ts
|
|
204
|
-
import { randomUUID
|
|
1078
|
+
import { randomUUID } from "crypto";
|
|
1079
|
+
var defaultAcquireTimeoutMs = 3e4;
|
|
1080
|
+
var maxTimerMs2 = 2147483647;
|
|
205
1081
|
var BrowserControlState = class {
|
|
206
1082
|
activeAgentActions = 0;
|
|
207
1083
|
pendingAcquire;
|
|
208
1084
|
activeLease;
|
|
209
1085
|
humanPending = false;
|
|
210
1086
|
destroyed = false;
|
|
1087
|
+
availability = "available";
|
|
211
1088
|
snapshot() {
|
|
212
1089
|
const lease = this.activeLease;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
1090
|
+
const state = lease !== void 0 ? "human" : this.humanPending ? "human-pending" : this.activeAgentActions > 0 ? "agent-active" : "agent";
|
|
1091
|
+
return Object.freeze({
|
|
1092
|
+
mode: lease === void 0 ? "agent" : "human",
|
|
1093
|
+
state,
|
|
1094
|
+
availability: this.availability,
|
|
1095
|
+
activeAgentActions: this.activeAgentActions,
|
|
1096
|
+
humanPending: this.humanPending,
|
|
1097
|
+
...lease === void 0 ? {} : {
|
|
1098
|
+
ownerId: lease.ownerId,
|
|
1099
|
+
expiresAt: lease.expiresAt
|
|
1100
|
+
}
|
|
217
1101
|
});
|
|
218
1102
|
}
|
|
219
1103
|
async acquireHumanControl(options) {
|
|
220
1104
|
validateAcquireOptions(options);
|
|
221
1105
|
this.assertActive();
|
|
222
|
-
options.abortSignal?.
|
|
223
|
-
|
|
224
|
-
|
|
1106
|
+
if (options.abortSignal?.aborted) {
|
|
1107
|
+
throw cancellationError(options.abortSignal.reason, "human-control-acquire");
|
|
1108
|
+
}
|
|
1109
|
+
if (this.activeLease !== void 0) {
|
|
1110
|
+
throw new BrowserError("Browser human control is already acquired.", "human_controlled", {
|
|
1111
|
+
phase: "human-control-acquire"
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
if (this.humanPending) {
|
|
1115
|
+
throw new BrowserError(
|
|
1116
|
+
"Browser human control acquisition is already pending.",
|
|
1117
|
+
"human_control_conflict",
|
|
1118
|
+
{ phase: "human-control-acquire" }
|
|
1119
|
+
);
|
|
225
1120
|
}
|
|
226
1121
|
this.humanPending = true;
|
|
227
1122
|
try {
|
|
@@ -232,16 +1127,30 @@ var BrowserControlState = class {
|
|
|
232
1127
|
pending.abortSignal = options.abortSignal;
|
|
233
1128
|
pending.abort = () => {
|
|
234
1129
|
if (this.pendingAcquire !== pending) return;
|
|
235
|
-
this.
|
|
236
|
-
reject(options.abortSignal?.reason);
|
|
1130
|
+
this.clearPendingAcquire(pending);
|
|
1131
|
+
reject(cancellationError(options.abortSignal?.reason, "human-control-acquire"));
|
|
237
1132
|
};
|
|
238
1133
|
options.abortSignal.addEventListener("abort", pending.abort, { once: true });
|
|
239
1134
|
}
|
|
1135
|
+
pending.timer = setTimeout(() => {
|
|
1136
|
+
if (this.pendingAcquire !== pending) return;
|
|
1137
|
+
this.clearPendingAcquire(pending);
|
|
1138
|
+
reject(
|
|
1139
|
+
new BrowserError(
|
|
1140
|
+
"Timed out waiting to acquire browser human control.",
|
|
1141
|
+
"human_control_conflict",
|
|
1142
|
+
{ phase: "human-control-acquire" }
|
|
1143
|
+
)
|
|
1144
|
+
);
|
|
1145
|
+
}, options.timeoutMs ?? defaultAcquireTimeoutMs);
|
|
1146
|
+
pending.timer.unref?.();
|
|
240
1147
|
this.pendingAcquire = pending;
|
|
241
1148
|
});
|
|
242
1149
|
}
|
|
243
1150
|
this.assertActive();
|
|
244
|
-
options.abortSignal?.
|
|
1151
|
+
if (options.abortSignal?.aborted) {
|
|
1152
|
+
throw cancellationError(options.abortSignal.reason, "human-control-acquire");
|
|
1153
|
+
}
|
|
245
1154
|
const lease = new BrowserHumanControlLeaseImpl({
|
|
246
1155
|
owner: this,
|
|
247
1156
|
ownerId: options.ownerId,
|
|
@@ -252,118 +1161,785 @@ var BrowserControlState = class {
|
|
|
252
1161
|
} finally {
|
|
253
1162
|
this.humanPending = false;
|
|
254
1163
|
}
|
|
255
|
-
}
|
|
256
|
-
async runAgentAction(operation) {
|
|
257
|
-
this.assertActive();
|
|
258
|
-
if (this.activeLease !== void 0
|
|
259
|
-
throw new BrowserError("Browser is controlled by a human viewer.", "human_controlled"
|
|
1164
|
+
}
|
|
1165
|
+
async runAgentAction(operation) {
|
|
1166
|
+
this.assertActive();
|
|
1167
|
+
if (this.activeLease !== void 0) {
|
|
1168
|
+
throw new BrowserError("Browser is controlled by a human viewer.", "human_controlled", {
|
|
1169
|
+
phase: "agent-action"
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
if (this.humanPending) {
|
|
1173
|
+
throw new BrowserError(
|
|
1174
|
+
"Browser human control acquisition is pending.",
|
|
1175
|
+
"human_control_conflict",
|
|
1176
|
+
{ phase: "agent-action" }
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
this.activeAgentActions += 1;
|
|
1180
|
+
try {
|
|
1181
|
+
return await operation();
|
|
1182
|
+
} finally {
|
|
1183
|
+
this.activeAgentActions -= 1;
|
|
1184
|
+
if (this.activeAgentActions === 0) this.resolvePendingAcquire();
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
renewLease(lease, options) {
|
|
1188
|
+
if (this.activeLease !== lease) {
|
|
1189
|
+
throw new BrowserError("Browser human control lease is no longer active.", "invalid_state");
|
|
1190
|
+
}
|
|
1191
|
+
assertPositiveSafeInteger3(options.leaseTimeoutMs, "leaseTimeoutMs");
|
|
1192
|
+
lease.resetExpiration(options.leaseTimeoutMs);
|
|
1193
|
+
return this.snapshot();
|
|
1194
|
+
}
|
|
1195
|
+
releaseLease(lease) {
|
|
1196
|
+
if (this.activeLease !== lease) return;
|
|
1197
|
+
this.activeLease = void 0;
|
|
1198
|
+
lease.markReleased();
|
|
1199
|
+
}
|
|
1200
|
+
destroy() {
|
|
1201
|
+
if (this.destroyed) return;
|
|
1202
|
+
this.destroyed = true;
|
|
1203
|
+
this.activeLease?.release();
|
|
1204
|
+
if (this.pendingAcquire !== void 0) {
|
|
1205
|
+
const pending = this.pendingAcquire;
|
|
1206
|
+
this.pendingAcquire = void 0;
|
|
1207
|
+
this.clearPendingAcquire(pending);
|
|
1208
|
+
pending.reject(new BrowserError("Browser was destroyed.", "runtime_destroyed"));
|
|
1209
|
+
}
|
|
1210
|
+
this.humanPending = false;
|
|
1211
|
+
this.availability = "destroyed";
|
|
1212
|
+
}
|
|
1213
|
+
setAvailability(availability) {
|
|
1214
|
+
if (this.destroyed) return;
|
|
1215
|
+
if (availability === "destroyed") {
|
|
1216
|
+
this.destroy();
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
this.availability = availability;
|
|
1220
|
+
if (availability !== "disconnected") return;
|
|
1221
|
+
this.activeLease?.release();
|
|
1222
|
+
if (this.pendingAcquire !== void 0) {
|
|
1223
|
+
const pending = this.pendingAcquire;
|
|
1224
|
+
this.clearPendingAcquire(pending);
|
|
1225
|
+
pending.reject(
|
|
1226
|
+
new BrowserError("Browser runtime is disconnected.", "connection_closed", {
|
|
1227
|
+
phase: "human-control-acquire"
|
|
1228
|
+
})
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
this.humanPending = false;
|
|
1232
|
+
}
|
|
1233
|
+
assertActive() {
|
|
1234
|
+
if (this.destroyed) throw new BrowserError("Browser was destroyed.", "runtime_destroyed");
|
|
1235
|
+
if (this.availability === "disconnected") {
|
|
1236
|
+
throw new BrowserError("Browser runtime is disconnected.", "connection_closed");
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
resolvePendingAcquire() {
|
|
1240
|
+
const pending = this.pendingAcquire;
|
|
1241
|
+
if (pending === void 0) return;
|
|
1242
|
+
this.clearPendingAcquire(pending);
|
|
1243
|
+
pending.resolve();
|
|
1244
|
+
}
|
|
1245
|
+
clearPendingAcquire(pending) {
|
|
1246
|
+
if (this.pendingAcquire === pending) this.pendingAcquire = void 0;
|
|
1247
|
+
if (pending.timer !== void 0) clearTimeout(pending.timer);
|
|
1248
|
+
if (pending.abort !== void 0) {
|
|
1249
|
+
pending.abortSignal?.removeEventListener("abort", pending.abort);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
var BrowserHumanControlLeaseImpl = class {
|
|
1254
|
+
id = randomUUID();
|
|
1255
|
+
ownerId;
|
|
1256
|
+
owner;
|
|
1257
|
+
expiration = 0;
|
|
1258
|
+
timer;
|
|
1259
|
+
released = false;
|
|
1260
|
+
constructor(options) {
|
|
1261
|
+
this.owner = options.owner;
|
|
1262
|
+
this.ownerId = options.ownerId;
|
|
1263
|
+
this.resetExpiration(options.leaseTimeoutMs);
|
|
1264
|
+
}
|
|
1265
|
+
get expiresAt() {
|
|
1266
|
+
return new Date(this.expiration).toISOString();
|
|
1267
|
+
}
|
|
1268
|
+
renew(options) {
|
|
1269
|
+
if (this.released) {
|
|
1270
|
+
throw new BrowserError("Browser human control lease is no longer active.", "invalid_state");
|
|
1271
|
+
}
|
|
1272
|
+
return this.owner.renewLease(this, options);
|
|
1273
|
+
}
|
|
1274
|
+
release() {
|
|
1275
|
+
this.owner.releaseLease(this);
|
|
1276
|
+
}
|
|
1277
|
+
markReleased() {
|
|
1278
|
+
if (this.released) return;
|
|
1279
|
+
this.released = true;
|
|
1280
|
+
if (this.timer !== void 0) clearTimeout(this.timer);
|
|
1281
|
+
this.timer = void 0;
|
|
1282
|
+
}
|
|
1283
|
+
resetExpiration(leaseTimeoutMs) {
|
|
1284
|
+
if (this.timer !== void 0) clearTimeout(this.timer);
|
|
1285
|
+
this.expiration = Date.now() + leaseTimeoutMs;
|
|
1286
|
+
this.timer = setTimeout(() => this.release(), leaseTimeoutMs);
|
|
1287
|
+
this.timer.unref?.();
|
|
1288
|
+
}
|
|
1289
|
+
async [Symbol.asyncDispose]() {
|
|
1290
|
+
this.release();
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
function validateAcquireOptions(options) {
|
|
1294
|
+
if (!isRecord2(options)) throw new TypeError("options must be an object.");
|
|
1295
|
+
if (typeof options.ownerId !== "string" || options.ownerId.length === 0) {
|
|
1296
|
+
throw new TypeError("ownerId must be a non-empty string.");
|
|
1297
|
+
}
|
|
1298
|
+
assertPositiveSafeInteger3(options.leaseTimeoutMs, "leaseTimeoutMs");
|
|
1299
|
+
if (options.timeoutMs !== void 0) assertPositiveSafeInteger3(options.timeoutMs, "timeoutMs");
|
|
1300
|
+
}
|
|
1301
|
+
function assertPositiveSafeInteger3(value, name) {
|
|
1302
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maxTimerMs2) {
|
|
1303
|
+
throw new RangeError(`${name} must be a positive integer no greater than ${maxTimerMs2}.`);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
function isRecord2(value) {
|
|
1307
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// src/lifecycle.ts
|
|
1311
|
+
var defaultLifecycleTimeoutMs = 12e4;
|
|
1312
|
+
var maxTimerMs3 = 2147483647;
|
|
1313
|
+
function boundedSignal(options, defaultTimeoutMs, phase) {
|
|
1314
|
+
const timeoutMs = options.timeoutMs ?? defaultTimeoutMs;
|
|
1315
|
+
assertPositiveSafeInteger4(timeoutMs, "timeoutMs");
|
|
1316
|
+
if (options.abortSignal?.aborted) throw cancellationError(options.abortSignal.reason, phase);
|
|
1317
|
+
const controller = new AbortController();
|
|
1318
|
+
const timer = setTimeout(
|
|
1319
|
+
() => controller.abort(
|
|
1320
|
+
new BrowserError(`Browser lifecycle operation timed out: ${phase}.`, "lifecycle_timeout", {
|
|
1321
|
+
phase
|
|
1322
|
+
})
|
|
1323
|
+
),
|
|
1324
|
+
timeoutMs
|
|
1325
|
+
);
|
|
1326
|
+
timer.unref?.();
|
|
1327
|
+
const signal = options.abortSignal === void 0 ? controller.signal : AbortSignal.any([options.abortSignal, controller.signal]);
|
|
1328
|
+
return {
|
|
1329
|
+
signal,
|
|
1330
|
+
timedOut: () => controller.signal.aborted,
|
|
1331
|
+
throwIfAborted: () => {
|
|
1332
|
+
if (options.abortSignal?.aborted) {
|
|
1333
|
+
throw cancellationError(options.abortSignal.reason, phase);
|
|
1334
|
+
}
|
|
1335
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
1336
|
+
},
|
|
1337
|
+
normalize: (error, message, code) => {
|
|
1338
|
+
if (options.abortSignal?.aborted) {
|
|
1339
|
+
return error instanceof AggregateError ? new BrowserError("Browser operation was cancelled by the caller.", "cancelled", {
|
|
1340
|
+
cause: error,
|
|
1341
|
+
phase
|
|
1342
|
+
}) : cancellationError(options.abortSignal.reason, phase);
|
|
1343
|
+
}
|
|
1344
|
+
if (controller.signal.aborted) {
|
|
1345
|
+
const reason = controller.signal.reason;
|
|
1346
|
+
return error === reason ? reason : new BrowserError(reason.message, "lifecycle_timeout", { cause: error, phase });
|
|
1347
|
+
}
|
|
1348
|
+
if (error instanceof BrowserError) return error;
|
|
1349
|
+
return new BrowserError(message, code, { cause: error, phase });
|
|
1350
|
+
},
|
|
1351
|
+
dispose: () => clearTimeout(timer)
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
function validateLifecycleOptions(options, phase) {
|
|
1355
|
+
if (options.timeoutMs !== void 0) assertPositiveSafeInteger4(options.timeoutMs, "timeoutMs");
|
|
1356
|
+
if (options.abortSignal?.aborted) {
|
|
1357
|
+
throw cancellationError(options.abortSignal.reason, phase);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
async function waitForSharedLifecycle(promise, options, phase) {
|
|
1361
|
+
const bounded = boundedSignal(options, defaultLifecycleTimeoutMs, phase);
|
|
1362
|
+
try {
|
|
1363
|
+
await waitForPromise(promise, bounded.signal);
|
|
1364
|
+
bounded.throwIfAborted();
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
throw bounded.normalize(error, `Unable to wait for ${phase}.`, "transport_failure");
|
|
1367
|
+
} finally {
|
|
1368
|
+
bounded.dispose();
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
function assertPositiveSafeInteger4(value, name) {
|
|
1372
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maxTimerMs3) {
|
|
1373
|
+
throw new RangeError(`${name} must be a positive integer no greater than ${maxTimerMs3}.`);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
function assertOptionsObject2(value) {
|
|
1377
|
+
if (!isRecord3(value)) throw new TypeError("options must be an object.");
|
|
1378
|
+
}
|
|
1379
|
+
function isRecord3(value) {
|
|
1380
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1381
|
+
}
|
|
1382
|
+
async function waitForPromise(promise, abortSignal) {
|
|
1383
|
+
abortSignal.throwIfAborted();
|
|
1384
|
+
return new Promise((resolve, reject) => {
|
|
1385
|
+
let settled = false;
|
|
1386
|
+
const finish = (callback) => {
|
|
1387
|
+
if (settled) return;
|
|
1388
|
+
settled = true;
|
|
1389
|
+
abortSignal.removeEventListener("abort", abort);
|
|
1390
|
+
callback();
|
|
1391
|
+
};
|
|
1392
|
+
const abort = () => finish(() => reject(abortSignal.reason));
|
|
1393
|
+
abortSignal.addEventListener("abort", abort, { once: true });
|
|
1394
|
+
void promise.then(
|
|
1395
|
+
(value) => finish(() => resolve(value)),
|
|
1396
|
+
(error) => finish(() => reject(error))
|
|
1397
|
+
);
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/readiness.ts
|
|
1402
|
+
var cdpPort = 9222;
|
|
1403
|
+
var noVncPort = 6080;
|
|
1404
|
+
var browserCapabilities = ["runtime", "browser", "automation", "desktop"];
|
|
1405
|
+
var automationProbeAttemptTimeoutMs = 5e3;
|
|
1406
|
+
async function probeBrowserCapability(options) {
|
|
1407
|
+
const { capability, sandbox, abortSignal, deadline } = options;
|
|
1408
|
+
switch (capability) {
|
|
1409
|
+
case "runtime":
|
|
1410
|
+
if (sandbox.state !== "running") {
|
|
1411
|
+
throw new BrowserError(`Browser is not running: ${sandbox.state}`, "invalid_state");
|
|
1412
|
+
}
|
|
1413
|
+
return;
|
|
1414
|
+
case "browser":
|
|
1415
|
+
await sandbox.runtime.waitForPort({
|
|
1416
|
+
containerPort: cdpPort,
|
|
1417
|
+
timeoutMs: remainingTime(deadline),
|
|
1418
|
+
abortSignal
|
|
1419
|
+
});
|
|
1420
|
+
await assertCdpHttpReady(`${endpointFor(sandbox, cdpPort)}/json/version`, abortSignal);
|
|
1421
|
+
return;
|
|
1422
|
+
case "desktop":
|
|
1423
|
+
await sandbox.runtime.waitForPort({
|
|
1424
|
+
containerPort: noVncPort,
|
|
1425
|
+
timeoutMs: remainingTime(deadline),
|
|
1426
|
+
abortSignal
|
|
1427
|
+
});
|
|
1428
|
+
await assertHttpReady(`${endpointFor(sandbox, noVncPort)}/vnc.html`, abortSignal);
|
|
1429
|
+
return;
|
|
1430
|
+
case "automation":
|
|
1431
|
+
await sandbox.runtime.waitForPort({
|
|
1432
|
+
containerPort: cdpPort,
|
|
1433
|
+
timeoutMs: remainingTime(deadline),
|
|
1434
|
+
abortSignal
|
|
1435
|
+
});
|
|
1436
|
+
await assertCdpHttpReady(`${endpointFor(sandbox, cdpPort)}/json/version`, abortSignal);
|
|
1437
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
1438
|
+
abortSignal.throwIfAborted();
|
|
1439
|
+
try {
|
|
1440
|
+
const connection = await options.connectBrowser({
|
|
1441
|
+
endpointUrl: endpointFor(sandbox, cdpPort),
|
|
1442
|
+
control: options.control,
|
|
1443
|
+
timeoutMs: Math.min(automationProbeAttemptTimeoutMs, remainingTime(deadline)),
|
|
1444
|
+
abortSignal
|
|
1445
|
+
});
|
|
1446
|
+
try {
|
|
1447
|
+
if (connection.closed) {
|
|
1448
|
+
throw new Error("CDP connection closed during initialization.");
|
|
1449
|
+
}
|
|
1450
|
+
} finally {
|
|
1451
|
+
await connection.disconnect({
|
|
1452
|
+
timeoutMs: remainingTime(deadline),
|
|
1453
|
+
abortSignal
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
abortSignal.throwIfAborted();
|
|
1457
|
+
return;
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
if (abortSignal.aborted) throw abortSignal.reason ?? error;
|
|
1460
|
+
await waitForRetry(abortSignal, Math.min(500, 50 * 2 ** Math.min(attempt, 3)));
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
function endpointFor(sandbox, containerPort) {
|
|
1466
|
+
const published = publishedPort(sandbox, containerPort);
|
|
1467
|
+
return `http://${hostForUrl(published.host)}:${published.hostPort}`;
|
|
1468
|
+
}
|
|
1469
|
+
function normalizeReadinessError(error, capability, state) {
|
|
1470
|
+
if (state.callerAborted) {
|
|
1471
|
+
return new BrowserError("Browser readiness was cancelled by the caller.", "cancelled", {
|
|
1472
|
+
cause: error,
|
|
1473
|
+
capability,
|
|
1474
|
+
phase: "readiness"
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
if (state.timedOut) {
|
|
1478
|
+
return new BrowserError(
|
|
1479
|
+
`Browser ${capability} capability did not become ready before the timeout.`,
|
|
1480
|
+
"readiness_timeout",
|
|
1481
|
+
{ cause: error, capability, phase: "readiness" }
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
if (error instanceof BrowserError && (error.code === "connection_closed" || error.code === "runtime_destroyed")) {
|
|
1485
|
+
return error;
|
|
1486
|
+
}
|
|
1487
|
+
return new BrowserError(`Browser ${capability} capability is not ready.`, "not_ready", {
|
|
1488
|
+
cause: error,
|
|
1489
|
+
capability,
|
|
1490
|
+
phase: "readiness"
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
async function assertCdpHttpReady(url, abortSignal) {
|
|
1494
|
+
await assertHttpReady(url, abortSignal, async (response) => {
|
|
1495
|
+
const value = await response.json();
|
|
1496
|
+
if (!isRecord4(value) || typeof value.webSocketDebuggerUrl !== "string" || !value.webSocketDebuggerUrl.startsWith("ws")) {
|
|
1497
|
+
throw new Error("CDP version endpoint did not expose a WebSocket debugger URL.");
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
async function assertHttpReady(url, abortSignal, verify) {
|
|
1502
|
+
while (true) {
|
|
1503
|
+
abortSignal.throwIfAborted();
|
|
1504
|
+
try {
|
|
1505
|
+
const response = await fetch(url, { signal: abortSignal, redirect: "error" });
|
|
1506
|
+
try {
|
|
1507
|
+
if (response.ok) {
|
|
1508
|
+
await verify?.(response);
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
} finally {
|
|
1512
|
+
await response.body?.cancel().catch(() => void 0);
|
|
1513
|
+
}
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
if (abortSignal.aborted) throw abortSignal.reason ?? error;
|
|
1516
|
+
}
|
|
1517
|
+
await waitForRetry(abortSignal);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
async function waitForRetry(abortSignal, delayMs = 50) {
|
|
1521
|
+
await new Promise((resolve, reject) => {
|
|
1522
|
+
const timer = setTimeout(() => finish(), delayMs);
|
|
1523
|
+
timer.unref?.();
|
|
1524
|
+
const abort = () => finish(abortSignal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
1525
|
+
abortSignal.addEventListener("abort", abort, { once: true });
|
|
1526
|
+
function finish(error) {
|
|
1527
|
+
clearTimeout(timer);
|
|
1528
|
+
abortSignal.removeEventListener("abort", abort);
|
|
1529
|
+
if (error === void 0) resolve();
|
|
1530
|
+
else reject(error);
|
|
1531
|
+
}
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
function remainingTime(deadline) {
|
|
1535
|
+
return Math.max(1, deadline - Date.now());
|
|
1536
|
+
}
|
|
1537
|
+
function publishedPort(sandbox, containerPort) {
|
|
1538
|
+
const port = sandbox.runtime.publishedPorts.find(
|
|
1539
|
+
(candidate) => candidate.containerPort === containerPort && candidate.protocol === "tcp"
|
|
1540
|
+
);
|
|
1541
|
+
if (port === void 0) {
|
|
1542
|
+
throw new BrowserError(`Browser port is not published: ${containerPort}`, "invalid_state");
|
|
1543
|
+
}
|
|
1544
|
+
return port;
|
|
1545
|
+
}
|
|
1546
|
+
function hostForUrl(host) {
|
|
1547
|
+
return host.includes(":") ? `[${host}]` : host;
|
|
1548
|
+
}
|
|
1549
|
+
function isRecord4(value) {
|
|
1550
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// src/docker-browser.ts
|
|
1554
|
+
var defaultConnectTimeoutMs = 3e4;
|
|
1555
|
+
var capabilities = browserCapabilities;
|
|
1556
|
+
var DockerBrowserHandle = class {
|
|
1557
|
+
id;
|
|
1558
|
+
sandbox;
|
|
1559
|
+
desktop;
|
|
1560
|
+
control = new BrowserControlState();
|
|
1561
|
+
connections = /* @__PURE__ */ new Set();
|
|
1562
|
+
ownedOperations = /* @__PURE__ */ new Set();
|
|
1563
|
+
capabilityStates = new Map(
|
|
1564
|
+
capabilities.map((capability) => [capability, Object.freeze({ capability, state: "unknown" })])
|
|
1565
|
+
);
|
|
1566
|
+
capabilityGenerations = new Map(
|
|
1567
|
+
capabilities.map((capability) => [capability, 0])
|
|
1568
|
+
);
|
|
1569
|
+
handleState = "active";
|
|
1570
|
+
connectionPending = false;
|
|
1571
|
+
stopPromise;
|
|
1572
|
+
destroyPromise;
|
|
1573
|
+
connectBrowser;
|
|
1574
|
+
constructor(sandbox, connectBrowser = connectPlaywrightBrowser) {
|
|
1575
|
+
this.sandbox = sandbox;
|
|
1576
|
+
this.id = sandbox.id;
|
|
1577
|
+
this.connectBrowser = connectBrowser;
|
|
1578
|
+
this.desktop = Object.freeze({
|
|
1579
|
+
protocol: "novnc",
|
|
1580
|
+
containerPort: noVncPort,
|
|
1581
|
+
control: this.control
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
get state() {
|
|
1585
|
+
return this.handleState === "active" ? this.sandbox.state : this.handleState;
|
|
1586
|
+
}
|
|
1587
|
+
inspector(options) {
|
|
1588
|
+
return this.sandbox.inspector(options);
|
|
1589
|
+
}
|
|
1590
|
+
readiness() {
|
|
1591
|
+
const terminal = this.handleState === "destroyed" || this.handleState === "destroying" ? "destroyed" : this.handleState === "stopped" || this.handleState === "stopping" ? "stopped" : void 0;
|
|
1592
|
+
const entries = Object.fromEntries(
|
|
1593
|
+
capabilities.map((capability) => {
|
|
1594
|
+
const current = this.capabilityStates.get(capability);
|
|
1595
|
+
return [
|
|
1596
|
+
capability,
|
|
1597
|
+
terminal === void 0 ? current : Object.freeze({ capability, state: terminal })
|
|
1598
|
+
];
|
|
1599
|
+
})
|
|
1600
|
+
);
|
|
1601
|
+
const values = Object.values(entries);
|
|
1602
|
+
const state = this.handleState === "error" ? "failed" : terminal ?? (values.some((value) => value.state === "checking") ? "checking" : values.every((value) => value.state === "unknown") ? "unknown" : values.every((value) => value.state === "ready") ? "ready" : values.some((value) => value.state === "failed") ? values.some((value) => value.state === "ready") ? "degraded" : "failed" : "partial");
|
|
1603
|
+
return Object.freeze({ state, capabilities: Object.freeze(entries) });
|
|
1604
|
+
}
|
|
1605
|
+
async waitForCapabilities(options) {
|
|
1606
|
+
validateReadinessOptions(options);
|
|
1607
|
+
this.assertRunning();
|
|
1608
|
+
if (options.abortSignal?.aborted) {
|
|
1609
|
+
throw cancellationError(options.abortSignal.reason, "readiness");
|
|
1610
|
+
}
|
|
1611
|
+
const requested = options.capabilities ?? capabilities;
|
|
1612
|
+
const ownedController = new AbortController();
|
|
1613
|
+
const timeoutController = new AbortController();
|
|
1614
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
1615
|
+
const timer = setTimeout(
|
|
1616
|
+
() => timeoutController.abort(
|
|
1617
|
+
new BrowserError("Browser readiness timed out.", "readiness_timeout", {
|
|
1618
|
+
phase: "readiness"
|
|
1619
|
+
})
|
|
1620
|
+
),
|
|
1621
|
+
options.timeoutMs
|
|
1622
|
+
);
|
|
1623
|
+
timer.unref?.();
|
|
1624
|
+
const signals = [ownedController.signal, timeoutController.signal];
|
|
1625
|
+
if (options.abortSignal !== void 0) signals.push(options.abortSignal);
|
|
1626
|
+
const abortSignal = AbortSignal.any(signals);
|
|
1627
|
+
const previous = new Map(
|
|
1628
|
+
requested.map((capability) => [capability, this.capabilityStates.get(capability)])
|
|
1629
|
+
);
|
|
1630
|
+
const generations = new Map(
|
|
1631
|
+
requested.map((capability) => {
|
|
1632
|
+
const generation = (this.capabilityGenerations.get(capability) ?? 0) + 1;
|
|
1633
|
+
this.capabilityGenerations.set(capability, generation);
|
|
1634
|
+
return [capability, generation];
|
|
1635
|
+
})
|
|
1636
|
+
);
|
|
1637
|
+
for (const capability of requested) this.setCapability(capability, "checking");
|
|
1638
|
+
const promise = Promise.allSettled(
|
|
1639
|
+
requested.map(async (capability) => {
|
|
1640
|
+
try {
|
|
1641
|
+
await probeBrowserCapability({
|
|
1642
|
+
capability,
|
|
1643
|
+
sandbox: this.sandbox,
|
|
1644
|
+
control: this.control,
|
|
1645
|
+
connectBrowser: this.connectBrowser,
|
|
1646
|
+
abortSignal,
|
|
1647
|
+
deadline
|
|
1648
|
+
});
|
|
1649
|
+
abortSignal.throwIfAborted();
|
|
1650
|
+
if (this.capabilityGenerations.get(capability) === generations.get(capability)) {
|
|
1651
|
+
this.setCapability(capability, "ready");
|
|
1652
|
+
}
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
const normalized = normalizeReadinessError(error, capability, {
|
|
1655
|
+
callerAborted: options.abortSignal?.aborted === true,
|
|
1656
|
+
timedOut: timeoutController.signal.aborted
|
|
1657
|
+
});
|
|
1658
|
+
if (this.capabilityGenerations.get(capability) === generations.get(capability)) {
|
|
1659
|
+
if (options.abortSignal?.aborted) {
|
|
1660
|
+
this.capabilityStates.set(capability, previous.get(capability));
|
|
1661
|
+
} else {
|
|
1662
|
+
this.setCapability(capability, "failed", normalized);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
throw normalized;
|
|
1666
|
+
}
|
|
1667
|
+
})
|
|
1668
|
+
);
|
|
1669
|
+
const owned = { controller: ownedController, promise };
|
|
1670
|
+
this.ownedOperations.add(owned);
|
|
1671
|
+
try {
|
|
1672
|
+
const results = await promise;
|
|
1673
|
+
if (options.abortSignal?.aborted) {
|
|
1674
|
+
throw cancellationError(options.abortSignal.reason, "readiness");
|
|
1675
|
+
}
|
|
1676
|
+
const failures = results.filter(
|
|
1677
|
+
(result) => result.status === "rejected"
|
|
1678
|
+
);
|
|
1679
|
+
this.updateControlAvailability();
|
|
1680
|
+
if (failures.length > 0) throw failures[0].reason;
|
|
1681
|
+
return this.readiness();
|
|
1682
|
+
} finally {
|
|
1683
|
+
clearTimeout(timer);
|
|
1684
|
+
this.ownedOperations.delete(owned);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
async waitUntilReady(options) {
|
|
1688
|
+
await this.waitForCapabilities(options);
|
|
1689
|
+
}
|
|
1690
|
+
async connect(options = {}) {
|
|
1691
|
+
assertOptionsObject2(options);
|
|
1692
|
+
if (options.timeoutMs !== void 0) assertPositiveSafeInteger4(options.timeoutMs, "timeoutMs");
|
|
1693
|
+
this.assertRunning();
|
|
1694
|
+
if (options.abortSignal?.aborted) {
|
|
1695
|
+
throw cancellationError(options.abortSignal.reason, "connect");
|
|
1696
|
+
}
|
|
1697
|
+
if (this.connectionPending || this.connections.size > 0) {
|
|
1698
|
+
throw new BrowserError(
|
|
1699
|
+
"Browser already has an active or pending automation connection.",
|
|
1700
|
+
"agent_action_busy",
|
|
1701
|
+
{ phase: "connect" }
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
this.connectionPending = true;
|
|
1705
|
+
const controller = new AbortController();
|
|
1706
|
+
const signals = [controller.signal];
|
|
1707
|
+
if (options.abortSignal !== void 0) signals.push(options.abortSignal);
|
|
1708
|
+
const abortSignal = AbortSignal.any(signals);
|
|
1709
|
+
let owned;
|
|
1710
|
+
const promise = (async () => {
|
|
1711
|
+
await Promise.resolve();
|
|
1712
|
+
try {
|
|
1713
|
+
abortSignal.throwIfAborted();
|
|
1714
|
+
const connection = await this.connectBrowser({
|
|
1715
|
+
endpointUrl: endpointFor(this.sandbox, cdpPort),
|
|
1716
|
+
control: this.control,
|
|
1717
|
+
timeoutMs: options.timeoutMs ?? defaultConnectTimeoutMs,
|
|
1718
|
+
abortSignal,
|
|
1719
|
+
scheduling: options.scheduling,
|
|
1720
|
+
onClosed: (closedConnection) => this.connections.delete(closedConnection)
|
|
1721
|
+
});
|
|
1722
|
+
this.connections.add(connection);
|
|
1723
|
+
const disconnectedDuringInitialization = connection.closed;
|
|
1724
|
+
if (disconnectedDuringInitialization || this.handleState !== "active" || this.state !== "running") {
|
|
1725
|
+
this.connections.delete(connection);
|
|
1726
|
+
let cleanupError;
|
|
1727
|
+
try {
|
|
1728
|
+
await connection.disconnect();
|
|
1729
|
+
} catch (caught) {
|
|
1730
|
+
cleanupError = caught;
|
|
1731
|
+
}
|
|
1732
|
+
if (disconnectedDuringInitialization && this.handleState === "active" && this.state === "running") {
|
|
1733
|
+
throw new BrowserError(
|
|
1734
|
+
"Browser disconnected while automation was initializing.",
|
|
1735
|
+
"connection_closed",
|
|
1736
|
+
{
|
|
1737
|
+
...cleanupError === void 0 ? {} : { cause: cleanupError },
|
|
1738
|
+
phase: "connect"
|
|
1739
|
+
}
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
const lifecycleError = this.lifecycleError(
|
|
1743
|
+
"Browser stopped while the connection was initializing."
|
|
1744
|
+
);
|
|
1745
|
+
throw cleanupError === void 0 ? lifecycleError : new BrowserError(lifecycleError.message, lifecycleError.code, {
|
|
1746
|
+
cause: cleanupError,
|
|
1747
|
+
...lifecycleError.phase === void 0 ? {} : { phase: lifecycleError.phase }
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
return connection;
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
if (options.abortSignal?.aborted) {
|
|
1753
|
+
throw cancellationError(options.abortSignal.reason, "connect");
|
|
1754
|
+
}
|
|
1755
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
1756
|
+
if (error instanceof BrowserError) throw error;
|
|
1757
|
+
throw new BrowserError("Unable to connect to Chromium over CDP.", "transport_failure", {
|
|
1758
|
+
cause: error,
|
|
1759
|
+
phase: "connect"
|
|
1760
|
+
});
|
|
1761
|
+
} finally {
|
|
1762
|
+
this.connectionPending = false;
|
|
1763
|
+
this.ownedOperations.delete(owned);
|
|
1764
|
+
}
|
|
1765
|
+
})();
|
|
1766
|
+
owned = { controller, promise };
|
|
1767
|
+
this.ownedOperations.add(owned);
|
|
1768
|
+
return promise;
|
|
1769
|
+
}
|
|
1770
|
+
async stop(options = {}) {
|
|
1771
|
+
assertOptionsObject2(options);
|
|
1772
|
+
if (this.handleState === "stopped") return;
|
|
1773
|
+
if (this.handleState === "destroyed" || this.handleState === "destroying") {
|
|
1774
|
+
throw new BrowserError("Browser runtime is destroyed.", "runtime_destroyed");
|
|
1775
|
+
}
|
|
1776
|
+
validateLifecycleOptions(options, "stop-browser");
|
|
1777
|
+
if (this.stopPromise !== void 0) {
|
|
1778
|
+
return waitForSharedLifecycle(this.stopPromise, options, "stop-browser");
|
|
260
1779
|
}
|
|
261
|
-
this.
|
|
1780
|
+
this.handleState = "stopping";
|
|
1781
|
+
this.control.setAvailability("disconnected");
|
|
1782
|
+
const promise = this.performStop(options);
|
|
1783
|
+
this.stopPromise = promise;
|
|
262
1784
|
try {
|
|
263
|
-
|
|
1785
|
+
await promise;
|
|
264
1786
|
} finally {
|
|
265
|
-
this.
|
|
266
|
-
if (this.activeAgentActions === 0) this.resolvePendingAcquire();
|
|
1787
|
+
if (this.stopPromise === promise) this.stopPromise = void 0;
|
|
267
1788
|
}
|
|
268
1789
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
1790
|
+
async destroy(options = {}) {
|
|
1791
|
+
assertOptionsObject2(options);
|
|
1792
|
+
if (this.handleState === "destroyed") return;
|
|
1793
|
+
validateLifecycleOptions(options, "destroy-browser");
|
|
1794
|
+
if (this.destroyPromise === void 0) {
|
|
1795
|
+
this.handleState = "destroying";
|
|
1796
|
+
this.control.destroy();
|
|
1797
|
+
const activeStop = this.stopPromise;
|
|
1798
|
+
const promise = (async () => {
|
|
1799
|
+
if (activeStop !== void 0) await activeStop.catch(() => void 0);
|
|
1800
|
+
await this.performDestroy();
|
|
1801
|
+
})();
|
|
1802
|
+
this.destroyPromise = promise;
|
|
1803
|
+
void promise.then(
|
|
1804
|
+
() => {
|
|
1805
|
+
if (this.destroyPromise === promise) this.destroyPromise = void 0;
|
|
1806
|
+
},
|
|
1807
|
+
() => {
|
|
1808
|
+
if (this.destroyPromise === promise) this.destroyPromise = void 0;
|
|
1809
|
+
}
|
|
1810
|
+
);
|
|
272
1811
|
}
|
|
273
|
-
|
|
274
|
-
lease.resetExpiration(options.leaseTimeoutMs);
|
|
275
|
-
return this.snapshot();
|
|
1812
|
+
return waitForSharedLifecycle(this.destroyPromise, options, "destroy-browser");
|
|
276
1813
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
this.activeLease = void 0;
|
|
280
|
-
lease.markReleased();
|
|
1814
|
+
async [Symbol.asyncDispose]() {
|
|
1815
|
+
await this.destroy();
|
|
281
1816
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
1817
|
+
async performStop(options) {
|
|
1818
|
+
const bounded = boundedSignal(options, defaultLifecycleTimeoutMs, "stop-browser");
|
|
1819
|
+
try {
|
|
1820
|
+
await this.abortOwnedOperations(
|
|
1821
|
+
new BrowserError("Browser runtime is stopping.", "connection_closed", {
|
|
1822
|
+
phase: "stop-browser"
|
|
1823
|
+
})
|
|
1824
|
+
);
|
|
1825
|
+
const disconnectFailures = await this.disconnectAll({ abortSignal: bounded.signal });
|
|
1826
|
+
await this.sandbox.stop({ abortSignal: bounded.signal });
|
|
1827
|
+
if (this.handleState === "stopping") this.handleState = "stopped";
|
|
1828
|
+
bounded.throwIfAborted();
|
|
1829
|
+
if (disconnectFailures.length > 0) {
|
|
1830
|
+
throw new BrowserError(
|
|
1831
|
+
"Browser stopped, but one or more automation workers failed to disconnect.",
|
|
1832
|
+
"transport_failure",
|
|
1833
|
+
{
|
|
1834
|
+
cause: new AggregateError(disconnectFailures, "Automation worker cleanup failed."),
|
|
1835
|
+
phase: "stop-browser",
|
|
1836
|
+
recovery: "restart"
|
|
1837
|
+
}
|
|
1838
|
+
);
|
|
291
1839
|
}
|
|
292
|
-
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
if (this.handleState === "stopping") this.handleState = "error";
|
|
1842
|
+
throw bounded.normalize(error, "Unable to stop browser runtime.", "transport_failure");
|
|
1843
|
+
} finally {
|
|
1844
|
+
bounded.dispose();
|
|
293
1845
|
}
|
|
294
1846
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
1847
|
+
async performDestroy() {
|
|
1848
|
+
try {
|
|
1849
|
+
await this.abortOwnedOperations(
|
|
1850
|
+
new BrowserError("Browser runtime was destroyed.", "runtime_destroyed", {
|
|
1851
|
+
phase: "destroy-browser"
|
|
1852
|
+
})
|
|
1853
|
+
);
|
|
1854
|
+
const disconnectFailures = await this.disconnectAll({});
|
|
1855
|
+
await this.sandbox.destroy();
|
|
1856
|
+
this.handleState = "destroyed";
|
|
1857
|
+
if (disconnectFailures.length > 0) {
|
|
1858
|
+
throw new BrowserError(
|
|
1859
|
+
"Browser was destroyed, but one or more automation workers failed to disconnect.",
|
|
1860
|
+
"transport_failure",
|
|
1861
|
+
{
|
|
1862
|
+
cause: new AggregateError(disconnectFailures, "Automation worker cleanup failed."),
|
|
1863
|
+
phase: "destroy-browser",
|
|
1864
|
+
retryable: false,
|
|
1865
|
+
recovery: "none"
|
|
1866
|
+
}
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
if (this.handleState !== "destroyed") this.handleState = "error";
|
|
1871
|
+
if (error instanceof BrowserError) throw error;
|
|
1872
|
+
throw new BrowserError("Unable to destroy browser runtime.", "transport_failure", {
|
|
1873
|
+
cause: error,
|
|
1874
|
+
phase: "destroy-browser",
|
|
1875
|
+
recovery: "retry"
|
|
1876
|
+
});
|
|
304
1877
|
}
|
|
305
|
-
pending.resolve();
|
|
306
1878
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
timer;
|
|
314
|
-
released = false;
|
|
315
|
-
constructor(options) {
|
|
316
|
-
this.owner = options.owner;
|
|
317
|
-
this.ownerId = options.ownerId;
|
|
318
|
-
this.resetExpiration(options.leaseTimeoutMs);
|
|
319
|
-
}
|
|
320
|
-
get expiresAt() {
|
|
321
|
-
return new Date(this.expiration).toISOString();
|
|
322
|
-
}
|
|
323
|
-
renew(options) {
|
|
324
|
-
if (this.released) {
|
|
325
|
-
throw new BrowserError("Browser human control lease is no longer active.", "invalid_state");
|
|
1879
|
+
assertRunning() {
|
|
1880
|
+
if (this.handleState === "destroyed" || this.handleState === "destroying") {
|
|
1881
|
+
throw new BrowserError("Browser runtime is destroyed.", "runtime_destroyed");
|
|
1882
|
+
}
|
|
1883
|
+
if (this.handleState !== "active" || this.state !== "running") {
|
|
1884
|
+
throw new BrowserError(`Browser is not running: ${this.state}`, "invalid_state");
|
|
326
1885
|
}
|
|
327
|
-
return this.owner.renewLease(this, options);
|
|
328
1886
|
}
|
|
329
|
-
|
|
330
|
-
this.
|
|
1887
|
+
lifecycleError(message) {
|
|
1888
|
+
return this.handleState === "destroying" || this.handleState === "destroyed" ? new BrowserError(message, "runtime_destroyed") : new BrowserError(message, "connection_closed", { phase: "connect" });
|
|
331
1889
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
this.timer = void 0;
|
|
1890
|
+
async abortOwnedOperations(error) {
|
|
1891
|
+
const operations = [...this.ownedOperations];
|
|
1892
|
+
for (const operation of operations) operation.controller.abort(error);
|
|
1893
|
+
await Promise.allSettled(operations.map((operation) => operation.promise));
|
|
337
1894
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
this.
|
|
341
|
-
|
|
342
|
-
|
|
1895
|
+
async disconnectAll(options) {
|
|
1896
|
+
const connections = [...this.connections];
|
|
1897
|
+
this.connections.clear();
|
|
1898
|
+
const results = await Promise.allSettled(
|
|
1899
|
+
connections.map((connection) => connection.disconnect(options))
|
|
1900
|
+
);
|
|
1901
|
+
return results.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
1902
|
+
}
|
|
1903
|
+
setCapability(capability, state, error) {
|
|
1904
|
+
this.capabilityStates.set(
|
|
1905
|
+
capability,
|
|
1906
|
+
Object.freeze({
|
|
1907
|
+
capability,
|
|
1908
|
+
state,
|
|
1909
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1910
|
+
...error === void 0 ? {} : { error }
|
|
1911
|
+
})
|
|
1912
|
+
);
|
|
343
1913
|
}
|
|
344
|
-
|
|
345
|
-
this.
|
|
1914
|
+
updateControlAvailability() {
|
|
1915
|
+
if (this.handleState !== "active" || this.state !== "running") {
|
|
1916
|
+
this.control.setAvailability("disconnected");
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
const failed = capabilities.some(
|
|
1920
|
+
(capability) => this.capabilityStates.get(capability)?.state === "failed"
|
|
1921
|
+
);
|
|
1922
|
+
this.control.setAvailability(failed ? "degraded" : "available");
|
|
346
1923
|
}
|
|
347
1924
|
};
|
|
348
|
-
function
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
1925
|
+
function validateReadinessOptions(options) {
|
|
1926
|
+
assertOptionsObject2(options);
|
|
1927
|
+
assertPositiveSafeInteger4(options.timeoutMs, "timeoutMs");
|
|
1928
|
+
if (options.capabilities === void 0) return;
|
|
1929
|
+
if (!Array.isArray(options.capabilities) || options.capabilities.length === 0) {
|
|
1930
|
+
throw new TypeError("capabilities must be a non-empty array.");
|
|
1931
|
+
}
|
|
1932
|
+
const unique = /* @__PURE__ */ new Set();
|
|
1933
|
+
for (const capability of options.capabilities) {
|
|
1934
|
+
if (!capabilities.includes(capability)) {
|
|
1935
|
+
throw new TypeError(`Unsupported browser capability: ${capability}`);
|
|
1936
|
+
}
|
|
1937
|
+
if (unique.has(capability)) throw new TypeError(`Duplicate browser capability: ${capability}`);
|
|
1938
|
+
unique.add(capability);
|
|
358
1939
|
}
|
|
359
1940
|
}
|
|
360
|
-
function isRecord(value) {
|
|
361
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
362
|
-
}
|
|
363
1941
|
|
|
364
|
-
// src/docker-browser.ts
|
|
365
|
-
var cdpPort = 9222;
|
|
366
|
-
var noVncPort = 6080;
|
|
1942
|
+
// src/docker-browser-client.ts
|
|
367
1943
|
var browserSchema = "1";
|
|
368
1944
|
var defaultSharedMemoryMb = 1024;
|
|
369
1945
|
var seccompProfilePath = fileURLToPath(
|
|
@@ -373,7 +1949,7 @@ var DockerBrowserClient = class {
|
|
|
373
1949
|
sandboxClient;
|
|
374
1950
|
image;
|
|
375
1951
|
constructor(options) {
|
|
376
|
-
if (!
|
|
1952
|
+
if (!isRecord3(options)) throw new TypeError("options must be an object.");
|
|
377
1953
|
if (!isSandboxClient(options.sandboxClient)) {
|
|
378
1954
|
throw new TypeError("sandboxClient must be a DockerSandboxClient.");
|
|
379
1955
|
}
|
|
@@ -382,16 +1958,20 @@ var DockerBrowserClient = class {
|
|
|
382
1958
|
this.image = options.image;
|
|
383
1959
|
}
|
|
384
1960
|
async pullImage(options = {}) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
1961
|
+
assertOptionsObject2(options);
|
|
1962
|
+
const bounded = boundedSignal(options, defaultLifecycleTimeoutMs, "pull-image");
|
|
1963
|
+
try {
|
|
1964
|
+
await this.sandboxClient.pullImage({ image: this.image, abortSignal: bounded.signal });
|
|
1965
|
+
bounded.throwIfAborted();
|
|
1966
|
+
} catch (error) {
|
|
1967
|
+
throw bounded.normalize(error, "Unable to pull the browser image.", "startup_failed");
|
|
1968
|
+
} finally {
|
|
1969
|
+
bounded.dispose();
|
|
389
1970
|
}
|
|
390
|
-
await this.sandboxClient.pullImage(pullOptions);
|
|
391
1971
|
}
|
|
392
1972
|
async createBrowser(options) {
|
|
393
1973
|
validateCreateOptions(options);
|
|
394
|
-
options
|
|
1974
|
+
const bounded = boundedSignal(options, defaultLifecycleTimeoutMs, "create-browser");
|
|
395
1975
|
const resources = {
|
|
396
1976
|
...options.resources,
|
|
397
1977
|
sharedMemoryMb: options.resources?.sharedMemoryMb ?? defaultSharedMemoryMb
|
|
@@ -402,252 +1982,122 @@ var DockerBrowserClient = class {
|
|
|
402
1982
|
workspace: options.workspace,
|
|
403
1983
|
network: { mode: "bridge", ports: [cdpPort, noVncPort] },
|
|
404
1984
|
user: "pwuser",
|
|
405
|
-
labels: {
|
|
406
|
-
"anvia.browser.schema": browserSchema
|
|
407
|
-
},
|
|
1985
|
+
labels: { "anvia.browser.schema": browserSchema },
|
|
408
1986
|
resources,
|
|
409
1987
|
security: {
|
|
410
1988
|
noNewPrivileges: true,
|
|
411
1989
|
dropCapabilities: ["ALL"],
|
|
412
1990
|
addCapabilities: ["SYS_CHROOT"],
|
|
413
1991
|
seccompProfile: { type: "path", path: seccompProfilePath }
|
|
414
|
-
}
|
|
1992
|
+
},
|
|
1993
|
+
abortSignal: bounded.signal
|
|
415
1994
|
};
|
|
416
1995
|
if (options.id !== void 0) sandboxOptions = { ...sandboxOptions, id: options.id };
|
|
417
1996
|
if (options.runtime !== void 0) {
|
|
418
1997
|
sandboxOptions = { ...sandboxOptions, runtime: options.runtime };
|
|
419
1998
|
}
|
|
420
|
-
|
|
421
|
-
sandboxOptions = { ...sandboxOptions, abortSignal: options.abortSignal };
|
|
422
|
-
}
|
|
423
|
-
const sandbox = await this.sandboxClient.createSandbox(sandboxOptions);
|
|
1999
|
+
let sandbox;
|
|
424
2000
|
try {
|
|
425
|
-
await
|
|
426
|
-
await
|
|
2001
|
+
sandbox = await this.sandboxClient.createSandbox(sandboxOptions);
|
|
2002
|
+
await configureBrowser(sandbox, options, bounded.signal);
|
|
2003
|
+
await startBrowserServices(sandbox, bounded.signal);
|
|
2004
|
+
bounded.throwIfAborted();
|
|
427
2005
|
return new DockerBrowserHandle(sandbox);
|
|
428
2006
|
} catch (error) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
})
|
|
2007
|
+
let cleanupError;
|
|
2008
|
+
try {
|
|
2009
|
+
await sandbox?.destroy();
|
|
2010
|
+
} catch (caught) {
|
|
2011
|
+
cleanupError = caught;
|
|
2012
|
+
}
|
|
2013
|
+
throw bounded.normalize(
|
|
2014
|
+
combineCleanupError2(error, cleanupError, "Browser sandbox cleanup also failed."),
|
|
2015
|
+
"Unable to configure the browser sandbox.",
|
|
2016
|
+
"startup_failed"
|
|
2017
|
+
);
|
|
2018
|
+
} finally {
|
|
2019
|
+
bounded.dispose();
|
|
433
2020
|
}
|
|
434
2021
|
}
|
|
435
2022
|
async resumeBrowser(options) {
|
|
436
|
-
|
|
2023
|
+
assertOptionsObject2(options);
|
|
437
2024
|
assertNonEmptyString(options.id, "id");
|
|
438
|
-
options
|
|
439
|
-
|
|
2025
|
+
const bounded = boundedSignal(options, defaultLifecycleTimeoutMs, "resume-browser");
|
|
2026
|
+
let sandbox;
|
|
440
2027
|
try {
|
|
441
|
-
await
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
} catch (error) {
|
|
445
|
-
await sandbox.stop().catch(() => void 0);
|
|
446
|
-
throw new BrowserError("Unable to resume browser services.", "startup_failed", {
|
|
447
|
-
cause: error
|
|
2028
|
+
sandbox = await this.sandboxClient.resumeSandbox({
|
|
2029
|
+
id: options.id,
|
|
2030
|
+
abortSignal: bounded.signal
|
|
448
2031
|
});
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
id;
|
|
454
|
-
sandbox;
|
|
455
|
-
desktop;
|
|
456
|
-
control = new BrowserControlState();
|
|
457
|
-
connections = /* @__PURE__ */ new Set();
|
|
458
|
-
constructor(sandbox) {
|
|
459
|
-
this.sandbox = sandbox;
|
|
460
|
-
this.id = sandbox.id;
|
|
461
|
-
this.desktop = Object.freeze({
|
|
462
|
-
protocol: "novnc",
|
|
463
|
-
containerPort: noVncPort,
|
|
464
|
-
control: this.control
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
get state() {
|
|
468
|
-
return this.sandbox.state;
|
|
469
|
-
}
|
|
470
|
-
inspector(options) {
|
|
471
|
-
return this.sandbox.inspector(options);
|
|
472
|
-
}
|
|
473
|
-
async waitUntilReady(options) {
|
|
474
|
-
assertOptionsObject(options);
|
|
475
|
-
assertPositiveSafeInteger2(options.timeoutMs, "timeoutMs");
|
|
476
|
-
this.assertRunning();
|
|
477
|
-
const timeout = AbortSignal.timeout(options.timeoutMs);
|
|
478
|
-
const abortSignal = options.abortSignal === void 0 ? timeout : AbortSignal.any([options.abortSignal, timeout]);
|
|
479
|
-
try {
|
|
480
|
-
await Promise.all([
|
|
481
|
-
this.sandbox.runtime.waitForPort({
|
|
482
|
-
containerPort: cdpPort,
|
|
483
|
-
timeoutMs: options.timeoutMs,
|
|
484
|
-
abortSignal
|
|
485
|
-
}),
|
|
486
|
-
this.sandbox.runtime.waitForPort({
|
|
487
|
-
containerPort: noVncPort,
|
|
488
|
-
timeoutMs: options.timeoutMs,
|
|
489
|
-
abortSignal
|
|
490
|
-
})
|
|
491
|
-
]);
|
|
492
|
-
await Promise.all([
|
|
493
|
-
assertHttpReady(`${endpointFor(this.sandbox, cdpPort)}/json/version`, abortSignal),
|
|
494
|
-
assertHttpReady(`${endpointFor(this.sandbox, noVncPort)}/vnc.html`, abortSignal)
|
|
495
|
-
]);
|
|
2032
|
+
await assertBrowserImage(sandbox, bounded.signal);
|
|
2033
|
+
await startBrowserServices(sandbox, bounded.signal);
|
|
2034
|
+
bounded.throwIfAborted();
|
|
2035
|
+
return new DockerBrowserHandle(sandbox);
|
|
496
2036
|
} catch (error) {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
})
|
|
501
|
-
|
|
502
|
-
}
|
|
503
|
-
async connect(options = {}) {
|
|
504
|
-
assertOptionsObject(options);
|
|
505
|
-
this.assertRunning();
|
|
506
|
-
try {
|
|
507
|
-
let connectOptions = {
|
|
508
|
-
endpointUrl: endpointFor(this.sandbox, cdpPort),
|
|
509
|
-
control: this.control
|
|
510
|
-
};
|
|
511
|
-
if (options.abortSignal !== void 0) {
|
|
512
|
-
connectOptions = { ...connectOptions, abortSignal: options.abortSignal };
|
|
2037
|
+
let cleanupError;
|
|
2038
|
+
try {
|
|
2039
|
+
await sandbox?.stop();
|
|
2040
|
+
} catch (caught) {
|
|
2041
|
+
cleanupError = caught;
|
|
513
2042
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
async stop(options = {}) {
|
|
525
|
-
assertOptionsObject(options);
|
|
526
|
-
await this.disconnectAll();
|
|
527
|
-
await this.sandbox.stop(options);
|
|
528
|
-
}
|
|
529
|
-
async destroy() {
|
|
530
|
-
this.control.destroy();
|
|
531
|
-
await this.disconnectAll();
|
|
532
|
-
await this.sandbox.destroy();
|
|
533
|
-
}
|
|
534
|
-
async [Symbol.asyncDispose]() {
|
|
535
|
-
await this.destroy();
|
|
536
|
-
}
|
|
537
|
-
assertRunning() {
|
|
538
|
-
if (this.state !== "running") {
|
|
539
|
-
throw new BrowserError(`Browser is not running: ${this.state}`, "invalid_state");
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
async disconnectAll() {
|
|
543
|
-
const connections = [...this.connections];
|
|
544
|
-
this.connections.clear();
|
|
545
|
-
const results = await Promise.allSettled(
|
|
546
|
-
connections.map((connection) => connection.disconnect())
|
|
547
|
-
);
|
|
548
|
-
const failures = results.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
549
|
-
if (failures.length > 0) {
|
|
550
|
-
throw new AggregateError(failures, "Unable to disconnect browser automation clients.");
|
|
2043
|
+
throw bounded.normalize(
|
|
2044
|
+
combineCleanupError2(error, cleanupError, "Resumed browser cleanup also failed."),
|
|
2045
|
+
"Unable to resume browser services.",
|
|
2046
|
+
"startup_failed"
|
|
2047
|
+
);
|
|
2048
|
+
} finally {
|
|
2049
|
+
bounded.dispose();
|
|
551
2050
|
}
|
|
552
2051
|
}
|
|
553
2052
|
};
|
|
554
|
-
async function configureBrowser(sandbox, options) {
|
|
555
|
-
|
|
2053
|
+
async function configureBrowser(sandbox, options, abortSignal) {
|
|
2054
|
+
const result = await sandbox.runtime.exec({
|
|
556
2055
|
command: "/usr/local/bin/anvia-browser-configure",
|
|
557
2056
|
input: JSON.stringify({
|
|
558
2057
|
password: options.desktop.password,
|
|
559
2058
|
width: options.desktop.viewport.width,
|
|
560
2059
|
height: options.desktop.viewport.height
|
|
561
|
-
})
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
execOptions = { ...execOptions, abortSignal: options.abortSignal };
|
|
565
|
-
}
|
|
566
|
-
const result = await sandbox.runtime.exec(execOptions);
|
|
2060
|
+
}),
|
|
2061
|
+
abortSignal
|
|
2062
|
+
});
|
|
567
2063
|
if (result.status !== "exited" || result.exitCode !== 0) {
|
|
568
2064
|
throw new Error("Browser image rejected its runtime configuration.");
|
|
569
2065
|
}
|
|
570
2066
|
}
|
|
571
2067
|
async function assertBrowserImage(sandbox, abortSignal) {
|
|
572
|
-
|
|
573
|
-
command: "/usr/local/bin/anvia-browser-version"
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const result = await sandbox.runtime.exec(execOptions);
|
|
2068
|
+
const result = await sandbox.runtime.exec({
|
|
2069
|
+
command: "/usr/local/bin/anvia-browser-version",
|
|
2070
|
+
abortSignal
|
|
2071
|
+
});
|
|
577
2072
|
if (result.status !== "exited" || result.exitCode !== 0 || new TextDecoder("utf-8", { fatal: true }).decode(result.stdout).trim() !== browserSchema) {
|
|
578
2073
|
throw new Error("Sandbox does not contain a compatible Anvia browser image.");
|
|
579
2074
|
}
|
|
580
2075
|
}
|
|
581
2076
|
async function startBrowserServices(sandbox, abortSignal) {
|
|
582
|
-
|
|
583
|
-
command: "/usr/local/bin/anvia-browser-start"
|
|
584
|
-
|
|
585
|
-
if (abortSignal !== void 0) startOptions = { ...startOptions, abortSignal };
|
|
586
|
-
await sandbox.runtime.startProcess(startOptions);
|
|
587
|
-
}
|
|
588
|
-
async function assertHttpReady(url, abortSignal) {
|
|
589
|
-
while (true) {
|
|
590
|
-
abortSignal.throwIfAborted();
|
|
591
|
-
try {
|
|
592
|
-
const response = await fetch(url, { signal: abortSignal, redirect: "error" });
|
|
593
|
-
if (response.ok) {
|
|
594
|
-
await response.body?.cancel();
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
await response.body?.cancel();
|
|
598
|
-
} catch (error) {
|
|
599
|
-
if (abortSignal.aborted) throw abortSignal.reason ?? error;
|
|
600
|
-
}
|
|
601
|
-
await waitForRetry(abortSignal);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
async function waitForRetry(abortSignal) {
|
|
605
|
-
await new Promise((resolve, reject) => {
|
|
606
|
-
const timeout = setTimeout(finish, 50);
|
|
607
|
-
const abort = () => finish(abortSignal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
608
|
-
abortSignal.addEventListener("abort", abort, { once: true });
|
|
609
|
-
function finish(error) {
|
|
610
|
-
clearTimeout(timeout);
|
|
611
|
-
abortSignal.removeEventListener("abort", abort);
|
|
612
|
-
if (error === void 0) resolve();
|
|
613
|
-
else reject(error);
|
|
614
|
-
}
|
|
2077
|
+
await sandbox.runtime.startProcess({
|
|
2078
|
+
command: "/usr/local/bin/anvia-browser-start",
|
|
2079
|
+
abortSignal
|
|
615
2080
|
});
|
|
616
2081
|
}
|
|
617
|
-
function endpointFor(sandbox, containerPort) {
|
|
618
|
-
const published = publishedPort(sandbox, containerPort);
|
|
619
|
-
return `http://${hostForUrl(published.host)}:${published.hostPort}`;
|
|
620
|
-
}
|
|
621
|
-
function publishedPort(sandbox, containerPort) {
|
|
622
|
-
const port = sandbox.runtime.publishedPorts.find(
|
|
623
|
-
(candidate) => candidate.containerPort === containerPort && candidate.protocol === "tcp"
|
|
624
|
-
);
|
|
625
|
-
if (port === void 0) {
|
|
626
|
-
throw new BrowserError(`Browser port is not published: ${containerPort}`, "invalid_state");
|
|
627
|
-
}
|
|
628
|
-
return port;
|
|
629
|
-
}
|
|
630
|
-
function hostForUrl(host) {
|
|
631
|
-
return host.includes(":") ? `[${host}]` : host;
|
|
632
|
-
}
|
|
633
2082
|
function validateCreateOptions(options) {
|
|
634
|
-
|
|
2083
|
+
assertOptionsObject2(options);
|
|
635
2084
|
if (options.id !== void 0) assertNonEmptyString(options.id, "id");
|
|
636
|
-
if (!
|
|
637
|
-
if (!
|
|
2085
|
+
if (!isRecord3(options.workspace)) throw new TypeError("workspace must be an object.");
|
|
2086
|
+
if (!isRecord3(options.network) || options.network.mode !== "bridge") {
|
|
638
2087
|
throw new TypeError('network must be { mode: "bridge" }.');
|
|
639
2088
|
}
|
|
640
|
-
if (!
|
|
2089
|
+
if (!isRecord3(options.desktop) || options.desktop.protocol !== "novnc") {
|
|
641
2090
|
throw new TypeError('desktop must use protocol: "novnc".');
|
|
642
2091
|
}
|
|
643
2092
|
if (!/^[\x20-\x7e]{8}$/.test(options.desktop.password)) {
|
|
644
2093
|
throw new TypeError("desktop.password must contain exactly 8 printable ASCII characters.");
|
|
645
2094
|
}
|
|
646
|
-
if (!
|
|
2095
|
+
if (!isRecord3(options.desktop.viewport)) {
|
|
647
2096
|
throw new TypeError("desktop.viewport must be an object.");
|
|
648
2097
|
}
|
|
649
2098
|
assertIntegerInRange(options.desktop.viewport.width, "desktop.viewport.width", 640, 3840);
|
|
650
2099
|
assertIntegerInRange(options.desktop.viewport.height, "desktop.viewport.height", 480, 2160);
|
|
2100
|
+
if (options.timeoutMs !== void 0) assertPositiveSafeInteger4(options.timeoutMs, "timeoutMs");
|
|
651
2101
|
if (options.runtime?.maxProcesses !== void 0 && options.runtime.maxProcesses < 1) {
|
|
652
2102
|
throw new RangeError("runtime.maxProcesses must allow the browser service process.");
|
|
653
2103
|
}
|
|
@@ -657,24 +2107,16 @@ function assertIntegerInRange(value, name, min, max) {
|
|
|
657
2107
|
throw new RangeError(`${name} must be a safe integer between ${min} and ${max}.`);
|
|
658
2108
|
}
|
|
659
2109
|
}
|
|
660
|
-
function assertPositiveSafeInteger2(value, name) {
|
|
661
|
-
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
662
|
-
throw new RangeError(`${name} must be a positive safe integer.`);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
2110
|
function assertNonEmptyString(value, name) {
|
|
666
2111
|
if (typeof value !== "string" || value.length === 0) {
|
|
667
2112
|
throw new TypeError(`${name} must be a non-empty string.`);
|
|
668
2113
|
}
|
|
669
2114
|
}
|
|
670
|
-
function assertOptionsObject(value) {
|
|
671
|
-
if (!isRecord2(value)) throw new TypeError("options must be an object.");
|
|
672
|
-
}
|
|
673
2115
|
function isSandboxClient(value) {
|
|
674
|
-
return
|
|
2116
|
+
return isRecord3(value) && typeof value.pullImage === "function" && typeof value.createSandbox === "function" && typeof value.resumeSandbox === "function";
|
|
675
2117
|
}
|
|
676
|
-
function
|
|
677
|
-
return
|
|
2118
|
+
function combineCleanupError2(error, cleanupError, message) {
|
|
2119
|
+
return cleanupError === void 0 ? error : new AggregateError([error, cleanupError], message);
|
|
678
2120
|
}
|
|
679
2121
|
|
|
680
2122
|
// src/tools.ts
|
|
@@ -699,15 +2141,18 @@ var targetSchema = z.discriminatedUnion("by", [
|
|
|
699
2141
|
]);
|
|
700
2142
|
var noInput = z.object({});
|
|
701
2143
|
var tabInput = z.object({ tabId: z.string().uuid() });
|
|
2144
|
+
var explicitTab = { tabId: z.string().uuid().optional() };
|
|
702
2145
|
var navigateInput = z.object({
|
|
2146
|
+
...explicitTab,
|
|
703
2147
|
url: z.url(),
|
|
704
2148
|
waitUntil: z.enum(["commit", "domcontentloaded", "load", "networkidle"]).optional()
|
|
705
2149
|
});
|
|
706
|
-
var clickInput = z.object({ target: targetSchema });
|
|
707
|
-
var typeInput = z.object({ target: targetSchema, text: z.string() });
|
|
708
|
-
var pressKeyInput = z.object({ key: z.string().min(1).max(100) });
|
|
709
|
-
var screenshotInput = z.object(
|
|
710
|
-
var
|
|
2150
|
+
var clickInput = z.object({ ...explicitTab, target: targetSchema });
|
|
2151
|
+
var typeInput = z.object({ ...explicitTab, target: targetSchema, text: z.string() });
|
|
2152
|
+
var pressKeyInput = z.object({ ...explicitTab, key: z.string().min(1).max(100) });
|
|
2153
|
+
var screenshotInput = z.object(explicitTab);
|
|
2154
|
+
var snapshotInput = z.object(explicitTab);
|
|
2155
|
+
var defaultActionTimeoutMs2 = 1e4;
|
|
711
2156
|
var defaultNavigationTimeoutMs = 3e4;
|
|
712
2157
|
var defaultSnapshotMaxChars = 5e4;
|
|
713
2158
|
var maxTimeoutMs = 3e5;
|
|
@@ -718,20 +2163,20 @@ function createBrowserTools(options) {
|
|
|
718
2163
|
const policy = snapshotNavigationPolicy(options.navigation);
|
|
719
2164
|
connection.setNavigationPolicy(policy);
|
|
720
2165
|
const limits = {
|
|
721
|
-
actionTimeoutMs: options.limits?.actionTimeoutMs ??
|
|
2166
|
+
actionTimeoutMs: options.limits?.actionTimeoutMs ?? defaultActionTimeoutMs2,
|
|
722
2167
|
navigationTimeoutMs: options.limits?.navigationTimeoutMs ?? defaultNavigationTimeoutMs,
|
|
723
2168
|
snapshotMaxChars: options.limits?.snapshotMaxChars ?? defaultSnapshotMaxChars
|
|
724
2169
|
};
|
|
725
2170
|
const tools = options.tools.map((name) => {
|
|
726
2171
|
switch (name) {
|
|
727
2172
|
case "browser_list_tabs":
|
|
728
|
-
return createListTabsTool(connection);
|
|
2173
|
+
return createListTabsTool(connection, limits.actionTimeoutMs);
|
|
729
2174
|
case "browser_open_tab":
|
|
730
|
-
return createOpenTabTool(connection);
|
|
2175
|
+
return createOpenTabTool(connection, limits.actionTimeoutMs);
|
|
731
2176
|
case "browser_select_tab":
|
|
732
|
-
return createSelectTabTool(connection);
|
|
2177
|
+
return createSelectTabTool(connection, limits.actionTimeoutMs);
|
|
733
2178
|
case "browser_close_tab":
|
|
734
|
-
return createCloseTabTool(connection);
|
|
2179
|
+
return createCloseTabTool(connection, limits.actionTimeoutMs);
|
|
735
2180
|
case "browser_navigate":
|
|
736
2181
|
return createNavigateTool(connection, policy, limits.navigationTimeoutMs);
|
|
737
2182
|
case "browser_snapshot":
|
|
@@ -741,185 +2186,174 @@ function createBrowserTools(options) {
|
|
|
741
2186
|
case "browser_type":
|
|
742
2187
|
return createTypeTool(connection, limits.actionTimeoutMs);
|
|
743
2188
|
case "browser_press_key":
|
|
744
|
-
return createPressKeyTool(connection);
|
|
2189
|
+
return createPressKeyTool(connection, limits.actionTimeoutMs);
|
|
745
2190
|
case "browser_screenshot":
|
|
746
|
-
return createScreenshotTool(connection);
|
|
2191
|
+
return createScreenshotTool(connection, limits.actionTimeoutMs);
|
|
747
2192
|
default:
|
|
748
2193
|
return assertNever(name);
|
|
749
2194
|
}
|
|
750
2195
|
});
|
|
751
2196
|
return Object.freeze(tools);
|
|
752
2197
|
}
|
|
753
|
-
function createListTabsTool(connection) {
|
|
2198
|
+
function createListTabsTool(connection, timeoutMs) {
|
|
754
2199
|
return createTool({
|
|
755
2200
|
name: "browser_list_tabs",
|
|
756
2201
|
description: "List Chromium tabs and identify the tab selected for subsequent browser tools.",
|
|
757
2202
|
inputSchema: noInput,
|
|
758
|
-
execute: async () => ({
|
|
2203
|
+
execute: async (_args, context) => ({
|
|
2204
|
+
tabs: await connection.listTabs({ abortSignal: context.abortSignal, timeoutMs })
|
|
2205
|
+
})
|
|
759
2206
|
});
|
|
760
2207
|
}
|
|
761
|
-
function createOpenTabTool(connection) {
|
|
2208
|
+
function createOpenTabTool(connection, timeoutMs) {
|
|
762
2209
|
return createTool({
|
|
763
2210
|
name: "browser_open_tab",
|
|
764
2211
|
description: "Open and select a new blank Chromium tab.",
|
|
765
2212
|
inputSchema: noInput,
|
|
766
|
-
execute: async (_args, context) => connection.
|
|
767
|
-
const page = await connection.openTab();
|
|
768
|
-
return tabResult(connection, page);
|
|
769
|
-
})
|
|
2213
|
+
execute: async (_args, context) => connection.openTab(context.abortSignal, timeoutMs)
|
|
770
2214
|
});
|
|
771
2215
|
}
|
|
772
|
-
function createSelectTabTool(connection) {
|
|
2216
|
+
function createSelectTabTool(connection, timeoutMs) {
|
|
773
2217
|
return createTool({
|
|
774
2218
|
name: "browser_select_tab",
|
|
775
2219
|
description: "Select an existing Chromium tab by its browser tab ID.",
|
|
776
2220
|
inputSchema: tabInput,
|
|
777
|
-
execute: async ({ tabId }, context) => connection.
|
|
778
|
-
context.abortSignal,
|
|
779
|
-
async () => tabResult(connection, connection.selectTab(tabId))
|
|
780
|
-
)
|
|
2221
|
+
execute: async ({ tabId }, context) => connection.selectTab(tabId, context.abortSignal, timeoutMs)
|
|
781
2222
|
});
|
|
782
2223
|
}
|
|
783
|
-
function createCloseTabTool(connection) {
|
|
2224
|
+
function createCloseTabTool(connection, timeoutMs) {
|
|
784
2225
|
return createTool({
|
|
785
2226
|
name: "browser_close_tab",
|
|
786
2227
|
description: "Close an existing Chromium tab by its browser tab ID.",
|
|
787
2228
|
inputSchema: tabInput,
|
|
788
|
-
execute: async ({ tabId }, context) => connection.
|
|
789
|
-
await connection.closeTab(tabId);
|
|
790
|
-
return { closedTabId: tabId, tabs: await connection.tabSummaries() };
|
|
791
|
-
})
|
|
2229
|
+
execute: async ({ tabId }, context) => connection.closeTab(tabId, context.abortSignal, timeoutMs)
|
|
792
2230
|
});
|
|
793
2231
|
}
|
|
794
2232
|
function createNavigateTool(connection, policy, timeoutMs) {
|
|
795
2233
|
return createTool({
|
|
796
2234
|
name: "browser_navigate",
|
|
797
|
-
description: "Navigate the selected
|
|
2235
|
+
description: "Navigate an explicit tab, or the selected tab in serial compatibility mode, to an allowed HTTP or HTTPS URL.",
|
|
798
2236
|
inputSchema: navigateInput,
|
|
799
|
-
execute: async ({ url, waitUntil }, context) =>
|
|
2237
|
+
execute: async ({ tabId, url, waitUntil }, context) => {
|
|
800
2238
|
assertNavigationAllowed(url, policy);
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
2239
|
+
const result = await connection.runTabCommand({
|
|
2240
|
+
tabId,
|
|
2241
|
+
abortSignal: context.abortSignal,
|
|
2242
|
+
timeoutMs,
|
|
2243
|
+
phase: "navigate",
|
|
2244
|
+
command: (targetTabId) => ({
|
|
2245
|
+
method: "navigate",
|
|
2246
|
+
params: {
|
|
2247
|
+
tabId: targetTabId,
|
|
2248
|
+
url,
|
|
2249
|
+
waitUntil: waitUntil ?? "load",
|
|
2250
|
+
timeoutMs
|
|
2251
|
+
}
|
|
2252
|
+
})
|
|
805
2253
|
});
|
|
806
|
-
assertNavigationAllowed(
|
|
807
|
-
return
|
|
808
|
-
}
|
|
2254
|
+
assertNavigationAllowed(result.url, policy);
|
|
2255
|
+
return result;
|
|
2256
|
+
}
|
|
809
2257
|
});
|
|
810
2258
|
}
|
|
811
2259
|
function createSnapshotTool(connection, timeoutMs, maxChars) {
|
|
812
2260
|
return createTool({
|
|
813
2261
|
name: "browser_snapshot",
|
|
814
|
-
description: "Inspect the selected tab using a bounded ARIA accessibility snapshot.",
|
|
815
|
-
inputSchema:
|
|
816
|
-
execute: async (
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
2262
|
+
description: "Inspect an explicit tab, or the selected tab in serial compatibility mode, using a bounded ARIA accessibility snapshot.",
|
|
2263
|
+
inputSchema: snapshotInput,
|
|
2264
|
+
execute: async ({ tabId }, context) => connection.runTabCommand({
|
|
2265
|
+
tabId,
|
|
2266
|
+
abortSignal: context.abortSignal,
|
|
2267
|
+
timeoutMs,
|
|
2268
|
+
phase: "snapshot",
|
|
2269
|
+
command: (targetTabId) => ({
|
|
2270
|
+
method: "snapshot",
|
|
2271
|
+
params: { tabId: targetTabId, timeoutMs, maxChars }
|
|
2272
|
+
})
|
|
825
2273
|
})
|
|
826
2274
|
});
|
|
827
2275
|
}
|
|
828
2276
|
function createClickTool(connection, timeoutMs) {
|
|
829
2277
|
return createTool({
|
|
830
2278
|
name: "browser_click",
|
|
831
|
-
description: "Click one strictly matched element in the selected tab.",
|
|
2279
|
+
description: "Click one strictly matched element in an explicit tab or the selected tab.",
|
|
832
2280
|
inputSchema: clickInput,
|
|
833
|
-
execute: async ({ target }, context) => connection.
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
2281
|
+
execute: async ({ tabId, target }, context) => connection.runTabCommand({
|
|
2282
|
+
tabId,
|
|
2283
|
+
abortSignal: context.abortSignal,
|
|
2284
|
+
timeoutMs,
|
|
2285
|
+
phase: "click",
|
|
2286
|
+
command: (targetTabId) => ({
|
|
2287
|
+
method: "click",
|
|
2288
|
+
params: { tabId: targetTabId, target, timeoutMs }
|
|
2289
|
+
})
|
|
837
2290
|
})
|
|
838
2291
|
});
|
|
839
2292
|
}
|
|
840
2293
|
function createTypeTool(connection, timeoutMs) {
|
|
841
2294
|
return createTool({
|
|
842
2295
|
name: "browser_type",
|
|
843
|
-
description: "Replace the value of one strictly matched editable element.",
|
|
2296
|
+
description: "Replace the value of one strictly matched editable element in an explicit or selected tab.",
|
|
844
2297
|
inputSchema: typeInput,
|
|
845
|
-
execute: async ({ target, text }, context) => connection.
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
2298
|
+
execute: async ({ tabId, target, text }, context) => connection.runTabCommand({
|
|
2299
|
+
tabId,
|
|
2300
|
+
abortSignal: context.abortSignal,
|
|
2301
|
+
timeoutMs,
|
|
2302
|
+
phase: "type",
|
|
2303
|
+
command: (targetTabId) => ({
|
|
2304
|
+
method: "type",
|
|
2305
|
+
params: { tabId: targetTabId, target, text, timeoutMs }
|
|
2306
|
+
})
|
|
849
2307
|
})
|
|
850
2308
|
});
|
|
851
2309
|
}
|
|
852
|
-
function createPressKeyTool(connection) {
|
|
2310
|
+
function createPressKeyTool(connection, timeoutMs) {
|
|
853
2311
|
return createTool({
|
|
854
2312
|
name: "browser_press_key",
|
|
855
|
-
description: "Press an explicit keyboard key or key combination in
|
|
2313
|
+
description: "Press an explicit keyboard key or key combination in an explicit or selected tab.",
|
|
856
2314
|
inputSchema: pressKeyInput,
|
|
857
|
-
execute: async ({ key }, context) => connection.
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
2315
|
+
execute: async ({ tabId, key }, context) => connection.runTabCommand({
|
|
2316
|
+
tabId,
|
|
2317
|
+
abortSignal: context.abortSignal,
|
|
2318
|
+
timeoutMs,
|
|
2319
|
+
phase: "press-key",
|
|
2320
|
+
command: (targetTabId) => ({
|
|
2321
|
+
method: "pressKey",
|
|
2322
|
+
params: { tabId: targetTabId, key }
|
|
2323
|
+
})
|
|
861
2324
|
})
|
|
862
2325
|
});
|
|
863
2326
|
}
|
|
864
|
-
function createScreenshotTool(connection) {
|
|
2327
|
+
function createScreenshotTool(connection, timeoutMs) {
|
|
865
2328
|
return createTool({
|
|
866
2329
|
name: "browser_screenshot",
|
|
867
|
-
description: "Capture the visible viewport of
|
|
2330
|
+
description: "Capture the visible viewport of an explicit or selected Chromium tab as PNG.",
|
|
868
2331
|
inputSchema: screenshotInput,
|
|
869
|
-
execute: async (
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
2332
|
+
execute: async ({ tabId }, context) => {
|
|
2333
|
+
const result = await connection.runTabCommand({
|
|
2334
|
+
tabId,
|
|
2335
|
+
abortSignal: context.abortSignal,
|
|
2336
|
+
timeoutMs,
|
|
2337
|
+
phase: "screenshot",
|
|
2338
|
+
command: (targetTabId) => ({
|
|
2339
|
+
method: "screenshot",
|
|
2340
|
+
params: { tabId: targetTabId, timeoutMs }
|
|
2341
|
+
})
|
|
2342
|
+
});
|
|
873
2343
|
return ToolOutput.content([
|
|
874
|
-
{ type: "text", text: JSON.stringify(metadata) },
|
|
2344
|
+
{ type: "text", text: JSON.stringify(result.metadata) },
|
|
875
2345
|
{
|
|
876
2346
|
type: "file",
|
|
877
|
-
data: { type: "data", data:
|
|
2347
|
+
data: { type: "data", data: result.pngBase64 },
|
|
878
2348
|
mediaType: "image/png",
|
|
879
2349
|
filename: "browser-screenshot.png"
|
|
880
2350
|
}
|
|
881
2351
|
]);
|
|
882
|
-
})
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
async function tabResult(connection, page) {
|
|
886
|
-
return {
|
|
887
|
-
tabId: connection.idFor(page),
|
|
888
|
-
title: await page.title(),
|
|
889
|
-
url: page.url()
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
function locatorFor(page, target) {
|
|
893
|
-
switch (target.by) {
|
|
894
|
-
case "role": {
|
|
895
|
-
const options = {};
|
|
896
|
-
if (target.name !== void 0) options.name = target.name;
|
|
897
|
-
if (target.exact !== void 0) options.exact = target.exact;
|
|
898
|
-
return page.getByRole(target.role, options);
|
|
899
|
-
}
|
|
900
|
-
case "text": {
|
|
901
|
-
const options = {};
|
|
902
|
-
if (target.exact !== void 0) options.exact = target.exact;
|
|
903
|
-
return page.getByText(target.text, options);
|
|
904
|
-
}
|
|
905
|
-
case "label": {
|
|
906
|
-
const options = {};
|
|
907
|
-
if (target.exact !== void 0) options.exact = target.exact;
|
|
908
|
-
return page.getByLabel(target.label, options);
|
|
909
|
-
}
|
|
910
|
-
case "placeholder": {
|
|
911
|
-
const options = {};
|
|
912
|
-
if (target.exact !== void 0) options.exact = target.exact;
|
|
913
|
-
return page.getByPlaceholder(target.placeholder, options);
|
|
914
2352
|
}
|
|
915
|
-
|
|
916
|
-
return page.getByTestId(target.testId);
|
|
917
|
-
case "css":
|
|
918
|
-
return page.locator(target.selector);
|
|
919
|
-
}
|
|
2353
|
+
});
|
|
920
2354
|
}
|
|
921
2355
|
function snapshotNavigationPolicy(policy) {
|
|
922
|
-
if (!
|
|
2356
|
+
if (!isRecord5(policy)) throw new TypeError("navigation must be an object.");
|
|
923
2357
|
if (policy.mode === "allow-all-http") return Object.freeze({ mode: "allow-all-http" });
|
|
924
2358
|
if (policy.mode !== "origins" || !Array.isArray(policy.origins) || policy.origins.length === 0) {
|
|
925
2359
|
throw new TypeError(
|
|
@@ -941,11 +2375,11 @@ function normalizeOrigin(value) {
|
|
|
941
2375
|
return url.origin;
|
|
942
2376
|
}
|
|
943
2377
|
function assertNavigationAllowed(value, policy) {
|
|
944
|
-
if (!
|
|
2378
|
+
if (!isNavigationAllowed(value, policy)) {
|
|
945
2379
|
throw new BrowserError(`Browser navigation is blocked: ${value}`, "navigation_blocked");
|
|
946
2380
|
}
|
|
947
2381
|
}
|
|
948
|
-
function
|
|
2382
|
+
function isNavigationAllowed(value, policy) {
|
|
949
2383
|
let url;
|
|
950
2384
|
try {
|
|
951
2385
|
url = parseHttpUrl(value);
|
|
@@ -965,7 +2399,7 @@ function parseHttpUrl(value) {
|
|
|
965
2399
|
return url;
|
|
966
2400
|
}
|
|
967
2401
|
function validateFactoryOptions(options) {
|
|
968
|
-
if (!
|
|
2402
|
+
if (!isRecord5(options)) throw new TypeError("options must be an object.");
|
|
969
2403
|
if (!Array.isArray(options.tools) || options.tools.length === 0) {
|
|
970
2404
|
throw new TypeError("tools must be a non-empty array.");
|
|
971
2405
|
}
|
|
@@ -976,7 +2410,7 @@ function validateFactoryOptions(options) {
|
|
|
976
2410
|
seen.add(name);
|
|
977
2411
|
}
|
|
978
2412
|
snapshotNavigationPolicy(options.navigation);
|
|
979
|
-
if (options.limits !== void 0 && !
|
|
2413
|
+
if (options.limits !== void 0 && !isRecord5(options.limits)) {
|
|
980
2414
|
throw new TypeError("limits must be an object.");
|
|
981
2415
|
}
|
|
982
2416
|
assertOptionalBoundedInteger(options.limits?.actionTimeoutMs, "actionTimeoutMs", 1, maxTimeoutMs);
|
|
@@ -1012,7 +2446,7 @@ function isToolName(value) {
|
|
|
1012
2446
|
"browser_screenshot"
|
|
1013
2447
|
].includes(value);
|
|
1014
2448
|
}
|
|
1015
|
-
function
|
|
2449
|
+
function isRecord5(value) {
|
|
1016
2450
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1017
2451
|
}
|
|
1018
2452
|
function assertNever(value) {
|