@effect-agent/platform-cloudflare 0.1.0-beta.36 → 0.1.0-beta.37

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.
@@ -16,6 +16,10 @@ const MAX_HANDOFF_TIMEOUT_MILLIS = 30 * 6e4;
16
16
  const MAX_HOST_TEXT_LENGTH = 8 * 1024;
17
17
  const CLEANUP_STEP_TIMEOUT_MILLIS = 1e4;
18
18
  const CLOSE_SESSION_TIMEOUT_MILLIS = 1e4;
19
+ const ACTION_NETWORK_QUIET_MILLIS = 200;
20
+ const ACTION_NETWORK_SETTLE_MILLIS = 2e3;
21
+ const ACTION_POST_STATE_MILLIS = 250;
22
+ const MAX_OBSERVED_CONTROLS = 64;
19
23
  const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0));
20
24
  const BoundedHostText = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(MAX_HOST_TEXT_LENGTH));
21
25
  const BoundedRemoteText = Schema.String.check(Schema.isMaxLength(MAX_TEXT_LENGTH));
@@ -30,6 +34,57 @@ const TextObservation = Schema.Union([
30
34
  observed: Schema.Natural
31
35
  })
32
36
  ]);
37
+ const ActionTargetState = Schema.Struct({
38
+ matchCount: Schema.Natural,
39
+ invalidSelector: Schema.optionalKey(Schema.Boolean),
40
+ kind: Schema.optionalKey(Schema.Literals([
41
+ "button",
42
+ "checkbox",
43
+ "radio",
44
+ "select",
45
+ "text",
46
+ "link",
47
+ "other"
48
+ ])),
49
+ checked: Schema.optionalKey(Schema.Boolean),
50
+ selected: Schema.optionalKey(Schema.Boolean),
51
+ disabled: Schema.optionalKey(Schema.Boolean),
52
+ required: Schema.optionalKey(Schema.Boolean),
53
+ valid: Schema.optionalKey(Schema.Boolean),
54
+ formValid: Schema.optionalKey(Schema.Boolean)
55
+ });
56
+ const ActionNetworkState = Schema.Struct({
57
+ total: Schema.Natural,
58
+ status2xx: Schema.Natural,
59
+ status3xx: Schema.Natural,
60
+ status4xx: Schema.Natural,
61
+ status5xx: Schema.Natural,
62
+ failed: Schema.Natural,
63
+ pending: Schema.Natural,
64
+ settleTimedOut: Schema.Boolean
65
+ });
66
+ const ActionObservation = Schema.Struct({
67
+ before: ActionTargetState,
68
+ after: Schema.optionalKey(ActionTargetState),
69
+ afterUnavailable: Schema.Boolean,
70
+ network: ActionNetworkState
71
+ });
72
+ const PageObservation = Schema.fromJsonString(Schema.Struct({
73
+ pageText: BoundedRemoteText,
74
+ selectorMatchCount: Schema.Natural,
75
+ controlsTruncated: Schema.Boolean,
76
+ controls: Schema.Array(Schema.Struct({
77
+ selector: BoundedRemoteText,
78
+ kind: BoundedRemoteText,
79
+ label: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(200))),
80
+ checked: Schema.optionalKey(Schema.Boolean),
81
+ selected: Schema.optionalKey(Schema.Boolean),
82
+ disabled: Schema.optionalKey(Schema.Boolean),
83
+ required: Schema.optionalKey(Schema.Boolean),
84
+ valid: Schema.optionalKey(Schema.Boolean),
85
+ formValid: Schema.optionalKey(Schema.Boolean)
86
+ })).check(Schema.isMaxLength(MAX_OBSERVED_CONTROLS))
87
+ }));
33
88
  const PngBytes = Schema.Uint8Array.check(Schema.isMaxLength(MAX_SCREENSHOT_BYTES), Schema.makeFilter((bytes) => bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10, { title: "PNG bytes" }));
34
89
  const BrowserRunSessionId = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256), Schema.makeFilter((value) => /^[A-Za-z0-9_-]+$/.test(value), { title: "a Browser Run session identifier" }));
35
90
  const LiveViewUrl = Schema.String.check(Schema.isMaxLength(MAX_HOST_TEXT_LENGTH), Schema.makeFilter((value) => {
@@ -52,6 +107,24 @@ const HandoffStateObservation = Schema.Union([Schema.Struct({
52
107
  handoffId: Schema.optionalKey(BoundedHostText),
53
108
  durationMs: Schema.optionalKey(HandoffDuration)
54
109
  })]);
110
+ /**
111
+ * Host presentation state in CSS pixels. Dimensions are integers in 1..2048,
112
+ * density is finite in 1..2 (default 1), and neither scaled dimension may exceed
113
+ * 2048 pixels. Mobile, touch, and orientation emulation are not supported.
114
+ */
115
+ var BrowserRunViewport = class extends Schema.Class("BrowserRunViewport")(Schema.Struct({
116
+ width: PositiveInt.check(Schema.isLessThanOrEqualTo(2048)),
117
+ height: PositiveInt.check(Schema.isLessThanOrEqualTo(2048)),
118
+ deviceScaleFactor: Schema.optionalKey(Schema.Finite.check(Schema.isBetween({
119
+ minimum: 1,
120
+ maximum: 2
121
+ })))
122
+ }).check(Schema.makeFilter((viewport) => Math.max(viewport.width, viewport.height) * (viewport.deviceScaleFactor ?? 1) <= 2048, { title: "a viewport with scaled dimensions no larger than 2048 pixels" }))) {};
123
+ const decodeViewport = (input) => Schema.decodeUnknownEffect(BrowserRunViewport)(input, { onExcessProperty: "error" }).pipe(Effect.mapError(() => policyError("The browser viewport is malformed")), Effect.map((viewport) => ({
124
+ width: viewport.width,
125
+ height: viewport.height,
126
+ deviceScaleFactor: viewport.deviceScaleFactor ?? 1
127
+ })));
55
128
  /** Host-only request for a redacted Cloudflare Live View URL. */
56
129
  var BrowserRunLiveViewRequest = class extends Schema.Class("BrowserRunLiveViewRequest")({
57
130
  mode: Schema.Literal("tab"),
@@ -75,10 +148,16 @@ var BrowserRunHandoffState = class extends Schema.Class("BrowserRunHandoffState"
75
148
  /** Host-supplied Browser Run binding projected into one fakeable launch operation. */
76
149
  var BrowserRunInteractiveBinding = class BrowserRunInteractiveBinding extends Context.Service()("@effect-agent/platform-cloudflare/BrowserRunInteractiveBinding") {
77
150
  static layer(options) {
78
- return Layer.succeed(BrowserRunInteractiveBinding)({
79
- launch: async (keepAliveMillis) => makeProductionBrowser(await puppeteer.launch(options.browser, { keep_alive: keepAliveMillis })),
80
- connect: async (sessionId) => makeProductionBrowser(await puppeteer.connect(options.browser, sessionId))
81
- });
151
+ return Layer.effect(BrowserRunInteractiveBinding)(Effect.gen(function* () {
152
+ const viewport = options.viewport === void 0 ? void 0 : yield* decodeViewport(options.viewport);
153
+ return {
154
+ launch: async (keepAliveMillis) => makeProductionBrowser(await puppeteer.launch(options.browser, {
155
+ keep_alive: keepAliveMillis,
156
+ ...viewport === void 0 ? {} : { defaultViewport: { ...viewport } }
157
+ })),
158
+ connect: async (sessionId) => makeProductionBrowser(await puppeteer.connect(options.browser, sessionId))
159
+ };
160
+ }));
82
161
  }
83
162
  };
84
163
  /** Cloudflare host authority kept separate from the provider-neutral browser handle. */
@@ -95,6 +174,205 @@ const makeProductionCdpSession = (session) => ({
95
174
  },
96
175
  detach: () => session.detach()
97
176
  });
177
+ var BrowserRunActionUndispatched = class extends Error {
178
+ matchCount;
179
+ constructor(matchCount) {
180
+ super("The browser action selector did not resolve to exactly one element");
181
+ this.matchCount = matchCount;
182
+ }
183
+ };
184
+ const readActionTarget = (page, selector) => page.evaluate((requestedSelector) => {
185
+ const pageDocument = Reflect.get(globalThis, "document");
186
+ let matches;
187
+ try {
188
+ matches = Reflect.apply(Reflect.get(pageDocument, "querySelectorAll"), pageDocument, [requestedSelector]);
189
+ } catch (cause) {
190
+ if (cause instanceof Error && cause.name === "SyntaxError") return {
191
+ matchCount: 0,
192
+ invalidSelector: true
193
+ };
194
+ throw cause;
195
+ }
196
+ if (typeof matches !== "object" || matches === null) throw new Error("Invalid query result");
197
+ const matchCount = Math.min(1e4, Reflect.get(matches, "length"));
198
+ const element = Reflect.get(matches, 0);
199
+ if (element === void 0) return { matchCount };
200
+ const associated = Reflect.get(element, "control") ?? element;
201
+ const tagName = String(Reflect.get(associated, "tagName") ?? "").toLowerCase();
202
+ const inputType = String(Reflect.get(associated, "type") ?? "").toLowerCase();
203
+ const role = String(Reflect.apply(Reflect.get(element, "getAttribute"), element, ["role"]) ?? "").toLowerCase();
204
+ const kind = tagName === "button" || role === "button" ? "button" : inputType === "checkbox" || role === "checkbox" || role === "switch" ? "checkbox" : inputType === "radio" || role === "radio" ? "radio" : tagName === "select" ? "select" : tagName === "input" || tagName === "textarea" ? "text" : tagName === "a" ? "link" : "other";
205
+ const checked = Reflect.get(associated, "checked");
206
+ const selected = tagName === "select" ? Reflect.get(associated, "selectedIndex") >= 0 : Reflect.get(associated, "selected");
207
+ const disabled = Reflect.get(associated, "disabled");
208
+ const required = Reflect.get(associated, "required");
209
+ const validity = Reflect.get(associated, "validity");
210
+ const form = Reflect.get(associated, "form");
211
+ const formMatches = form === null || form === void 0 ? void 0 : Reflect.get(form, "matches");
212
+ const ariaChecked = Reflect.apply(Reflect.get(element, "getAttribute"), element, ["aria-checked"]);
213
+ const ariaDisabled = Reflect.apply(Reflect.get(element, "getAttribute"), element, ["aria-disabled"]);
214
+ const ariaSelected = Reflect.apply(Reflect.get(element, "getAttribute"), element, ["aria-selected"]);
215
+ return {
216
+ matchCount,
217
+ kind,
218
+ ...typeof checked === "boolean" ? { checked } : ariaChecked === "true" || ariaChecked === "false" ? { checked: ariaChecked === "true" } : {},
219
+ ...typeof selected === "boolean" ? { selected } : ariaSelected === "true" || ariaSelected === "false" ? { selected: ariaSelected === "true" } : {},
220
+ disabled: typeof disabled === "boolean" ? disabled : ariaDisabled === "true",
221
+ ...typeof required === "boolean" ? { required } : {},
222
+ ...validity !== void 0 && typeof Reflect.get(validity, "valid") === "boolean" ? { valid: Reflect.get(validity, "valid") } : {},
223
+ ...typeof formMatches === "function" ? { formValid: Reflect.apply(formMatches, form, [":valid"]) } : {}
224
+ };
225
+ }, selector);
226
+ const boundedBestEffort = async (promise, millis) => {
227
+ let timer;
228
+ try {
229
+ return await Promise.race([promise.catch(() => void 0), new Promise((resolve) => {
230
+ timer = setTimeout(resolve, millis);
231
+ })]);
232
+ } finally {
233
+ clearTimeout(timer);
234
+ }
235
+ };
236
+ const readActionTargetAfter = (page, selector) => boundedBestEffort(readActionTarget(page, selector).then(Schema.decodeUnknownSync(ActionTargetState)), ACTION_POST_STATE_MILLIS);
237
+ const makeActionRequestTracker = (page, signal) => {
238
+ const pending = /* @__PURE__ */ new Set();
239
+ let total = 0;
240
+ let status2xx = 0;
241
+ let status3xx = 0;
242
+ let status4xx = 0;
243
+ let status5xx = 0;
244
+ let failed = 0;
245
+ let lastChange = performance.now();
246
+ let closed = false;
247
+ let wake;
248
+ let timer;
249
+ const relevant = (request) => {
250
+ const resourceType = request.resourceType();
251
+ return resourceType === "fetch" || resourceType === "xhr";
252
+ };
253
+ const onRequest = (request) => {
254
+ if (!relevant(request)) return;
255
+ pending.add(request);
256
+ total++;
257
+ lastChange = performance.now();
258
+ };
259
+ const onFinished = (request) => {
260
+ if (!pending.delete(request)) return;
261
+ let status;
262
+ try {
263
+ status = request.response()?.status();
264
+ } catch {}
265
+ if (status !== void 0) {
266
+ if (status >= 200 && status < 300) status2xx++;
267
+ else if (status >= 300 && status < 400) status3xx++;
268
+ else if (status >= 400 && status < 500) status4xx++;
269
+ else if (status >= 500 && status < 600) status5xx++;
270
+ }
271
+ lastChange = performance.now();
272
+ };
273
+ const onFailed = (request) => {
274
+ if (!pending.delete(request)) return;
275
+ failed++;
276
+ lastChange = performance.now();
277
+ };
278
+ const close = () => {
279
+ if (closed) return;
280
+ closed = true;
281
+ page.off("request", onRequest);
282
+ page.off("requestfinished", onFinished);
283
+ page.off("requestfailed", onFailed);
284
+ signal.removeEventListener("abort", close);
285
+ clearTimeout(timer);
286
+ wake?.();
287
+ };
288
+ try {
289
+ page.on("request", onRequest);
290
+ page.on("requestfinished", onFinished);
291
+ page.on("requestfailed", onFailed);
292
+ signal.addEventListener("abort", close, { once: true });
293
+ if (signal.aborted) close();
294
+ } catch (cause) {
295
+ close();
296
+ throw cause;
297
+ }
298
+ const wait = async () => {
299
+ const startedAt = performance.now();
300
+ let settleTimedOut = false;
301
+ while (!closed) {
302
+ const now = performance.now();
303
+ if (pending.size === 0 && now - lastChange >= ACTION_NETWORK_QUIET_MILLIS) break;
304
+ if (now - startedAt >= ACTION_NETWORK_SETTLE_MILLIS) {
305
+ settleTimedOut = true;
306
+ break;
307
+ }
308
+ await new Promise((resolve) => {
309
+ wake = resolve;
310
+ timer = setTimeout(() => resolve(), 50);
311
+ });
312
+ wake = void 0;
313
+ }
314
+ return {
315
+ total,
316
+ status2xx,
317
+ status3xx,
318
+ status4xx,
319
+ status5xx,
320
+ failed,
321
+ pending: pending.size,
322
+ settleTimedOut
323
+ };
324
+ };
325
+ return {
326
+ wait,
327
+ close,
328
+ markActionSettled: () => {
329
+ lastChange = performance.now();
330
+ }
331
+ };
332
+ };
333
+ const disposeActionHandles = async (handles) => {
334
+ await boundedBestEffort(Promise.allSettled(handles.map(async (handle) => handle.dispose())), ACTION_POST_STATE_MILLIS);
335
+ };
336
+ const runObservedPageAction = async (page, selector, signal, onDispatch, action) => {
337
+ if (signal.aborted) throw new BrowserRunActionUndispatched(0);
338
+ const before = Schema.decodeUnknownSync(ActionTargetState)(await readActionTarget(page, selector));
339
+ if (before.matchCount !== 1 || before.invalidSelector === true) throw new BrowserRunActionUndispatched(before.matchCount);
340
+ const matches = await page.$$(selector);
341
+ if (matches.length !== 1 || matches[0] === void 0) {
342
+ await disposeActionHandles(matches);
343
+ throw new BrowserRunActionUndispatched(Math.min(1e4, matches.length));
344
+ }
345
+ if (signal.aborted) {
346
+ await disposeActionHandles(matches);
347
+ throw new BrowserRunActionUndispatched(1);
348
+ }
349
+ let tracker;
350
+ let disposing;
351
+ const dispose = () => disposing ??= disposeActionHandles(matches);
352
+ const onAbort = () => {
353
+ dispose();
354
+ };
355
+ signal.addEventListener("abort", onAbort, { once: true });
356
+ try {
357
+ tracker = makeActionRequestTracker(page, signal);
358
+ if (signal.aborted) throw new BrowserRunActionUndispatched(1);
359
+ onDispatch();
360
+ await action(matches[0]);
361
+ tracker.markActionSettled();
362
+ const network = await tracker.wait();
363
+ const after = signal.aborted ? void 0 : await readActionTargetAfter(page, selector);
364
+ return {
365
+ before,
366
+ ...after === void 0 ? {} : { after },
367
+ afterUnavailable: after === void 0,
368
+ network
369
+ };
370
+ } finally {
371
+ tracker?.close();
372
+ signal.removeEventListener("abort", onAbort);
373
+ await dispose();
374
+ }
375
+ };
98
376
  const makeProductionPage = (page) => {
99
377
  const listeners = /* @__PURE__ */ new Map();
100
378
  return {
@@ -115,51 +393,146 @@ const makeProductionPage = (page) => {
115
393
  },
116
394
  goto: async (url) => {
117
395
  await page.goto(url, {
118
- waitUntil: "networkidle0",
119
- timeout: 0
396
+ waitUntil: "domcontentloaded",
397
+ timeout: 3e4
120
398
  });
121
399
  },
122
400
  url: () => page.url(),
123
- readText: (selector, maximumBytes) => page.evaluate((requestedSelector, maximum) => {
124
- const pageDocument = Reflect.get(globalThis, "document");
125
- const element = requestedSelector === void 0 ? Reflect.get(pageDocument, "body") : Reflect.apply(Reflect.get(pageDocument, "querySelector"), pageDocument, [requestedSelector]);
126
- if (element === null) return { _tag: "MissingElement" };
127
- const innerText = Reflect.get(element, "innerText");
128
- const textContent = Reflect.get(element, "textContent");
129
- const text = typeof innerText === "string" ? innerText : typeof textContent === "string" ? textContent : "";
130
- const observed = new TextEncoder().encode(text).byteLength;
131
- return observed > maximum ? {
132
- _tag: "OverLimit",
133
- observed
134
- } : {
135
- _tag: "Text",
136
- text
137
- };
138
- }, selector, maximumBytes),
139
- fill: async (selector, value) => {
140
- await page.$eval(selector, (element, nextValue) => {
141
- let prototype = Reflect.getPrototypeOf(element);
142
- let setValue;
143
- while (prototype !== null) {
144
- const setter = Reflect.getOwnPropertyDescriptor(prototype, "value")?.set;
145
- if (typeof setter === "function") {
146
- setValue = setter;
147
- break;
401
+ readText: async (selector, maximumBytes) => {
402
+ const observation = Schema.decodeUnknownSync(TextObservation)(await page.evaluate((requestedSelector, maximum, maximumControls) => {
403
+ const pageDocument = Reflect.get(globalThis, "document");
404
+ const matches = requestedSelector === void 0 ? void 0 : Reflect.apply(Reflect.get(pageDocument, "querySelectorAll"), pageDocument, [requestedSelector]);
405
+ const selectorMatchCount = matches === void 0 || matches === null ? 1 : Math.min(1e4, Reflect.get(matches, "length"));
406
+ const element = matches === void 0 || matches === null ? Reflect.get(pageDocument, "body") : Reflect.get(matches, 0);
407
+ if (element === null) return { _tag: "MissingElement" };
408
+ if (element === void 0) return { _tag: "MissingElement" };
409
+ const innerText = Reflect.get(element, "innerText");
410
+ const textContent = Reflect.get(element, "textContent");
411
+ const pageText = typeof innerText === "string" ? innerText : typeof textContent === "string" ? textContent : "";
412
+ const primaryControlSelector = "input,select,textarea,label,button,[role=\"checkbox\"],[role=\"radio\"],[role=\"option\"],[role=\"switch\"],[role=\"tab\"],[role=\"button\"]";
413
+ const optionSelector = "select option";
414
+ const secondaryControlSelector = "a[href]";
415
+ const controlSelector = `${primaryControlSelector},${optionSelector},${secondaryControlSelector}`;
416
+ const selectorFor = (candidate) => {
417
+ const parts = [];
418
+ let current = candidate;
419
+ while (current !== null && current !== void 0) {
420
+ const tagName = String(Reflect.get(current, "tagName") ?? "").toLowerCase();
421
+ if (tagName === "") break;
422
+ const parent = Reflect.get(current, "parentElement");
423
+ if (parent === null) {
424
+ parts.push(tagName);
425
+ break;
426
+ }
427
+ let sibling = Reflect.get(current, "previousElementSibling");
428
+ let index = 1;
429
+ while (sibling !== null && sibling !== void 0) {
430
+ if (String(Reflect.get(sibling, "tagName") ?? "").toLowerCase() === tagName) index++;
431
+ sibling = Reflect.get(sibling, "previousElementSibling");
432
+ }
433
+ parts.push(`${tagName}:nth-of-type(${index})`);
434
+ current = parent;
148
435
  }
149
- prototype = Reflect.getPrototypeOf(prototype);
150
- }
151
- if (setValue === void 0) throw new Error("The selector did not resolve to a fillable field");
152
- const focus = Reflect.get(element, "focus");
153
- if (typeof focus === "function") Reflect.apply(focus, element, []);
154
- Reflect.apply(setValue, element, [nextValue]);
155
- const dispatchEvent = Reflect.get(element, "dispatchEvent");
156
- if (typeof dispatchEvent === "function") {
157
- Reflect.apply(dispatchEvent, element, [new Event("input", { bubbles: true })]);
158
- Reflect.apply(dispatchEvent, element, [new Event("change", { bubbles: true })]);
159
- }
160
- }, value);
436
+ return parts.reverse().join(" > ");
437
+ };
438
+ const visible = (candidate) => {
439
+ const hidden = Reflect.get(candidate, "hidden");
440
+ const ariaHidden = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-hidden"]);
441
+ const rects = Reflect.apply(Reflect.get(candidate, "getClientRects"), candidate, []);
442
+ return hidden !== true && ariaHidden !== "true" && typeof rects === "object" && rects !== null && Reflect.get(rects, "length") > 0;
443
+ };
444
+ const candidates = [];
445
+ let controlsTruncated = false;
446
+ const consider = (candidate) => {
447
+ const tagName = String(Reflect.get(candidate, "tagName") ?? "").toLowerCase();
448
+ const visibilityTarget = tagName === "option" ? Reflect.apply(Reflect.get(candidate, "closest"), candidate, ["select"]) : candidate;
449
+ if (!(tagName !== "label" || Reflect.get(candidate, "control") !== null) || typeof visibilityTarget !== "object" || visibilityTarget === null || !visible(visibilityTarget)) return;
450
+ candidates.push(candidate);
451
+ if (candidates.length > maximumControls) controlsTruncated = true;
452
+ };
453
+ const elementMatches = Reflect.get(element, "matches");
454
+ if (typeof elementMatches === "function" && Reflect.apply(elementMatches, element, [controlSelector])) consider(element);
455
+ const considerSelector = (candidateSelector) => {
456
+ const descendants = Reflect.apply(Reflect.get(element, "querySelectorAll"), element, [candidateSelector]);
457
+ if (typeof descendants !== "object" || descendants === null) return;
458
+ const descendantCount = Reflect.get(descendants, "length");
459
+ for (let index = 0; index < descendantCount && !controlsTruncated; index++) consider(Reflect.get(descendants, index));
460
+ };
461
+ considerSelector(primaryControlSelector);
462
+ if (!controlsTruncated) considerSelector(optionSelector);
463
+ if (!controlsTruncated) considerSelector(secondaryControlSelector);
464
+ const controls = candidates.slice(0, maximumControls).map((candidate) => {
465
+ const associated = Reflect.get(candidate, "control") ?? candidate;
466
+ const tagName = String(Reflect.get(candidate, "tagName") ?? "").toLowerCase();
467
+ const inputType = String(Reflect.get(associated, "type") ?? "").toLowerCase();
468
+ const role = String(Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["role"]) ?? "").toLowerCase();
469
+ const ariaLabel = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-label"]);
470
+ const candidateText = tagName === "textarea" || tagName === "input" || tagName === "select" ? void 0 : Reflect.get(candidate, tagName === "option" ? "label" : "innerText");
471
+ const associatedLabels = Reflect.get(associated, "labels");
472
+ const associatedLabel = associatedLabels !== void 0 && associatedLabels !== null && Reflect.get(associatedLabels, "length") > 0 ? Reflect.get(Reflect.get(associatedLabels, 0), "innerText") : void 0;
473
+ const label = String(typeof ariaLabel === "string" && ariaLabel !== "" ? ariaLabel : typeof candidateText === "string" && candidateText !== "" ? candidateText : associatedLabel ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
474
+ const checked = Reflect.get(associated, "checked");
475
+ const selected = tagName === "select" ? Reflect.get(associated, "selectedIndex") >= 0 : Reflect.get(associated, "selected");
476
+ const disabled = Reflect.get(associated, "disabled");
477
+ const required = Reflect.get(associated, "required");
478
+ const validity = Reflect.get(associated, "validity");
479
+ const form = Reflect.get(associated, "form");
480
+ const formMatches = form === null || form === void 0 ? void 0 : Reflect.get(form, "matches");
481
+ const ariaChecked = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-checked"]);
482
+ const ariaSelected = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-selected"]);
483
+ const ariaDisabled = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-disabled"]);
484
+ return {
485
+ selector: selectorFor(candidate),
486
+ kind: tagName === "label" ? `label:${inputType || "control"}` : tagName === "input" ? `input:${inputType || "text"}` : role !== "" ? `role:${role}` : tagName,
487
+ ...label === "" ? {} : { label },
488
+ ...typeof checked === "boolean" ? { checked } : ariaChecked === "true" || ariaChecked === "false" ? { checked: ariaChecked === "true" } : {},
489
+ ...typeof selected === "boolean" ? { selected } : ariaSelected === "true" || ariaSelected === "false" ? { selected: ariaSelected === "true" } : {},
490
+ ...typeof disabled === "boolean" ? { disabled } : ariaDisabled === "true" || ariaDisabled === "false" ? { disabled: ariaDisabled === "true" } : {},
491
+ ...typeof required === "boolean" ? { required } : {},
492
+ ...validity !== void 0 && typeof Reflect.get(validity, "valid") === "boolean" ? { valid: Reflect.get(validity, "valid") } : {},
493
+ ...typeof formMatches === "function" ? { formValid: Reflect.apply(formMatches, form, [":valid"]) } : {}
494
+ };
495
+ });
496
+ const text = JSON.stringify({
497
+ pageText,
498
+ selectorMatchCount,
499
+ controls,
500
+ controlsTruncated
501
+ });
502
+ const observed = new TextEncoder().encode(text).byteLength;
503
+ return observed > maximum ? {
504
+ _tag: "OverLimit",
505
+ observed
506
+ } : {
507
+ _tag: "Text",
508
+ text
509
+ };
510
+ }, selector, maximumBytes, MAX_OBSERVED_CONTROLS));
511
+ if (observation._tag === "Text") Schema.decodeUnknownSync(PageObservation)(observation.text);
512
+ return observation;
161
513
  },
162
- click: (selector) => page.click(selector),
514
+ fill: (selector, value, signal, onDispatch) => runObservedPageAction(page, selector, signal, onDispatch, (element) => element.evaluate((element, nextValue) => {
515
+ let prototype = Reflect.getPrototypeOf(element);
516
+ let setValue;
517
+ while (prototype !== null) {
518
+ const setter = Reflect.getOwnPropertyDescriptor(prototype, "value")?.set;
519
+ if (typeof setter === "function") {
520
+ setValue = setter;
521
+ break;
522
+ }
523
+ prototype = Reflect.getPrototypeOf(prototype);
524
+ }
525
+ if (setValue === void 0) throw new Error("The selector did not resolve to a fillable field");
526
+ const focus = Reflect.get(element, "focus");
527
+ if (typeof focus === "function") Reflect.apply(focus, element, []);
528
+ Reflect.apply(setValue, element, [nextValue]);
529
+ const dispatchEvent = Reflect.get(element, "dispatchEvent");
530
+ if (typeof dispatchEvent === "function") {
531
+ Reflect.apply(dispatchEvent, element, [new Event("input", { bubbles: true })]);
532
+ Reflect.apply(dispatchEvent, element, [new Event("change", { bubbles: true })]);
533
+ }
534
+ }, value)),
535
+ click: (selector, signal, onDispatch) => runObservedPageAction(page, selector, signal, onDispatch, (element) => element.click()),
163
536
  screenshot: (fullPage) => page.screenshot({
164
537
  type: "png",
165
538
  fullPage
@@ -172,7 +545,8 @@ const makeProductionPage = (page) => {
172
545
  behavior: "instant"
173
546
  }]);
174
547
  }, deltaX, deltaY),
175
- createCdpSession: async () => makeProductionCdpSession(await page.createCDPSession())
548
+ createCdpSession: async () => makeProductionCdpSession(await page.createCDPSession()),
549
+ setViewport: (viewport) => page.setViewport(viewport)
176
550
  };
177
551
  };
178
552
  const makeProductionContext = (context) => ({
@@ -202,6 +576,13 @@ const actionError = (operation, cause) => InteractiveBrowserActionError.make({
202
576
  message: `The interactive browser ${operation} operation failed`,
203
577
  ...cause === void 0 ? {} : { cause }
204
578
  });
579
+ const undispatchedActionError = (operation) => InteractiveBrowserActionError.make({
580
+ implementation: browserRunInteractiveImplementation,
581
+ operation,
582
+ message: `The interactive browser ${operation} operation was not dispatched`
583
+ });
584
+ /** Recognizes a local pre-dispatch refusal without exposing selector or page content. */
585
+ const isBrowserRunUndispatchedActionError = (error) => Schema.is(InteractiveBrowserActionError)(error) && error.implementation.identity === browserRunInteractiveImplementation.identity && (error.operation === "click" || error.operation === "fill") && error.message === `The interactive browser ${error.operation} operation was not dispatched`;
205
586
  const policyError = (message) => InteractiveBrowserPolicyDeniedError.make({
206
587
  implementation: browserRunInteractiveImplementation,
207
588
  message
@@ -282,6 +663,12 @@ const stateFailure = (state) => {
282
663
  if (state.violation.value !== void 0) return state.violation.value;
283
664
  if (state.closed.value || state.disconnected.value || state.uncertain.value) return expiredError();
284
665
  };
666
+ var BrowserRunRemoteFailure = class {
667
+ cause;
668
+ constructor(cause) {
669
+ this.cause = cause;
670
+ }
671
+ };
285
672
  const awaitPendingRequests = (state) => Effect.suspend(() => {
286
673
  const pending = [...state.pendingRequests];
287
674
  return pending.length === 0 ? Effect.void : Effect.promise(() => Promise.all(pending)).pipe(Effect.asVoid, Effect.andThen(awaitPendingRequests(state)));
@@ -331,32 +718,87 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page
331
718
  const actions = yield* Ref.make(0);
332
719
  const remote = (operation, evaluate) => Effect.tryPromise({
333
720
  try: evaluate,
334
- catch: (cause) => {
335
- if (state.disconnected.value || isRemoteClosure(cause)) {
336
- state.disconnected.value = true;
337
- return expiredError();
338
- }
339
- return actionError(operation, cause);
721
+ catch: (cause) => new BrowserRunRemoteFailure(cause)
722
+ }).pipe(Effect.catch((failure) => {
723
+ const cause = failure.cause;
724
+ if (cause instanceof BrowserRunActionUndispatched) return Effect.logInfo("Browser interactive action was not dispatched").pipe(Effect.annotateLogs({
725
+ "browser.action": operation,
726
+ "browser.selector_match_count": cause.matchCount
727
+ }), Effect.andThen(Effect.fail(undispatchedActionError(operation))));
728
+ if (state.disconnected.value || isRemoteClosure(cause)) {
729
+ state.disconnected.value = true;
730
+ return Effect.fail(expiredError());
340
731
  }
732
+ return Effect.fail(actionError(operation, cause));
733
+ }));
734
+ const observedAction = (operation, evaluate) => Effect.suspend(() => {
735
+ let dispatched = false;
736
+ let pending;
737
+ return remote(operation, (signal) => {
738
+ pending = evaluate(signal, () => {
739
+ dispatched = true;
740
+ });
741
+ return pending;
742
+ }).pipe(Effect.onInterrupt(() => Effect.gen(function* () {
743
+ state.uncertain.value = true;
744
+ yield* Effect.logWarning("Browser interactive action interrupted").pipe(Effect.annotateLogs({
745
+ "browser.action": operation,
746
+ "browser.action_dispatched": dispatched,
747
+ "browser.action_outcome_unknown": dispatched
748
+ }));
749
+ const completion = pending;
750
+ if (completion !== void 0) yield* Effect.promise(() => boundedBestEffort(completion, 500));
751
+ })));
752
+ });
753
+ const decodeActionObservation = Effect.fn("BrowserRunInteractive.decodeActionObservation")(function* (raw) {
754
+ return yield* Schema.decodeUnknownEffect(ActionObservation)(raw).pipe(Effect.mapError((cause) => protocolError("The browser returned a malformed action observation", cause)));
341
755
  });
342
- const run = (effect, preflight = Effect.void) => permits.withPermitsIfAvailable(1)(Effect.gen(function* () {
756
+ const logActionObservation = (operation, observation) => Effect.logInfo("Browser interactive action observed").pipe(Effect.annotateLogs({
757
+ "browser.action": operation,
758
+ "browser.selector_match_count": observation.before.matchCount,
759
+ ...observation.before.kind === void 0 ? {} : { "browser.target_kind": observation.before.kind },
760
+ ...observation.before.checked === void 0 ? {} : { "browser.target_checked_before": observation.before.checked },
761
+ ...observation.after?.checked === void 0 ? {} : { "browser.target_checked_after": observation.after.checked },
762
+ ...observation.before.selected === void 0 ? {} : { "browser.target_selected_before": observation.before.selected },
763
+ ...observation.after?.selected === void 0 ? {} : { "browser.target_selected_after": observation.after.selected },
764
+ ...observation.before.disabled === void 0 ? {} : { "browser.target_disabled_before": observation.before.disabled },
765
+ ...observation.after?.disabled === void 0 ? {} : { "browser.target_disabled_after": observation.after.disabled },
766
+ ...observation.before.required === void 0 ? {} : { "browser.target_required_before": observation.before.required },
767
+ ...observation.after?.required === void 0 ? {} : { "browser.target_required_after": observation.after.required },
768
+ ...observation.before.valid === void 0 ? {} : { "browser.target_valid_before": observation.before.valid },
769
+ ...observation.after?.valid === void 0 ? {} : { "browser.target_valid_after": observation.after.valid },
770
+ ...observation.before.formValid === void 0 ? {} : { "browser.form_valid_before": observation.before.formValid },
771
+ ...observation.after?.formValid === void 0 ? {} : { "browser.form_valid_after": observation.after.formValid },
772
+ "browser.target_after_unavailable": observation.afterUnavailable,
773
+ "browser.fetch_xhr_total": observation.network.total,
774
+ "browser.fetch_xhr_2xx": observation.network.status2xx,
775
+ "browser.fetch_xhr_3xx": observation.network.status3xx,
776
+ "browser.fetch_xhr_4xx": observation.network.status4xx,
777
+ "browser.fetch_xhr_5xx": observation.network.status5xx,
778
+ "browser.fetch_xhr_failed": observation.network.failed,
779
+ "browser.fetch_xhr_pending": observation.network.pending,
780
+ "browser.network_settle_timed_out": observation.network.settleTimedOut
781
+ }));
782
+ const run = (effect, preflight = Effect.void, consumeAction = true) => permits.withPermitsIfAvailable(1)(Effect.gen(function* () {
343
783
  const unavailable = stateFailure(state);
344
784
  if (unavailable !== void 0) return yield* unavailable;
345
785
  yield* preflight;
346
- const admitted = yield* Ref.modify(actions, (count) => count >= policy.maxActions ? [{
347
- allowed: false,
348
- observed: count + 1
349
- }, count] : [{
350
- allowed: true,
351
- observed: count + 1
352
- }, count + 1]);
353
- if (!admitted.allowed) return yield* InteractiveBrowserLimitError.make({
354
- implementation: browserRunInteractiveImplementation,
355
- limit: "actions",
356
- maximum: policy.maxActions,
357
- observed: admitted.observed,
358
- message: "The browser action limit was reached"
359
- });
786
+ if (consumeAction) {
787
+ const admitted = yield* Ref.modify(actions, (count) => count >= policy.maxActions ? [{
788
+ allowed: false,
789
+ observed: count + 1
790
+ }, count] : [{
791
+ allowed: true,
792
+ observed: count + 1
793
+ }, count + 1]);
794
+ if (!admitted.allowed) return yield* InteractiveBrowserLimitError.make({
795
+ implementation: browserRunInteractiveImplementation,
796
+ limit: "actions",
797
+ maximum: policy.maxActions,
798
+ observed: admitted.observed,
799
+ message: "The browser action limit was reached"
800
+ });
801
+ }
360
802
  const completed = effect.pipe(Effect.catch((error) => {
361
803
  const failure = stateFailure(state);
362
804
  return Effect.fail(failure ?? error);
@@ -371,6 +813,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page
371
813
  })), Effect.catch((error) => {
372
814
  if (Schema.is(InteractiveBrowserLimitError)(error) && error.limit === "elapsed") return Effect.fail(error);
373
815
  const failure = stateFailure(state);
816
+ if (Schema.is(InteractiveBrowserActionError)(error) && !isBrowserRunUndispatchedActionError(error)) state.uncertain.value = true;
374
817
  return Effect.fail(failure ?? error);
375
818
  }));
376
819
  })).pipe(Effect.flatMap((result) => Option.isSome(result) ? Effect.succeed(result.value) : Effect.fail(InteractiveBrowserBusyError.make({
@@ -404,8 +847,16 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page
404
847
  });
405
848
  return yield* Schema.decodeUnknownEffect(BrowserTextResult)({ text: observation.text }).pipe(Effect.mapError((cause) => protocolError("The browser returned malformed page text", cause)));
406
849
  })),
407
- fill: (request) => run(remote("fill", () => page.fill(request.selector, request.value)).pipe(Effect.andThen(decodeActionResult(page, policy)))),
408
- click: (request) => run(remote("click", () => page.click(request.selector)).pipe(Effect.andThen(decodeActionResult(page, policy)))),
850
+ fill: (request) => run(Effect.gen(function* () {
851
+ const observation = yield* observedAction("fill", (signal, onDispatch) => page.fill(request.selector, request.value, signal, onDispatch)).pipe(Effect.flatMap(decodeActionObservation));
852
+ yield* logActionObservation("fill", observation);
853
+ return yield* decodeActionResult(page, policy);
854
+ })),
855
+ click: (request) => run(Effect.gen(function* () {
856
+ const observation = yield* observedAction("click", (signal, onDispatch) => page.click(request.selector, signal, onDispatch)).pipe(Effect.flatMap(decodeActionObservation));
857
+ yield* logActionObservation("click", observation);
858
+ return yield* decodeActionResult(page, policy);
859
+ })),
409
860
  screenshot: (request) => Schema.decodeUnknownEffect(BrowserScreenshotRequest)(request).pipe(Effect.mapError(() => policyError("The browser screenshot request is malformed")), Effect.flatMap((decoded) => run(Effect.gen(function* () {
410
861
  const raw = yield* remote("screenshot", () => page.screenshot(decoded.fullPage));
411
862
  const bytes = yield* Schema.decodeUnknownEffect(PngBytes)(raw).pipe(Effect.mapError(() => protocolError("The browser returned a malformed PNG screenshot")));
@@ -561,6 +1012,16 @@ const makeHostService = (binding) => {
561
1012
  return {
562
1013
  handle: runtime.handle,
563
1014
  sessionId: Redacted.make(sessionIdValue),
1015
+ resizeViewport: (viewport) => decodeViewport(viewport).pipe(Effect.flatMap((decoded) => runtime.run(Effect.tryPromise({
1016
+ try: () => page.setViewport(decoded),
1017
+ catch: (cause) => {
1018
+ if (state.disconnected.value || isRemoteClosure(cause)) {
1019
+ state.disconnected.value = true;
1020
+ return expiredError();
1021
+ }
1022
+ return protocolError("Resizing the browser viewport failed", cause);
1023
+ }
1024
+ }), currentPagePreflight, false))),
564
1025
  getLiveView: (request) => Schema.decodeUnknownEffect(BrowserRunLiveViewRequest)(request).pipe(Effect.mapError(() => policyError("The Live View request is malformed")), Effect.flatMap((decoded) => runtime.run(cdpCommand(page, state, "Cloudflare.getLiveView", {
565
1026
  mode: decoded.mode,
566
1027
  expiresInMs: decoded.expiresInMs
@@ -613,6 +1074,6 @@ const browserRunInteractiveLayer = () => Layer.effect(InteractiveBrowser, Effect
613
1074
  return InteractiveBrowser.of({ open: (policy) => host.open(policy).pipe(Effect.map((session) => session.handle)) });
614
1075
  }));
615
1076
  //#endregion
616
- export { BrowserRunHandoffRequest, BrowserRunHandoffResult, BrowserRunHandoffState, BrowserRunInteractiveBinding, BrowserRunInteractiveHost, BrowserRunLiveViewRequest, BrowserRunLiveViewResult, browserRunInteractiveHostLayer, browserRunInteractiveImplementation, browserRunInteractiveLayer };
1077
+ export { BrowserRunHandoffRequest, BrowserRunHandoffResult, BrowserRunHandoffState, BrowserRunInteractiveBinding, BrowserRunInteractiveHost, BrowserRunLiveViewRequest, BrowserRunLiveViewResult, BrowserRunViewport, browserRunInteractiveHostLayer, browserRunInteractiveImplementation, browserRunInteractiveLayer, isBrowserRunUndispatchedActionError };
617
1078
 
618
1079
  //# sourceMappingURL=interactive-browser.mjs.map