effect-browserbase 0.1.0-beta.102 → 0.2.0-beta.0

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.
Files changed (130) hide show
  1. package/README.md +61 -231
  2. package/dist/Account.d.mts +4 -6
  3. package/dist/Account.mjs +4 -9
  4. package/dist/Account.mjs.map +1 -1
  5. package/dist/{Acquisition-WBafHwXH.mjs → Acquisition-D8o4jtCg.mjs} +22 -68
  6. package/dist/Acquisition-D8o4jtCg.mjs.map +1 -0
  7. package/dist/Agents.d.mts +4 -6
  8. package/dist/Agents.mjs +3 -19
  9. package/dist/Agents.mjs.map +1 -1
  10. package/dist/Allocation.d.mts +9 -11
  11. package/dist/Allocation.mjs +2 -4
  12. package/dist/Allocation.mjs.map +1 -1
  13. package/dist/Browser.d.mts +25 -140
  14. package/dist/Browser.mjs +266 -1
  15. package/dist/Browser.mjs.map +1 -0
  16. package/dist/BrowserBinding.d.mts +7 -34
  17. package/dist/BrowserBinding.mjs +30 -1
  18. package/dist/BrowserBinding.mjs.map +1 -0
  19. package/dist/Certificates.d.mts +3 -5
  20. package/dist/Certificates.mjs +2 -7
  21. package/dist/Certificates.mjs.map +1 -1
  22. package/dist/{Cleanup-G2YpTCuU.d.mts → Cleanup-DMTDBir8.d.mts} +5 -6
  23. package/dist/Cleanup.d.mts +3 -2
  24. package/dist/Cleanup.mjs +1 -6
  25. package/dist/Cleanup.mjs.map +1 -1
  26. package/dist/{Client-D1_xYM9o.mjs → Client-BQteUePT.mjs} +2 -4
  27. package/dist/{Client-D1_xYM9o.mjs.map → Client-BQteUePT.mjs.map} +1 -1
  28. package/dist/Client.d.mts +51 -2
  29. package/dist/Client.mjs +1 -1
  30. package/dist/Connection-DcvJZEdc.mjs +65 -0
  31. package/dist/Connection-DcvJZEdc.mjs.map +1 -0
  32. package/dist/ContextCoordination.d.mts +5 -6
  33. package/dist/ContextCoordination.mjs +1 -8
  34. package/dist/ContextCoordination.mjs.map +1 -1
  35. package/dist/Contexts.d.mts +4 -6
  36. package/dist/Contexts.mjs +2 -8
  37. package/dist/Contexts.mjs.map +1 -1
  38. package/dist/Downloads.d.mts +6 -8
  39. package/dist/Downloads.mjs +3 -8
  40. package/dist/Downloads.mjs.map +1 -1
  41. package/dist/Errors.d.mts +3 -26
  42. package/dist/Errors.mjs +1 -137
  43. package/dist/Errors.mjs.map +1 -1
  44. package/dist/{Extensions-ClZbYb1H.mjs → Extensions-nMNMPGCc.mjs} +3 -9
  45. package/dist/{Extensions-ClZbYb1H.mjs.map → Extensions-nMNMPGCc.mjs.map} +1 -1
  46. package/dist/Extensions.d.mts +4 -6
  47. package/dist/Extensions.mjs +1 -1
  48. package/dist/Functions.d.mts +4 -6
  49. package/dist/Functions.mjs +3 -17
  50. package/dist/Functions.mjs.map +1 -1
  51. package/dist/{Launch-BHcAEq05.mjs → Launch-m0fxzJ49.mjs} +5 -5
  52. package/dist/Launch-m0fxzJ49.mjs.map +1 -0
  53. package/dist/Launch.d.mts +130 -2
  54. package/dist/Launch.mjs +4 -11
  55. package/dist/Launch.mjs.map +1 -1
  56. package/dist/PageFetch.d.mts +3 -5
  57. package/dist/PageFetch.mjs +2 -8
  58. package/dist/PageFetch.mjs.map +1 -1
  59. package/dist/Projects.d.mts +3 -5
  60. package/dist/Projects.mjs +2 -8
  61. package/dist/Projects.mjs.map +1 -1
  62. package/dist/Recordings.d.mts +6 -8
  63. package/dist/Recordings.mjs +3 -5
  64. package/dist/Recordings.mjs.map +1 -1
  65. package/dist/{References-URDAtapt.d.mts → References-DOH3Wu44.d.mts} +5 -6
  66. package/dist/References.d.mts +3 -2
  67. package/dist/References.mjs +10 -18
  68. package/dist/References.mjs.map +1 -1
  69. package/dist/Replays.d.mts +6 -8
  70. package/dist/Replays.mjs +3 -5
  71. package/dist/Replays.mjs.map +1 -1
  72. package/dist/Search.d.mts +3 -5
  73. package/dist/Search.mjs +2 -9
  74. package/dist/Search.mjs.map +1 -1
  75. package/dist/{SessionData-DRKcKCqw.d.mts → SessionData-5xYZDshJ.d.mts} +5 -6
  76. package/dist/SessionData.d.mts +3 -2
  77. package/dist/SessionData.mjs +1 -8
  78. package/dist/SessionData.mjs.map +1 -1
  79. package/dist/{Sessions-CtbseWEh.mjs → Sessions-DoQGg9nB.mjs} +34 -18
  80. package/dist/Sessions-DoQGg9nB.mjs.map +1 -0
  81. package/dist/{Sessions-B8E4kMxo.d.mts → Sessions-tFJYaXFq.d.mts} +8 -8
  82. package/dist/Sessions.d.mts +4 -3
  83. package/dist/Sessions.mjs +1 -1
  84. package/dist/{Transfers-Czv5IZzJ.d.mts → Transfers-GVqzrDWl.d.mts} +22 -9
  85. package/dist/Transfers.d.mts +3 -2
  86. package/dist/Transfers.mjs +12 -28
  87. package/dist/Transfers.mjs.map +1 -1
  88. package/dist/Uploads.d.mts +6 -8
  89. package/dist/Uploads.mjs +3 -5
  90. package/dist/Uploads.mjs.map +1 -1
  91. package/dist/Webhooks.d.mts +3 -5
  92. package/dist/Webhooks.mjs +2 -11
  93. package/dist/Webhooks.mjs.map +1 -1
  94. package/dist/{WriterFacts-osvYAsIx.d.mts → WriterFacts-5CPBGG1P.d.mts} +4 -3
  95. package/dist/index.d.mts +27 -30
  96. package/dist/index.mjs +26 -30
  97. package/package.json +3 -24
  98. package/dist/Acquisition-WBafHwXH.mjs.map +0 -1
  99. package/dist/Association-CEps4hYP.mjs +0 -10
  100. package/dist/Association-CEps4hYP.mjs.map +0 -1
  101. package/dist/Bootstrap-BXuzZojd.mjs +0 -194
  102. package/dist/Bootstrap-BXuzZojd.mjs.map +0 -1
  103. package/dist/Bootstrap-m7lHgj0D.d.mts +0 -177
  104. package/dist/Bootstrap.d.mts +0 -2
  105. package/dist/Bootstrap.mjs +0 -2
  106. package/dist/Browser-joE4Zgys.mjs +0 -1557
  107. package/dist/Browser-joE4Zgys.mjs.map +0 -1
  108. package/dist/BrowserBinding-qbQA6JhH.mjs +0 -2452
  109. package/dist/BrowserBinding-qbQA6JhH.mjs.map +0 -1
  110. package/dist/BrowserData-LpcAuAub.d.mts +0 -369
  111. package/dist/BrowserData.d.mts +0 -2
  112. package/dist/BrowserData.mjs +0 -442
  113. package/dist/BrowserData.mjs.map +0 -1
  114. package/dist/Capture-CZJPuD7b.mjs +0 -532
  115. package/dist/Capture-CZJPuD7b.mjs.map +0 -1
  116. package/dist/Capture-Dr5MkIJb.d.mts +0 -125
  117. package/dist/Capture.d.mts +0 -2
  118. package/dist/Capture.mjs +0 -2
  119. package/dist/Client-Ca0zSg1n.d.mts +0 -53
  120. package/dist/Images-DgscEfSf.mjs +0 -127
  121. package/dist/Images-DgscEfSf.mjs.map +0 -1
  122. package/dist/Launch-BHcAEq05.mjs.map +0 -1
  123. package/dist/Launch-C8CfaYxb.d.mts +0 -132
  124. package/dist/PageControl.d.mts +0 -16
  125. package/dist/PageControl.mjs +0 -36
  126. package/dist/PageControl.mjs.map +0 -1
  127. package/dist/PageControlAssociation-DabEg5AR.mjs +0 -10
  128. package/dist/PageControlAssociation-DabEg5AR.mjs.map +0 -1
  129. package/dist/Sessions-CtbseWEh.mjs.map +0 -1
  130. package/dist/rolldown-runtime-D7D4PA-g.mjs +0 -13
@@ -1,2452 +0,0 @@
1
- import { Identifier } from "./References.mjs";
2
- import { BrowserError, InitializationError } from "./Errors.mjs";
3
- import { SafeFilename } from "./Transfers.mjs";
4
- import { ControlFacts, FrameInfo, ObservedControl, PageExecutionState, PageInfo, PageSuspension } from "./BrowserData.mjs";
5
- import { a as failure, c as sanitize, i as closeWithin, l as timeout, n as pngGeometry, o as publicError, r as NativeFailure, s as safeDecode } from "./Images-DgscEfSf.mjs";
6
- import { Context, Effect, Layer, Redacted, Schema } from "effect";
7
- //#region src/internal/browser/Binding.ts
8
- const fromNativeAttempt = (attempt) => ({ connect: (request) => Effect.tryPromise({
9
- try: (signal) => {
10
- const pending = attempt(request, signal).then(async (driver) => {
11
- if (signal.aborted) {
12
- request.onAbandoned();
13
- driver.fenceInitialization?.();
14
- await driver.disconnect().catch(() => {});
15
- throw BrowserError.make({
16
- operation: "connect",
17
- reason: "interrupted"
18
- });
19
- }
20
- return driver;
21
- });
22
- pending.then(request.onSettled, request.onSettled);
23
- return pending;
24
- },
25
- catch: (error) => Schema.is(InitializationError)(error) ? error : publicError(error, "connect", { reason: "provider" })
26
- }) });
27
- /**
28
- * Binding identity is issuance, never shape: a value that merely looks like a binding has no
29
- * entry here and cannot reach the owner, so only this package's constructors supply an engine.
30
- */
31
- const issued = /* @__PURE__ */ new WeakMap();
32
- const issueBinding = (binding, implementation) => {
33
- issued.set(binding, implementation);
34
- return Object.freeze(binding);
35
- };
36
- const bindingImplementation = (binding) => issued.get(binding);
37
- //#endregion
38
- //#region src/internal/browser/Actions.ts
39
- const waitEvent = (add, remove, ticket, accepts = () => true) => {
40
- let done = false;
41
- let resolve = () => {};
42
- let reject = () => {};
43
- const promise = new Promise((yes, no) => {
44
- resolve = yes;
45
- reject = no;
46
- });
47
- promise.catch(() => {});
48
- const cleanup = () => {
49
- remove(listener);
50
- ticket.signal.removeEventListener("abort", abort);
51
- clearTimeout(timer);
52
- };
53
- const abort = () => {
54
- if (!done) {
55
- done = true;
56
- cleanup();
57
- reject(failure("interrupted"));
58
- }
59
- };
60
- const listener = (value) => {
61
- if (!done && accepts(value)) {
62
- done = true;
63
- cleanup();
64
- resolve(value);
65
- }
66
- };
67
- const timer = setTimeout(() => {
68
- if (!done) {
69
- done = true;
70
- cleanup();
71
- reject(failure("timeout"));
72
- }
73
- }, ticket.remainingMillis());
74
- add(listener);
75
- ticket.signal.addEventListener("abort", abort, { once: true });
76
- if (ticket.signal.aborted) abort();
77
- return {
78
- promise,
79
- cancel: abort
80
- };
81
- };
82
- /**
83
- * In-memory bytes and provider-stored paths reach the page by different mechanisms and are
84
- * never mixed: the first is streamed from this client, the second is opened by the browser
85
- * process itself. A mixed request is a configuration error, not a native surprise later.
86
- */
87
- const nativeSelection = (files) => {
88
- if (files.length === 0) throw failure("configuration", "undispatched");
89
- const inline = files.filter((file) => file._tag === "Inline");
90
- const remote = files.filter((file) => file._tag === "Remote");
91
- if (inline.length === files.length) return {
92
- _tag: "Inline",
93
- payload: inline.map((file) => ({
94
- name: file.name,
95
- mimeType: file.mediaType,
96
- buffer: Buffer.from(file.bytes)
97
- }))
98
- };
99
- if (remote.length === files.length) return {
100
- _tag: "Remote",
101
- paths: remote.map((file) => file.path)
102
- };
103
- throw failure("configuration", "undispatched");
104
- };
105
- /**
106
- * Input to the selected document. A mutation marks its ticket dispatched immediately before the
107
- * one native command that may change the page; any outcome after that point is uncertain.
108
- */
109
- const makeActions = (context, targets, observation) => {
110
- const { current } = targets;
111
- let downloadSerial = 0;
112
- /** A result URL is only ever an http(s) address without credentials. */
113
- const httpUrl = (value) => {
114
- let url;
115
- try {
116
- url = new URL(value);
117
- } catch {
118
- throw failure("malformed");
119
- }
120
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw failure("malformed");
121
- return value;
122
- };
123
- const postUrl = () => httpUrl(targets.selectedUrl());
124
- /**
125
- * Acts on the exact attached node a target names. The observation seam establishes that it is
126
- * still the control that was inspected and that the host's policy, if any, admits it. `admit`
127
- * then sees that node before anything is dispatched, so a refusal it raises is undispatched
128
- * too, and what it learns reaches the action without the node being resolved a second time.
129
- */
130
- const withAdmittedElement = async (target, ticket, admit, action, policy) => {
131
- const { element, kept } = await observation.resolve(target, ticket, policy);
132
- try {
133
- const admitted = await admit(element);
134
- ticket.check();
135
- ticket.dispatch();
136
- return await action(element, admitted);
137
- } finally {
138
- if (!kept) await closeWithin(() => element.dispose()).catch(() => {});
139
- }
140
- };
141
- const withElement = (target, ticket, action, policy) => withAdmittedElement(target, ticket, async () => {}, action, policy);
142
- const click = (target, ticket, policy) => sanitize(async () => {
143
- await withElement(target, ticket, (element) => element.click({ timeout: timeout(ticket) }), policy);
144
- ticket.check();
145
- return postUrl();
146
- });
147
- /**
148
- * A file the provider already stores is named to the browser process, which opens it; this
149
- * client never reads that path. Exactly one main-frame node may match, and exactly one
150
- * command dispatches. A retained node or child frame is refused rather than approximated.
151
- */
152
- const attachStoredFiles = async (target, paths, ticket) => {
153
- if (typeof target !== "string") throw failure("unsupported", "undispatched");
154
- const { entry, frame } = current();
155
- if (frame !== entry.page.mainFrame()) throw failure("unsupported", "undispatched");
156
- const cdp = await context.newCDPSession(entry.page);
157
- try {
158
- ticket.check();
159
- const root = safeDecode(Schema.Struct({ root: Schema.Struct({ nodeId: Schema.Int }) }), await cdp.send("DOM.getDocument", { depth: 0 }));
160
- const matched = safeDecode(Schema.Struct({ nodeIds: Schema.Array(Schema.Int).check(Schema.isMaxLength(64)) }), await cdp.send("DOM.querySelectorAll", {
161
- nodeId: root.root.nodeId,
162
- selector: target
163
- }));
164
- ticket.check();
165
- const nodeId = matched.nodeIds[0];
166
- if (matched.nodeIds.length !== 1 || nodeId === void 0) throw failure(matched.nodeIds.length === 0 ? "not-found" : "ambiguous", "undispatched");
167
- ticket.dispatch();
168
- await cdp.send("DOM.setFileInputFiles", {
169
- files: [...paths],
170
- nodeId
171
- });
172
- } finally {
173
- await closeWithin(() => cdp.detach()).catch(() => {});
174
- }
175
- };
176
- const beginNavigation = (url, timeoutMillis, ticket) => sanitize(async () => {
177
- const { entry, frame } = current();
178
- ticket.dispatch();
179
- targets.navigating.begin(entry.id);
180
- const settled = sanitize(async () => {
181
- try {
182
- await frame.goto(url, {
183
- waitUntil: "domcontentloaded",
184
- timeout: timeoutMillis
185
- });
186
- } finally {
187
- targets.navigating.end(entry.id);
188
- }
189
- return httpUrl(targets.urlOf(frame));
190
- });
191
- settled.catch(() => {});
192
- return {
193
- pageId: entry.id,
194
- settled,
195
- stop: () => sanitize(async () => {
196
- const cdp = await context.newCDPSession(entry.page);
197
- try {
198
- await cdp.send("Page.stopLoading");
199
- } finally {
200
- await closeWithin(() => cdp.detach()).catch(() => {});
201
- }
202
- })
203
- };
204
- });
205
- const fill = (target, value, ticket, policy) => sanitize(async () => {
206
- await withElement(target, ticket, (element) => element.fill(value, { timeout: timeout(ticket) }), policy);
207
- ticket.check();
208
- return postUrl();
209
- });
210
- const scroll = (deltaX, deltaY, ticket) => sanitize(async () => {
211
- const { frame } = current();
212
- ticket.dispatch();
213
- await frame.evaluate(({ x, y }) => window.scrollBy({
214
- left: x,
215
- top: y,
216
- behavior: "instant"
217
- }), {
218
- x: deltaX,
219
- y: deltaY
220
- });
221
- ticket.check();
222
- return postUrl();
223
- });
224
- const waitFor = (selector, state, ticket) => sanitize(async () => {
225
- await (await current().frame.waitForSelector(selector, {
226
- state,
227
- strict: true,
228
- timeout: timeout(ticket)
229
- }))?.dispose();
230
- ticket.check();
231
- });
232
- const clickAndWait = (target, ticket) => sanitize(async () => {
233
- const { entry, frame } = current();
234
- const observer = waitEvent((on) => entry.page.on("framenavigated", on), (off) => entry.page.off("framenavigated", off), ticket, (changedFrame) => changedFrame === frame);
235
- try {
236
- await click(target, ticket);
237
- await observer.promise;
238
- await frame.waitForLoadState("domcontentloaded", { timeout: timeout(ticket) });
239
- ticket.check();
240
- return postUrl();
241
- } finally {
242
- observer.cancel();
243
- }
244
- });
245
- const clickForDownload = (target, ticket) => sanitize(async () => {
246
- const page = current().entry.page;
247
- const observer = waitEvent((on) => page.on("download", on), (off) => page.off("download", off), ticket);
248
- try {
249
- await click(target, ticket);
250
- const download = await observer.promise;
251
- const filename = safeDecode(SafeFilename, download.suggestedFilename());
252
- const error = await download.failure();
253
- ticket.check();
254
- return {
255
- downloadId: `native-download-${++downloadSerial}`,
256
- filename,
257
- state: error === null ? "completed" : "failed"
258
- };
259
- } finally {
260
- observer.cancel();
261
- }
262
- });
263
- const selectFiles = (target, files, ticket) => sanitize(async () => {
264
- const selection = nativeSelection(files);
265
- if (selection._tag === "Remote") await attachStoredFiles(target, selection.paths, ticket);
266
- else await withElement(target, ticket, (element) => element.setInputFiles(selection.payload, { timeout: timeout(ticket) }));
267
- ticket.check();
268
- return postUrl();
269
- });
270
- const clickForFileSelection = (target, files, ticket) => sanitize(async () => {
271
- const selection = nativeSelection(files);
272
- if (selection._tag === "Remote") throw failure("unsupported", "undispatched");
273
- const page = current().entry.page;
274
- const observer = waitEvent((on) => page.on("filechooser", on), (off) => page.off("filechooser", off), ticket);
275
- try {
276
- await click(target, ticket);
277
- const chooser = await observer.promise;
278
- ticket.check();
279
- if (!chooser.isMultiple() && selection.payload.length > 1) throw failure("unsupported");
280
- await chooser.setFiles(selection.payload, { timeout: timeout(ticket) });
281
- ticket.check();
282
- return postUrl();
283
- } finally {
284
- observer.cancel();
285
- }
286
- });
287
- return {
288
- withAdmittedElement,
289
- beginNavigation,
290
- click,
291
- fill,
292
- scroll,
293
- waitFor,
294
- clickAndWait,
295
- clickForDownload,
296
- selectFiles,
297
- clickForFileSelection
298
- };
299
- };
300
- //#endregion
301
- //#region src/internal/browser/CallbackTasks.ts
302
- /** Native callbacks cannot await a Semaphore. Reserve a bounded slot synchronously
303
- * before starting native work; never construct an already-running Promise first.
304
- * Fail-session pressure/failure fences the connection once. Reject-call pressure or
305
- * failure remains local to that invocation and reopens admission when accepted work
306
- * settles. Accepted rejections remain observed after close, and no callback-side Fiber
307
- * or unbounded producer queue is created. */
308
- var CallbackTasks = class {
309
- pending = /* @__PURE__ */ new Set();
310
- stopped = false;
311
- faulted = false;
312
- capacity;
313
- onFault;
314
- constructor(capacity, onFault) {
315
- if (!Number.isSafeInteger(capacity) || capacity < 1) throw new RangeError("Invalid callback capacity");
316
- this.capacity = capacity;
317
- this.onFault = onFault;
318
- }
319
- fault() {
320
- if (this.stopped || this.faulted) return;
321
- this.faulted = true;
322
- this.onFault();
323
- }
324
- submit(action, failureMode = "fail-session") {
325
- if (this.stopped || this.faulted) return false;
326
- if (this.pending.size >= this.capacity) {
327
- if (failureMode === "fail-session") this.fault();
328
- return false;
329
- }
330
- const task = Promise.resolve().then(action).then(() => {}, () => {
331
- if (failureMode === "fail-session") this.fault();
332
- }).finally(() => this.pending.delete(task));
333
- this.pending.add(task);
334
- return true;
335
- }
336
- stop() {
337
- this.stopped = true;
338
- }
339
- async settle() {
340
- await Promise.all(this.pending);
341
- }
342
- };
343
- //#endregion
344
- //#region src/internal/browser/CaptureSource.ts
345
- /**
346
- * Screencast sources bound to one page and frame, and the watchers that learn when that binding
347
- * stops describing what the page shows. A main-frame change invalidates every watcher on the page.
348
- */
349
- const makeCaptureSources = (targets) => {
350
- const { current, entries, frameId } = targets;
351
- const captureWatchers = /* @__PURE__ */ new Map();
352
- const invalidate = (entry, reason, frame) => {
353
- const watchers = captureWatchers.get(entry.id);
354
- if (watchers === void 0) return;
355
- const changedFrameId = frame === void 0 ? void 0 : frameId(frame);
356
- const mainFrameId = frameId(entry.page.mainFrame());
357
- for (const watcher of [...watchers]) {
358
- if (frame !== void 0 && changedFrameId !== mainFrameId && changedFrameId !== watcher.frameId) continue;
359
- if (frame !== void 0 && watcher.document !== void 0 && reason === "target-changed" && changedFrameId === mainFrameId && watcher.frameId === mainFrameId) watcher.document(frame.url());
360
- else watcher.invalidate(reason);
361
- }
362
- };
363
- const forget = (entry) => {
364
- captureWatchers.delete(entry.id);
365
- };
366
- const clear = () => {
367
- captureWatchers.clear();
368
- };
369
- const capture = (target) => sanitize(async () => {
370
- let entry;
371
- let captureFrame;
372
- if (target === void 0) {
373
- const selectedTarget = current();
374
- entry = selectedTarget.entry;
375
- captureFrame = selectedTarget.frame;
376
- } else {
377
- const requested = entries.get(target.pageId);
378
- if (requested === void 0 || requested.page.isClosed()) throw failure("not-found", "undispatched");
379
- if (await targets.targetId(requested) !== target.targetId) throw failure("stale", "undispatched");
380
- entry = requested;
381
- captureFrame = entry.page.mainFrame();
382
- }
383
- const page = entry.page;
384
- const targetId = await targets.targetId(entry);
385
- const watchedFrameId = frameId(captureFrame);
386
- if (page.screencast === void 0) throw failure("unsupported");
387
- let watcherSet;
388
- let watcher;
389
- return {
390
- pageId: entry.id,
391
- targetId,
392
- frameId: watchedFrameId,
393
- source: {
394
- start: ({ receive, quality, size, invalidate, opened, document }) => sanitize(async () => {
395
- watcherSet = captureWatchers.get(entry.id) ?? /* @__PURE__ */ new Set();
396
- captureWatchers.set(entry.id, watcherSet);
397
- watcher = {
398
- frameId: watchedFrameId,
399
- invalidate,
400
- ...document === void 0 ? {} : { document }
401
- };
402
- watcherSet.add(watcher);
403
- opened?.(captureFrame.url());
404
- try {
405
- await page.screencast.start({
406
- quality,
407
- ...size === void 0 ? {} : { size },
408
- onFrame: (frame) => {
409
- receive(frame);
410
- }
411
- });
412
- } catch (error) {
413
- watcherSet.delete(watcher);
414
- watcher = void 0;
415
- if (watcherSet.size === 0) captureWatchers.delete(entry.id);
416
- throw error;
417
- }
418
- }),
419
- stop: () => sanitize(async () => {
420
- if (watcher !== void 0 && watcherSet !== void 0) {
421
- watcherSet.delete(watcher);
422
- watcher = void 0;
423
- if (watcherSet.size === 0) captureWatchers.delete(entry.id);
424
- }
425
- if (page.isClosed()) return;
426
- try {
427
- await page.screencast.stop();
428
- } catch (error) {
429
- if (!page.isClosed()) throw error;
430
- }
431
- })
432
- }
433
- };
434
- });
435
- return {
436
- invalidate,
437
- forget,
438
- clear,
439
- capture
440
- };
441
- };
442
- //#endregion
443
- //#region src/internal/browser/NativeBindings.ts
444
- const ContextCreated = Schema.Struct({ context: Schema.Struct({
445
- id: Schema.Int.check(Schema.isGreaterThan(0)),
446
- uniqueId: Schema.NonEmptyString.check(Schema.isMaxLength(256)),
447
- origin: Schema.String.check(Schema.isMaxLength(2048)),
448
- auxData: Schema.optionalKey(Schema.Struct({
449
- isDefault: Schema.Boolean,
450
- frameId: Identifier
451
- }))
452
- }) });
453
- const ContextDestroyed = Schema.Struct({ executionContextId: Schema.Int });
454
- const TargetIdentity = Schema.Struct({ targetInfo: Schema.Struct({ targetId: Identifier }) });
455
- const Call = Schema.Struct({
456
- name: Identifier,
457
- sequence: Schema.Int.check(Schema.isBetween({
458
- minimum: 1,
459
- maximum: Number.MAX_SAFE_INTEGER
460
- })),
461
- input: Schema.String.check(Schema.isMaxLength(1048576))
462
- });
463
- const error = (step, reason) => InitializationError.make({
464
- operation: "callback",
465
- step,
466
- reason
467
- });
468
- /**
469
- * A small page-side transport, not a second host scheduler. Each document owns its finite
470
- * pending replies and one startup timer; host authority always comes from Chromium's native
471
- * execution-context event, never the fields in this message or a page-reported origin.
472
- */
473
- const pageBundle = (nativeName, controllerName, bindings) => `(() => {
474
- const nativeName = ${JSON.stringify(nativeName)};
475
- const controllerName = ${JSON.stringify(controllerName)};
476
- if (Object.prototype.hasOwnProperty.call(globalThis, controllerName)) return;
477
- const definitions = ${JSON.stringify(bindings.map(({ name, maxConcurrent, maxInputBytes, timeoutMillis }) => ({
478
- name,
479
- maximum: maxConcurrent + 1,
480
- maxInputBytes,
481
- timeoutMillis
482
- })))};
483
- const stringify = JSON.stringify.bind(JSON);
484
- const parse = JSON.parse.bind(JSON);
485
- const encode = new TextEncoder();
486
- const descriptors = Object.getOwnPropertyDescriptors;
487
- const ownKeys = Reflect.ownKeys;
488
- const prototype = Object.getPrototypeOf;
489
- const plain = Object.prototype;
490
- const arrayPrototype = Array.prototype;
491
- const pending = new Map();
492
- const functions = new Map();
493
- const counts = new Map();
494
- let sequence = 0;
495
- let closed = false;
496
- let startup;
497
- const rejected = () => {
498
- const value = new Error('Browser binding call rejected');
499
- value.name = 'BrowserbaseBindingError';
500
- value.stack = '';
501
- return value;
502
- };
503
- const jsonInput = (value, maximum) => {
504
- let nodes = 0;
505
- const ancestors = new Set();
506
- const visit = (item, depth) => {
507
- if (++nodes > Math.min(maximum, 65536) || depth > 64) throw rejected();
508
- if (item === null || typeof item === 'boolean') return;
509
- if (typeof item === 'string') { if (item.length > maximum) throw rejected(); return; }
510
- if (typeof item === 'number') { if (!Number.isFinite(item)) throw rejected(); return; }
511
- if (typeof item !== 'object' || ancestors.has(item)) throw rejected();
512
- const array = Array.isArray(item);
513
- const parent = prototype(item);
514
- if (array ? parent !== arrayPrototype : parent !== plain && parent !== null) throw rejected();
515
- ancestors.add(item);
516
- const fields = descriptors(item);
517
- const keys = ownKeys(fields);
518
- if (keys.length > Math.min(maximum, 65536)) throw rejected();
519
- if (array && keys.length !== item.length + 1) throw rejected();
520
- for (const key of keys) {
521
- if (array && key === 'length') continue;
522
- if (typeof key !== 'string' || key.length > maximum || key === 'toJSON') throw rejected();
523
- const field = fields[key];
524
- if (!field.enumerable || !Object.prototype.hasOwnProperty.call(field, 'value')) throw rejected();
525
- if (array && (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= item.length)) throw rejected();
526
- visit(field.value, depth + 1);
527
- }
528
- ancestors.delete(item);
529
- };
530
- visit(value, 0);
531
- return stringify(value);
532
- };
533
- const settle = (id, packet) => {
534
- const call = pending.get(id);
535
- if (!call) return;
536
- pending.delete(id);
537
- clearTimeout(call.timer);
538
- counts.set(call.name, (counts.get(call.name) || 1) - 1);
539
- if (packet && packet.ok === true) {
540
- try { call.resolve(parse(packet.text)); } catch { call.reject(rejected()); }
541
- } else call.reject(rejected());
542
- };
543
- const dispatch = () => {
544
- clearTimeout(startup);
545
- startup = undefined;
546
- if (closed) return;
547
- const native = globalThis[nativeName];
548
- if (typeof native !== 'function') {
549
- if (pending.size > 0) startup = setTimeout(dispatch, 10);
550
- return;
551
- }
552
- for (const [id, call] of pending) {
553
- if (call.sent) continue;
554
- call.sent = true;
555
- try { native(call.message); } catch { settle(id); }
556
- }
557
- };
558
- const close = () => {
559
- if (closed) return;
560
- closed = true;
561
- clearTimeout(startup);
562
- for (const id of [...pending.keys()]) settle(id);
563
- for (const [name, fn] of functions) {
564
- if (globalThis[name] === fn) delete globalThis[name];
565
- }
566
- };
567
- const controller = Object.freeze({ deliver: settle, close,
568
- get status() { return closed ? 'closed' : functions.size === definitions.length ? 'ready' : 'installing'; }
569
- });
570
- Object.defineProperty(globalThis, controllerName, { value: controller, configurable: true });
571
- for (const definition of definitions) {
572
- if (Object.prototype.hasOwnProperty.call(globalThis, definition.name)) { close(); return; }
573
- const fn = (...args) => new Promise((resolve, reject) => {
574
- if (closed || args.length !== 1 || sequence >= Number.MAX_SAFE_INTEGER ||
575
- (counts.get(definition.name) || 0) >= definition.maximum) {
576
- reject(rejected()); return;
577
- }
578
- let input;
579
- try {
580
- input = jsonInput(args[0], definition.maxInputBytes);
581
- if (typeof input !== 'string' || input.length > definition.maxInputBytes ||
582
- encode.encode(input).byteLength > definition.maxInputBytes) throw rejected();
583
- } catch { reject(rejected()); return; }
584
- const id = ++sequence;
585
- const timer = setTimeout(() => settle(id), definition.timeoutMillis);
586
- counts.set(definition.name, (counts.get(definition.name) || 0) + 1);
587
- pending.set(id, { name: definition.name, resolve, reject, timer, sent: false,
588
- message: stringify({ name: definition.name, sequence: id, input }) });
589
- dispatch();
590
- });
591
- functions.set(definition.name, fn);
592
- Object.defineProperty(globalThis, definition.name, { value: fn, configurable: true });
593
- }
594
- })();`;
595
- /**
596
- * The pinned Playwright binding callback reports a Frame but no calling document identity.
597
- * That Frame survives navigation, so checking frame.url() would authorize an old document
598
- * using its replacement's origin. Chromium's maintained Runtime binding protocol supplies
599
- * the original executionContextId; uniqueId prevents context-id reuse when publishing replies.
600
- * These are child CDP sessions on the existing owned Playwright connection, not another owner,
601
- * websocket, browser resource or publicly exposed raw-protocol capability.
602
- */
603
- const makeNativeBindings = (context, bindings, fault) => {
604
- const identity = globalThis.crypto.randomUUID().replaceAll("-", "");
605
- const nativeName = `__effect_agent_binding_${identity}`;
606
- const controllerName = `__effect_agent_bindings_${identity}`;
607
- const bundle = pageBundle(nativeName, controllerName, bindings);
608
- const byName = new Map(bindings.map((binding) => [binding.name, binding]));
609
- const targets = /* @__PURE__ */ new Map();
610
- const currentDocuments = /* @__PURE__ */ new Map();
611
- const attaching = /* @__PURE__ */ new WeakSet();
612
- const maximumReplies = 16 + bindings.reduce((sum, binding) => sum + binding.maxConcurrent, 0);
613
- let replies = 0;
614
- let closing = false;
615
- let disposed;
616
- const close = () => {
617
- closing = true;
618
- };
619
- const attach = async (subject, page) => {
620
- if (closing || page.isClosed() || attaching.has(subject)) return;
621
- attaching.add(subject);
622
- let cdp;
623
- let retained = false;
624
- try {
625
- try {
626
- cdp = await context.newCDPSession(subject);
627
- } catch (cause) {
628
- if (subject !== page && cause instanceof Error && cause.message.includes("This frame does not have a separate CDP session")) return;
629
- if (page.isClosed() || subject !== page && "isDetached" in subject && subject.isDetached()) return;
630
- throw cause;
631
- }
632
- const native = cdp;
633
- const targetId = Schema.decodeSync(TargetIdentity)(await native.send("Target.getTargetInfo")).targetInfo.targetId;
634
- if (closing || page.isClosed() || targets.has(targetId)) return;
635
- if (targets.size >= 128) throw error("bindings", "busy");
636
- const documents = /* @__PURE__ */ new Map();
637
- const created = (raw) => {
638
- const parsed = Schema.decodeUnknownOption(ContextCreated)(raw);
639
- if (parsed._tag === "None") return;
640
- const value = parsed.value.context;
641
- if (closing || value.auxData?.isDefault !== true) return;
642
- if (documents.get(value.id)?.uniqueId === value.uniqueId) return;
643
- const previous = currentDocuments.get(value.auxData.frameId);
644
- if (previous !== void 0) {
645
- const previousDocuments = targets.get(previous.targetId)?.documents;
646
- if (previousDocuments?.get(previous.id) === previous) previousDocuments.delete(previous.id);
647
- currentDocuments.delete(value.auxData.frameId);
648
- }
649
- if (documents.size >= 128 && !documents.has(value.id)) {
650
- fault();
651
- return;
652
- }
653
- const document = {
654
- ...value,
655
- sequence: 0,
656
- targetId
657
- };
658
- currentDocuments.set(value.auxData.frameId, document);
659
- documents.set(value.id, document);
660
- };
661
- const destroyed = (raw) => {
662
- const parsed = Schema.decodeUnknownOption(ContextDestroyed)(raw);
663
- if (parsed._tag === "Some") {
664
- const document = documents.get(parsed.value.executionContextId);
665
- const frameId = document?.auxData?.frameId;
666
- if (frameId !== void 0 && currentDocuments.get(frameId) === document) currentDocuments.delete(frameId);
667
- documents.delete(parsed.value.executionContextId);
668
- }
669
- };
670
- const cleared = () => {
671
- for (const document of documents.values()) {
672
- const frameId = document.auxData?.frameId;
673
- if (frameId !== void 0 && currentDocuments.get(frameId) === document) currentDocuments.delete(frameId);
674
- }
675
- documents.clear();
676
- };
677
- const called = (event) => {
678
- if (closing || event.name !== nativeName || replies >= maximumReplies) return;
679
- const document = documents.get(event.executionContextId);
680
- if (document === void 0 || event.payload.length > 2097664) return;
681
- const parsed = Schema.decodeOption(Schema.fromJsonString(Call))(event.payload);
682
- if (parsed._tag === "None") return;
683
- const call = parsed.value;
684
- const binding = byName.get(call.name);
685
- if (binding === void 0 || call.sequence <= document.sequence) return;
686
- document.sequence = call.sequence;
687
- replies++;
688
- let retired = false;
689
- const check = async (signal) => {
690
- const current = () => {
691
- if (closing || retired || signal.aborted || page.isClosed()) throw error(binding.name, "closed");
692
- if (documents.get(document.id) !== document || currentDocuments.get(document.auxData?.frameId ?? "") !== document) throw error(binding.name, "stale");
693
- if (!binding.origins.includes(document.origin)) throw error(binding.name, "origin");
694
- };
695
- current();
696
- const result = await native.send("Runtime.evaluate", {
697
- expression: "true",
698
- uniqueContextId: document.uniqueId,
699
- returnByValue: true,
700
- silent: true,
701
- timeout: binding.timeoutMillis
702
- });
703
- current();
704
- if (result.exceptionDetails !== void 0 || result.result.value !== true) throw error(binding.name, "stale");
705
- };
706
- const reply = async (packet) => {
707
- if (closing || page.isClosed() || documents.get(document.id) !== document || currentDocuments.get(document.auxData?.frameId ?? "") !== document) return;
708
- await native.send("Runtime.evaluate", {
709
- expression: `globalThis[${JSON.stringify(controllerName)}]?.deliver(${call.sequence},${JSON.stringify(packet)})`,
710
- uniqueContextId: document.uniqueId,
711
- returnByValue: true,
712
- silent: true,
713
- timeout: binding.timeoutMillis
714
- });
715
- };
716
- binding.invoke({
717
- read: async (signal) => {
718
- await check(signal);
719
- return call.input;
720
- },
721
- check,
722
- dispose: async () => {
723
- retired = true;
724
- }
725
- }).then((text) => reply({
726
- ok: true,
727
- text
728
- }), () => reply({ ok: false })).catch(() => {}).finally(() => {
729
- replies--;
730
- });
731
- };
732
- const off = () => {
733
- native.off("Runtime.executionContextCreated", created);
734
- native.off("Runtime.executionContextDestroyed", destroyed);
735
- native.off("Runtime.executionContextsCleared", cleared);
736
- native.off("Runtime.bindingCalled", called);
737
- cleared();
738
- };
739
- const target = {
740
- cdp: native,
741
- documents,
742
- page,
743
- off,
744
- closed: false
745
- };
746
- const onClosed = () => {
747
- target.closed = true;
748
- off();
749
- if (targets.get(targetId) === target) targets.delete(targetId);
750
- native.off("close", onClosed);
751
- };
752
- native.on("close", onClosed);
753
- targets.set(targetId, target);
754
- retained = true;
755
- native.on("Runtime.executionContextCreated", created);
756
- native.on("Runtime.executionContextDestroyed", destroyed);
757
- native.on("Runtime.executionContextsCleared", cleared);
758
- native.on("Runtime.bindingCalled", called);
759
- await native.send("Runtime.enable");
760
- if (closing) return;
761
- await native.send("Runtime.addBinding", { name: nativeName });
762
- if (closing) {
763
- await native.send("Runtime.removeBinding", { name: nativeName }).catch(() => {});
764
- return;
765
- }
766
- for (const document of documents.values()) {
767
- if (closing) return;
768
- const installed = await native.send("Runtime.evaluate", {
769
- expression: `${bundle}\nglobalThis[${JSON.stringify(controllerName)}]?.status`,
770
- uniqueContextId: document.uniqueId,
771
- returnByValue: true,
772
- silent: true,
773
- timeout: 2e3
774
- });
775
- if (documents.get(document.id) !== document || currentDocuments.get(document.auxData?.frameId ?? "") !== document) continue;
776
- if (installed.exceptionDetails !== void 0 || installed.result.value !== "ready") throw InitializationError.make({
777
- operation: "register",
778
- step: "bindings",
779
- reason: "configuration"
780
- });
781
- }
782
- } finally {
783
- attaching.delete(subject);
784
- if (cdp !== void 0 && (!retained || closing)) await cdp.detach().catch(() => {});
785
- }
786
- };
787
- const dispose = () => {
788
- close();
789
- disposed ??= (async () => {
790
- const settled = await Promise.allSettled([...targets.values()].map(async (target) => {
791
- const documents = [...target.documents.values()];
792
- target.off();
793
- const removal = await Promise.allSettled([(async () => {
794
- if (!target.closed && !target.page.isClosed()) {
795
- for (const document of documents) await target.cdp.send("Runtime.evaluate", {
796
- expression: `globalThis[${JSON.stringify(controllerName)}]?.close()`,
797
- uniqueContextId: document.uniqueId,
798
- returnByValue: true,
799
- silent: true,
800
- timeout: 1e3
801
- }).catch(() => {});
802
- if (!target.closed) await target.cdp.send("Runtime.removeBinding", { name: nativeName });
803
- }
804
- })()]);
805
- const wasClosed = target.closed || target.page.isClosed();
806
- const detachment = await Promise.allSettled([target.closed ? Promise.resolve() : target.cdp.detach()]);
807
- if (!wasClosed && removal.some((result) => result.status === "rejected")) throw InitializationError.make({
808
- operation: "dispose",
809
- step: "bindings",
810
- reason: "native"
811
- });
812
- if (!target.closed && !target.page.isClosed() && detachment.some((result) => result.status === "rejected")) throw InitializationError.make({
813
- operation: "dispose",
814
- step: "bindings",
815
- reason: "native"
816
- });
817
- }));
818
- targets.clear();
819
- currentDocuments.clear();
820
- if (settled.some((result) => result.status === "rejected")) throw InitializationError.make({
821
- operation: "dispose",
822
- step: "bindings",
823
- reason: "native"
824
- });
825
- })();
826
- return disposed;
827
- };
828
- return {
829
- bundle,
830
- attach,
831
- close,
832
- dispose
833
- };
834
- };
835
- //#endregion
836
- //#region src/internal/browser/Initialization.ts
837
- /**
838
- * The connection's bootstrap registrations and native bindings, and the readiness of the selected
839
- * document under that bootstrap. Fencing is synchronous; disposal removes what was registered.
840
- */
841
- const makeInitialization = (context, options, targets, callbacks, events, closing) => {
842
- const { current, entries, epochOf } = targets;
843
- const registeredEpochs = /* @__PURE__ */ new WeakMap();
844
- const readyDocuments = /* @__PURE__ */ new WeakMap();
845
- const registrations = [];
846
- let initializationClosed = false;
847
- let disposal;
848
- const initializationFault = (error) => {
849
- if (closing() || initializationClosed) return;
850
- if (options.onBindingFault === void 0) events.fault();
851
- else options.onBindingFault(error);
852
- };
853
- const bindingTask = (action) => {
854
- if (!callbacks.submit(async () => {
855
- try {
856
- await action();
857
- } catch (cause) {
858
- initializationFault(Schema.is(InitializationError)(cause) ? cause : InitializationError.make({
859
- operation: "register",
860
- step: "bindings",
861
- reason: "native"
862
- }));
863
- }
864
- }, "reject-call")) initializationFault(InitializationError.make({
865
- operation: "register",
866
- step: "bindings",
867
- reason: "busy"
868
- }));
869
- };
870
- const bindings = options.bindings === void 0 || options.bindings.length === 0 ? void 0 : makeNativeBindings(context, options.bindings, () => initializationFault(InitializationError.make({
871
- operation: "register",
872
- step: "bindings",
873
- reason: "busy"
874
- })));
875
- const attach = async (page) => {
876
- if (bindings === void 0) return;
877
- await bindings.attach(page, page);
878
- for (const frame of page.frames()) if (frame !== page.mainFrame()) await bindings.attach(frame, page);
879
- };
880
- const fence = () => {
881
- initializationClosed = true;
882
- bindings?.close();
883
- };
884
- const dispose = () => {
885
- fence();
886
- disposal ??= (async () => {
887
- if ((await Promise.allSettled([...registrations.splice(0).map((registration) => registration.dispose()), ...bindings === void 0 ? [] : [bindings.dispose()]])).some((result) => result.status === "rejected")) throw failure("provider");
888
- })();
889
- return disposal;
890
- };
891
- /** Attaches a page this connection registered after initialization, off the event path. */
892
- const attachPage = (page) => {
893
- if (bindings !== void 0) bindingTask(() => attach(page));
894
- };
895
- /** Attaches the new document of a frame that navigated after initialization. */
896
- const attachFrame = (frame, page) => {
897
- if (bindings !== void 0) bindingTask(() => bindings.attach(frame, page));
898
- };
899
- /**
900
- * Registrations are installed once per connection and before this connection creates any
901
- * document. Documents that were already running keep their recorded epoch, so the readiness
902
- * policy can tell them apart from documents that actually ran the bundle.
903
- */
904
- const install = async () => {
905
- const bootstrap = options.bootstrap;
906
- if (bootstrap === void 0 && bindings === void 0) return;
907
- for (const entry of entries.values()) for (const frame of entry.page.frames()) registeredEpochs.set(frame, epochOf(frame));
908
- for (const grant of bootstrap?.permissions ?? []) await context.grantPermissions([...grant.permissions], { origin: grant.origin });
909
- const content = [bindings?.bundle, bootstrap?.bundle].filter((part) => part !== void 0).join("\n");
910
- if (content.length > 0) {
911
- const registration = await context.addInitScript({ content });
912
- if (initializationClosed) await registration.dispose();
913
- else registrations.push(registration);
914
- }
915
- };
916
- const documentOrigin = (frame) => {
917
- try {
918
- const url = new URL(frame.url());
919
- return ["http:", "https:"].includes(url.protocol) ? url.origin : "";
920
- } catch {
921
- return "";
922
- }
923
- };
924
- /** A finite wait for a page promise. Native work continues; only this wait is bounded. */
925
- const evaluateWithin = async (frame, expression, milliseconds) => {
926
- let timer;
927
- try {
928
- return await Promise.race([frame.evaluate(expression), new Promise((_, reject) => {
929
- timer = setTimeout(() => reject(failure("timeout")), milliseconds);
930
- })]);
931
- } finally {
932
- clearTimeout(timer);
933
- }
934
- };
935
- const readiness = async (bootstrap, ticket) => {
936
- const { frame } = current();
937
- const epoch = epochOf(frame);
938
- if (readyDocuments.get(frame) === epoch) return { _tag: "Ready" };
939
- ticket.check();
940
- if (epoch <= (registeredEpochs.get(frame) ?? -1)) {
941
- if (bootstrap.existingDocuments === "RequireFreshNavigation") return { _tag: "RequiresNavigation" };
942
- }
943
- const origin = documentOrigin(frame);
944
- const applicable = bootstrap.readiness.filter((requirement) => requirement.origins === void 0 || requirement.origins.includes(origin));
945
- if (applicable.length === 0) {
946
- readyDocuments.set(frame, epoch);
947
- return { _tag: "NotApplicable" };
948
- }
949
- for (const requirement of applicable) {
950
- const budget = Math.max(1, Math.min(requirement.timeoutMillis, ticket.remainingMillis()));
951
- let value;
952
- try {
953
- value = await evaluateWithin(frame, requirement.expression, budget);
954
- } catch (error) {
955
- if (epochOf(frame) !== epoch || frame.isDetached()) return {
956
- _tag: "NotReady",
957
- step: requirement.step,
958
- reason: "stale"
959
- };
960
- return {
961
- _tag: "NotReady",
962
- step: requirement.step,
963
- reason: Schema.is(NativeFailure)(error) && error.reason === "timeout" ? "timeout" : "failed"
964
- };
965
- }
966
- ticket.check();
967
- if (epochOf(frame) !== epoch) return {
968
- _tag: "NotReady",
969
- step: requirement.step,
970
- reason: "stale"
971
- };
972
- if (value !== true) return {
973
- _tag: "NotReady",
974
- step: requirement.step,
975
- reason: "failed"
976
- };
977
- }
978
- readyDocuments.set(frame, epoch);
979
- return { _tag: "Ready" };
980
- };
981
- const documentReadiness = (ticket) => sanitize(async () => {
982
- const bootstrap = options.bootstrap;
983
- return bootstrap === void 0 || bootstrap.readiness.length === 0 ? { _tag: "Ready" } : readiness(bootstrap, ticket);
984
- });
985
- return {
986
- attach,
987
- attachPage,
988
- attachFrame,
989
- fence,
990
- dispose,
991
- install,
992
- documentReadiness
993
- };
994
- };
995
- //#endregion
996
- //#region src/internal/browser/Keyboard.ts
997
- /**
998
- * Real key input to the selected page. Every stroke is one the browser would receive from a
999
- * keyboard, so handlers see trusted `keydown` and `keyup`, and the browser itself decides what
1000
- * has focus. Nothing here focuses, paces or retries: a caller that wants a cadence sends its keys.
1001
- */
1002
- const makeKeyboard = (targets, actions, receipt) => {
1003
- const { current } = targets;
1004
- /**
1005
- * Keys reach the focused element of the focused document, so the guard requires both: the
1006
- * element named is that element or holds it, through any open shadow root, and its document
1007
- * has focus. The pinned Chromium clears a frame's active element when focus leaves that
1008
- * frame, which makes the second check redundant wherever that holds. It is kept because that
1009
- * is the browser's bookkeeping, not a guarantee, and where it failed the keys would land in
1010
- * another frame than the one this guard was asked about.
1011
- */
1012
- const requireFocus = async (element) => {
1013
- if (await element.evaluate((node) => {
1014
- if (!node.ownerDocument.hasFocus()) return false;
1015
- let active = node.ownerDocument.activeElement;
1016
- while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
1017
- for (let at = active; at !== null;) {
1018
- if (at === node) return true;
1019
- at = at.parentNode ?? (at instanceof ShadowRoot ? at.host : null);
1020
- }
1021
- return false;
1022
- }) !== true) throw failure("not-focused", "undispatched");
1023
- };
1024
- /** Sends under one dispatch, either to whatever has focus or to the one element that must. */
1025
- const send = async (into, ticket, policy, keys) => {
1026
- const { page } = current().entry;
1027
- if (into === void 0) {
1028
- ticket.dispatch();
1029
- await keys(page);
1030
- } else await actions.withAdmittedElement(into, ticket, requireFocus, () => keys(page), policy);
1031
- ticket.check();
1032
- return receipt(page);
1033
- };
1034
- const press = (key, modifiers, into, ticket, policy) => sanitize(() => send(into, ticket, policy, (page) => page.keyboard.press([...modifiers, key].join("+"))));
1035
- const type = (text, into, ticket, policy) => sanitize(() => send(into, ticket, policy, async (page) => {
1036
- for (const character of text) {
1037
- ticket.check();
1038
- await page.keyboard.type(character);
1039
- }
1040
- }));
1041
- return {
1042
- press,
1043
- type
1044
- };
1045
- };
1046
- //#endregion
1047
- //#region src/internal/browser/PageExecution.ts
1048
- const fail = (reason, outcome = "undispatched") => BrowserError.make({
1049
- operation: "page-control",
1050
- reason,
1051
- outcome
1052
- });
1053
- /** Owned by one native page/connection. Cleanup never resumes or rolls back a hold. */
1054
- var PageExecution = class {
1055
- phase = "running";
1056
- revision = 0;
1057
- receipt;
1058
- priorRate;
1059
- disposed = false;
1060
- pageId;
1061
- targetId;
1062
- port;
1063
- freshId;
1064
- constructor(pageId, targetId, port, freshId = () => globalThis.crypto.randomUUID()) {
1065
- this.pageId = pageId;
1066
- this.targetId = targetId;
1067
- this.port = port;
1068
- this.freshId = freshId;
1069
- }
1070
- state() {
1071
- if (this.disposed || this.port.closed()) throw fail("closed");
1072
- return PageExecutionState.make({
1073
- pageId: this.pageId,
1074
- targetId: this.targetId,
1075
- state: this.phase,
1076
- ...this.receipt === void 0 ? {} : { suspensionId: this.receipt.suspensionId }
1077
- });
1078
- }
1079
- assertRunning() {
1080
- if (this.disposed || this.port.closed()) throw fail("closed");
1081
- if (this.phase !== "running") throw fail("busy");
1082
- }
1083
- invalidate() {
1084
- const held = this.phase !== "running";
1085
- this.revision++;
1086
- this.receipt = void 0;
1087
- if (held) this.phase = "unknown";
1088
- return held;
1089
- }
1090
- check(ticket, revision) {
1091
- ticket.check();
1092
- const outcome = ticket.dispatched ? "unknown" : "undispatched";
1093
- if (this.disposed || this.port.closed()) throw fail("closed", outcome);
1094
- if (this.revision !== revision) throw fail("stale", outcome);
1095
- }
1096
- async write(ticket, revision, command) {
1097
- this.check(ticket, revision);
1098
- ticket.dispatch();
1099
- await command();
1100
- this.check(ticket, revision);
1101
- }
1102
- async suspend(ticket) {
1103
- this.assertRunning();
1104
- const revision = this.revision;
1105
- this.check(ticket, revision);
1106
- const rate = await this.port.readRate();
1107
- this.check(ticket, revision);
1108
- if (!Number.isFinite(rate)) throw fail("malformed");
1109
- this.phase = "unknown";
1110
- this.priorRate = rate;
1111
- await this.write(ticket, revision, () => this.port.rate(0));
1112
- await this.write(ticket, revision, () => this.port.focus(false));
1113
- await this.write(ticket, revision, () => this.port.lifecycle("frozen"));
1114
- const receipt = Object.freeze(PageSuspension.make({
1115
- pageId: this.pageId,
1116
- targetId: this.targetId,
1117
- suspensionId: this.freshId()
1118
- }));
1119
- this.check(ticket, revision);
1120
- this.receipt = receipt;
1121
- this.phase = "suspended";
1122
- return receipt;
1123
- }
1124
- async resume(receipt, ticket) {
1125
- this.check(ticket, this.revision);
1126
- if (this.phase !== "suspended" || this.receipt === void 0 || receipt.pageId !== this.pageId || receipt.targetId !== this.targetId || receipt.suspensionId !== this.receipt.suspensionId || this.priorRate === void 0) throw fail("stale");
1127
- const revision = this.revision;
1128
- const rate = this.priorRate;
1129
- this.receipt = void 0;
1130
- this.phase = "unknown";
1131
- await this.write(ticket, revision, () => this.port.lifecycle("active"));
1132
- await this.write(ticket, revision, () => this.port.activate());
1133
- await this.write(ticket, revision, () => this.port.focus(true));
1134
- await this.write(ticket, revision, () => this.port.frameBarrier(ticket));
1135
- await this.write(ticket, revision, () => this.port.rate(rate));
1136
- this.priorRate = void 0;
1137
- this.phase = "running";
1138
- }
1139
- async dispose() {
1140
- if (this.disposed) return;
1141
- this.disposed = true;
1142
- this.phase = "unknown";
1143
- this.receipt = void 0;
1144
- this.revision++;
1145
- await this.port.detach();
1146
- }
1147
- };
1148
- //#endregion
1149
- //#region src/internal/browser/NativePageControl.ts
1150
- /**
1151
- * One native page execution per registered page, created once over its own CDP session. Its
1152
- * receipt is invalidated when the page closes or its main frame navigates, and holds and
1153
- * resumes act only on the exact page and target they name.
1154
- */
1155
- const makePageControl = (browser, context, options, targets, callbacks, events, closing, held) => {
1156
- const { current, entries } = targets;
1157
- const execution = (entry) => {
1158
- if (!options.pageControl) return Promise.reject(failure("unsupported", "undispatched"));
1159
- entry.execution ??= sanitize(async () => {
1160
- const cdp = await context.newCDPSession(entry.page);
1161
- try {
1162
- const targetId = await targets.targetId(entry);
1163
- if (closing() || entry.page.isClosed()) throw failure("closed");
1164
- await cdp.send("Emulation.setFocusEmulationEnabled", { enabled: true });
1165
- if (closing() || entry.page.isClosed()) throw failure("closed");
1166
- const control = new PageExecution(entry.id, targetId, {
1167
- readRate: async () => safeDecode(Schema.Struct({ playbackRate: Schema.Finite }), await cdp.send("Animation.getPlaybackRate")).playbackRate,
1168
- rate: async (playbackRate) => {
1169
- await cdp.send("Animation.setPlaybackRate", { playbackRate });
1170
- },
1171
- focus: async (enabled) => {
1172
- await cdp.send("Emulation.setFocusEmulationEnabled", { enabled });
1173
- },
1174
- lifecycle: async (state) => {
1175
- await cdp.send("Page.setWebLifecycleState", { state });
1176
- },
1177
- activate: async () => {
1178
- await cdp.send("Page.bringToFront");
1179
- },
1180
- closed: () => closing() || !browser.isConnected() || entry.page.isClosed(),
1181
- detach: async () => {
1182
- if (entry.page.isClosed() || !browser.isConnected()) return;
1183
- try {
1184
- await closeWithin(() => cdp.detach());
1185
- } catch (error) {
1186
- if (!entry.page.isClosed() && browser.isConnected()) throw error;
1187
- }
1188
- },
1189
- frameBarrier: async (ticket) => {
1190
- ticket.check();
1191
- const tree = safeDecode(Schema.Struct({ frameTree: Schema.Struct({ frame: Schema.Struct({ id: Identifier }) }) }), await cdp.send("Page.getFrameTree"));
1192
- ticket.check();
1193
- const world = await cdp.send("Page.createIsolatedWorld", {
1194
- frameId: tree.frameTree.frame.id,
1195
- worldName: "effect-agent-browserbase-page-control"
1196
- });
1197
- ticket.check();
1198
- const result = await cdp.send("Runtime.evaluate", {
1199
- expression: "new Promise(resolve => requestAnimationFrame(() => resolve(true)))",
1200
- contextId: safeDecode(Schema.Int.check(Schema.isGreaterThan(0)), world.executionContextId),
1201
- awaitPromise: true,
1202
- returnByValue: true,
1203
- userGesture: false,
1204
- timeout: Math.min(2e3, ticket.remainingMillis())
1205
- });
1206
- ticket.check();
1207
- if (result.exceptionDetails !== void 0 || result.result.value !== true) throw failure("malformed");
1208
- }
1209
- });
1210
- entry.executionValue = control;
1211
- return control;
1212
- } catch (error) {
1213
- await closeWithin(() => cdp.detach()).catch(() => {});
1214
- throw error;
1215
- }
1216
- });
1217
- return entry.execution;
1218
- };
1219
- const explicit = async (target, ticket) => {
1220
- ticket.check();
1221
- const entry = entries.get(target.pageId);
1222
- if (entry === void 0 || entry.page.isClosed()) throw failure("closed", "undispatched");
1223
- const control = await execution(entry);
1224
- ticket.check();
1225
- if (control.targetId !== target.targetId) throw failure("stale", "undispatched");
1226
- return control;
1227
- };
1228
- /** A closed page's execution is invalidated at once and disposed outside the event. */
1229
- const closed = (entry) => {
1230
- entry.executionValue?.invalidate();
1231
- if (entry.execution !== void 0) callbacks.submit(async () => {
1232
- await (await entry.execution)?.dispose();
1233
- });
1234
- };
1235
- /** A main-frame navigation invalidates the execution; one that was held faults the session. */
1236
- const navigating = (entry, frame) => {
1237
- if (frame === entry.page.mainFrame() && entry.executionValue?.invalidate()) events.fault();
1238
- };
1239
- const operations = {
1240
- state: (page, ticket) => sanitize(async () => (await explicit(page, ticket)).state()),
1241
- suspend: (page, ticket) => sanitize(async () => {
1242
- const control = await explicit(page, ticket);
1243
- held(control.pageId);
1244
- return control.suspend(ticket);
1245
- }),
1246
- resume: (receipt, ticket) => sanitize(async () => {
1247
- const control = await explicit(receipt, ticket);
1248
- held(control.pageId);
1249
- return control.resume(receipt, ticket);
1250
- }),
1251
- checkSelected: (ticket) => sanitize(async () => {
1252
- ticket.check();
1253
- const control = await execution(current().entry);
1254
- ticket.check();
1255
- control.assertRunning();
1256
- })
1257
- };
1258
- const dispose = () => Promise.allSettled([...entries.values()].map(async (entry) => {
1259
- if (entry.execution !== void 0) await (await entry.execution).dispose();
1260
- }));
1261
- return {
1262
- execution,
1263
- closed,
1264
- navigating,
1265
- operations,
1266
- dispose
1267
- };
1268
- };
1269
- //#endregion
1270
- //#region src/internal/browser/PageRead.ts
1271
- /**
1272
- * What the page reader returns. It is decoded on the host, so a page that tampers with its own
1273
- * prototypes can make a read fail but cannot make it return something unbounded or untyped.
1274
- */
1275
- const PageReadResult = Schema.Struct({
1276
- text: Schema.String.check(Schema.isMaxLength(131072)),
1277
- textTruncated: Schema.Boolean,
1278
- controlsTruncated: Schema.Boolean,
1279
- controls: Schema.Array(ControlFacts).check(Schema.isMaxLength(64)),
1280
- viewport: Schema.Struct({
1281
- width: Schema.Finite,
1282
- height: Schema.Finite,
1283
- clippedText: Schema.Natural,
1284
- coveredText: Schema.Natural,
1285
- uncertainText: Schema.Natural,
1286
- unreachableControls: Schema.Natural,
1287
- exhausted: Schema.Boolean
1288
- })
1289
- });
1290
- /** What makes a control the one that was inspected. Geometry is excluded: scrolling moves it. */
1291
- const identityOf = (facts) => JSON.stringify([
1292
- facts.kind,
1293
- facts.label,
1294
- facts.disabled,
1295
- facts.editable,
1296
- facts.inputType ?? null,
1297
- facts.autocomplete ?? null,
1298
- facts.destination ?? null,
1299
- facts.formMethod ?? null
1300
- ]);
1301
- /** The model-facing projection: no destination, form or geometry ever leaves through it. */
1302
- const observedControl = (facts, elementId) => ObservedControl.make({
1303
- elementId,
1304
- kind: facts.kind,
1305
- label: facts.label,
1306
- disabled: facts.disabled
1307
- });
1308
- /**
1309
- * Runs inside the page, so it is one self-contained function: it may not reference anything in
1310
- * this module. It reads; it never scrolls, focuses, mutates or dispatches.
1311
- *
1312
- * Visibility here is geometry and hit-testing, never a pixel comparison. Text is kept only when
1313
- * its line boxes intersect the viewport and the browser finds its own element at a sampled
1314
- * point. Text under something that takes no pointer events cannot be hit-tested at all, so it is
1315
- * counted as uncertain and left out rather than promoted to visible evidence.
1316
- */
1317
- const readPage = (request) => {
1318
- const { scope, maximumBytes, controlLimit, nodeBudget, only } = request;
1319
- const width = window.innerWidth;
1320
- const height = window.innerHeight;
1321
- const evidence = {
1322
- width,
1323
- height,
1324
- clippedText: 0,
1325
- coveredText: 0,
1326
- uncertainText: 0,
1327
- unreachableControls: 0,
1328
- exhausted: false
1329
- };
1330
- const intersects = (rect) => rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.right > 0 && rect.top < height && rect.left < width;
1331
- const within = (rect) => rect.top >= 0 && rect.left >= 0 && rect.bottom <= height && rect.right <= width;
1332
- const passThrough = [];
1333
- let visited = 0;
1334
- if (only === void 0 && scope === "viewport") {
1335
- const all = document.body?.getElementsByTagName("*") ?? [];
1336
- for (let i = 0; i < all.length; i++) {
1337
- if (++visited > nodeBudget) {
1338
- evidence.exhausted = true;
1339
- break;
1340
- }
1341
- const element = all[i];
1342
- if (element === void 0) continue;
1343
- const style = getComputedStyle(element);
1344
- if (style.pointerEvents !== "none" || style.visibility === "hidden") continue;
1345
- if (passThrough.some((known) => known.element.contains(element))) continue;
1346
- const rect = element.getBoundingClientRect();
1347
- if (intersects(rect) && Number(style.opacity) > 0) passThrough.push({
1348
- element,
1349
- rect
1350
- });
1351
- }
1352
- }
1353
- /** Whether the browser finds `owner` at this point, and how far that can be trusted. */
1354
- const hitTest = (owner, rect) => {
1355
- const x = Math.min(width - 1, Math.max(0, rect.left + rect.width / 2));
1356
- const y = Math.min(height - 1, Math.max(0, rect.top + rect.height / 2));
1357
- let hit = document.elementFromPoint(x, y);
1358
- while (hit?.shadowRoot) {
1359
- const inner = hit.shadowRoot.elementFromPoint(x, y);
1360
- if (inner === null || inner === hit) break;
1361
- hit = inner;
1362
- }
1363
- if (hit === null) return "uncertain";
1364
- if (hit !== owner && !owner.contains(hit) && !hit.contains(owner)) return "covered";
1365
- return passThrough.some((layer) => !layer.element.contains(owner) && !owner.contains(layer.element) && x >= layer.rect.left && x < layer.rect.right && y >= layer.rect.top && y < layer.rect.bottom) ? "uncertain" : "self";
1366
- };
1367
- const factsOf = (node) => {
1368
- const tag = node.tagName.toLowerCase();
1369
- const field = node instanceof HTMLInputElement || node instanceof HTMLButtonElement || node instanceof HTMLSelectElement || node instanceof HTMLTextAreaElement ? node : void 0;
1370
- const form = node instanceof HTMLButtonElement && node.type === "submit" || node instanceof HTMLInputElement && (node.type === "submit" || node.type === "image") ? field?.form : void 0;
1371
- const target = node instanceof HTMLAnchorElement || node instanceof HTMLAreaElement ? node.href : form === void 0 || form === null ? void 0 : node.hasAttribute("formaction") && field !== void 0 && "formAction" in field ? field.formAction : form.action;
1372
- const method = form === void 0 || form === null ? void 0 : (node.hasAttribute("formmethod") && field !== void 0 && "formMethod" in field ? field.formMethod : form.method).toLowerCase();
1373
- const disabled = node.matches(":disabled") || node.getAttribute("aria-disabled") === "true";
1374
- const rect = node.getBoundingClientRect();
1375
- const placement = !intersects(rect) ? "outside" : within(rect) ? "inside" : "partial";
1376
- const autocomplete = node instanceof HTMLInputElement || node instanceof HTMLSelectElement || node instanceof HTMLTextAreaElement ? node.autocomplete : "";
1377
- return {
1378
- kind: tag === "a" ? "link" : [
1379
- "button",
1380
- "input",
1381
- "select",
1382
- "textarea"
1383
- ].includes(tag) ? tag : "other",
1384
- label: (node.getAttribute("aria-label") ?? node.getAttribute("placeholder") ?? (node instanceof HTMLInputElement ? node.labels?.[0]?.textContent : node.textContent) ?? "").slice(0, 256),
1385
- disabled,
1386
- editable: !disabled && (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement ? !node.readOnly : node instanceof HTMLSelectElement),
1387
- ...node instanceof HTMLInputElement || node instanceof HTMLButtonElement ? { inputType: node.type.slice(0, 32) } : {},
1388
- ...autocomplete === "" ? {} : { autocomplete: autocomplete.slice(0, 128) },
1389
- ...target === void 0 || target.length > 2048 ? {} : { destination: target },
1390
- ...method === "get" || method === "post" || method === "dialog" ? { formMethod: method } : {},
1391
- box: {
1392
- x: rect.left,
1393
- y: rect.top,
1394
- width: rect.width,
1395
- height: rect.height
1396
- },
1397
- placement,
1398
- hitTest: placement === "outside" ? "unsampled" : hitTest(node, rect),
1399
- mainFrame: window.top === window
1400
- };
1401
- };
1402
- if (only !== void 0) return {
1403
- nodes: [only],
1404
- data: { facts: factsOf(only) }
1405
- };
1406
- const nodes = [];
1407
- const controls = [];
1408
- const candidates = document.querySelectorAll("a[href],button,input,select,textarea,[role=button]");
1409
- let controlsTruncated = false;
1410
- for (let i = 0; i < candidates.length; i++) {
1411
- if (++visited > nodeBudget) {
1412
- evidence.exhausted = true;
1413
- controlsTruncated = true;
1414
- break;
1415
- }
1416
- const node = candidates[i];
1417
- if (node === void 0) continue;
1418
- const facts = factsOf(node);
1419
- if (scope === "viewport" && facts.hitTest !== "self") {
1420
- if (facts.placement !== "outside") evidence.unreachableControls++;
1421
- continue;
1422
- }
1423
- if (nodes.length >= controlLimit) {
1424
- controlsTruncated = true;
1425
- break;
1426
- }
1427
- nodes.push(node);
1428
- controls.push(facts);
1429
- }
1430
- const encoder = new TextEncoder();
1431
- let text = "";
1432
- let textTruncated = false;
1433
- if (scope === "document") {
1434
- const encoded = encoder.encode(document.body?.innerText ?? "");
1435
- let end = Math.min(encoded.length, maximumBytes);
1436
- while (end > 0 && end < encoded.length && ((encoded[end] ?? 0) & 192) === 128) end--;
1437
- text = new TextDecoder().decode(encoded.subarray(0, end));
1438
- textTruncated = end < encoded.length;
1439
- } else if (document.body !== null) {
1440
- const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
1441
- const range = document.createRange();
1442
- let bytes = 0;
1443
- let lastBottom = Number.NEGATIVE_INFINITY;
1444
- const boxAt = (node, index) => {
1445
- const length = node.data.length;
1446
- for (let k = index; k < Math.min(length, index + 8); k++) {
1447
- range.setStart(node, k);
1448
- range.setEnd(node, k + 1);
1449
- const rect = range.getClientRects()[0];
1450
- if (rect !== void 0 && rect.width + rect.height > 0) return rect;
1451
- }
1452
- };
1453
- for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
1454
- if (++visited > nodeBudget) {
1455
- evidence.exhausted = true;
1456
- textTruncated = true;
1457
- break;
1458
- }
1459
- const owner = node.parentElement;
1460
- if (!(node instanceof Text) || owner === null || node.data.trim() === "") continue;
1461
- if ([
1462
- "SCRIPT",
1463
- "STYLE",
1464
- "NOSCRIPT",
1465
- "TEMPLATE"
1466
- ].includes(owner.tagName)) continue;
1467
- if (owner.checkVisibility?.({
1468
- opacityProperty: true,
1469
- visibilityProperty: true
1470
- }) === false) continue;
1471
- range.selectNodeContents(node);
1472
- const first = [...range.getClientRects()].filter(intersects)[0];
1473
- if (first === void 0) continue;
1474
- const verdict = hitTest(owner, first);
1475
- if (verdict !== "self") {
1476
- if (verdict === "covered") evidence.coveredText++;
1477
- else evidence.uncertainText++;
1478
- continue;
1479
- }
1480
- range.selectNodeContents(node);
1481
- let visible = node.data;
1482
- if (![...range.getClientRects()].every(within)) {
1483
- const length = node.data.length;
1484
- let low = 0;
1485
- let high = length;
1486
- while (low < high) {
1487
- const middle = low + high >> 1;
1488
- const rect = boxAt(node, middle);
1489
- if (rect === void 0 || rect.bottom <= 0) low = middle + 1;
1490
- else high = middle;
1491
- }
1492
- const start = low;
1493
- high = length;
1494
- while (low < high) {
1495
- const middle = low + high >> 1;
1496
- const rect = boxAt(node, middle);
1497
- if (rect !== void 0 && rect.top >= height) high = middle;
1498
- else low = middle + 1;
1499
- }
1500
- visible = node.data.slice(start, low);
1501
- evidence.clippedText++;
1502
- }
1503
- const fragment = visible.replace(/\s+/g, " ").trim();
1504
- if (fragment === "") continue;
1505
- const joined = (text === "" ? "" : first.top >= lastBottom - 1 ? "\n" : " ") + fragment;
1506
- const size = encoder.encode(joined).length;
1507
- if (bytes + size > maximumBytes) {
1508
- textTruncated = true;
1509
- break;
1510
- }
1511
- text += joined;
1512
- bytes += size;
1513
- lastBottom = Math.max(lastBottom, first.bottom);
1514
- }
1515
- }
1516
- return {
1517
- nodes,
1518
- data: {
1519
- text,
1520
- textTruncated,
1521
- controlsTruncated,
1522
- controls,
1523
- viewport: evidence
1524
- }
1525
- };
1526
- };
1527
- //#endregion
1528
- //#region src/internal/browser/Observation.ts
1529
- const TextResult = Schema.Struct({
1530
- text: Schema.String,
1531
- missing: Schema.Boolean,
1532
- overLimit: Schema.Boolean
1533
- });
1534
- const Geometry = Schema.Struct({
1535
- width: Schema.Natural,
1536
- height: Schema.Natural
1537
- });
1538
- const Count = Schema.Natural.check(Schema.isLessThanOrEqualTo(1e6));
1539
- const Facts = Schema.Struct({ facts: ControlFacts });
1540
- /** Bounds the page traversal itself, so a huge document costs a bounded read. */
1541
- const NodeBudget = 2e4;
1542
- /**
1543
- * What is read from the selected document, and the one retained observation whose nodes a later
1544
- * action may name. That observation is valid only until the next invalidating event.
1545
- */
1546
- const makeObservation = (targets, events) => {
1547
- const { current } = targets;
1548
- let observation;
1549
- let observationSerial = 0;
1550
- const invalidate = () => {
1551
- if (observation !== void 0) observation.validity = "invalid";
1552
- };
1553
- /**
1554
- * A read is ordered against document replacement by failing: if the document it was reading
1555
- * was replaced underneath it, or a navigation is still in flight on its page, the native error
1556
- * is `target-changed` and `undispatched`, so a caller knows to read again. A read is never a
1557
- * mutation, so reading again is always safe.
1558
- */
1559
- const reading = async (body) => {
1560
- const { entry, frame } = current();
1561
- const epoch = targets.epochOf(frame);
1562
- try {
1563
- return await body();
1564
- } catch (error) {
1565
- if (!Schema.is(BrowserError)(error) && (!Schema.is(NativeFailure)(error) || error.reason === "provider") && (targets.epochOf(frame) !== epoch || targets.navigating.has(entry.id))) throw failure("target-changed", "undispatched");
1566
- throw error;
1567
- }
1568
- };
1569
- const changed = (reason) => {
1570
- invalidate();
1571
- events.invalidate(reason);
1572
- };
1573
- /**
1574
- * Holding or resuming a page may run its `freeze` and `resume` handlers, so nothing observed
1575
- * on it may be acted on unchecked. Another page's observation is untouched: a stage hold is
1576
- * independent of the page an agent is driving.
1577
- */
1578
- const held = (pageId) => {
1579
- if (observation?.pageId === pageId && observation.validity === "valid") {
1580
- observation.validity = "suspended";
1581
- observation.revalidated.clear();
1582
- }
1583
- };
1584
- const dispose = async () => {
1585
- const old = observation;
1586
- observation = void 0;
1587
- if (old !== void 0) {
1588
- old.validity = "invalid";
1589
- await closeWithin(() => Promise.allSettled([...old.nodes.values()].map((node) => node.handle.dispose())));
1590
- }
1591
- };
1592
- const exactElement = async (selector, ticket) => {
1593
- ticket.check();
1594
- const holder = await current().frame.evaluateHandle((requested) => {
1595
- try {
1596
- const matches = document.querySelectorAll(requested);
1597
- return {
1598
- count: matches.length,
1599
- node: matches.length === 1 ? matches[0] : null
1600
- };
1601
- } catch {
1602
- return {
1603
- count: 0,
1604
- node: null
1605
- };
1606
- }
1607
- }, selector);
1608
- let node;
1609
- try {
1610
- const countHandle = await holder.getProperty("count");
1611
- let count;
1612
- try {
1613
- count = safeDecode(Count, await countHandle.jsonValue());
1614
- } finally {
1615
- await countHandle.dispose();
1616
- }
1617
- ticket.check();
1618
- if (count !== 1) throw failure(count === 0 ? "not-found" : "ambiguous", "undispatched");
1619
- node = await holder.getProperty("node");
1620
- const element = node.asElement();
1621
- if (element === null) throw failure("not-found", "undispatched");
1622
- ticket.check();
1623
- return element;
1624
- } catch (error) {
1625
- await node?.dispose().catch(() => {});
1626
- throw error;
1627
- } finally {
1628
- await holder.dispose();
1629
- }
1630
- };
1631
- /** A retained node is only as current as the observation that produced it. */
1632
- const retained = (target, allowSuspended = false) => {
1633
- const snapshot = observation;
1634
- if (snapshot === void 0 || snapshot.id !== target.observationId) throw failure("stale", "undispatched");
1635
- const usable = snapshot.validity === "valid" || snapshot.validity === "suspended" && (allowSuspended || snapshot.revalidated.has(target.elementId));
1636
- const node = snapshot.nodes.get(target.elementId);
1637
- if (!usable || node === void 0) throw failure("stale", "undispatched");
1638
- return node;
1639
- };
1640
- /**
1641
- * Read from the exact node, never re-resolved from a selector or a label. It is the same page
1642
- * function an observation uses, told to read one node and traverse nothing.
1643
- */
1644
- const factsOf = async (element) => {
1645
- const holder = await current().frame.evaluateHandle(readPage, {
1646
- scope: "document",
1647
- maximumBytes: 0,
1648
- controlLimit: 0,
1649
- nodeBudget: 0,
1650
- only: element
1651
- });
1652
- try {
1653
- const data = await holder.getProperty("data");
1654
- try {
1655
- return safeDecode(Facts, await data.jsonValue()).facts;
1656
- } finally {
1657
- await data.dispose();
1658
- }
1659
- } finally {
1660
- await holder.dispose();
1661
- }
1662
- };
1663
- /**
1664
- * The exact attached node a target names, checked against the page right now. A selector must
1665
- * still match it and nothing else. A retained node must still be the control that was
1666
- * inspected: the same node with a different destination, type or label is not. A host policy
1667
- * then decides on those fresh facts, and a refusal or a policy that throws sends nothing.
1668
- */
1669
- const resolve = async (target, ticket, policy, allowSuspended = false) => {
1670
- const kept = typeof target !== "string";
1671
- const node = typeof target === "string" ? void 0 : retained(target, allowSuspended);
1672
- const element = typeof target === "string" ? await exactElement(target, ticket) : node?.handle;
1673
- if (element === void 0) throw failure("stale", "undispatched");
1674
- try {
1675
- if (await element.evaluate((candidate, selector) => {
1676
- if (!candidate.isConnected) return false;
1677
- if (selector === void 0) return true;
1678
- const matches = candidate.ownerDocument.querySelectorAll(selector);
1679
- return matches.length === 1 && matches[0] === candidate;
1680
- }, typeof target === "string" ? target : void 0) !== true) throw failure("stale", "undispatched");
1681
- if (node !== void 0 || policy !== void 0) {
1682
- const facts = await factsOf(element);
1683
- if (node !== void 0 && identityOf(facts) !== node.identity) throw failure("stale", "undispatched");
1684
- if (policy !== void 0) {
1685
- let admitted = false;
1686
- try {
1687
- admitted = policy(facts) === true;
1688
- } catch {
1689
- admitted = false;
1690
- }
1691
- if (!admitted) throw failure("denied", "undispatched");
1692
- }
1693
- }
1694
- ticket.check();
1695
- return {
1696
- element,
1697
- kept
1698
- };
1699
- } catch (error) {
1700
- if (!kept) await closeWithin(() => element.dispose()).catch(() => {});
1701
- throw error;
1702
- }
1703
- };
1704
- const controlFacts = (target, ticket) => sanitize(async () => {
1705
- const { element } = await resolve(target, ticket);
1706
- return factsOf(element);
1707
- });
1708
- /** After a hold, one node at a time: still attached, and still the control inspected. */
1709
- const revalidate = (target, ticket) => sanitize(async () => {
1710
- await resolve(target, ticket, void 0, true);
1711
- observation?.revalidated.add(target.elementId);
1712
- });
1713
- const readText = (selector, maximumBytes, ticket) => sanitize(async () => {
1714
- ticket.check();
1715
- const raw = await reading(() => current().frame.evaluate(({ selector, maximumBytes }) => {
1716
- const element = selector === void 0 ? document.body : document.querySelector(selector);
1717
- if (element === null) return {
1718
- text: "",
1719
- missing: true,
1720
- overLimit: false
1721
- };
1722
- const text = element instanceof HTMLElement ? element.innerText : element.textContent ?? "";
1723
- if (new TextEncoder().encode(text).length > maximumBytes) return {
1724
- text: "",
1725
- missing: false,
1726
- overLimit: true
1727
- };
1728
- return {
1729
- text,
1730
- missing: false,
1731
- overLimit: false
1732
- };
1733
- }, {
1734
- selector,
1735
- maximumBytes
1736
- }));
1737
- ticket.check();
1738
- const value = safeDecode(TextResult, raw);
1739
- if (value.missing) throw failure("not-found");
1740
- if (value.overLimit || new TextEncoder().encode(value.text).length > maximumBytes) throw failure("limit");
1741
- return value.text;
1742
- });
1743
- /** One bounded read of the selected frame, and the node handles it names, in order. */
1744
- const read = async (scope, maximumBytes, controlLimit, ticket, keepNodes) => {
1745
- const holder = await current().frame.evaluateHandle(readPage, {
1746
- scope,
1747
- maximumBytes,
1748
- controlLimit,
1749
- nodeBudget: NodeBudget
1750
- });
1751
- const handles = [];
1752
- let nodesHandle;
1753
- try {
1754
- const dataHandle = await holder.getProperty("data");
1755
- let data;
1756
- try {
1757
- data = safeDecode(PageReadResult, await dataHandle.jsonValue());
1758
- } finally {
1759
- await dataHandle.dispose();
1760
- }
1761
- if (new TextEncoder().encode(data.text).length > maximumBytes || data.controls.length > controlLimit) throw failure("limit");
1762
- if (keepNodes) {
1763
- nodesHandle = await holder.getProperty("nodes");
1764
- for (let i = 0; i < data.controls.length; i++) {
1765
- const node = await nodesHandle.getProperty(String(i));
1766
- const element = node.asElement();
1767
- if (element === null) {
1768
- await node.dispose();
1769
- throw failure("malformed");
1770
- }
1771
- handles.push(element);
1772
- }
1773
- }
1774
- ticket.check();
1775
- return {
1776
- data,
1777
- handles
1778
- };
1779
- } catch (error) {
1780
- await Promise.allSettled(handles.map((handle) => handle.dispose()));
1781
- throw error;
1782
- } finally {
1783
- await nodesHandle?.dispose().catch(() => {});
1784
- await holder.dispose();
1785
- }
1786
- };
1787
- const observe = (scope, maximumBytes, controlLimit, ticket) => sanitize(async () => {
1788
- await dispose();
1789
- ticket.check();
1790
- const pageId = current().entry.id;
1791
- const { data, handles } = await reading(() => read(scope, maximumBytes, controlLimit, ticket, true));
1792
- const id = `observation-${++observationSerial}`;
1793
- const nodes = /* @__PURE__ */ new Map();
1794
- handles.forEach((handle, i) => {
1795
- const facts = data.controls[i];
1796
- if (facts !== void 0) nodes.set(`element-${i}`, {
1797
- handle,
1798
- identity: identityOf(facts)
1799
- });
1800
- });
1801
- if (nodes.size !== data.controls.length) {
1802
- await Promise.allSettled(handles.map((handle) => handle.dispose()));
1803
- throw failure("malformed");
1804
- }
1805
- observation = {
1806
- id,
1807
- pageId,
1808
- validity: "valid",
1809
- nodes,
1810
- revalidated: /* @__PURE__ */ new Set()
1811
- };
1812
- return {
1813
- observationId: id,
1814
- scope,
1815
- url: targets.selectedUrl(),
1816
- text: data.text,
1817
- textTruncated: data.textTruncated,
1818
- controlsTruncated: data.controlsTruncated,
1819
- controls: data.controls.map((facts, i) => observedControl(facts, `element-${i}`)),
1820
- viewport: data.viewport
1821
- };
1822
- });
1823
- const screenshot = (fullPage, maximumBytes, ticket) => sanitize(() => reading(async () => {
1824
- const page = current().entry.page;
1825
- ticket.check();
1826
- const raw = await page.evaluate((full) => ({
1827
- width: full ? Math.max(document.documentElement.scrollWidth, window.innerWidth) : window.innerWidth,
1828
- height: full ? Math.max(document.documentElement.scrollHeight, window.innerHeight) : window.innerHeight
1829
- }), fullPage);
1830
- const geometry = safeDecode(Geometry, raw);
1831
- if (geometry.width < 1 || geometry.height < 1 || geometry.width > 16384 || geometry.height > 16384 || geometry.width * geometry.height > 33554432) throw failure("limit");
1832
- ticket.check();
1833
- const bytes = await page.screenshot({
1834
- type: "png",
1835
- fullPage,
1836
- scale: "css",
1837
- timeout: timeout(ticket)
1838
- });
1839
- if (!(bytes instanceof Uint8Array)) throw failure("malformed");
1840
- if (bytes.length > maximumBytes) throw failure("limit");
1841
- const actual = pngGeometry(bytes);
1842
- if (actual.width > 16384 || actual.height > 16384 || actual.width * actual.height > 33554432) throw failure("limit");
1843
- ticket.check();
1844
- return new Uint8Array(bytes);
1845
- }));
1846
- /**
1847
- * Passive: it reads what is on screen and takes a picture, keeps no node, and leaves the
1848
- * retained observation exactly as it was. Text and picture are sampled one after the other,
1849
- * so a document replaced in between is reported rather than hidden.
1850
- */
1851
- const checkpoint = (maximumBytes, controlLimit, pictureBytes, ticket) => sanitize(async () => {
1852
- ticket.check();
1853
- const { frame } = current();
1854
- const epoch = targets.epochOf(frame);
1855
- const { data } = await reading(() => read("viewport", maximumBytes, controlLimit, ticket, false));
1856
- const picture = pictureBytes === void 0 ? void 0 : await screenshot(false, pictureBytes, ticket);
1857
- return {
1858
- url: targets.selectedUrl(),
1859
- ...data,
1860
- ...picture === void 0 ? {} : { picture },
1861
- documentChanged: targets.epochOf(frame) !== epoch
1862
- };
1863
- });
1864
- return {
1865
- invalidate,
1866
- changed,
1867
- held,
1868
- dispose,
1869
- resolve,
1870
- controlFacts,
1871
- revalidate,
1872
- readText,
1873
- observe,
1874
- screenshot,
1875
- checkpoint
1876
- };
1877
- };
1878
- //#endregion
1879
- //#region src/internal/browser/Pointer.ts
1880
- const Reach = Schema.Struct({
1881
- viewportWidth: Schema.Finite,
1882
- viewportHeight: Schema.Finite,
1883
- reachable: Schema.Boolean
1884
- });
1885
- /**
1886
- * Real pointer input to the selected page. Every command is one the browser would receive from
1887
- * a person, so handlers see trusted events and the browser decides what is under the pointer.
1888
- * Nothing here scrolls, eases or retries: a caller that wants a trajectory sends its points.
1889
- */
1890
- const makePointer = (targets, actions) => {
1891
- const { current } = targets;
1892
- const positions = /* @__PURE__ */ new WeakMap();
1893
- const moveTo = async (page, point) => {
1894
- await page.mouse.move(point.x, point.y);
1895
- positions.set(page, point);
1896
- };
1897
- const receipt = (page) => ({ position: positions.get(page) ?? null });
1898
- const pointerMove = (point, ticket) => sanitize(async () => {
1899
- const { page } = current().entry;
1900
- ticket.dispatch();
1901
- await moveTo(page, point);
1902
- ticket.check();
1903
- return receipt(page);
1904
- });
1905
- /**
1906
- * The centre of the element's box, in main-frame viewport pixels, and only if the pointer can
1907
- * really be placed there: the box has area, the point is inside the viewport, and the element
1908
- * is what the browser finds at that point. An open shadow root is followed to the node a
1909
- * person would actually be over.
1910
- */
1911
- const reachablePoint = async (page, element) => {
1912
- const box = await element.boundingBox();
1913
- if (box === null || box.width <= 0 || box.height <= 0) throw failure("not-visible", "undispatched");
1914
- const point = {
1915
- x: box.x + box.width / 2,
1916
- y: box.y + box.height / 2
1917
- };
1918
- const reach = safeDecode(Reach, await element.evaluate((node) => {
1919
- const rect = node.getBoundingClientRect();
1920
- const x = rect.left + rect.width / 2;
1921
- const y = rect.top + rect.height / 2;
1922
- let hit = node.ownerDocument.elementFromPoint(x, y);
1923
- while (hit?.shadowRoot) {
1924
- const inner = hit.shadowRoot.elementFromPoint(x, y);
1925
- if (inner === null || inner === hit) break;
1926
- hit = inner;
1927
- }
1928
- return {
1929
- viewportWidth: window.top === window ? window.innerWidth : Number.POSITIVE_INFINITY,
1930
- viewportHeight: window.top === window ? window.innerHeight : Number.POSITIVE_INFINITY,
1931
- reachable: hit !== null && (hit === node || node.contains(hit))
1932
- };
1933
- }));
1934
- const viewport = Number.isFinite(reach.viewportWidth) ? {
1935
- width: reach.viewportWidth,
1936
- height: reach.viewportHeight
1937
- } : safeDecode(Schema.Struct({
1938
- width: Schema.Finite,
1939
- height: Schema.Finite
1940
- }), await page.evaluate(() => ({
1941
- width: window.innerWidth,
1942
- height: window.innerHeight
1943
- })));
1944
- if (!reach.reachable || point.x < 0 || point.y < 0 || point.x >= viewport.width || point.y >= viewport.height) throw failure("not-visible", "undispatched");
1945
- return point;
1946
- };
1947
- const hover = (target, ticket, policy) => sanitize(async () => {
1948
- const { page } = current().entry;
1949
- await actions.withAdmittedElement(target, ticket, (element) => reachablePoint(page, element), (_element, point) => moveTo(page, point), policy);
1950
- ticket.check();
1951
- return receipt(page);
1952
- });
1953
- const wheel = (deltaX, deltaY, at, ticket) => sanitize(async () => {
1954
- const { page } = current().entry;
1955
- ticket.dispatch();
1956
- if (at !== void 0) await moveTo(page, at);
1957
- await page.mouse.wheel(deltaX, deltaY);
1958
- ticket.check();
1959
- return receipt(page);
1960
- });
1961
- return {
1962
- pointerMove,
1963
- hover,
1964
- wheel,
1965
- receipt
1966
- };
1967
- };
1968
- //#endregion
1969
- //#region src/internal/browser/Targets.ts
1970
- const TargetInfo = Schema.Struct({ targetInfo: Schema.Struct({
1971
- targetId: Identifier,
1972
- type: Schema.Literal("page")
1973
- }) });
1974
- const URLText = Schema.String.check(Schema.isMaxLength(8192));
1975
- /**
1976
- * The connection's pages, frames and document epochs, and the one selected target every other
1977
- * seam acts on. A page beyond the configured bound is closed and faulted, never tracked.
1978
- */
1979
- const makeTargets = (browser, context, options, callbacks, events, closing, hooks) => {
1980
- const entries = /* @__PURE__ */ new Map();
1981
- const byPage = /* @__PURE__ */ new WeakMap();
1982
- const frameIds = /* @__PURE__ */ new WeakMap();
1983
- const documentEpochs = /* @__PURE__ */ new WeakMap();
1984
- const selection = {};
1985
- const navigating = /* @__PURE__ */ new Set();
1986
- let serial = 0, frameSerial = 0;
1987
- let creatingPage = false;
1988
- const epochOf = (frame) => documentEpochs.get(frame) ?? 0;
1989
- const frameId = (frame) => {
1990
- let id = frameIds.get(frame);
1991
- if (id === void 0) {
1992
- id = `frame-${++frameSerial}`;
1993
- frameIds.set(frame, id);
1994
- }
1995
- return id;
1996
- };
1997
- const register = (page) => {
1998
- const existing = byPage.get(page);
1999
- if (existing !== void 0) return existing;
2000
- const entry = {
2001
- id: `page-${++serial}`,
2002
- page,
2003
- off: []
2004
- };
2005
- byPage.set(page, entry);
2006
- if (entries.size >= options.maxPages) {
2007
- callbacks.submit(() => closeWithin(() => page.close()));
2008
- events.fault();
2009
- return entry;
2010
- }
2011
- entries.set(entry.id, entry);
2012
- for (const frame of page.frames()) frameId(frame);
2013
- const onClose = () => {
2014
- hooks.closed(entry);
2015
- entries.delete(entry.id);
2016
- for (const off of entry.off.splice(0)) off();
2017
- if (selection.entry === entry) {
2018
- selection.entry = void 0;
2019
- selection.frame = void 0;
2020
- hooks.changed("target-changed");
2021
- }
2022
- };
2023
- const onNavigation = (frame) => {
2024
- hooks.navigating(entry, frame);
2025
- documentEpochs.set(frame, epochOf(frame) + 1);
2026
- hooks.navigated(entry, frame);
2027
- frameId(frame);
2028
- hooks.frameChanged(entry, frame);
2029
- if (selection.entry === entry && (selection.frame === frame || frame === page.mainFrame())) hooks.changed("target-changed");
2030
- };
2031
- const onDetached = (frame) => {
2032
- hooks.frameChanged(entry, frame);
2033
- if (selection.frame === frame) {
2034
- selection.frame = void 0;
2035
- hooks.changed("target-changed");
2036
- }
2037
- };
2038
- const onDialog = (dialog) => {
2039
- hooks.dialog(dialog);
2040
- };
2041
- page.on("close", onClose);
2042
- page.on("framenavigated", onNavigation);
2043
- page.on("framedetached", onDetached);
2044
- page.on("dialog", onDialog);
2045
- entry.off.push(() => page.off("close", onClose), () => page.off("framenavigated", onNavigation), () => page.off("framedetached", onDetached), () => page.off("dialog", onDialog));
2046
- hooks.opened(entry, creatingPage);
2047
- return entry;
2048
- };
2049
- const current = () => {
2050
- if (closing() || !browser.isConnected() || selection.entry === void 0 || selection.entry.page.isClosed() || selection.frame === void 0 || selection.frame.isDetached()) throw failure("closed", "undispatched");
2051
- return {
2052
- entry: selection.entry,
2053
- frame: selection.frame
2054
- };
2055
- };
2056
- const targetId = async (entry) => {
2057
- if (entry.targetId !== void 0) return entry.targetId;
2058
- const cdp = await context.newCDPSession(entry.page);
2059
- try {
2060
- const info = await cdp.send("Target.getTargetInfo");
2061
- entry.targetId = safeDecode(TargetInfo, info).targetInfo.targetId;
2062
- return entry.targetId;
2063
- } finally {
2064
- await closeWithin(() => cdp.detach()).catch(() => {});
2065
- }
2066
- };
2067
- const urlOf = (frame) => safeDecode(URLText, frame.url());
2068
- const selectedUrl = () => urlOf(current().frame);
2069
- /**
2070
- * Chooses the connection's first target: a page it opens itself, the one the caller named, or
2071
- * the only page present. Assignments land on the live selection as they happen.
2072
- */
2073
- const selectInitial = async () => {
2074
- if (options.newPage || entries.size === 0) {
2075
- creatingPage = true;
2076
- try {
2077
- selection.entry = register(await context.newPage());
2078
- } finally {
2079
- creatingPage = false;
2080
- }
2081
- } else if (options.initialTargetId !== void 0) {
2082
- for (const entry of entries.values()) if (await targetId(entry) === options.initialTargetId) selection.entry = entry;
2083
- if (selection.entry === void 0) throw failure("not-found");
2084
- } else {
2085
- if (entries.size !== 1) throw failure("ambiguous");
2086
- selection.entry = entries.values().next().value;
2087
- }
2088
- };
2089
- const clear = () => {
2090
- entries.clear();
2091
- selection.entry = void 0;
2092
- selection.frame = void 0;
2093
- };
2094
- const selected = () => {
2095
- const { entry, frame } = current();
2096
- return {
2097
- pageId: entry.id,
2098
- frameId: frameId(frame)
2099
- };
2100
- };
2101
- const selectedTargetId = () => sanitize(() => targetId(current().entry));
2102
- const listPages = (ticket) => sanitize(async () => {
2103
- ticket.check();
2104
- const output = [];
2105
- for (const entry of entries.values()) {
2106
- const id = await targetId(entry);
2107
- const title = await entry.page.title();
2108
- ticket.check();
2109
- if (typeof title !== "string") throw failure("malformed");
2110
- output.push(safeDecode(PageInfo, {
2111
- pageId: entry.id,
2112
- targetId: id,
2113
- title: title.slice(0, 512),
2114
- url: entry.page.url(),
2115
- selected: selection.entry === entry
2116
- }));
2117
- }
2118
- return output;
2119
- });
2120
- const selectPage = (id, ticket) => sanitize(async () => {
2121
- const entry = entries.get(id);
2122
- ticket.check();
2123
- if (entry === void 0 || entry.page.isClosed()) throw failure("not-found", "undispatched");
2124
- await hooks.release();
2125
- ticket.check();
2126
- selection.entry = entry;
2127
- selection.frame = entry.page.mainFrame();
2128
- hooks.changed("target-changed");
2129
- });
2130
- const newPage = (ticket) => sanitize(async () => {
2131
- if (entries.size >= options.maxPages) throw failure("limit", "undispatched");
2132
- ticket.dispatch();
2133
- creatingPage = true;
2134
- try {
2135
- const page = await context.newPage();
2136
- if (ticket.signal.aborted) {
2137
- await closeWithin(() => page.close()).catch(() => {});
2138
- ticket.check();
2139
- }
2140
- return register(page).id;
2141
- } finally {
2142
- creatingPage = false;
2143
- }
2144
- });
2145
- const closePage = (id, ticket) => sanitize(async () => {
2146
- const entry = entries.get(id);
2147
- if (entry === void 0) throw failure("not-found", "undispatched");
2148
- ticket.dispatch();
2149
- await entry.page.close({ runBeforeUnload: false });
2150
- ticket.check();
2151
- });
2152
- const listFrames = (ticket) => sanitize(async () => {
2153
- ticket.check();
2154
- const frames = current().entry.page.frames();
2155
- if (frames.length > 128) throw failure("limit");
2156
- return frames.map((frame) => safeDecode(FrameInfo, {
2157
- frameId: frameId(frame),
2158
- parentFrameId: frame.parentFrame() === null ? null : frameId(frame.parentFrame()),
2159
- url: frame.url(),
2160
- name: frame.name().slice(0, 256)
2161
- }));
2162
- });
2163
- const selectFrame = (id, ticket) => sanitize(async () => {
2164
- const entry = selection.entry;
2165
- if (entry === void 0) throw failure("closed");
2166
- const frame = entry.page.frames().find((f) => frameId(f) === id);
2167
- if (frame === void 0 || frame.isDetached()) throw failure("not-found", "undispatched");
2168
- await hooks.release();
2169
- ticket.check();
2170
- selection.frame = frame;
2171
- hooks.changed("target-changed");
2172
- });
2173
- return {
2174
- entries,
2175
- selection,
2176
- epochOf,
2177
- frameId,
2178
- register,
2179
- current,
2180
- targetId,
2181
- urlOf,
2182
- /** A navigation this driver began on the page is still in flight. */
2183
- navigating: {
2184
- begin: (pageId) => void navigating.add(pageId),
2185
- end: (pageId) => void navigating.delete(pageId),
2186
- has: (pageId) => navigating.has(pageId)
2187
- },
2188
- selectedUrl,
2189
- selectInitial,
2190
- clear,
2191
- selected,
2192
- selectedTargetId,
2193
- listPages,
2194
- selectPage,
2195
- newPage,
2196
- closePage,
2197
- listFrames,
2198
- selectFrame
2199
- };
2200
- };
2201
- //#endregion
2202
- //#region src/internal/browser/Playwright.ts
2203
- const NativeWindow = Schema.Struct({ windowId: Schema.Natural });
2204
- /**
2205
- * The provider-issued address is accepted only as a credential-free Browserbase WSS URL. Every
2206
- * binding applies this first, including one a host routes elsewhere, so routing can never be
2207
- * reached with an address the provider did not plausibly issue.
2208
- */
2209
- const validateConnection = (connection) => {
2210
- if (typeof connection !== "string" || connection.length > 16384) throw failure("malformed");
2211
- let url;
2212
- try {
2213
- url = new URL(connection);
2214
- } catch {
2215
- throw failure("malformed");
2216
- }
2217
- if (url.protocol !== "wss:" || url.username || url.password || url.port || !url.hostname.endsWith(".browserbase.com")) throw failure("unsafe-url");
2218
- return connection;
2219
- };
2220
- /**
2221
- * Connects to an already validated or host-resolved CDP endpoint and builds the one driver. The
2222
- * import is lazy, so constructing any Layer cannot load Playwright or allocate a browser. A
2223
- * trusted observer sees the engine's browser before the driver exists and never replaces it.
2224
- */
2225
- const connectPlaywrightEndpoint = async (endpoint, signal, options, events, observe) => {
2226
- if (signal.aborted) throw failure("interrupted");
2227
- const { chromium } = await import("playwright-core");
2228
- const browser = await sanitize(() => chromium.connectOverCDP(endpoint, {
2229
- timeout: 15e3,
2230
- ...options.pageControl ? { noDefaults: true } : {}
2231
- }));
2232
- if (signal.aborted) {
2233
- await closeWithin(() => browser.close()).catch(() => {});
2234
- throw failure("interrupted");
2235
- }
2236
- try {
2237
- observe?.(browser);
2238
- const driver = await makePlaywrightDriver(browser, options, events);
2239
- if (signal.aborted) {
2240
- await driver.disconnect().catch(() => {});
2241
- throw failure("interrupted");
2242
- }
2243
- return driver;
2244
- } catch (error) {
2245
- await closeWithin(() => browser.close()).catch(() => {});
2246
- throw Schema.is(BrowserError)(error) || Schema.is(InitializationError)(error) || Schema.is(NativeFailure)(error) ? error : failure("provider");
2247
- }
2248
- };
2249
- /** Tests may supply a real already-connected browser. This is deliberately not an exported subpath. */
2250
- const makePlaywrightDriver = async (browser, options, events) => {
2251
- const contexts = browser.contexts();
2252
- if (contexts.length !== 1) throw failure("ambiguous");
2253
- const context = contexts[0];
2254
- const dialogs = /* @__PURE__ */ new Set();
2255
- const callbacks = new CallbackTasks(32, () => events.fault());
2256
- let closing = false;
2257
- let initialized = false;
2258
- let browserCdp;
2259
- const targets = makeTargets(browser, context, options, callbacks, events, () => closing, {
2260
- opened: (entry, created) => {
2261
- if (initialized && options.pageControl) callbacks.submit(async () => {
2262
- await pageControl.execution(entry);
2263
- });
2264
- if (initialized) initialization.attachPage(entry.page);
2265
- if (initialized && !created) {
2266
- if (options.popupPolicy === "close") callbacks.submit(() => closeWithin(() => entry.page.close()));
2267
- else if (options.popupPolicy === "pause") events.pause();
2268
- }
2269
- },
2270
- closed: (entry) => {
2271
- pageControl.closed(entry);
2272
- captures.invalidate(entry, "target-changed");
2273
- captures.forget(entry);
2274
- },
2275
- navigating: (entry, frame) => pageControl.navigating(entry, frame),
2276
- navigated: (entry, frame) => {
2277
- if (initialized) initialization.attachFrame(frame, entry.page);
2278
- },
2279
- frameChanged: (entry, frame) => {
2280
- captures.invalidate(entry, "target-changed", frame);
2281
- },
2282
- dialog: (dialog) => {
2283
- if (options.dialogPolicy === "dismiss") callbacks.submit(() => closeWithin(() => dialog.dismiss()));
2284
- else if (dialogs.size >= 8) {
2285
- events.fault();
2286
- callbacks.submit(() => closeWithin(() => dialog.dismiss()));
2287
- } else {
2288
- dialogs.add(dialog);
2289
- observation.invalidate();
2290
- events.pause();
2291
- }
2292
- },
2293
- release: () => observation.dispose(),
2294
- changed: (reason) => observation.changed(reason)
2295
- });
2296
- const { current, entries, register } = targets;
2297
- const initialization = makeInitialization(context, options, targets, callbacks, events, () => closing);
2298
- const observation = makeObservation(targets, events);
2299
- const actions = makeActions(context, targets, observation);
2300
- const pointer = makePointer(targets, actions);
2301
- const keyboard = makeKeyboard(targets, actions, pointer.receipt);
2302
- const captures = makeCaptureSources(targets);
2303
- const pageControl = makePageControl(browser, context, options, targets, callbacks, events, () => closing, (pageId) => observation.held(pageId));
2304
- const onPage = (page) => {
2305
- register(page);
2306
- };
2307
- const onDisconnected = () => {
2308
- if (!closing) {
2309
- observation.invalidate();
2310
- events.disconnected();
2311
- }
2312
- };
2313
- context.on("page", onPage);
2314
- browser.on("disconnected", onDisconnected);
2315
- const sizeNativeContents = async (entry) => {
2316
- if (browserCdp === void 0) throw failure("closed", "undispatched");
2317
- const targetId = await targets.targetId(entry);
2318
- const current = await browserCdp.send("Browser.getWindowForTarget", { targetId });
2319
- const native = safeDecode(NativeWindow, current);
2320
- await browserCdp.send("Browser.setContentsSize", {
2321
- windowId: native.windowId,
2322
- width: options.viewport.width,
2323
- height: options.viewport.height
2324
- });
2325
- };
2326
- const driver = {
2327
- ...options.pageControl ? { pageControl: pageControl.operations } : {},
2328
- selected: targets.selected,
2329
- selectedTargetId: targets.selectedTargetId,
2330
- listPages: targets.listPages,
2331
- selectPage: targets.selectPage,
2332
- newPage: targets.newPage,
2333
- closePage: targets.closePage,
2334
- listFrames: targets.listFrames,
2335
- selectFrame: targets.selectFrame,
2336
- beginNavigation: actions.beginNavigation,
2337
- readText: observation.readText,
2338
- observe: observation.observe,
2339
- checkpoint: observation.checkpoint,
2340
- controlFacts: observation.controlFacts,
2341
- revalidate: observation.revalidate,
2342
- click: actions.click,
2343
- fill: actions.fill,
2344
- scroll: actions.scroll,
2345
- pointerMove: pointer.pointerMove,
2346
- hover: pointer.hover,
2347
- wheel: pointer.wheel,
2348
- press: keyboard.press,
2349
- type: keyboard.type,
2350
- screenshot: observation.screenshot,
2351
- resize: (viewport, ticket) => sanitize(async () => {
2352
- const page = current().entry.page;
2353
- const entry = current().entry;
2354
- ticket.dispatch();
2355
- captures.invalidate(entry, "resized");
2356
- observation.changed("resized");
2357
- await page.setViewportSize(viewport);
2358
- ticket.check();
2359
- }),
2360
- waitFor: actions.waitFor,
2361
- clickAndWait: actions.clickAndWait,
2362
- clickForDownload: actions.clickForDownload,
2363
- selectFiles: actions.selectFiles,
2364
- clickForFileSelection: actions.clickForFileSelection,
2365
- documentReadiness: initialization.documentReadiness,
2366
- dismissDialogs: (ticket) => sanitize(async () => {
2367
- for (const dialog of [...dialogs]) {
2368
- ticket.dispatch();
2369
- await dialog.dismiss();
2370
- dialogs.delete(dialog);
2371
- }
2372
- }),
2373
- capture: captures.capture,
2374
- invalidateObservation: observation.invalidate,
2375
- fenceInitialization: initialization.fence,
2376
- disposeInitialization: initialization.dispose,
2377
- disconnect: () => sanitize(async () => {
2378
- closing = true;
2379
- initialization.fence();
2380
- callbacks.stop();
2381
- observation.invalidate();
2382
- context.off("page", onPage);
2383
- browser.off("disconnected", onDisconnected);
2384
- for (const entry of entries.values()) for (const off of entry.off.splice(0)) off();
2385
- captures.clear();
2386
- await closeWithin(initialization.dispose).catch(() => {});
2387
- await observation.dispose().catch(() => {});
2388
- await closeWithin(() => Promise.allSettled([...dialogs].map((dialog) => dialog.dismiss()))).catch(() => {});
2389
- dialogs.clear();
2390
- await closeWithin(() => callbacks.settle()).catch(() => {});
2391
- await closeWithin(() => pageControl.dispose()).catch(() => {});
2392
- await closeWithin(() => browserCdp?.detach() ?? Promise.resolve()).catch(() => {});
2393
- await closeWithin(() => browser.close());
2394
- targets.clear();
2395
- })
2396
- };
2397
- try {
2398
- for (const page of context.pages()) register(page);
2399
- browserCdp = await browser.newBrowserCDPSession();
2400
- await browserCdp.send("Browser.setDownloadBehavior", {
2401
- behavior: "allow",
2402
- downloadPath: "downloads",
2403
- eventsEnabled: true
2404
- });
2405
- await initialization.install();
2406
- for (const entry of entries.values()) await initialization.attach(entry.page);
2407
- await targets.selectInitial();
2408
- const { selection } = targets;
2409
- if (selection.entry === void 0) throw failure("not-found");
2410
- await initialization.attach(selection.entry.page);
2411
- selection.frame = selection.entry.page.mainFrame();
2412
- if (!options.preserveViewport) {
2413
- await sizeNativeContents(selection.entry);
2414
- await selection.entry.page.setViewportSize(options.viewport);
2415
- }
2416
- await targets.targetId(selection.entry);
2417
- if (options.pageControl) for (const entry of entries.values()) await pageControl.execution(entry);
2418
- initialized = true;
2419
- return driver;
2420
- } catch (error) {
2421
- await driver.disconnect().catch(() => {});
2422
- throw error;
2423
- }
2424
- };
2425
- //#endregion
2426
- //#region src/BrowserBinding.ts
2427
- const invalid = (connection) => Effect.try({
2428
- try: () => validateConnection(connection),
2429
- catch: (error) => publicError(error, "connect", { reason: "malformed" })
2430
- });
2431
- /** Playwright over CDP, lazily loaded at connection time and never at Layer construction. */
2432
- const playwright = (options = {}) => {
2433
- const { resolveEndpoint, onConnected } = options;
2434
- return issueBinding({ _tag: "BrowserbaseBrowserBinding" }, { connect: (request) => Effect.gen(function* () {
2435
- const validated = yield* invalid(request.connection);
2436
- const endpoint = resolveEndpoint === void 0 ? validated : yield* resolveEndpoint({ url: Redacted.make(validated) });
2437
- return yield* fromNativeAttempt((attempt, signal) => connectPlaywrightEndpoint(endpoint, signal, attempt.options, attempt.events, onConnected === void 0 ? void 0 : (native) => onConnected({
2438
- native,
2439
- endpoint
2440
- }))).connect(request);
2441
- }) });
2442
- };
2443
- const defaultBinding = playwright();
2444
- /** Defaults to Playwright with the provider's own address, so no consumer has to provide it. */
2445
- const BrowserbaseBrowserBinding = Context.Reference("effect-browserbase/BrowserBinding", { defaultValue: () => defaultBinding });
2446
- /** Supply a binding to every browser Layer built beneath it. */
2447
- const layer = (binding) => Layer.succeed(BrowserbaseBrowserBinding, binding);
2448
- const layerPlaywright = layer(defaultBinding);
2449
- //#endregion
2450
- export { bindingImplementation as a, playwright as i, layer as n, layerPlaywright as r, BrowserbaseBrowserBinding as t };
2451
-
2452
- //# sourceMappingURL=BrowserBinding-qbQA6JhH.mjs.map