@effect-agent/platform-cloudflare 0.1.0-beta.110 → 0.1.0-beta.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Alarm.d.mts +34 -8
- package/dist/Alarm.mjs +231 -41
- package/dist/Alarm.mjs.map +1 -1
- package/dist/CloudflareThreadClient.d.mts +49 -42
- package/dist/CloudflareThreadClient.mjs +1 -1
- package/dist/{InteractiveBrowser-CKp6pqmw.d.mts → InteractiveBrowser-BQo7QKaP.d.mts} +64 -7
- package/dist/{InteractiveBrowser-l6J4eP6T.mjs → InteractiveBrowser-CBUFAECo.mjs} +545 -223
- package/dist/InteractiveBrowser-CBUFAECo.mjs.map +1 -0
- package/dist/InteractiveBrowser.d.mts +2 -2
- package/dist/InteractiveBrowser.mjs +2 -2
- package/dist/ProtectedBrowser.d.mts +1 -1
- package/dist/ProtectedBrowser.mjs +1 -1
- package/dist/{ThreadObject-BGla_Obk.mjs → ThreadObject-0k-SVpY_.mjs} +2 -2
- package/dist/ThreadObject-0k-SVpY_.mjs.map +1 -0
- package/dist/{ThreadObject-MSlpVBUD.d.mts → ThreadObject-DVxgrU7c.d.mts} +46 -39
- package/dist/ThreadObject.d.mts +1 -1
- package/dist/ThreadObject.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/Alarm.ts +432 -72
- package/src/InteractiveBrowser.ts +1129 -500
- package/src/ThreadObject.ts +2 -2
- package/dist/InteractiveBrowser-l6J4eP6T.mjs.map +0 -1
- package/dist/ThreadObject-BGla_Obk.mjs.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Cause, Clock, Context, Data, Duration, Effect, Encoding, ErrorReporter, Layer, Option, Predicate, Redacted,
|
|
1
|
+
import { Cause, Clock, Context, Data, Duration, Effect, Encoding, ErrorReporter, Layer, Option, Predicate, Redacted, Schema, Scope, Semaphore, Stream } from "effect";
|
|
2
2
|
import { PageScreenshotResult } from "effect-agent/page-screenshot";
|
|
3
3
|
import { SandboxImplementation } from "effect-agent/sandbox";
|
|
4
4
|
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
5
5
|
import puppeteer from "@cloudflare/puppeteer";
|
|
6
|
-
import { BrowserActionResult, BrowserFileSelectionResult, BrowserNavigationResult, BrowserScreenshotRequest, BrowserScrollRequest, BrowserSelectFileRequest, BrowserTextResult, InteractiveBrowser, InteractiveBrowserActionError, InteractiveBrowserBusyError, InteractiveBrowserCapacityError, InteractiveBrowserExpiredError, InteractiveBrowserLimitError, InteractiveBrowserPolicy, InteractiveBrowserPolicyDeniedError, InteractiveBrowserProtocolError, InteractiveBrowserTargetUrl, InteractiveBrowserUnsupportedError } from "effect-agent/interactive-browser";
|
|
6
|
+
import { BrowserActionResult, BrowserExpectedTargetState, BrowserFileSelectionResult, BrowserNavigationResult, BrowserScreenshotRequest, BrowserScrollRequest, BrowserSelectFileRequest, BrowserTextResult, InteractiveBrowser, InteractiveBrowserActionError, InteractiveBrowserBusyError, InteractiveBrowserCapacityError, InteractiveBrowserExpiredError, InteractiveBrowserLimitError, InteractiveBrowserPolicy, InteractiveBrowserPolicyDeniedError, InteractiveBrowserProtocolError, InteractiveBrowserTargetUrl, InteractiveBrowserUnsupportedError } from "effect-agent/interactive-browser";
|
|
7
7
|
//#region src/internal/browser-failure.ts
|
|
8
8
|
/** Source-authored stages and classifications only; never retain SDK text or session capabilities. */
|
|
9
9
|
var BrowserRunFailure = class extends Data.TaggedError("BrowserRunFailure") {
|
|
@@ -328,22 +328,19 @@ const ActionObservation = Schema.Struct({
|
|
|
328
328
|
before: ActionTargetState,
|
|
329
329
|
after: Schema.optionalKey(ActionTargetState),
|
|
330
330
|
afterUnavailable: Schema.Boolean,
|
|
331
|
-
network: ActionNetworkState
|
|
331
|
+
network: ActionNetworkState,
|
|
332
|
+
inputMillis: Schema.optionalKey(Schema.Finite),
|
|
333
|
+
observationMillis: Schema.optionalKey(Schema.Finite)
|
|
332
334
|
});
|
|
333
|
-
const
|
|
335
|
+
const BrowserRunPageObservation = Schema.fromJsonString(Schema.Struct({
|
|
336
|
+
documentId: Schema.NonEmptyString,
|
|
334
337
|
pageText: BoundedRemoteText,
|
|
335
338
|
selectorMatchCount: Schema.Natural,
|
|
336
339
|
controlsTruncated: Schema.Boolean,
|
|
337
340
|
controls: Schema.Array(Schema.Struct({
|
|
341
|
+
nodeId: Schema.NonEmptyString,
|
|
338
342
|
selector: BoundedRemoteText,
|
|
339
|
-
|
|
340
|
-
label: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(200))),
|
|
341
|
-
checked: Schema.optionalKey(Schema.Boolean),
|
|
342
|
-
selected: Schema.optionalKey(Schema.Boolean),
|
|
343
|
-
disabled: Schema.optionalKey(Schema.Boolean),
|
|
344
|
-
required: Schema.optionalKey(Schema.Boolean),
|
|
345
|
-
valid: Schema.optionalKey(Schema.Boolean),
|
|
346
|
-
formValid: Schema.optionalKey(Schema.Boolean)
|
|
343
|
+
...BrowserExpectedTargetState.fields
|
|
347
344
|
})).check(Schema.isMaxLength(MAX_OBSERVED_CONTROLS))
|
|
348
345
|
}));
|
|
349
346
|
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" }));
|
|
@@ -421,6 +418,24 @@ var BrowserRunInteractiveBinding = class BrowserRunInteractiveBinding extends Co
|
|
|
421
418
|
})).pipe(Layer.provide(BrowserRunBinding.layer(options.browser)));
|
|
422
419
|
}
|
|
423
420
|
};
|
|
421
|
+
/** Private target identity; never include this checkpoint in model-visible records. */
|
|
422
|
+
var BrowserRunPageIdentity = class extends Schema.Class("BrowserRunPageIdentity")({
|
|
423
|
+
contextId: BrowserRunSessionId,
|
|
424
|
+
targetId: BrowserRunSessionId
|
|
425
|
+
}) {};
|
|
426
|
+
/** Host-owned recovery data. Persist with the application's input receipts and ownership fence. */
|
|
427
|
+
var BrowserRunInteractiveCheckpoint = class extends Schema.Class("BrowserRunInteractiveCheckpoint")({
|
|
428
|
+
sessionId: Schema.RedactedFromValue(BrowserRunSessionId),
|
|
429
|
+
page: BrowserRunPageIdentity,
|
|
430
|
+
policy: InteractiveBrowserPolicy,
|
|
431
|
+
startedAt: Schema.Natural,
|
|
432
|
+
consumedActions: Schema.Natural.check(Schema.isLessThanOrEqualTo(1e3)),
|
|
433
|
+
inputState: Schema.Literals([
|
|
434
|
+
"idle",
|
|
435
|
+
"running",
|
|
436
|
+
"unknown"
|
|
437
|
+
])
|
|
438
|
+
}) {};
|
|
424
439
|
/** Cloudflare host authority kept separate from the provider-neutral browser handle. */
|
|
425
440
|
var BrowserRunInteractiveHost = class extends Context.Service()("@effect-agent/platform-cloudflare/BrowserRunInteractiveHost") {};
|
|
426
441
|
const makeProductionRequest = (request) => ({
|
|
@@ -594,7 +609,186 @@ const makeActionRequestTracker = (page, signal) => {
|
|
|
594
609
|
const disposeActionHandles = async (handles) => {
|
|
595
610
|
await boundedBestEffort(Promise.allSettled(handles.map(async (handle) => handle.dispose())), ACTION_POST_STATE_MILLIS);
|
|
596
611
|
};
|
|
597
|
-
|
|
612
|
+
/** Runs entirely in the page realm for both observations and the last bound-element guard. */
|
|
613
|
+
const observePage = (target, requestedSelector, maximum, maximumControls, input) => {
|
|
614
|
+
const pageDocument = Reflect.get(globalThis, "document");
|
|
615
|
+
const matches = requestedSelector === void 0 ? void 0 : Reflect.apply(Reflect.get(pageDocument, "querySelectorAll"), pageDocument, [requestedSelector]);
|
|
616
|
+
const selectorMatchCount = matches === void 0 || matches === null ? 1 : Math.min(1e4, Reflect.get(matches, "length"));
|
|
617
|
+
const element = target ?? (matches === void 0 || matches === null ? Reflect.get(pageDocument, "body") : Reflect.get(matches, 0));
|
|
618
|
+
if (target !== void 0 && Reflect.get(target, "isConnected") !== true) return { _tag: "MissingElement" };
|
|
619
|
+
if (element === null) return { _tag: "MissingElement" };
|
|
620
|
+
if (element === void 0) return { _tag: "MissingElement" };
|
|
621
|
+
const innerText = Reflect.get(element, "innerText");
|
|
622
|
+
const textContent = Reflect.get(element, "textContent");
|
|
623
|
+
const pageText = typeof innerText === "string" ? innerText : typeof textContent === "string" ? textContent : "";
|
|
624
|
+
const primaryControlSelector = "input,select,textarea,label,button,[role=\"checkbox\"],[role=\"radio\"],[role=\"option\"],[role=\"switch\"],[role=\"tab\"],[role=\"button\"]";
|
|
625
|
+
const optionSelector = "select option";
|
|
626
|
+
const secondaryControlSelector = "a[href]";
|
|
627
|
+
const controlSelector = `${primaryControlSelector},${optionSelector},${secondaryControlSelector}`;
|
|
628
|
+
const selectorFor = (candidate) => {
|
|
629
|
+
const parts = [];
|
|
630
|
+
let current = candidate;
|
|
631
|
+
while (current !== null && current !== void 0) {
|
|
632
|
+
const tagName = String(Reflect.get(current, "tagName") ?? "").toLowerCase();
|
|
633
|
+
if (tagName === "") break;
|
|
634
|
+
const parent = Reflect.get(current, "parentElement");
|
|
635
|
+
if (parent === null) {
|
|
636
|
+
parts.push(tagName);
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
let sibling = Reflect.get(current, "previousElementSibling");
|
|
640
|
+
let index = 1;
|
|
641
|
+
while (sibling !== null && sibling !== void 0) {
|
|
642
|
+
if (String(Reflect.get(sibling, "tagName") ?? "").toLowerCase() === tagName) index++;
|
|
643
|
+
sibling = Reflect.get(sibling, "previousElementSibling");
|
|
644
|
+
}
|
|
645
|
+
parts.push(`${tagName}:nth-of-type(${index})`);
|
|
646
|
+
current = parent;
|
|
647
|
+
}
|
|
648
|
+
return parts.reverse().join(" > ");
|
|
649
|
+
};
|
|
650
|
+
const visible = (candidate) => {
|
|
651
|
+
const hidden = Reflect.get(candidate, "hidden");
|
|
652
|
+
const ariaHidden = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-hidden"]);
|
|
653
|
+
const rects = Reflect.apply(Reflect.get(candidate, "getClientRects"), candidate, []);
|
|
654
|
+
return hidden !== true && ariaHidden !== "true" && typeof rects === "object" && rects !== null && Reflect.get(rects, "length") > 0;
|
|
655
|
+
};
|
|
656
|
+
const candidates = [];
|
|
657
|
+
let controlsTruncated = false;
|
|
658
|
+
const consider = (candidate) => {
|
|
659
|
+
const tagName = String(Reflect.get(candidate, "tagName") ?? "").toLowerCase();
|
|
660
|
+
const visibilityTarget = tagName === "option" ? Reflect.apply(Reflect.get(candidate, "closest"), candidate, ["select"]) : candidate;
|
|
661
|
+
if (!(tagName !== "label" || Reflect.get(candidate, "control") !== null) || typeof visibilityTarget !== "object" || visibilityTarget === null || !visible(visibilityTarget)) return;
|
|
662
|
+
candidates.push(candidate);
|
|
663
|
+
if (candidates.length > maximumControls) controlsTruncated = true;
|
|
664
|
+
};
|
|
665
|
+
const elementMatches = Reflect.get(element, "matches");
|
|
666
|
+
if (typeof elementMatches === "function" && Reflect.apply(elementMatches, element, [controlSelector])) consider(element);
|
|
667
|
+
const considerSelector = (candidateSelector) => {
|
|
668
|
+
const descendants = Reflect.apply(Reflect.get(element, "querySelectorAll"), element, [candidateSelector]);
|
|
669
|
+
if (typeof descendants !== "object" || descendants === null) return;
|
|
670
|
+
const descendantCount = Reflect.get(descendants, "length");
|
|
671
|
+
for (let index = 0; index < descendantCount && !controlsTruncated; index++) consider(Reflect.get(descendants, index));
|
|
672
|
+
};
|
|
673
|
+
considerSelector(primaryControlSelector);
|
|
674
|
+
if (!controlsTruncated) considerSelector(optionSelector);
|
|
675
|
+
if (!controlsTruncated) considerSelector(secondaryControlSelector);
|
|
676
|
+
const identityKey = Symbol.for("@effect-agent/browser-observation");
|
|
677
|
+
let identity = Reflect.get(globalThis, identityKey);
|
|
678
|
+
if (identity === void 0 || identity.document !== pageDocument) {
|
|
679
|
+
identity = {
|
|
680
|
+
document: pageDocument,
|
|
681
|
+
documentId: crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(4)).join("-"),
|
|
682
|
+
nodes: /* @__PURE__ */ new WeakMap()
|
|
683
|
+
};
|
|
684
|
+
Reflect.set(globalThis, identityKey, identity);
|
|
685
|
+
}
|
|
686
|
+
const controls = candidates.slice(0, maximumControls).map((candidate) => {
|
|
687
|
+
let nodeId = identity.nodes.get(candidate);
|
|
688
|
+
if (nodeId === void 0) {
|
|
689
|
+
nodeId = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(4)).join("-");
|
|
690
|
+
identity.nodes.set(candidate, nodeId);
|
|
691
|
+
}
|
|
692
|
+
const associated = Reflect.get(candidate, "control") ?? candidate;
|
|
693
|
+
const tagName = String(Reflect.get(candidate, "tagName") ?? "").toLowerCase();
|
|
694
|
+
const inputType = String(Reflect.get(associated, "type") ?? "").toLowerCase();
|
|
695
|
+
const role = String(Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["role"]) ?? "").toLowerCase();
|
|
696
|
+
const ariaLabel = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-label"]);
|
|
697
|
+
const candidateText = tagName === "textarea" || tagName === "input" || tagName === "select" ? void 0 : Reflect.get(candidate, tagName === "option" ? "label" : "innerText");
|
|
698
|
+
const associatedLabels = Reflect.get(associated, "labels");
|
|
699
|
+
const associatedLabel = associatedLabels !== void 0 && associatedLabels !== null && Reflect.get(associatedLabels, "length") > 0 ? Reflect.get(Reflect.get(associatedLabels, 0), "innerText") : void 0;
|
|
700
|
+
const label = String(typeof ariaLabel === "string" && ariaLabel !== "" ? ariaLabel : typeof candidateText === "string" && candidateText !== "" ? candidateText : associatedLabel ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
701
|
+
const checked = Reflect.get(associated, "checked");
|
|
702
|
+
const selected = tagName === "select" ? Reflect.get(associated, "selectedIndex") >= 0 : Reflect.get(associated, "selected");
|
|
703
|
+
const disabled = Reflect.get(associated, "disabled");
|
|
704
|
+
const required = Reflect.get(associated, "required");
|
|
705
|
+
const validity = Reflect.get(associated, "validity");
|
|
706
|
+
const form = Reflect.get(associated, "form");
|
|
707
|
+
const formMatches = form === null || form === void 0 ? void 0 : Reflect.get(form, "matches");
|
|
708
|
+
const ariaChecked = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-checked"]);
|
|
709
|
+
const ariaSelected = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-selected"]);
|
|
710
|
+
const ariaDisabled = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-disabled"]);
|
|
711
|
+
return {
|
|
712
|
+
nodeId,
|
|
713
|
+
...inputType === "" ? {} : { inputType: inputType.slice(0, 64) },
|
|
714
|
+
selector: selectorFor(candidate),
|
|
715
|
+
kind: (tagName === "label" ? `label:${inputType || "control"}` : tagName === "input" ? `input:${inputType || "text"}` : role !== "" ? `role:${role}` : tagName).slice(0, 128),
|
|
716
|
+
...label === "" ? {} : { label },
|
|
717
|
+
...typeof checked === "boolean" ? { checked } : ariaChecked === "true" || ariaChecked === "false" ? { checked: ariaChecked === "true" } : {},
|
|
718
|
+
...typeof selected === "boolean" ? { selected } : ariaSelected === "true" || ariaSelected === "false" ? { selected: ariaSelected === "true" } : {},
|
|
719
|
+
...typeof disabled === "boolean" ? { disabled } : ariaDisabled === "true" || ariaDisabled === "false" ? { disabled: ariaDisabled === "true" } : {},
|
|
720
|
+
...typeof required === "boolean" ? { required } : {},
|
|
721
|
+
...validity !== void 0 && typeof Reflect.get(validity, "valid") === "boolean" ? { valid: Reflect.get(validity, "valid") } : {},
|
|
722
|
+
...typeof formMatches === "function" ? { formValid: Reflect.apply(formMatches, form, [":valid"]) } : {}
|
|
723
|
+
};
|
|
724
|
+
});
|
|
725
|
+
if (input !== void 0) {
|
|
726
|
+
const expected = input.expectedTarget;
|
|
727
|
+
const control = controls.find((candidate) => candidate.nodeId === expected.nodeId);
|
|
728
|
+
if (target === void 0 || identity.documentId !== expected.documentId || identity.nodes.get(target) !== expected.nodeId || control === void 0 || expected.state !== void 0 && [
|
|
729
|
+
"kind",
|
|
730
|
+
"inputType",
|
|
731
|
+
"label",
|
|
732
|
+
"checked",
|
|
733
|
+
"selected",
|
|
734
|
+
"disabled",
|
|
735
|
+
"required",
|
|
736
|
+
"valid",
|
|
737
|
+
"formValid"
|
|
738
|
+
].some((field) => control[field] !== expected.state?.[field]) || Reflect.get(target, "isConnected") !== true) return { _tag: "MissingElement" };
|
|
739
|
+
if (expected.scopeSelector !== void 0) {
|
|
740
|
+
let roots;
|
|
741
|
+
try {
|
|
742
|
+
roots = Reflect.apply(Reflect.get(pageDocument, "querySelectorAll"), pageDocument, [expected.scopeSelector]);
|
|
743
|
+
} catch {
|
|
744
|
+
return { _tag: "MissingElement" };
|
|
745
|
+
}
|
|
746
|
+
if (typeof roots !== "object" || roots === null) return { _tag: "MissingElement" };
|
|
747
|
+
const root = Reflect.get(roots, 0);
|
|
748
|
+
if (Reflect.get(roots, "length") !== 1 || root === void 0 || !Reflect.apply(Reflect.get(root, "contains"), root, [target])) return { _tag: "MissingElement" };
|
|
749
|
+
}
|
|
750
|
+
if (input.mode === "click") {
|
|
751
|
+
const prototype = Reflect.get(Reflect.get(globalThis, "HTMLElement"), "prototype");
|
|
752
|
+
Reflect.apply(Reflect.get(prototype, "click"), target, []);
|
|
753
|
+
} else {
|
|
754
|
+
let prototype = Reflect.getPrototypeOf(target);
|
|
755
|
+
let setValue;
|
|
756
|
+
while (prototype !== null) {
|
|
757
|
+
const setter = Reflect.getOwnPropertyDescriptor(prototype, "value")?.set;
|
|
758
|
+
if (typeof setter === "function") {
|
|
759
|
+
setValue = setter;
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
prototype = Reflect.getPrototypeOf(prototype);
|
|
763
|
+
}
|
|
764
|
+
if (setValue === void 0 || input.value === void 0) return { _tag: "MissingElement" };
|
|
765
|
+
Reflect.apply(setValue, target, [input.value]);
|
|
766
|
+
const dispatchEvent = Reflect.get(target, "dispatchEvent");
|
|
767
|
+
Reflect.apply(dispatchEvent, target, [new Event("input", { bubbles: true })]);
|
|
768
|
+
Reflect.apply(dispatchEvent, target, [new Event("change", { bubbles: true })]);
|
|
769
|
+
}
|
|
770
|
+
return {
|
|
771
|
+
_tag: "Text",
|
|
772
|
+
text: ""
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
const text = JSON.stringify({
|
|
776
|
+
documentId: identity.documentId,
|
|
777
|
+
pageText,
|
|
778
|
+
selectorMatchCount,
|
|
779
|
+
controls,
|
|
780
|
+
controlsTruncated
|
|
781
|
+
});
|
|
782
|
+
const observed = new TextEncoder().encode(text).byteLength;
|
|
783
|
+
return observed > maximum ? {
|
|
784
|
+
_tag: "OverLimit",
|
|
785
|
+
observed
|
|
786
|
+
} : {
|
|
787
|
+
_tag: "Text",
|
|
788
|
+
text
|
|
789
|
+
};
|
|
790
|
+
};
|
|
791
|
+
const runObservedPageAction = async (page, selector, signal, onDispatch, action, validate, onComplete) => {
|
|
598
792
|
if (signal.aborted) throw new BrowserRunActionUndispatched(0);
|
|
599
793
|
const before = Schema.decodeUnknownSync(ActionTargetState)(await readActionTarget(page, selector));
|
|
600
794
|
if (before.matchCount !== 1 || before.invalidSelector === true) throw new BrowserRunActionUndispatched(before.matchCount);
|
|
@@ -619,12 +813,17 @@ const runObservedPageAction = async (page, selector, signal, onDispatch, action,
|
|
|
619
813
|
tracker = makeActionRequestTracker(page, signal);
|
|
620
814
|
if (signal.aborted) throw new BrowserRunActionUndispatched(1);
|
|
621
815
|
onDispatch();
|
|
816
|
+
const inputStarted = performance.now();
|
|
622
817
|
await action(matches[0]);
|
|
818
|
+
const inputFinished = performance.now();
|
|
819
|
+
onComplete?.();
|
|
623
820
|
tracker.markActionSettled();
|
|
624
821
|
const network = await tracker.wait();
|
|
625
822
|
const after = signal.aborted ? void 0 : await readActionTargetAfter(page, selector);
|
|
626
823
|
return {
|
|
627
824
|
before,
|
|
825
|
+
inputMillis: Math.max(0, inputFinished - inputStarted),
|
|
826
|
+
observationMillis: Math.max(0, performance.now() - inputFinished),
|
|
628
827
|
...after === void 0 ? {} : { after },
|
|
629
828
|
afterUnavailable: after === void 0,
|
|
630
829
|
network
|
|
@@ -635,10 +834,29 @@ const runObservedPageAction = async (page, selector, signal, onDispatch, action,
|
|
|
635
834
|
await dispose();
|
|
636
835
|
}
|
|
637
836
|
};
|
|
837
|
+
const guardedPageInput = async (element, expectedTarget, mode, value) => {
|
|
838
|
+
if (Schema.decodeUnknownSync(TextObservation)(await element.evaluate(observePage, void 0, 262144, 1, {
|
|
839
|
+
mode,
|
|
840
|
+
expectedTarget,
|
|
841
|
+
...value === void 0 ? {} : { value }
|
|
842
|
+
}))._tag !== "Text") throw new BrowserRunActionUndispatched(1);
|
|
843
|
+
};
|
|
638
844
|
const makeProductionPage = (page) => {
|
|
639
845
|
const prepareFileSelection = makeFileSelection(page);
|
|
640
846
|
const listeners = /* @__PURE__ */ new Map();
|
|
641
847
|
return {
|
|
848
|
+
identity: async () => {
|
|
849
|
+
const cdp = await page.createCDPSession();
|
|
850
|
+
try {
|
|
851
|
+
const { targetInfo } = await cdp.send("Target.getTargetInfo");
|
|
852
|
+
return Schema.decodeUnknownSync(BrowserRunPageIdentity)({
|
|
853
|
+
contextId: targetInfo.browserContextId,
|
|
854
|
+
targetId: targetInfo.targetId
|
|
855
|
+
});
|
|
856
|
+
} finally {
|
|
857
|
+
await cdp.detach();
|
|
858
|
+
}
|
|
859
|
+
},
|
|
642
860
|
close: async () => {
|
|
643
861
|
if (!page.browser().isConnected()) return;
|
|
644
862
|
try {
|
|
@@ -669,119 +887,11 @@ const makeProductionPage = (page) => {
|
|
|
669
887
|
},
|
|
670
888
|
url: () => page.url(),
|
|
671
889
|
readText: async (selector, maximumBytes) => {
|
|
672
|
-
const observation = Schema.decodeUnknownSync(TextObservation)(await page.evaluate(
|
|
673
|
-
|
|
674
|
-
const matches = requestedSelector === void 0 ? void 0 : Reflect.apply(Reflect.get(pageDocument, "querySelectorAll"), pageDocument, [requestedSelector]);
|
|
675
|
-
const selectorMatchCount = matches === void 0 || matches === null ? 1 : Math.min(1e4, Reflect.get(matches, "length"));
|
|
676
|
-
const element = matches === void 0 || matches === null ? Reflect.get(pageDocument, "body") : Reflect.get(matches, 0);
|
|
677
|
-
if (element === null) return { _tag: "MissingElement" };
|
|
678
|
-
if (element === void 0) return { _tag: "MissingElement" };
|
|
679
|
-
const innerText = Reflect.get(element, "innerText");
|
|
680
|
-
const textContent = Reflect.get(element, "textContent");
|
|
681
|
-
const pageText = typeof innerText === "string" ? innerText : typeof textContent === "string" ? textContent : "";
|
|
682
|
-
const primaryControlSelector = "input,select,textarea,label,button,[role=\"checkbox\"],[role=\"radio\"],[role=\"option\"],[role=\"switch\"],[role=\"tab\"],[role=\"button\"]";
|
|
683
|
-
const optionSelector = "select option";
|
|
684
|
-
const secondaryControlSelector = "a[href]";
|
|
685
|
-
const controlSelector = `${primaryControlSelector},${optionSelector},${secondaryControlSelector}`;
|
|
686
|
-
const selectorFor = (candidate) => {
|
|
687
|
-
const parts = [];
|
|
688
|
-
let current = candidate;
|
|
689
|
-
while (current !== null && current !== void 0) {
|
|
690
|
-
const tagName = String(Reflect.get(current, "tagName") ?? "").toLowerCase();
|
|
691
|
-
if (tagName === "") break;
|
|
692
|
-
const parent = Reflect.get(current, "parentElement");
|
|
693
|
-
if (parent === null) {
|
|
694
|
-
parts.push(tagName);
|
|
695
|
-
break;
|
|
696
|
-
}
|
|
697
|
-
let sibling = Reflect.get(current, "previousElementSibling");
|
|
698
|
-
let index = 1;
|
|
699
|
-
while (sibling !== null && sibling !== void 0) {
|
|
700
|
-
if (String(Reflect.get(sibling, "tagName") ?? "").toLowerCase() === tagName) index++;
|
|
701
|
-
sibling = Reflect.get(sibling, "previousElementSibling");
|
|
702
|
-
}
|
|
703
|
-
parts.push(`${tagName}:nth-of-type(${index})`);
|
|
704
|
-
current = parent;
|
|
705
|
-
}
|
|
706
|
-
return parts.reverse().join(" > ");
|
|
707
|
-
};
|
|
708
|
-
const visible = (candidate) => {
|
|
709
|
-
const hidden = Reflect.get(candidate, "hidden");
|
|
710
|
-
const ariaHidden = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-hidden"]);
|
|
711
|
-
const rects = Reflect.apply(Reflect.get(candidate, "getClientRects"), candidate, []);
|
|
712
|
-
return hidden !== true && ariaHidden !== "true" && typeof rects === "object" && rects !== null && Reflect.get(rects, "length") > 0;
|
|
713
|
-
};
|
|
714
|
-
const candidates = [];
|
|
715
|
-
let controlsTruncated = false;
|
|
716
|
-
const consider = (candidate) => {
|
|
717
|
-
const tagName = String(Reflect.get(candidate, "tagName") ?? "").toLowerCase();
|
|
718
|
-
const visibilityTarget = tagName === "option" ? Reflect.apply(Reflect.get(candidate, "closest"), candidate, ["select"]) : candidate;
|
|
719
|
-
if (!(tagName !== "label" || Reflect.get(candidate, "control") !== null) || typeof visibilityTarget !== "object" || visibilityTarget === null || !visible(visibilityTarget)) return;
|
|
720
|
-
candidates.push(candidate);
|
|
721
|
-
if (candidates.length > maximumControls) controlsTruncated = true;
|
|
722
|
-
};
|
|
723
|
-
const elementMatches = Reflect.get(element, "matches");
|
|
724
|
-
if (typeof elementMatches === "function" && Reflect.apply(elementMatches, element, [controlSelector])) consider(element);
|
|
725
|
-
const considerSelector = (candidateSelector) => {
|
|
726
|
-
const descendants = Reflect.apply(Reflect.get(element, "querySelectorAll"), element, [candidateSelector]);
|
|
727
|
-
if (typeof descendants !== "object" || descendants === null) return;
|
|
728
|
-
const descendantCount = Reflect.get(descendants, "length");
|
|
729
|
-
for (let index = 0; index < descendantCount && !controlsTruncated; index++) consider(Reflect.get(descendants, index));
|
|
730
|
-
};
|
|
731
|
-
considerSelector(primaryControlSelector);
|
|
732
|
-
if (!controlsTruncated) considerSelector(optionSelector);
|
|
733
|
-
if (!controlsTruncated) considerSelector(secondaryControlSelector);
|
|
734
|
-
const controls = candidates.slice(0, maximumControls).map((candidate) => {
|
|
735
|
-
const associated = Reflect.get(candidate, "control") ?? candidate;
|
|
736
|
-
const tagName = String(Reflect.get(candidate, "tagName") ?? "").toLowerCase();
|
|
737
|
-
const inputType = String(Reflect.get(associated, "type") ?? "").toLowerCase();
|
|
738
|
-
const role = String(Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["role"]) ?? "").toLowerCase();
|
|
739
|
-
const ariaLabel = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-label"]);
|
|
740
|
-
const candidateText = tagName === "textarea" || tagName === "input" || tagName === "select" ? void 0 : Reflect.get(candidate, tagName === "option" ? "label" : "innerText");
|
|
741
|
-
const associatedLabels = Reflect.get(associated, "labels");
|
|
742
|
-
const associatedLabel = associatedLabels !== void 0 && associatedLabels !== null && Reflect.get(associatedLabels, "length") > 0 ? Reflect.get(Reflect.get(associatedLabels, 0), "innerText") : void 0;
|
|
743
|
-
const label = String(typeof ariaLabel === "string" && ariaLabel !== "" ? ariaLabel : typeof candidateText === "string" && candidateText !== "" ? candidateText : associatedLabel ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
744
|
-
const checked = Reflect.get(associated, "checked");
|
|
745
|
-
const selected = tagName === "select" ? Reflect.get(associated, "selectedIndex") >= 0 : Reflect.get(associated, "selected");
|
|
746
|
-
const disabled = Reflect.get(associated, "disabled");
|
|
747
|
-
const required = Reflect.get(associated, "required");
|
|
748
|
-
const validity = Reflect.get(associated, "validity");
|
|
749
|
-
const form = Reflect.get(associated, "form");
|
|
750
|
-
const formMatches = form === null || form === void 0 ? void 0 : Reflect.get(form, "matches");
|
|
751
|
-
const ariaChecked = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-checked"]);
|
|
752
|
-
const ariaSelected = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-selected"]);
|
|
753
|
-
const ariaDisabled = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-disabled"]);
|
|
754
|
-
return {
|
|
755
|
-
selector: selectorFor(candidate),
|
|
756
|
-
kind: tagName === "label" ? `label:${inputType || "control"}` : tagName === "input" ? `input:${inputType || "text"}` : role !== "" ? `role:${role}` : tagName,
|
|
757
|
-
...label === "" ? {} : { label },
|
|
758
|
-
...typeof checked === "boolean" ? { checked } : ariaChecked === "true" || ariaChecked === "false" ? { checked: ariaChecked === "true" } : {},
|
|
759
|
-
...typeof selected === "boolean" ? { selected } : ariaSelected === "true" || ariaSelected === "false" ? { selected: ariaSelected === "true" } : {},
|
|
760
|
-
...typeof disabled === "boolean" ? { disabled } : ariaDisabled === "true" || ariaDisabled === "false" ? { disabled: ariaDisabled === "true" } : {},
|
|
761
|
-
...typeof required === "boolean" ? { required } : {},
|
|
762
|
-
...validity !== void 0 && typeof Reflect.get(validity, "valid") === "boolean" ? { valid: Reflect.get(validity, "valid") } : {},
|
|
763
|
-
...typeof formMatches === "function" ? { formValid: Reflect.apply(formMatches, form, [":valid"]) } : {}
|
|
764
|
-
};
|
|
765
|
-
});
|
|
766
|
-
const text = JSON.stringify({
|
|
767
|
-
pageText,
|
|
768
|
-
selectorMatchCount,
|
|
769
|
-
controls,
|
|
770
|
-
controlsTruncated
|
|
771
|
-
});
|
|
772
|
-
const observed = new TextEncoder().encode(text).byteLength;
|
|
773
|
-
return observed > maximum ? {
|
|
774
|
-
_tag: "OverLimit",
|
|
775
|
-
observed
|
|
776
|
-
} : {
|
|
777
|
-
_tag: "Text",
|
|
778
|
-
text
|
|
779
|
-
};
|
|
780
|
-
}, selector, maximumBytes, MAX_OBSERVED_CONTROLS));
|
|
781
|
-
if (observation._tag === "Text") Schema.decodeSync(PageObservation)(observation.text);
|
|
890
|
+
const observation = Schema.decodeUnknownSync(TextObservation)(await page.evaluate(observePage, void 0, selector, maximumBytes, MAX_OBSERVED_CONTROLS));
|
|
891
|
+
if (observation._tag === "Text") Schema.decodeSync(BrowserRunPageObservation)(observation.text);
|
|
782
892
|
return observation;
|
|
783
893
|
},
|
|
784
|
-
fill: (selector, value, signal, onDispatch) => runObservedPageAction(page, selector, signal, onDispatch, (element) => element.evaluate((element, nextValue) => {
|
|
894
|
+
fill: (selector, value, signal, onDispatch, onComplete, expectedTarget) => runObservedPageAction(page, selector, signal, onDispatch, (element) => expectedTarget !== void 0 ? guardedPageInput(element, expectedTarget, "fill", value) : element.evaluate((element, nextValue) => {
|
|
785
895
|
let prototype = Reflect.getPrototypeOf(element);
|
|
786
896
|
let setValue;
|
|
787
897
|
while (prototype !== null) {
|
|
@@ -801,12 +911,12 @@ const makeProductionPage = (page) => {
|
|
|
801
911
|
Reflect.apply(dispatchEvent, element, [new Event("input", { bubbles: true })]);
|
|
802
912
|
Reflect.apply(dispatchEvent, element, [new Event("change", { bubbles: true })]);
|
|
803
913
|
}
|
|
804
|
-
}, value)),
|
|
805
|
-
click: (selector, signal, onDispatch) => runObservedPageAction(page, selector, signal, onDispatch, (element) => element.click()),
|
|
806
|
-
selectFile: async (request, signal, onDispatch) => {
|
|
914
|
+
}, value), void 0, onComplete),
|
|
915
|
+
click: (selector, signal, onDispatch, onComplete, expectedTarget) => runObservedPageAction(page, selector, signal, onDispatch, (element) => expectedTarget === void 0 ? element.click() : guardedPageInput(element, expectedTarget, "click"), void 0, onComplete),
|
|
916
|
+
selectFile: async (request, signal, onDispatch, onComplete) => {
|
|
807
917
|
const selection = await prepareFileSelection(request, signal);
|
|
808
918
|
try {
|
|
809
|
-
return await runObservedPageAction(page, request.selector, signal, onDispatch, selection.select, selection.validate);
|
|
919
|
+
return await runObservedPageAction(page, request.selector, signal, onDispatch, selection.select, selection.validate, onComplete);
|
|
810
920
|
} finally {
|
|
811
921
|
await selection.close();
|
|
812
922
|
}
|
|
@@ -843,6 +953,20 @@ const makeProductionContext = (context, viewport) => ({
|
|
|
843
953
|
}
|
|
844
954
|
});
|
|
845
955
|
const makeProductionBrowser = (browser, viewport) => ({
|
|
956
|
+
detach: () => browser.disconnect(),
|
|
957
|
+
reattach: async (identity) => {
|
|
958
|
+
const context = browser.browserContexts().find((candidate) => candidate.id === identity.contextId);
|
|
959
|
+
if (context === void 0) return void 0;
|
|
960
|
+
const pages = await context.pages();
|
|
961
|
+
if (pages.length > 64) throw new Error("The browser target inventory exceeds its bound");
|
|
962
|
+
for (const candidate of pages) {
|
|
963
|
+
const page = makeProductionPage(candidate);
|
|
964
|
+
if ((await page.identity?.())?.targetId === identity.targetId) return {
|
|
965
|
+
context: makeProductionContext(context),
|
|
966
|
+
page
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
},
|
|
846
970
|
createContext: async () => makeProductionContext(await browser.createBrowserContext(), viewport),
|
|
847
971
|
close: async () => {
|
|
848
972
|
if (!browser.isConnected()) return;
|
|
@@ -866,26 +990,42 @@ const protocolError = (message, cause) => InteractiveBrowserProtocolError.make({
|
|
|
866
990
|
message,
|
|
867
991
|
...cause === void 0 ? {} : { cause }
|
|
868
992
|
});
|
|
869
|
-
const
|
|
993
|
+
const failReportedProtocol = (operation, error, evidence = error.evidence) => reportBrowserCause(operation, Cause.fail(error)).pipe(Effect.andThen(Effect.fail(reportedBrowserError(InteractiveBrowserProtocolError.make({
|
|
994
|
+
implementation: browserRunInteractiveImplementation,
|
|
995
|
+
message: error.message,
|
|
996
|
+
...evidence === void 0 ? {} : { evidence }
|
|
997
|
+
})))));
|
|
998
|
+
const actionError = (operation, cause, evidence) => InteractiveBrowserActionError.make({
|
|
870
999
|
implementation: browserRunInteractiveImplementation,
|
|
871
1000
|
operation,
|
|
1001
|
+
...evidence === void 0 ? {} : { evidence },
|
|
872
1002
|
message: `The interactive browser ${operation} operation failed`,
|
|
873
1003
|
...cause === void 0 ? {} : { cause }
|
|
874
1004
|
});
|
|
875
1005
|
const undispatchedActionError = (operation) => InteractiveBrowserActionError.make({
|
|
876
1006
|
implementation: browserRunInteractiveImplementation,
|
|
877
1007
|
operation,
|
|
878
|
-
message: `The interactive browser ${operation} operation was not dispatched
|
|
1008
|
+
message: `The interactive browser ${operation} operation was not dispatched`,
|
|
1009
|
+
evidence: {
|
|
1010
|
+
stage: "preparation",
|
|
1011
|
+
dispatch: "not-dispatched",
|
|
1012
|
+
session: "attached"
|
|
1013
|
+
}
|
|
879
1014
|
});
|
|
880
1015
|
/** Recognizes a local pre-dispatch refusal without exposing selector or page content. */
|
|
881
|
-
const isBrowserRunUndispatchedActionError = (error) => Schema.is(InteractiveBrowserActionError)(error) && error.implementation.identity === browserRunInteractiveImplementation.identity &&
|
|
1016
|
+
const isBrowserRunUndispatchedActionError = (error) => Schema.is(InteractiveBrowserActionError)(error) && error.implementation.identity === browserRunInteractiveImplementation.identity && error.evidence?.dispatch === "not-dispatched";
|
|
882
1017
|
const policyError = (message) => InteractiveBrowserPolicyDeniedError.make({
|
|
883
1018
|
implementation: browserRunInteractiveImplementation,
|
|
884
1019
|
message
|
|
885
1020
|
});
|
|
886
|
-
const expiredError = () => InteractiveBrowserExpiredError.make({
|
|
1021
|
+
const expiredError = (session = "lost") => InteractiveBrowserExpiredError.make({
|
|
887
1022
|
implementation: browserRunInteractiveImplementation,
|
|
888
|
-
message: "The remote browser is no longer usable"
|
|
1023
|
+
message: "The remote browser is no longer usable",
|
|
1024
|
+
evidence: {
|
|
1025
|
+
stage: "preparation",
|
|
1026
|
+
dispatch: "not-dispatched",
|
|
1027
|
+
session
|
|
1028
|
+
}
|
|
889
1029
|
});
|
|
890
1030
|
const causeText = (cause) => {
|
|
891
1031
|
if (cause instanceof Error) return cause.message.slice(0, 8e3);
|
|
@@ -893,6 +1033,7 @@ const causeText = (cause) => {
|
|
|
893
1033
|
};
|
|
894
1034
|
const isCapacityRefusal = (cause) => cause instanceof BrowserRunFailure && cause.status === 429;
|
|
895
1035
|
const isRemoteClosure = (cause) => /target closed|browser.*closed|session.*closed|connection.*closed|not connected|websocket.*closed/i.test(causeText(cause));
|
|
1036
|
+
const inputMayStillRun = (cause) => isRemoteClosure(cause) || cause instanceof Error && cause.name === "TimeoutError" || cause instanceof BrowserRunFailure && cause.reason === "timeout";
|
|
896
1037
|
const snapshotPolicy = Effect.fn("BrowserRunInteractive.snapshotPolicy")(function* (input) {
|
|
897
1038
|
const decoded = yield* Schema.decodeEffect(InteractiveBrowserPolicy, { onExcessProperty: "error" })(input).pipe(Effect.mapError(() => policyError("The interactive browser policy is malformed")));
|
|
898
1039
|
if (decoded.network._tag === "PublicWeb") return yield* InteractiveBrowserUnsupportedError.make({
|
|
@@ -956,9 +1097,28 @@ const withinDeadline = Effect.fn("BrowserRunInteractive.withinDeadline")(functio
|
|
|
956
1097
|
orElse: () => Effect.sync(() => onTimeout?.()).pipe(Effect.andThen(deadlineError(policy, startedAt)))
|
|
957
1098
|
}));
|
|
958
1099
|
});
|
|
1100
|
+
const trackPendingInput = (state, pending) => {
|
|
1101
|
+
state.pendingInputs.add(pending);
|
|
1102
|
+
pending.then(() => state.pendingInputs.delete(pending), (cause) => {
|
|
1103
|
+
state.pendingInputs.delete(pending);
|
|
1104
|
+
if (state.disconnected.value || inputMayStillRun(cause)) state.uncertain.value = true;
|
|
1105
|
+
if (isRemoteClosure(cause)) state.disconnected.value = true;
|
|
1106
|
+
});
|
|
1107
|
+
return pending;
|
|
1108
|
+
};
|
|
1109
|
+
const makeHostInputObserver = Effect.fnUntraced(function* (state, operation) {
|
|
1110
|
+
const reporterContext = yield* Effect.context();
|
|
1111
|
+
return (pending, signal) => {
|
|
1112
|
+
pending.then(void 0, (cause) => {
|
|
1113
|
+
if (signal.aborted) return Effect.runPromiseWith(reporterContext)(reportBrowserCause(operation, Cause.fail(cause)));
|
|
1114
|
+
});
|
|
1115
|
+
return trackPendingInput(state, pending);
|
|
1116
|
+
};
|
|
1117
|
+
});
|
|
959
1118
|
const stateFailure = (state) => {
|
|
960
1119
|
if (state.violation.value !== void 0) return state.violation.value;
|
|
961
|
-
if (state.closed.value
|
|
1120
|
+
if (state.closed.value) return expiredError();
|
|
1121
|
+
if (state.disconnected.value) return expiredError("disconnected");
|
|
962
1122
|
};
|
|
963
1123
|
var BrowserRunRemoteFailure = class {
|
|
964
1124
|
cause;
|
|
@@ -1012,46 +1172,92 @@ const makeRequestListener = (policy, state) => (request) => {
|
|
|
1012
1172
|
};
|
|
1013
1173
|
const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page, policy, startedAt, state, close) {
|
|
1014
1174
|
const permits = yield* Semaphore.make(1);
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1175
|
+
let currentOperation;
|
|
1176
|
+
let generation = 0;
|
|
1177
|
+
let evidence = {
|
|
1178
|
+
stage: "preparation",
|
|
1179
|
+
dispatch: "not-dispatched",
|
|
1180
|
+
session: "attached"
|
|
1181
|
+
};
|
|
1182
|
+
const reporterContext = yield* Effect.context();
|
|
1183
|
+
const remote = (operation, evaluate, observed = false) => Effect.suspend(() => {
|
|
1184
|
+
currentOperation = operation;
|
|
1185
|
+
const mutating = operation !== "read-text" && operation !== "screenshot";
|
|
1186
|
+
const currentGeneration = ++generation;
|
|
1187
|
+
evidence = {
|
|
1188
|
+
stage: observed ? "preparation" : mutating ? "input" : "observation",
|
|
1189
|
+
dispatch: observed || !mutating ? "not-dispatched" : "running",
|
|
1190
|
+
session: "attached"
|
|
1191
|
+
};
|
|
1192
|
+
return Effect.tryPromise({
|
|
1193
|
+
try: (signal) => {
|
|
1194
|
+
const pending = evaluate(signal);
|
|
1195
|
+
if (mutating) trackPendingInput(state, pending);
|
|
1196
|
+
pending.then(() => {
|
|
1197
|
+
if (mutating && generation === currentGeneration) evidence = {
|
|
1198
|
+
...evidence,
|
|
1199
|
+
stage: "observation",
|
|
1200
|
+
dispatch: "completed"
|
|
1201
|
+
};
|
|
1202
|
+
}, (cause) => {
|
|
1203
|
+
if (state.disconnected.value || isRemoteClosure(cause)) {
|
|
1204
|
+
state.disconnected.value = true;
|
|
1205
|
+
if (mutating) state.uncertain.value = true;
|
|
1206
|
+
}
|
|
1207
|
+
if (generation === currentGeneration && evidence.dispatch === "running") evidence = {
|
|
1208
|
+
...evidence,
|
|
1209
|
+
dispatch: "unknown"
|
|
1210
|
+
};
|
|
1211
|
+
if (signal.aborted && !(cause instanceof BrowserRunActionUndispatched)) Effect.runPromiseWith(reporterContext)(reportBrowserCause("interactive.input.late", Cause.fail(cause)));
|
|
1212
|
+
});
|
|
1213
|
+
return pending;
|
|
1214
|
+
},
|
|
1215
|
+
catch: (cause) => new BrowserRunRemoteFailure(cause)
|
|
1216
|
+
}).pipe(Effect.catch((failure) => {
|
|
1217
|
+
if (failure.cause instanceof BrowserRunActionUndispatched) return Effect.logInfo("Browser interactive action was not dispatched").pipe(Effect.annotateLogs({
|
|
1042
1218
|
"browser.action": operation,
|
|
1043
|
-
"browser.
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1219
|
+
"browser.selector_match_count": failure.cause.matchCount
|
|
1220
|
+
}), Effect.andThen(Effect.fail(undispatchedActionError(operation))));
|
|
1221
|
+
if (mutating && inputMayStillRun(failure.cause)) state.uncertain.value = true;
|
|
1222
|
+
if (isRemoteClosure(failure.cause)) {
|
|
1223
|
+
state.disconnected.value = true;
|
|
1224
|
+
if (mutating) state.uncertain.value = true;
|
|
1225
|
+
}
|
|
1226
|
+
evidence = {
|
|
1227
|
+
...evidence,
|
|
1228
|
+
dispatch: evidence.dispatch === "running" ? "unknown" : evidence.dispatch,
|
|
1229
|
+
session: state.disconnected.value ? "disconnected" : "attached"
|
|
1230
|
+
};
|
|
1231
|
+
return reportBrowserCause(`interactive.${operation}.${evidence.stage}.${evidence.dispatch}`, Cause.fail(failure.cause)).pipe(Effect.andThen(Effect.fail(reportedBrowserError(actionError(operation, void 0, evidence)))));
|
|
1232
|
+
}));
|
|
1049
1233
|
});
|
|
1234
|
+
const observedAction = (operation, evaluate) => remote(operation, (signal) => {
|
|
1235
|
+
const observedGeneration = generation;
|
|
1236
|
+
return evaluate(signal, () => {
|
|
1237
|
+
if (generation === observedGeneration) evidence = {
|
|
1238
|
+
...evidence,
|
|
1239
|
+
stage: "input",
|
|
1240
|
+
dispatch: "running"
|
|
1241
|
+
};
|
|
1242
|
+
}, () => {
|
|
1243
|
+
if (generation === observedGeneration) evidence = {
|
|
1244
|
+
...evidence,
|
|
1245
|
+
stage: "observation",
|
|
1246
|
+
dispatch: "completed"
|
|
1247
|
+
};
|
|
1248
|
+
});
|
|
1249
|
+
}, true).pipe(Effect.onInterrupt(() => Effect.logWarning("Browser interactive action interrupted").pipe(Effect.annotateLogs({
|
|
1250
|
+
"browser.action": operation,
|
|
1251
|
+
"browser.action_dispatched": evidence.dispatch !== "not-dispatched",
|
|
1252
|
+
"browser.action_outcome_unknown": evidence.dispatch === "running" || evidence.dispatch === "unknown"
|
|
1253
|
+
}))));
|
|
1050
1254
|
const decodeActionObservation = Effect.fn("BrowserRunInteractive.decodeActionObservation")(function* (raw) {
|
|
1051
1255
|
return yield* Schema.decodeUnknownEffect(ActionObservation)(raw).pipe(Effect.mapError((cause) => protocolError("The browser returned a malformed action observation", cause)));
|
|
1052
1256
|
});
|
|
1053
1257
|
const logActionObservation = (operation, observation) => Effect.logInfo("Browser interactive action observed").pipe(Effect.annotateLogs({
|
|
1054
1258
|
"browser.action": operation,
|
|
1259
|
+
"browser.input_millis": observation.inputMillis,
|
|
1260
|
+
"browser.observation_millis": observation.observationMillis,
|
|
1055
1261
|
"browser.selector_match_count": observation.before.matchCount,
|
|
1056
1262
|
...observation.before.kind === void 0 ? {} : { "browser.target_kind": observation.before.kind },
|
|
1057
1263
|
...observation.before.checked === void 0 ? {} : { "browser.target_checked_before": observation.before.checked },
|
|
@@ -1076,18 +1282,38 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page
|
|
|
1076
1282
|
"browser.fetch_xhr_pending": observation.network.pending,
|
|
1077
1283
|
"browser.network_settle_timed_out": observation.network.settleTimedOut
|
|
1078
1284
|
}));
|
|
1079
|
-
const run = (effect, preflight = Effect.void, consumeAction = true) => permits.withPermitsIfAvailable(1)(Effect.gen(function* () {
|
|
1285
|
+
const run = (effect, preflight = Effect.void, consumeAction = true, mutating = true) => permits.withPermitsIfAvailable(1)(Effect.gen(function* () {
|
|
1080
1286
|
const unavailable = stateFailure(state);
|
|
1081
|
-
if (unavailable !== void 0)
|
|
1287
|
+
if (unavailable !== void 0) {
|
|
1288
|
+
if (unavailable._tag === "InteractiveBrowserProtocolError") {
|
|
1289
|
+
yield* reportBrowserCause("interactive.policy", Cause.fail(unavailable));
|
|
1290
|
+
const safe = reportedBrowserError(InteractiveBrowserProtocolError.make({
|
|
1291
|
+
implementation: browserRunInteractiveImplementation,
|
|
1292
|
+
message: unavailable.message,
|
|
1293
|
+
evidence: unavailable.evidence ?? {
|
|
1294
|
+
stage: "preparation",
|
|
1295
|
+
dispatch: "not-dispatched",
|
|
1296
|
+
session: "attached"
|
|
1297
|
+
}
|
|
1298
|
+
}));
|
|
1299
|
+
if (state.violation.value === unavailable) state.violation.value = safe;
|
|
1300
|
+
return yield* safe;
|
|
1301
|
+
}
|
|
1302
|
+
return yield* unavailable;
|
|
1303
|
+
}
|
|
1304
|
+
if (mutating && (state.uncertain.value || state.pendingInputs.size > 0)) return yield* InteractiveBrowserBusyError.make({
|
|
1305
|
+
implementation: browserRunInteractiveImplementation,
|
|
1306
|
+
message: "Earlier browser input has not been proven stopped"
|
|
1307
|
+
});
|
|
1308
|
+
currentOperation = void 0;
|
|
1309
|
+
generation++;
|
|
1082
1310
|
yield* preflight;
|
|
1083
1311
|
if (consumeAction) {
|
|
1084
|
-
const admitted =
|
|
1085
|
-
allowed:
|
|
1086
|
-
observed:
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
observed: count + 1
|
|
1090
|
-
}, count + 1]);
|
|
1312
|
+
const admitted = {
|
|
1313
|
+
allowed: state.actions.value < policy.maxActions,
|
|
1314
|
+
observed: state.actions.value + 1
|
|
1315
|
+
};
|
|
1316
|
+
if (admitted.allowed) state.actions.value++;
|
|
1091
1317
|
if (!admitted.allowed) return yield* InteractiveBrowserLimitError.make({
|
|
1092
1318
|
implementation: browserRunInteractiveImplementation,
|
|
1093
1319
|
limit: "actions",
|
|
@@ -1096,22 +1322,29 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page
|
|
|
1096
1322
|
message: "The browser action limit was reached"
|
|
1097
1323
|
});
|
|
1098
1324
|
}
|
|
1099
|
-
const completed = effect.pipe(Effect.
|
|
1100
|
-
const failure = stateFailure(state);
|
|
1101
|
-
return Effect.fail(failure ?? error);
|
|
1102
|
-
}), Effect.flatMap((result) => awaitPendingRequests(state).pipe(Effect.flatMap(() => {
|
|
1325
|
+
const completed = effect.pipe(Effect.flatMap((result) => awaitPendingRequests(state).pipe(Effect.andThen(Effect.suspend(() => {
|
|
1103
1326
|
const failure = stateFailure(state);
|
|
1104
1327
|
return failure === void 0 ? Effect.succeed(result) : Effect.fail(failure);
|
|
1105
|
-
}))));
|
|
1106
|
-
return yield* withinDeadline(completed, policy, startedAt
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1328
|
+
})))));
|
|
1329
|
+
return yield* withinDeadline(completed, policy, startedAt).pipe(Effect.mapError((error) => currentOperation === void 0 ? error : error._tag === "InteractiveBrowserLimitError" ? InteractiveBrowserLimitError.make({
|
|
1330
|
+
implementation: error.implementation,
|
|
1331
|
+
limit: error.limit,
|
|
1332
|
+
maximum: error.maximum,
|
|
1333
|
+
observed: error.observed,
|
|
1334
|
+
message: error.message,
|
|
1335
|
+
evidence
|
|
1336
|
+
}) : error._tag === "InteractiveBrowserPolicyDeniedError" ? InteractiveBrowserPolicyDeniedError.make({
|
|
1337
|
+
implementation: error.implementation,
|
|
1338
|
+
message: error.message,
|
|
1339
|
+
evidence
|
|
1340
|
+
}) : error), Effect.catchIf((error) => currentOperation !== void 0 && error._tag !== "InteractiveBrowserActionError" && error._tag !== "InteractiveBrowserLimitError" && error._tag !== "InteractiveBrowserPolicyDeniedError" && error._tag !== "InteractiveBrowserExpiredError", (error) => {
|
|
1341
|
+
const safe = reportedBrowserError(error._tag === "InteractiveBrowserProtocolError" ? InteractiveBrowserProtocolError.make({
|
|
1342
|
+
implementation: browserRunInteractiveImplementation,
|
|
1343
|
+
message: error.message,
|
|
1344
|
+
evidence
|
|
1345
|
+
}) : actionError(currentOperation ?? "read-text", void 0, evidence));
|
|
1346
|
+
if (state.violation.value === error) state.violation.value = safe;
|
|
1347
|
+
return reportBrowserCause(`interactive.${currentOperation}.observation`, Cause.fail(error)).pipe(Effect.andThen(Effect.fail(safe)));
|
|
1115
1348
|
}));
|
|
1116
1349
|
})).pipe(Effect.flatMap(Effect.fromOption(() => InteractiveBrowserBusyError.make({
|
|
1117
1350
|
implementation: browserRunInteractiveImplementation,
|
|
@@ -1126,7 +1359,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page
|
|
|
1126
1359
|
readText: (request) => run(Effect.gen(function* () {
|
|
1127
1360
|
const raw = yield* remote("read-text", () => page.readText(request.selector, policy.maxReturnedBytes));
|
|
1128
1361
|
const observation = yield* Schema.decodeUnknownEffect(TextObservation)(raw).pipe(Effect.mapError((cause) => protocolError("The browser returned a malformed text observation", cause)));
|
|
1129
|
-
if (observation._tag === "MissingElement") return yield* actionError("read-text");
|
|
1362
|
+
if (observation._tag === "MissingElement") return yield* actionError("read-text", void 0, evidence);
|
|
1130
1363
|
if (observation._tag === "OverLimit") return yield* InteractiveBrowserLimitError.make({
|
|
1131
1364
|
implementation: browserRunInteractiveImplementation,
|
|
1132
1365
|
limit: "returned-bytes",
|
|
@@ -1143,19 +1376,19 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page
|
|
|
1143
1376
|
message: "The browser returned-text limit was reached"
|
|
1144
1377
|
});
|
|
1145
1378
|
return yield* Schema.decodeEffect(BrowserTextResult)({ text: observation.text }).pipe(Effect.mapError((cause) => protocolError("The browser returned malformed page text", cause)));
|
|
1146
|
-
})),
|
|
1379
|
+
}), Effect.void, true, false),
|
|
1147
1380
|
fill: (request) => run(Effect.gen(function* () {
|
|
1148
|
-
const observation = yield* observedAction("fill", (signal, onDispatch) => page.fill(request.selector, request.value, signal, onDispatch)).pipe(Effect.flatMap(decodeActionObservation));
|
|
1381
|
+
const observation = yield* observedAction("fill", (signal, onDispatch, onComplete) => page.fill(request.selector, request.value, signal, onDispatch, onComplete, request.expectedTarget)).pipe(Effect.flatMap(decodeActionObservation));
|
|
1149
1382
|
yield* logActionObservation("fill", observation);
|
|
1150
1383
|
return yield* decodeActionResult(page, policy);
|
|
1151
1384
|
})),
|
|
1152
1385
|
click: (request) => run(Effect.gen(function* () {
|
|
1153
|
-
const observation = yield* observedAction("click", (signal, onDispatch) => page.click(request.selector, signal, onDispatch)).pipe(Effect.flatMap(decodeActionObservation));
|
|
1386
|
+
const observation = yield* observedAction("click", (signal, onDispatch, onComplete) => page.click(request.selector, signal, onDispatch, onComplete, request.expectedTarget)).pipe(Effect.flatMap(decodeActionObservation));
|
|
1154
1387
|
yield* logActionObservation("click", observation);
|
|
1155
1388
|
return yield* decodeActionResult(page, policy);
|
|
1156
1389
|
})),
|
|
1157
1390
|
selectFile: (request) => Schema.decodeEffect(BrowserSelectFileRequest)(request).pipe(Effect.mapError(() => policyError("The browser file selection request is malformed")), Effect.flatMap((decoded) => run(Effect.gen(function* () {
|
|
1158
|
-
const observation = yield* observedAction("select-file", (signal, onDispatch) => page.selectFile(decoded, signal, onDispatch)).pipe(Effect.flatMap(decodeActionObservation));
|
|
1391
|
+
const observation = yield* observedAction("select-file", (signal, onDispatch, onComplete) => page.selectFile(decoded, signal, onDispatch, onComplete)).pipe(Effect.flatMap(decodeActionObservation));
|
|
1159
1392
|
yield* logActionObservation("select-file", observation);
|
|
1160
1393
|
const result = yield* decodeActionResult(page, policy);
|
|
1161
1394
|
return BrowserFileSelectionResult.make({
|
|
@@ -1180,7 +1413,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page
|
|
|
1180
1413
|
mediaType: "image/png",
|
|
1181
1414
|
bytes: new Uint8Array(bytes)
|
|
1182
1415
|
}).pipe(Effect.mapError(() => protocolError("The browser returned a malformed PNG screenshot")));
|
|
1183
|
-
}), decodeActionResult(page, policy).pipe(Effect.asVoid)))),
|
|
1416
|
+
}), decodeActionResult(page, policy).pipe(Effect.asVoid), true, false))),
|
|
1184
1417
|
scroll: (request) => Schema.decodeEffect(BrowserScrollRequest)(request).pipe(Effect.mapError(() => policyError("The browser scroll request is malformed")), Effect.flatMap((decoded) => run(remote("scroll", () => page.scroll(decoded.deltaX, decoded.deltaY)).pipe(Effect.andThen(decodeActionResult(page, policy))), decodeActionResult(page, policy).pipe(Effect.asVoid)))),
|
|
1185
1418
|
close
|
|
1186
1419
|
},
|
|
@@ -1217,6 +1450,7 @@ const runTeardown = (entries) => Effect.forEach([...entries].reverse(), (entry)
|
|
|
1217
1450
|
}))).pipe(Effect.map((failures) => failures.filter((failure) => failure !== void 0)));
|
|
1218
1451
|
const cdpCommand = (page, state, command, parameters, output, malformedMessage) => Effect.scoped(Effect.gen(function* () {
|
|
1219
1452
|
const cleanupContext = yield* Effect.context();
|
|
1453
|
+
const observeInput = yield* makeHostInputObserver(state, "interactive.control.late");
|
|
1220
1454
|
const cdp = yield* Effect.acquireRelease(Effect.tryPromise({
|
|
1221
1455
|
try: (signal) => {
|
|
1222
1456
|
const pending = closeLateAcquisition(signal, page.createCdpSession, (acquired) => Effect.runPromiseWith(cleanupContext)(Effect.tryPromise({
|
|
@@ -1231,13 +1465,19 @@ const cdpCommand = (page, state, command, parameters, output, malformedMessage)
|
|
|
1231
1465
|
catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the Cloudflare browser control session failed", cause)
|
|
1232
1466
|
}), (acquired) => closeWithWarning(acquired.detach, "Detaching the Cloudflare browser control session failed"), { interruptible: true });
|
|
1233
1467
|
const raw = yield* Effect.tryPromise({
|
|
1234
|
-
try: () =>
|
|
1468
|
+
try: (signal) => {
|
|
1469
|
+
const pending = cdp.send(command, parameters);
|
|
1470
|
+
return command === "Cloudflare.getHandoffState" ? pending : observeInput(pending, signal);
|
|
1471
|
+
},
|
|
1235
1472
|
catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("The Cloudflare browser control command failed", cause)
|
|
1236
1473
|
});
|
|
1237
1474
|
return yield* Schema.decodeUnknownEffect(output)(raw).pipe(Effect.catch((error) => reportBrowserCause("interactive.command.decode", Cause.fail(error)).pipe(Effect.andThen(Effect.fail(reportedBrowserError(protocolError(malformedMessage)))))));
|
|
1238
|
-
}));
|
|
1475
|
+
})).pipe(Effect.catchTag("InteractiveBrowserProtocolError", (error) => failReportedProtocol("interactive.control", error)));
|
|
1239
1476
|
const makeHostService = (binding) => {
|
|
1240
|
-
const
|
|
1477
|
+
const owners = /* @__PURE__ */ new Map();
|
|
1478
|
+
const closeSession = (sessionId) => binding.closeSession(sessionId).pipe(Effect.tap(() => Effect.sync(() => {
|
|
1479
|
+
owners.delete(Redacted.value(sessionId));
|
|
1480
|
+
})));
|
|
1241
1481
|
const terminate = Effect.fn("BrowserRunInteractiveHost.terminate")(function* (sessionId, entries) {
|
|
1242
1482
|
const deadline = (yield* Clock.currentTimeMillis) + 1e4;
|
|
1243
1483
|
yield* closeSession(sessionId).pipe(Effect.interruptible, Effect.timeoutOrElse({
|
|
@@ -1255,10 +1495,10 @@ const makeHostService = (binding) => {
|
|
|
1255
1495
|
}).pipe(Effect.flatMap(Schema.decodeUnknownEffect(BrowserRunSessionId)), Effect.mapError((cause) => actionError("close", cause)), Effect.onError(() => closeWithWarning(browser.close, "Closing an unidentified browser failed")));
|
|
1256
1496
|
yield* terminate(Redacted.make(sessionId), [closeEntry(browser.close, "Closing the local browser connection failed")]);
|
|
1257
1497
|
});
|
|
1258
|
-
const acquire = Effect.fn("BrowserRunInteractiveHost.acquire")(function* (policy) {
|
|
1498
|
+
const acquire = Effect.fn("BrowserRunInteractiveHost.acquire")(function* (policy, resume, pendingInput = false) {
|
|
1259
1499
|
const scope = yield* Scope.Scope;
|
|
1260
1500
|
const fixedPolicy = yield* snapshotPolicy(policy);
|
|
1261
|
-
const startedAt = yield*
|
|
1501
|
+
const startedAt = resume?.startedAt ?? (yield* Clock.currentTimeMillis);
|
|
1262
1502
|
const runCleanup = (effect) => Effect.runPromiseWith(cleanupContext)(effect.pipe(Effect.tapCause((cause) => reportBrowserCause("interactive.lateCleanup", cause))));
|
|
1263
1503
|
const cleanupContext = yield* Effect.context();
|
|
1264
1504
|
const observeLate = (signal, pending) => {
|
|
@@ -1270,29 +1510,56 @@ const makeHostService = (binding) => {
|
|
|
1270
1510
|
const state = {
|
|
1271
1511
|
closed: { value: false },
|
|
1272
1512
|
disconnected: { value: false },
|
|
1273
|
-
uncertain: { value:
|
|
1513
|
+
uncertain: { value: pendingInput || resume !== void 0 && resume.inputState !== "idle" },
|
|
1514
|
+
pendingInputs: /* @__PURE__ */ new Set(),
|
|
1515
|
+
actions: { value: resume?.consumedActions ?? 0 },
|
|
1274
1516
|
violation: { value: void 0 },
|
|
1275
1517
|
pendingRequests: /* @__PURE__ */ new Set()
|
|
1276
1518
|
};
|
|
1277
|
-
const lifecycle = {
|
|
1519
|
+
const lifecycle = {
|
|
1520
|
+
managedTeardownInstalled: false,
|
|
1521
|
+
retained: resume !== void 0
|
|
1522
|
+
};
|
|
1278
1523
|
const closers = [];
|
|
1279
|
-
const releaseBeforeManaged = (entry) => Effect.suspend(() => lifecycle.managedTeardownInstalled ? Effect.void : entry.close.pipe(Effect.catchCause((cause) => reportBrowserCause("interactive.cleanup", cause).pipe(Effect.andThen(Effect.logWarning(entry.warning))))));
|
|
1280
|
-
const sessionIdValue = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
|
|
1524
|
+
const releaseBeforeManaged = (entry) => Effect.suspend(() => lifecycle.managedTeardownInstalled || lifecycle.retained ? Effect.void : entry.close.pipe(Effect.catchCause((cause) => reportBrowserCause("interactive.cleanup", cause).pipe(Effect.andThen(Effect.logWarning(entry.warning))))));
|
|
1525
|
+
const sessionIdValue = yield* Effect.acquireRelease(withinDeadline(resume === void 0 ? Effect.tryPromise({
|
|
1281
1526
|
try: (signal) => observeLate(signal, closeLateAcquisition(signal, () => binding.acquire(keepAliveMillis(fixedPolicy)), (id) => runCleanup(Schema.decodeUnknownEffect(BrowserRunSessionId)(id).pipe(Effect.flatMap((value) => terminate(Redacted.make(value), [])))))),
|
|
1282
1527
|
catch: (cause) => isCapacityRefusal(cause) ? InteractiveBrowserCapacityError.make({
|
|
1283
1528
|
implementation: browserRunInteractiveImplementation,
|
|
1284
1529
|
message: "Browser Run has no capacity for a new browser session"
|
|
1285
1530
|
}) : protocolError("Acquiring the Browser Run session failed", cause)
|
|
1286
|
-
}).pipe(Effect.flatMap(Schema.decodeUnknownEffect(BrowserRunSessionId)), Effect.catchTag("SchemaError", () => protocolError("The Browser Run session identity was malformed"))), fixedPolicy, startedAt), (id) => Effect.suspend(() => {
|
|
1531
|
+
}).pipe(Effect.flatMap(Schema.decodeUnknownEffect(BrowserRunSessionId)), Effect.catchTag("SchemaError", () => protocolError("The Browser Run session identity was malformed"))) : Effect.succeed(Redacted.value(resume.sessionId)), fixedPolicy, startedAt), (id) => Effect.suspend(() => {
|
|
1287
1532
|
state.closed.value = true;
|
|
1288
1533
|
state.disconnected.value = true;
|
|
1289
|
-
return lifecycle.managedTeardownInstalled ? Effect.void : terminate(Redacted.make(id), []).pipe(Effect.catchCause((cause) => reportBrowserCause("interactive.close", cause).pipe(Effect.andThen(Effect.logWarning("Whole-browser cleanup remains unconfirmed")))));
|
|
1534
|
+
return lifecycle.managedTeardownInstalled || lifecycle.retained ? Effect.void : terminate(Redacted.make(id), []).pipe(Effect.catchCause((cause) => reportBrowserCause("interactive.close", cause).pipe(Effect.andThen(Effect.logWarning("Whole-browser cleanup remains unconfirmed")))));
|
|
1290
1535
|
}), { interruptible: true });
|
|
1536
|
+
const owner = Symbol();
|
|
1537
|
+
const releaseOwner = () => {
|
|
1538
|
+
if (owners.get(sessionIdValue) === owner) owners.delete(sessionIdValue);
|
|
1539
|
+
};
|
|
1291
1540
|
const connect = yield* Effect.cached(Effect.gen(function* () {
|
|
1541
|
+
if (owners.has(sessionIdValue)) return yield* InteractiveBrowserBusyError.make({
|
|
1542
|
+
implementation: browserRunInteractiveImplementation,
|
|
1543
|
+
message: "The previous local browser owner must detach before resuming"
|
|
1544
|
+
});
|
|
1545
|
+
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
1546
|
+
owners.set(sessionIdValue, owner);
|
|
1547
|
+
}), () => Effect.sync(() => {
|
|
1548
|
+
if (!lifecycle.managedTeardownInstalled) releaseOwner();
|
|
1549
|
+
}));
|
|
1292
1550
|
const browser = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
|
|
1293
|
-
try: (signal) => observeLate(signal, closeLateAcquisition(signal, () => binding.connect(sessionIdValue), (acquired) => runCleanup(closeAcquired(acquired)))),
|
|
1294
|
-
catch: (cause) => protocolError("Connecting to the Browser Run session failed", cause)
|
|
1295
|
-
})
|
|
1551
|
+
try: (signal) => observeLate(signal, closeLateAcquisition(signal, () => binding.connect(sessionIdValue), (acquired) => runCleanup(resume === void 0 ? closeAcquired(acquired) : closeEntry(acquired.detach ?? (async () => {}), "Detaching the browser failed").close))),
|
|
1552
|
+
catch: (cause) => resume !== void 0 && cause instanceof BrowserRunFailure && (cause.status === 404 || cause.status === 410) ? expiredError() : protocolError("Connecting to the Browser Run session failed", cause)
|
|
1553
|
+
}).pipe(Effect.timeoutOrElse({
|
|
1554
|
+
duration: "10 seconds",
|
|
1555
|
+
orElse: () => Effect.fail(protocolError("Connecting to the Browser Run session timed out"))
|
|
1556
|
+
})), fixedPolicy, startedAt), (acquired) => lifecycle.managedTeardownInstalled ? Effect.void : lifecycle.retained ? closeEntry(acquired.detach ?? (async () => {}), "Detaching the browser failed").close.pipe(Effect.tap(() => Effect.sync(releaseOwner)), Effect.catchCause((cause) => reportBrowserCause("interactive.detach", cause))) : releaseBeforeManaged(closeEntry(acquired.close, "Closing the local browser connection failed")), { interruptible: true });
|
|
1557
|
+
if (resume !== void 0) {
|
|
1558
|
+
if ((yield* Effect.try({
|
|
1559
|
+
try: browser.sessionId,
|
|
1560
|
+
catch: (cause) => protocolError("Reading the reattached browser identity failed", cause)
|
|
1561
|
+
})) !== sessionIdValue) return yield* protocolError("The reattached browser session identity did not match");
|
|
1562
|
+
}
|
|
1296
1563
|
closers.push(closeEntry(browser.close, "Closing the interactive browser failed"));
|
|
1297
1564
|
const disconnected = () => {
|
|
1298
1565
|
state.disconnected.value = true;
|
|
@@ -1309,21 +1576,32 @@ const makeHostService = (binding) => {
|
|
|
1309
1576
|
state.disconnected.value = true;
|
|
1310
1577
|
return yield* expiredError();
|
|
1311
1578
|
}
|
|
1312
|
-
const
|
|
1579
|
+
const reattached = resume === void 0 ? void 0 : yield* withinDeadline(Effect.tryPromise({
|
|
1580
|
+
try: async () => {
|
|
1581
|
+
if (browser.reattach === void 0 || browser.detach === void 0) throw new Error("The browser binding does not support exact reattachment");
|
|
1582
|
+
return await browser.reattach(resume.page);
|
|
1583
|
+
},
|
|
1584
|
+
catch: (cause) => protocolError("Reattaching the exact browser page failed", cause)
|
|
1585
|
+
}).pipe(Effect.timeoutOrElse({
|
|
1586
|
+
duration: "10 seconds",
|
|
1587
|
+
orElse: () => Effect.fail(protocolError("Reattaching the exact browser page timed out"))
|
|
1588
|
+
})), fixedPolicy, startedAt);
|
|
1589
|
+
if (resume !== void 0 && reattached === void 0) return yield* expiredError();
|
|
1590
|
+
const context = reattached?.context ?? (yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
|
|
1313
1591
|
try: (signal) => observeLate(signal, closeLateAcquisition(signal, browser.createContext, (acquired) => runCleanup(Effect.tryPromise({
|
|
1314
1592
|
try: () => acquired.close(),
|
|
1315
1593
|
catch: (cause) => actionError("close", cause)
|
|
1316
1594
|
})))),
|
|
1317
1595
|
catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the browser context failed", cause)
|
|
1318
|
-
}), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser context failed")), { interruptible: true });
|
|
1596
|
+
}), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser context failed")), { interruptible: true }));
|
|
1319
1597
|
closers.push(closeEntry(context.close, "Closing the interactive browser context failed"));
|
|
1320
|
-
const page = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
|
|
1598
|
+
const page = reattached?.page ?? (yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
|
|
1321
1599
|
try: (signal) => observeLate(signal, closeLateAcquisition(signal, context.newPage, (acquired) => runCleanup(Effect.tryPromise({
|
|
1322
1600
|
try: () => acquired.close(),
|
|
1323
1601
|
catch: (cause) => actionError("close", cause)
|
|
1324
1602
|
})))),
|
|
1325
1603
|
catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the browser page failed", cause)
|
|
1326
|
-
}), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser page failed")), { interruptible: true });
|
|
1604
|
+
}), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser page failed")), { interruptible: true }));
|
|
1327
1605
|
closers.push(closeEntry(page.close, "Closing the interactive browser page failed"));
|
|
1328
1606
|
yield* withinDeadline(Effect.tryPromise({
|
|
1329
1607
|
try: () => page.setBypassServiceWorker(true),
|
|
@@ -1342,27 +1620,66 @@ const makeHostService = (binding) => {
|
|
|
1342
1620
|
yield* withinDeadline(awaitPendingRequests(state), fixedPolicy, startedAt);
|
|
1343
1621
|
const setupFailure = stateFailure(state);
|
|
1344
1622
|
if (setupFailure !== void 0) return yield* setupFailure;
|
|
1623
|
+
const detach = yield* Effect.cached(Effect.uninterruptible(Effect.gen(function* () {
|
|
1624
|
+
if (browser.detach === void 0) return yield* protocolError("The browser binding does not support detachment");
|
|
1625
|
+
lifecycle.retained = true;
|
|
1626
|
+
state.closed.value = true;
|
|
1627
|
+
state.disconnected.value = true;
|
|
1628
|
+
if (state.pendingInputs.size > 0) state.uncertain.value = true;
|
|
1629
|
+
yield* closeEntry(async () => {
|
|
1630
|
+
page.offRequest(requestListener);
|
|
1631
|
+
browser.offDisconnected(disconnected);
|
|
1632
|
+
await browser.detach?.();
|
|
1633
|
+
}, "Detaching the browser transport failed").close;
|
|
1634
|
+
releaseOwner();
|
|
1635
|
+
}).pipe(Effect.catchTag("InteractiveBrowserActionError", (error) => reportBrowserCause("interactive.detach", Cause.fail(error)).pipe(Effect.andThen(Effect.fail(reportedBrowserError(actionError("close")))))), Effect.catchTag("InteractiveBrowserProtocolError", (error) => failReportedProtocol("interactive.detach", error)))));
|
|
1345
1636
|
const teardown = yield* Effect.uninterruptible(Effect.gen(function* () {
|
|
1346
1637
|
const cached = yield* Effect.cached(terminate(Redacted.make(sessionIdValue), closers).pipe(Effect.tapCause((cause) => reportBrowserCause("interactive.close", cause)), Effect.mapError(reportedBrowserError)));
|
|
1347
1638
|
lifecycle.managedTeardownInstalled = true;
|
|
1639
|
+
lifecycle.retained = false;
|
|
1348
1640
|
yield* Effect.addFinalizer(() => Effect.uninterruptible(Effect.sync(() => {
|
|
1349
1641
|
state.closed.value = true;
|
|
1350
1642
|
state.disconnected.value = true;
|
|
1351
|
-
}).pipe(Effect.andThen(cached), Effect.catchCause((cause) => reportBrowserCause("interactive.close", cause).pipe(Effect.andThen(Effect.logWarning("Whole-browser cleanup remains unconfirmed")))))));
|
|
1643
|
+
}).pipe(Effect.andThen(Effect.suspend(() => lifecycle.retained ? detach : cached)), Effect.catchCause((cause) => reportBrowserCause("interactive.close", cause).pipe(Effect.andThen(Effect.logWarning("Whole-browser cleanup remains unconfirmed")))))));
|
|
1352
1644
|
return cached;
|
|
1353
1645
|
}));
|
|
1354
|
-
const close = Effect.uninterruptible(Effect.
|
|
1646
|
+
const close = Effect.uninterruptible(Effect.suspend(() => {
|
|
1647
|
+
if (lifecycle.retained || owners.get(sessionIdValue) !== owner) return Effect.void;
|
|
1355
1648
|
state.closed.value = true;
|
|
1356
1649
|
state.disconnected.value = true;
|
|
1357
|
-
|
|
1650
|
+
return teardown;
|
|
1651
|
+
}));
|
|
1358
1652
|
const runtime = yield* makeHandle(page, fixedPolicy, startedAt, state, close);
|
|
1359
1653
|
const currentPagePreflight = decodeActionResult(page, fixedPolicy).pipe(Effect.asVoid);
|
|
1360
1654
|
const requestFitsSession = (requestedMillis) => remainingMillis(fixedPolicy, startedAt).pipe(Effect.flatMap((remaining) => remaining > 0 && requestedMillis <= remaining ? Effect.void : Effect.fail(policyError("The host browser request exceeds the remaining session time"))));
|
|
1655
|
+
const inputState = Effect.sync(() => state.uncertain.value ? "unknown" : state.pendingInputs.size > 0 ? "running" : "idle");
|
|
1656
|
+
const pageIdentity = yield* Effect.cached(Effect.tryPromise({
|
|
1657
|
+
try: async () => {
|
|
1658
|
+
if (page.identity === void 0) throw new Error("The browser binding does not expose page identity");
|
|
1659
|
+
return await page.identity();
|
|
1660
|
+
},
|
|
1661
|
+
catch: (cause) => protocolError("Reading the exact browser page identity failed", cause)
|
|
1662
|
+
}).pipe(Effect.catchTag("InteractiveBrowserProtocolError", (error) => failReportedProtocol("interactive.checkpoint", error))));
|
|
1663
|
+
const observeResize = yield* makeHostInputObserver(state, "interactive.resize.late");
|
|
1361
1664
|
return {
|
|
1362
1665
|
handle: runtime.handle,
|
|
1666
|
+
checkpoint: withinDeadline(Effect.gen(function* () {
|
|
1667
|
+
const identity = resume?.page ?? (yield* pageIdentity);
|
|
1668
|
+
return BrowserRunInteractiveCheckpoint.make({
|
|
1669
|
+
sessionId: Redacted.make(sessionIdValue),
|
|
1670
|
+
page: identity,
|
|
1671
|
+
policy: InteractiveBrowserPolicy.make(fixedPolicy),
|
|
1672
|
+
startedAt,
|
|
1673
|
+
consumedActions: state.actions.value,
|
|
1674
|
+
inputState: yield* inputState
|
|
1675
|
+
});
|
|
1676
|
+
}), fixedPolicy, startedAt),
|
|
1677
|
+
detach,
|
|
1678
|
+
inputState,
|
|
1679
|
+
drainInput: Effect.suspend(() => Effect.promise(() => boundedBestEffort(Promise.allSettled(state.pendingInputs), 500)).pipe(Effect.andThen(inputState))),
|
|
1363
1680
|
sessionId: Redacted.make(sessionIdValue),
|
|
1364
1681
|
resizeViewport: (viewport) => decodeViewport(viewport).pipe(Effect.flatMap((decoded) => runtime.run(Effect.tryPromise({
|
|
1365
|
-
try: () => page.setViewport(decoded),
|
|
1682
|
+
try: (signal) => observeResize(page.setViewport(decoded), signal),
|
|
1366
1683
|
catch: (cause) => {
|
|
1367
1684
|
if (state.disconnected.value || isRemoteClosure(cause)) {
|
|
1368
1685
|
state.disconnected.value = true;
|
|
@@ -1370,11 +1687,11 @@ const makeHostService = (binding) => {
|
|
|
1370
1687
|
}
|
|
1371
1688
|
return protocolError("Resizing the browser viewport failed", cause);
|
|
1372
1689
|
}
|
|
1373
|
-
}), currentPagePreflight, false))),
|
|
1690
|
+
}).pipe(Effect.catchTag("InteractiveBrowserProtocolError", (error) => failReportedProtocol("interactive.resize", error))), currentPagePreflight, false))),
|
|
1374
1691
|
getLiveView: (request) => Schema.decodeEffect(BrowserRunLiveViewRequest)(request).pipe(Effect.mapError(() => policyError("The Live View request is malformed")), Effect.flatMap((decoded) => runtime.run(cdpCommand(page, state, "Cloudflare.getLiveView", {
|
|
1375
1692
|
mode: decoded.mode,
|
|
1376
1693
|
expiresInMs: decoded.expiresInMs
|
|
1377
|
-
}, LiveViewObservation, "Cloudflare returned a malformed Live View response").pipe(Effect.flatMap((observation) => Schema.decodeEffect(BrowserRunLiveViewResult)({ devtoolsFrontendUrl: Redacted.make(observation.devtoolsFrontendUrl) }).pipe(Effect.mapError(() => protocolError("Cloudflare returned a malformed Live View response"))))), currentPagePreflight.pipe(Effect.andThen(requestFitsSession(decoded.expiresInMs)))))),
|
|
1694
|
+
}, LiveViewObservation, "Cloudflare returned a malformed Live View response").pipe(Effect.flatMap((observation) => Schema.decodeEffect(BrowserRunLiveViewResult)({ devtoolsFrontendUrl: Redacted.make(observation.devtoolsFrontendUrl) }).pipe(Effect.mapError(() => protocolError("Cloudflare returned a malformed Live View response"))))), currentPagePreflight.pipe(Effect.andThen(requestFitsSession(decoded.expiresInMs))), true, true))),
|
|
1378
1695
|
handoff: (request) => Schema.decodeEffect(BrowserRunHandoffRequest)(request).pipe(Effect.mapError(() => policyError("The browser handoff request is malformed")), Effect.flatMap((decoded) => runtime.run(cdpCommand(page, state, "Cloudflare.handoff", {
|
|
1379
1696
|
instructions: decoded.instructions,
|
|
1380
1697
|
timeout: decoded.timeout
|
|
@@ -1383,7 +1700,7 @@ const makeHostService = (binding) => {
|
|
|
1383
1700
|
active: observation.active,
|
|
1384
1701
|
...observation.handoffId === void 0 ? {} : { handoffId: Redacted.make(observation.handoffId) },
|
|
1385
1702
|
...observation.durationMs === void 0 ? {} : { durationMs: observation.durationMs }
|
|
1386
|
-
}).pipe(Effect.mapError(() => protocolError("Cloudflare returned a malformed browser handoff state"))))), currentPagePreflight),
|
|
1703
|
+
}).pipe(Effect.mapError(() => protocolError("Cloudflare returned a malformed browser handoff state"))))), currentPagePreflight, true, false),
|
|
1387
1704
|
close
|
|
1388
1705
|
};
|
|
1389
1706
|
}).pipe(Effect.provideService(Scope.Scope, scope)));
|
|
@@ -1395,6 +1712,11 @@ const makeHostService = (binding) => {
|
|
|
1395
1712
|
return BrowserRunInteractiveHost.of({
|
|
1396
1713
|
acquire,
|
|
1397
1714
|
open: (policy) => acquire(policy).pipe(Effect.flatMap((acquired) => acquired.connect)),
|
|
1715
|
+
resume: (checkpoint, options) => Schema.decodeEffect(Schema.toType(BrowserRunInteractiveCheckpoint))(checkpoint).pipe(Effect.mapError(() => policyError("The browser checkpoint is malformed")), Effect.flatMap((decoded) => acquire(decoded.policy, decoded, options.pendingInput)), Effect.flatMap((acquired) => acquired.connect), Effect.catchTag("InteractiveBrowserProtocolError", (error) => failReportedProtocol("interactive.resume", error, {
|
|
1716
|
+
stage: "preparation",
|
|
1717
|
+
dispatch: "not-dispatched",
|
|
1718
|
+
session: "disconnected"
|
|
1719
|
+
}))),
|
|
1398
1720
|
closeSession,
|
|
1399
1721
|
cleanupSemantics: "confirmed-terminal"
|
|
1400
1722
|
});
|
|
@@ -1422,6 +1744,6 @@ const CloudflareInteractiveBrowser = {
|
|
|
1422
1744
|
hostLayer: (options) => browserRunInteractiveHostLayer().pipe(Layer.provide(interactiveBindingLayer(options)))
|
|
1423
1745
|
};
|
|
1424
1746
|
//#endregion
|
|
1425
|
-
export {
|
|
1747
|
+
export { reportBrowserCause as C, inheritBrowserReport as S, BrowserRunCleanupError as _, BrowserRunInteractiveCheckpoint as a, BrowserRunFailure as b, BrowserRunLiveViewResult as c, BrowserRunViewport as d, CloudflareInteractiveBrowser as f, isBrowserRunUndispatchedActionError as g, browserRunInteractiveLayer as h, BrowserRunInteractiveBinding as i, BrowserRunPageIdentity as l, browserRunInteractiveImplementation as m, BrowserRunHandoffResult as n, BrowserRunInteractiveHost as o, browserRunInteractiveHostLayer as p, BrowserRunHandoffState as r, BrowserRunLiveViewRequest as s, BrowserRunHandoffRequest as t, BrowserRunPageObservation as u, BrowserRunSessionLifecycle as v, reportedBrowserError as w, browserFailure as x, BrowserRunBinding as y };
|
|
1426
1748
|
|
|
1427
|
-
//# sourceMappingURL=InteractiveBrowser-
|
|
1749
|
+
//# sourceMappingURL=InteractiveBrowser-CBUFAECo.mjs.map
|