@effect-agent/platform-cloudflare 0.1.0-beta.42 → 0.1.0-beta.44
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/browser-quick-action.mjs.map +1 -1
- package/dist/browser-rest-capture.mjs.map +1 -1
- package/dist/browser-rest-crawl.mjs.map +1 -1
- package/dist/browser-session-lifecycle-DqntvG-Y.d.mts +21 -0
- package/dist/browser-session-lifecycle-ZGgb3pnK.mjs +84 -0
- package/dist/browser-session-lifecycle-ZGgb3pnK.mjs.map +1 -0
- package/dist/index.d.mts +69 -56
- package/dist/index.mjs.map +1 -1
- package/dist/interactive-browser.d.mts +1 -18
- package/dist/interactive-browser.mjs +2 -81
- package/dist/interactive-browser.mjs.map +1 -1
- package/dist/prepared-admission-BKp_Upw2.mjs.map +1 -1
- package/dist/protected-browser.d.mts +62 -0
- package/dist/protected-browser.mjs +714 -0
- package/dist/protected-browser.mjs.map +1 -0
- package/dist/scheduling.mjs.map +1 -1
- package/dist/subscriptions.mjs.map +1 -1
- package/package.json +1 -81
- package/src/alarm.ts +41 -0
- package/src/bindings.ts +5 -0
- package/src/boundary.ts +4 -0
- package/src/browser-quick-action.ts +52 -0
- package/src/browser-rest-capture.ts +30 -0
- package/src/browser-rest-crawl.ts +43 -0
- package/src/browser-session-lifecycle.ts +18 -0
- package/src/client.ts +40 -0
- package/src/code-mode-executor.ts +50 -0
- package/src/interactive-browser.ts +176 -0
- package/src/layers.ts +10 -0
- package/src/memory.ts +42 -3
- package/src/prepared-admission.ts +1 -0
- package/src/progress-wait.ts +14 -0
- package/src/protected-browser/binding.ts +185 -0
- package/src/protected-browser/inspect-frame.ts +82 -0
- package/src/protected-browser/native.ts +384 -0
- package/src/protected-browser/policy.ts +594 -0
- package/src/protected-browser.ts +2 -0
- package/src/scheduling.ts +34 -0
- package/src/subscriptions.ts +50 -0
- package/src/thread-object.ts +55 -1
- package/src/transport.ts +1 -0
- package/src/wake-scheduler.ts +1 -0
|
@@ -69,16 +69,20 @@ const ACTION_NETWORK_SETTLE_MILLIS = 2_000;
|
|
|
69
69
|
const ACTION_POST_STATE_MILLIS = 250;
|
|
70
70
|
const MAX_OBSERVED_CONTROLS = 64;
|
|
71
71
|
const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0));
|
|
72
|
+
|
|
72
73
|
const BoundedHostText = Schema.String.check(
|
|
73
74
|
Schema.isMinLength(1),
|
|
74
75
|
Schema.isMaxLength(MAX_HOST_TEXT_LENGTH),
|
|
75
76
|
);
|
|
77
|
+
|
|
76
78
|
const BoundedRemoteText = Schema.String.check(Schema.isMaxLength(MAX_TEXT_LENGTH));
|
|
79
|
+
|
|
77
80
|
const TextObservation = Schema.Union([
|
|
78
81
|
Schema.Struct({ _tag: Schema.Literal("Text"), text: BoundedRemoteText }),
|
|
79
82
|
Schema.Struct({ _tag: Schema.Literal("MissingElement") }),
|
|
80
83
|
Schema.Struct({ _tag: Schema.Literal("OverLimit"), observed: Schema.Natural }),
|
|
81
84
|
]);
|
|
85
|
+
|
|
82
86
|
const ActionTargetState = Schema.Struct({
|
|
83
87
|
matchCount: Schema.Natural,
|
|
84
88
|
invalidSelector: Schema.optionalKey(Schema.Boolean),
|
|
@@ -92,6 +96,7 @@ const ActionTargetState = Schema.Struct({
|
|
|
92
96
|
valid: Schema.optionalKey(Schema.Boolean),
|
|
93
97
|
formValid: Schema.optionalKey(Schema.Boolean),
|
|
94
98
|
});
|
|
99
|
+
|
|
95
100
|
const ActionNetworkState = Schema.Struct({
|
|
96
101
|
total: Schema.Natural,
|
|
97
102
|
status2xx: Schema.Natural,
|
|
@@ -102,12 +107,14 @@ const ActionNetworkState = Schema.Struct({
|
|
|
102
107
|
pending: Schema.Natural,
|
|
103
108
|
settleTimedOut: Schema.Boolean,
|
|
104
109
|
});
|
|
110
|
+
|
|
105
111
|
const ActionObservation = Schema.Struct({
|
|
106
112
|
before: ActionTargetState,
|
|
107
113
|
after: Schema.optionalKey(ActionTargetState),
|
|
108
114
|
afterUnavailable: Schema.Boolean,
|
|
109
115
|
network: ActionNetworkState,
|
|
110
116
|
});
|
|
117
|
+
|
|
111
118
|
const PageObservation = Schema.fromJsonString(
|
|
112
119
|
Schema.Struct({
|
|
113
120
|
pageText: BoundedRemoteText,
|
|
@@ -128,6 +135,7 @@ const PageObservation = Schema.fromJsonString(
|
|
|
128
135
|
).check(Schema.isMaxLength(MAX_OBSERVED_CONTROLS)),
|
|
129
136
|
}),
|
|
130
137
|
);
|
|
138
|
+
|
|
131
139
|
const PngBytes = Schema.Uint8Array.check(
|
|
132
140
|
Schema.isMaxLength(MAX_SCREENSHOT_BYTES),
|
|
133
141
|
Schema.makeFilter(
|
|
@@ -144,6 +152,7 @@ const PngBytes = Schema.Uint8Array.check(
|
|
|
144
152
|
{ title: "PNG bytes" },
|
|
145
153
|
),
|
|
146
154
|
);
|
|
155
|
+
|
|
147
156
|
const BrowserRunSessionId = Schema.String.check(
|
|
148
157
|
Schema.isMinLength(1),
|
|
149
158
|
Schema.isMaxLength(256),
|
|
@@ -151,12 +160,14 @@ const BrowserRunSessionId = Schema.String.check(
|
|
|
151
160
|
title: "a Browser Run session identifier",
|
|
152
161
|
}),
|
|
153
162
|
);
|
|
163
|
+
|
|
154
164
|
const LiveViewUrl = Schema.String.check(
|
|
155
165
|
Schema.isMaxLength(MAX_HOST_TEXT_LENGTH),
|
|
156
166
|
Schema.makeFilter(
|
|
157
167
|
(value) => {
|
|
158
168
|
try {
|
|
159
169
|
const url = new URL(value);
|
|
170
|
+
|
|
160
171
|
return (
|
|
161
172
|
url.protocol === "https:" &&
|
|
162
173
|
url.host === "live.browser.run" &&
|
|
@@ -173,11 +184,14 @@ const LiveViewUrl = Schema.String.check(
|
|
|
173
184
|
{ title: "a Cloudflare Live View HTTPS URL" },
|
|
174
185
|
),
|
|
175
186
|
);
|
|
187
|
+
|
|
176
188
|
const LiveViewObservation = Schema.Struct({ devtoolsFrontendUrl: LiveViewUrl });
|
|
177
189
|
const HandoffObservation = Schema.Struct({ handoffId: BoundedHostText });
|
|
190
|
+
|
|
178
191
|
const HandoffDuration = Schema.Natural.check(
|
|
179
192
|
Schema.isLessThanOrEqualTo(MAX_HANDOFF_TIMEOUT_MILLIS),
|
|
180
193
|
);
|
|
194
|
+
|
|
181
195
|
const HandoffStateObservation = Schema.Union([
|
|
182
196
|
Schema.Struct({
|
|
183
197
|
active: Schema.Literal(true),
|
|
@@ -353,8 +367,10 @@ export class BrowserRunInteractiveBinding extends Context.Service<
|
|
|
353
367
|
return Layer.effect(BrowserRunInteractiveBinding)(
|
|
354
368
|
Effect.gen(function* () {
|
|
355
369
|
const lifecycle = yield* BrowserRunSessionLifecycle;
|
|
370
|
+
|
|
356
371
|
const viewport =
|
|
357
372
|
options.viewport === undefined ? undefined : yield* decodeViewport(options.viewport);
|
|
373
|
+
|
|
358
374
|
return {
|
|
359
375
|
launch: async (keepAliveMillis: number) =>
|
|
360
376
|
makeProductionBrowser(
|
|
@@ -418,6 +434,7 @@ const makeProductionRequest = (request: HTTPRequest): BrowserRunInteractiveReque
|
|
|
418
434
|
const makeProductionCdpSession = (session: CDPSession): BrowserRunInteractiveCdpSession => ({
|
|
419
435
|
send: async (command, parameters) => {
|
|
420
436
|
const send = Reflect.get(session, "send");
|
|
437
|
+
|
|
421
438
|
return await Reflect.apply(send, session, [command, parameters]);
|
|
422
439
|
},
|
|
423
440
|
detach: () => session.detach(),
|
|
@@ -436,6 +453,7 @@ const readActionTarget = (page: Page, selector: string): Promise<unknown> =>
|
|
|
436
453
|
page.evaluate((requestedSelector) => {
|
|
437
454
|
const pageDocument = Reflect.get(globalThis, "document");
|
|
438
455
|
let matches;
|
|
456
|
+
|
|
439
457
|
try {
|
|
440
458
|
matches = Reflect.apply(Reflect.get(pageDocument, "querySelectorAll"), pageDocument, [
|
|
441
459
|
requestedSelector,
|
|
@@ -449,14 +467,17 @@ const readActionTarget = (page: Page, selector: string): Promise<unknown> =>
|
|
|
449
467
|
if (typeof matches !== "object" || matches === null) throw new Error("Invalid query result");
|
|
450
468
|
const matchCount = Math.min(10_000, Reflect.get(matches, "length"));
|
|
451
469
|
const element = Reflect.get(matches, 0);
|
|
470
|
+
|
|
452
471
|
if (element === undefined) return { matchCount };
|
|
453
472
|
|
|
454
473
|
const associated = Reflect.get(element, "control") ?? element;
|
|
455
474
|
const tagName = String(Reflect.get(associated, "tagName") ?? "").toLowerCase();
|
|
456
475
|
const inputType = String(Reflect.get(associated, "type") ?? "").toLowerCase();
|
|
476
|
+
|
|
457
477
|
const role = String(
|
|
458
478
|
Reflect.apply(Reflect.get(element, "getAttribute"), element, ["role"]) ?? "",
|
|
459
479
|
).toLowerCase();
|
|
480
|
+
|
|
460
481
|
const kind =
|
|
461
482
|
tagName === "button" || role === "button"
|
|
462
483
|
? "button"
|
|
@@ -471,23 +492,30 @@ const readActionTarget = (page: Page, selector: string): Promise<unknown> =>
|
|
|
471
492
|
: tagName === "a"
|
|
472
493
|
? "link"
|
|
473
494
|
: "other";
|
|
495
|
+
|
|
474
496
|
const checked = Reflect.get(associated, "checked");
|
|
497
|
+
|
|
475
498
|
const selected =
|
|
476
499
|
tagName === "select"
|
|
477
500
|
? Reflect.get(associated, "selectedIndex") >= 0
|
|
478
501
|
: Reflect.get(associated, "selected");
|
|
502
|
+
|
|
479
503
|
const disabled = Reflect.get(associated, "disabled");
|
|
480
504
|
const required = Reflect.get(associated, "required");
|
|
481
505
|
const validity = Reflect.get(associated, "validity");
|
|
482
506
|
const form = Reflect.get(associated, "form");
|
|
507
|
+
|
|
483
508
|
const formMatches =
|
|
484
509
|
form === null || form === undefined ? undefined : Reflect.get(form, "matches");
|
|
510
|
+
|
|
485
511
|
const ariaChecked = Reflect.apply(Reflect.get(element, "getAttribute"), element, [
|
|
486
512
|
"aria-checked",
|
|
487
513
|
]);
|
|
514
|
+
|
|
488
515
|
const ariaDisabled = Reflect.apply(Reflect.get(element, "getAttribute"), element, [
|
|
489
516
|
"aria-disabled",
|
|
490
517
|
]);
|
|
518
|
+
|
|
491
519
|
const ariaSelected = Reflect.apply(Reflect.get(element, "getAttribute"), element, [
|
|
492
520
|
"aria-selected",
|
|
493
521
|
]);
|
|
@@ -523,6 +551,7 @@ const boundedBestEffort = async <A>(
|
|
|
523
551
|
millis: number,
|
|
524
552
|
): Promise<A | undefined> => {
|
|
525
553
|
let timer: ReturnType<typeof setTimeout> | number | undefined;
|
|
554
|
+
|
|
526
555
|
try {
|
|
527
556
|
return await Promise.race([
|
|
528
557
|
promise.catch(() => undefined),
|
|
@@ -556,17 +585,21 @@ const makeActionRequestTracker = (page: Page, signal: AbortSignal) => {
|
|
|
556
585
|
|
|
557
586
|
const relevant = (request: HTTPRequest) => {
|
|
558
587
|
const resourceType = request.resourceType();
|
|
588
|
+
|
|
559
589
|
return resourceType === "fetch" || resourceType === "xhr";
|
|
560
590
|
};
|
|
591
|
+
|
|
561
592
|
const onRequest = (request: HTTPRequest) => {
|
|
562
593
|
if (!relevant(request)) return;
|
|
563
594
|
pending.add(request);
|
|
564
595
|
total++;
|
|
565
596
|
lastChange = performance.now();
|
|
566
597
|
};
|
|
598
|
+
|
|
567
599
|
const onFinished = (request: HTTPRequest) => {
|
|
568
600
|
if (!pending.delete(request)) return;
|
|
569
601
|
let status: number | undefined;
|
|
602
|
+
|
|
570
603
|
try {
|
|
571
604
|
status = request.response()?.status();
|
|
572
605
|
} catch {
|
|
@@ -580,11 +613,13 @@ const makeActionRequestTracker = (page: Page, signal: AbortSignal) => {
|
|
|
580
613
|
}
|
|
581
614
|
lastChange = performance.now();
|
|
582
615
|
};
|
|
616
|
+
|
|
583
617
|
const onFailed = (request: HTTPRequest) => {
|
|
584
618
|
if (!pending.delete(request)) return;
|
|
585
619
|
failed++;
|
|
586
620
|
lastChange = performance.now();
|
|
587
621
|
};
|
|
622
|
+
|
|
588
623
|
const close = () => {
|
|
589
624
|
if (closed) return;
|
|
590
625
|
closed = true;
|
|
@@ -610,8 +645,10 @@ const makeActionRequestTracker = (page: Page, signal: AbortSignal) => {
|
|
|
610
645
|
const wait = async () => {
|
|
611
646
|
const startedAt = performance.now();
|
|
612
647
|
let settleTimedOut = false;
|
|
648
|
+
|
|
613
649
|
while (!closed) {
|
|
614
650
|
const now = performance.now();
|
|
651
|
+
|
|
615
652
|
if (pending.size === 0 && now - lastChange >= ACTION_NETWORK_QUIET_MILLIS) break;
|
|
616
653
|
if (now - startedAt >= ACTION_NETWORK_SETTLE_MILLIS) {
|
|
617
654
|
settleTimedOut = true;
|
|
@@ -623,6 +660,7 @@ const makeActionRequestTracker = (page: Page, signal: AbortSignal) => {
|
|
|
623
660
|
});
|
|
624
661
|
wake = undefined;
|
|
625
662
|
}
|
|
663
|
+
|
|
626
664
|
return {
|
|
627
665
|
total,
|
|
628
666
|
status2xx,
|
|
@@ -659,13 +697,16 @@ const runObservedPageAction = async (
|
|
|
659
697
|
action: (element: NonNullable<Awaited<ReturnType<Page["$"]>>>) => Promise<void>,
|
|
660
698
|
): Promise<unknown> => {
|
|
661
699
|
if (signal.aborted) throw new BrowserRunActionUndispatched(0);
|
|
700
|
+
|
|
662
701
|
const before = Schema.decodeUnknownSync(ActionTargetState)(
|
|
663
702
|
await readActionTarget(page, selector),
|
|
664
703
|
);
|
|
704
|
+
|
|
665
705
|
if (before.matchCount !== 1 || before.invalidSelector === true) {
|
|
666
706
|
throw new BrowserRunActionUndispatched(before.matchCount);
|
|
667
707
|
}
|
|
668
708
|
const matches = await page.$$(selector);
|
|
709
|
+
|
|
669
710
|
if (matches.length !== 1 || matches[0] === undefined) {
|
|
670
711
|
await disposeActionHandles(matches);
|
|
671
712
|
throw new BrowserRunActionUndispatched(Math.min(10_000, matches.length));
|
|
@@ -678,9 +719,11 @@ const runObservedPageAction = async (
|
|
|
678
719
|
let tracker: ReturnType<typeof makeActionRequestTracker> | undefined;
|
|
679
720
|
let disposing: Promise<void> | undefined;
|
|
680
721
|
const dispose = () => (disposing ??= disposeActionHandles(matches));
|
|
722
|
+
|
|
681
723
|
const onAbort = () => {
|
|
682
724
|
void dispose();
|
|
683
725
|
};
|
|
726
|
+
|
|
684
727
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
685
728
|
try {
|
|
686
729
|
tracker = makeActionRequestTracker(page, signal);
|
|
@@ -692,6 +735,7 @@ const runObservedPageAction = async (
|
|
|
692
735
|
tracker.markActionSettled();
|
|
693
736
|
const network = await tracker.wait();
|
|
694
737
|
const after = signal.aborted ? undefined : await readActionTargetAfter(page, selector);
|
|
738
|
+
|
|
695
739
|
return {
|
|
696
740
|
before,
|
|
697
741
|
...(after === undefined ? {} : { after }),
|
|
@@ -707,17 +751,20 @@ const runObservedPageAction = async (
|
|
|
707
751
|
|
|
708
752
|
const makeProductionPage = (page: Page): BrowserRunInteractivePage => {
|
|
709
753
|
const listeners = new Map<BrowserRunInteractiveRequestListener, (request: HTTPRequest) => void>();
|
|
754
|
+
|
|
710
755
|
return {
|
|
711
756
|
close: () => page.close(),
|
|
712
757
|
setBypassServiceWorker: (enabled) => page.setBypassServiceWorker(enabled),
|
|
713
758
|
setRequestInterception: (enabled) => page.setRequestInterception(enabled),
|
|
714
759
|
onRequest: (listener) => {
|
|
715
760
|
const sdkListener = (request: HTTPRequest) => listener(makeProductionRequest(request));
|
|
761
|
+
|
|
716
762
|
listeners.set(listener, sdkListener);
|
|
717
763
|
page.on("request", sdkListener);
|
|
718
764
|
},
|
|
719
765
|
offRequest: (listener) => {
|
|
720
766
|
const sdkListener = listeners.get(listener);
|
|
767
|
+
|
|
721
768
|
if (sdkListener !== undefined) {
|
|
722
769
|
page.off("request", sdkListener);
|
|
723
770
|
listeners.delete(listener);
|
|
@@ -732,48 +779,60 @@ const makeProductionPage = (page: Page): BrowserRunInteractivePage => {
|
|
|
732
779
|
await page.evaluate(
|
|
733
780
|
(requestedSelector, maximum, maximumControls) => {
|
|
734
781
|
const pageDocument = Reflect.get(globalThis, "document");
|
|
782
|
+
|
|
735
783
|
const matches =
|
|
736
784
|
requestedSelector === undefined
|
|
737
785
|
? undefined
|
|
738
786
|
: Reflect.apply(Reflect.get(pageDocument, "querySelectorAll"), pageDocument, [
|
|
739
787
|
requestedSelector,
|
|
740
788
|
]);
|
|
789
|
+
|
|
741
790
|
const selectorMatchCount =
|
|
742
791
|
matches === undefined || matches === null
|
|
743
792
|
? 1
|
|
744
793
|
: Math.min(10_000, Reflect.get(matches, "length"));
|
|
794
|
+
|
|
745
795
|
const element =
|
|
746
796
|
matches === undefined || matches === null
|
|
747
797
|
? Reflect.get(pageDocument, "body")
|
|
748
798
|
: Reflect.get(matches, 0);
|
|
799
|
+
|
|
749
800
|
if (element === null) return { _tag: "MissingElement" };
|
|
750
801
|
if (element === undefined) return { _tag: "MissingElement" };
|
|
751
802
|
const innerText = Reflect.get(element, "innerText");
|
|
752
803
|
const textContent = Reflect.get(element, "textContent");
|
|
804
|
+
|
|
753
805
|
const pageText =
|
|
754
806
|
typeof innerText === "string"
|
|
755
807
|
? innerText
|
|
756
808
|
: typeof textContent === "string"
|
|
757
809
|
? textContent
|
|
758
810
|
: "";
|
|
811
|
+
|
|
759
812
|
const primaryControlSelector =
|
|
760
813
|
'input,select,textarea,label,button,[role="checkbox"],[role="radio"],[role="option"],[role="switch"],[role="tab"],[role="button"]';
|
|
814
|
+
|
|
761
815
|
const optionSelector = "select option";
|
|
762
816
|
const secondaryControlSelector = "a[href]";
|
|
763
817
|
const controlSelector = `${primaryControlSelector},${optionSelector},${secondaryControlSelector}`;
|
|
818
|
+
|
|
764
819
|
const selectorFor = (candidate: object) => {
|
|
765
820
|
const parts: Array<string> = [];
|
|
766
821
|
let current: object | null = candidate;
|
|
822
|
+
|
|
767
823
|
while (current !== null && current !== undefined) {
|
|
768
824
|
const tagName = String(Reflect.get(current, "tagName") ?? "").toLowerCase();
|
|
825
|
+
|
|
769
826
|
if (tagName === "") break;
|
|
770
827
|
const parent: object | null = Reflect.get(current, "parentElement");
|
|
828
|
+
|
|
771
829
|
if (parent === null) {
|
|
772
830
|
parts.push(tagName);
|
|
773
831
|
break;
|
|
774
832
|
}
|
|
775
833
|
let sibling = Reflect.get(current, "previousElementSibling");
|
|
776
834
|
let index = 1;
|
|
835
|
+
|
|
777
836
|
while (sibling !== null && sibling !== undefined) {
|
|
778
837
|
if (String(Reflect.get(sibling, "tagName") ?? "").toLowerCase() === tagName)
|
|
779
838
|
index++;
|
|
@@ -782,14 +841,19 @@ const makeProductionPage = (page: Page): BrowserRunInteractivePage => {
|
|
|
782
841
|
parts.push(`${tagName}:nth-of-type(${index})`);
|
|
783
842
|
current = parent;
|
|
784
843
|
}
|
|
844
|
+
|
|
785
845
|
return parts.reverse().join(" > ");
|
|
786
846
|
};
|
|
847
|
+
|
|
787
848
|
const visible = (candidate: object) => {
|
|
788
849
|
const hidden = Reflect.get(candidate, "hidden");
|
|
850
|
+
|
|
789
851
|
const ariaHidden = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, [
|
|
790
852
|
"aria-hidden",
|
|
791
853
|
]);
|
|
854
|
+
|
|
792
855
|
const rects = Reflect.apply(Reflect.get(candidate, "getClientRects"), candidate, []);
|
|
856
|
+
|
|
793
857
|
return (
|
|
794
858
|
hidden !== true &&
|
|
795
859
|
ariaHidden !== "true" &&
|
|
@@ -798,17 +862,22 @@ const makeProductionPage = (page: Page): BrowserRunInteractivePage => {
|
|
|
798
862
|
Reflect.get(rects, "length") > 0
|
|
799
863
|
);
|
|
800
864
|
};
|
|
865
|
+
|
|
801
866
|
const candidates: Array<object> = [];
|
|
802
867
|
let controlsTruncated = false;
|
|
868
|
+
|
|
803
869
|
const consider = (candidate: object) => {
|
|
804
870
|
const tagName = String(Reflect.get(candidate, "tagName") ?? "").toLowerCase();
|
|
871
|
+
|
|
805
872
|
// Collapsed native options have no client rects. Their owning
|
|
806
873
|
// select determines visibility; observe text/selection, never value.
|
|
807
874
|
const visibilityTarget =
|
|
808
875
|
tagName === "option"
|
|
809
876
|
? Reflect.apply(Reflect.get(candidate, "closest"), candidate, ["select"])
|
|
810
877
|
: candidate;
|
|
878
|
+
|
|
811
879
|
const actionable = tagName !== "label" || Reflect.get(candidate, "control") !== null;
|
|
880
|
+
|
|
812
881
|
if (
|
|
813
882
|
!actionable ||
|
|
814
883
|
typeof visibilityTarget !== "object" ||
|
|
@@ -819,47 +888,60 @@ const makeProductionPage = (page: Page): BrowserRunInteractivePage => {
|
|
|
819
888
|
candidates.push(candidate);
|
|
820
889
|
if (candidates.length > maximumControls) controlsTruncated = true;
|
|
821
890
|
};
|
|
891
|
+
|
|
822
892
|
const elementMatches = Reflect.get(element, "matches");
|
|
893
|
+
|
|
823
894
|
if (
|
|
824
895
|
typeof elementMatches === "function" &&
|
|
825
896
|
Reflect.apply(elementMatches, element, [controlSelector])
|
|
826
897
|
) {
|
|
827
898
|
consider(element);
|
|
828
899
|
}
|
|
900
|
+
|
|
829
901
|
const considerSelector = (candidateSelector: string) => {
|
|
830
902
|
const descendants = Reflect.apply(Reflect.get(element, "querySelectorAll"), element, [
|
|
831
903
|
candidateSelector,
|
|
832
904
|
]);
|
|
905
|
+
|
|
833
906
|
if (typeof descendants !== "object" || descendants === null) return;
|
|
834
907
|
const descendantCount = Reflect.get(descendants, "length");
|
|
908
|
+
|
|
835
909
|
for (let index = 0; index < descendantCount && !controlsTruncated; index++) {
|
|
836
910
|
consider(Reflect.get(descendants, index));
|
|
837
911
|
}
|
|
838
912
|
};
|
|
913
|
+
|
|
839
914
|
considerSelector(primaryControlSelector);
|
|
840
915
|
if (!controlsTruncated) considerSelector(optionSelector);
|
|
841
916
|
if (!controlsTruncated) considerSelector(secondaryControlSelector);
|
|
917
|
+
|
|
842
918
|
const controls = candidates.slice(0, maximumControls).map((candidate) => {
|
|
843
919
|
const associated = Reflect.get(candidate, "control") ?? candidate;
|
|
844
920
|
const tagName = String(Reflect.get(candidate, "tagName") ?? "").toLowerCase();
|
|
845
921
|
const inputType = String(Reflect.get(associated, "type") ?? "").toLowerCase();
|
|
922
|
+
|
|
846
923
|
const role = String(
|
|
847
924
|
Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["role"]) ?? "",
|
|
848
925
|
).toLowerCase();
|
|
926
|
+
|
|
849
927
|
const ariaLabel = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, [
|
|
850
928
|
"aria-label",
|
|
851
929
|
]);
|
|
930
|
+
|
|
852
931
|
const candidateText =
|
|
853
932
|
tagName === "textarea" || tagName === "input" || tagName === "select"
|
|
854
933
|
? undefined
|
|
855
934
|
: Reflect.get(candidate, tagName === "option" ? "label" : "innerText");
|
|
935
|
+
|
|
856
936
|
const associatedLabels = Reflect.get(associated, "labels");
|
|
937
|
+
|
|
857
938
|
const associatedLabel =
|
|
858
939
|
associatedLabels !== undefined &&
|
|
859
940
|
associatedLabels !== null &&
|
|
860
941
|
Reflect.get(associatedLabels, "length") > 0
|
|
861
942
|
? Reflect.get(Reflect.get(associatedLabels, 0), "innerText")
|
|
862
943
|
: undefined;
|
|
944
|
+
|
|
863
945
|
const label = String(
|
|
864
946
|
typeof ariaLabel === "string" && ariaLabel !== ""
|
|
865
947
|
? ariaLabel
|
|
@@ -870,25 +952,32 @@ const makeProductionPage = (page: Page): BrowserRunInteractivePage => {
|
|
|
870
952
|
.replace(/\s+/g, " ")
|
|
871
953
|
.trim()
|
|
872
954
|
.slice(0, 200);
|
|
955
|
+
|
|
873
956
|
const checked = Reflect.get(associated, "checked");
|
|
957
|
+
|
|
874
958
|
const selected =
|
|
875
959
|
tagName === "select"
|
|
876
960
|
? Reflect.get(associated, "selectedIndex") >= 0
|
|
877
961
|
: Reflect.get(associated, "selected");
|
|
962
|
+
|
|
878
963
|
const disabled = Reflect.get(associated, "disabled");
|
|
879
964
|
const required = Reflect.get(associated, "required");
|
|
880
965
|
const validity = Reflect.get(associated, "validity");
|
|
881
966
|
const form = Reflect.get(associated, "form");
|
|
967
|
+
|
|
882
968
|
const formMatches =
|
|
883
969
|
form === null || form === undefined ? undefined : Reflect.get(form, "matches");
|
|
970
|
+
|
|
884
971
|
const ariaChecked = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, [
|
|
885
972
|
"aria-checked",
|
|
886
973
|
]);
|
|
974
|
+
|
|
887
975
|
const ariaSelected = Reflect.apply(
|
|
888
976
|
Reflect.get(candidate, "getAttribute"),
|
|
889
977
|
candidate,
|
|
890
978
|
["aria-selected"],
|
|
891
979
|
);
|
|
980
|
+
|
|
892
981
|
const ariaDisabled = Reflect.apply(
|
|
893
982
|
Reflect.get(candidate, "getAttribute"),
|
|
894
983
|
candidate,
|
|
@@ -930,6 +1019,7 @@ const makeProductionPage = (page: Page): BrowserRunInteractivePage => {
|
|
|
930
1019
|
: {}),
|
|
931
1020
|
};
|
|
932
1021
|
});
|
|
1022
|
+
|
|
933
1023
|
// JSON is the bounded wire representation of this existing text result.
|
|
934
1024
|
// eslint-disable-next-line no-restricted-properties
|
|
935
1025
|
const text = JSON.stringify({
|
|
@@ -938,7 +1028,9 @@ const makeProductionPage = (page: Page): BrowserRunInteractivePage => {
|
|
|
938
1028
|
controls,
|
|
939
1029
|
controlsTruncated,
|
|
940
1030
|
});
|
|
1031
|
+
|
|
941
1032
|
const observed = new TextEncoder().encode(text).byteLength;
|
|
1033
|
+
|
|
942
1034
|
return observed > maximum ? { _tag: "OverLimit", observed } : { _tag: "Text", text };
|
|
943
1035
|
},
|
|
944
1036
|
selector,
|
|
@@ -946,7 +1038,9 @@ const makeProductionPage = (page: Page): BrowserRunInteractivePage => {
|
|
|
946
1038
|
MAX_OBSERVED_CONTROLS,
|
|
947
1039
|
),
|
|
948
1040
|
);
|
|
1041
|
+
|
|
949
1042
|
if (observation._tag === "Text") Schema.decodeUnknownSync(PageObservation)(observation.text);
|
|
1043
|
+
|
|
950
1044
|
return observation;
|
|
951
1045
|
},
|
|
952
1046
|
fill: (selector, value, signal, onDispatch) =>
|
|
@@ -955,8 +1049,10 @@ const makeProductionPage = (page: Page): BrowserRunInteractivePage => {
|
|
|
955
1049
|
// Bypass instance setters so React can detect the change when events fire.
|
|
956
1050
|
let prototype = Reflect.getPrototypeOf(element);
|
|
957
1051
|
let setValue: ((value: string) => void) | undefined;
|
|
1052
|
+
|
|
958
1053
|
while (prototype !== null) {
|
|
959
1054
|
const setter = Reflect.getOwnPropertyDescriptor(prototype, "value")?.set;
|
|
1055
|
+
|
|
960
1056
|
if (typeof setter === "function") {
|
|
961
1057
|
setValue = setter;
|
|
962
1058
|
break;
|
|
@@ -967,9 +1063,11 @@ const makeProductionPage = (page: Page): BrowserRunInteractivePage => {
|
|
|
967
1063
|
throw new Error("The selector did not resolve to a fillable field");
|
|
968
1064
|
}
|
|
969
1065
|
const focus = Reflect.get(element, "focus");
|
|
1066
|
+
|
|
970
1067
|
if (typeof focus === "function") Reflect.apply(focus, element, []);
|
|
971
1068
|
Reflect.apply(setValue, element, [nextValue]);
|
|
972
1069
|
const dispatchEvent = Reflect.get(element, "dispatchEvent");
|
|
1070
|
+
|
|
973
1071
|
if (typeof dispatchEvent === "function") {
|
|
974
1072
|
Reflect.apply(dispatchEvent, element, [new Event("input", { bubbles: true })]);
|
|
975
1073
|
Reflect.apply(dispatchEvent, element, [new Event("change", { bubbles: true })]);
|
|
@@ -985,6 +1083,7 @@ const makeProductionPage = (page: Page): BrowserRunInteractivePage => {
|
|
|
985
1083
|
page.evaluate(
|
|
986
1084
|
(x, y) => {
|
|
987
1085
|
const scrollBy = Reflect.get(globalThis, "scrollBy");
|
|
1086
|
+
|
|
988
1087
|
Reflect.apply(scrollBy, globalThis, [{ left: x, top: y, behavior: "instant" }]);
|
|
989
1088
|
},
|
|
990
1089
|
deltaX,
|
|
@@ -1056,6 +1155,7 @@ const expiredError = (): InteractiveBrowserExpiredError =>
|
|
|
1056
1155
|
|
|
1057
1156
|
const causeText = (cause: unknown): string => {
|
|
1058
1157
|
if (cause instanceof Error) return cause.message.slice(0, 8_000);
|
|
1158
|
+
|
|
1059
1159
|
return String(cause).slice(0, 8_000);
|
|
1060
1160
|
};
|
|
1061
1161
|
|
|
@@ -1076,6 +1176,7 @@ const snapshotPolicy = Effect.fn("BrowserRunInteractive.snapshotPolicy")(functio
|
|
|
1076
1176
|
const decoded = yield* Schema.decodeUnknownEffect(InteractiveBrowserPolicy)(input).pipe(
|
|
1077
1177
|
Effect.mapError(() => policyError("The interactive browser policy is malformed")),
|
|
1078
1178
|
);
|
|
1179
|
+
|
|
1079
1180
|
if (decoded.network._tag === "PublicWeb") {
|
|
1080
1181
|
return yield* InteractiveBrowserUnsupportedError.make({
|
|
1081
1182
|
implementation: browserRunInteractiveImplementation,
|
|
@@ -1084,6 +1185,7 @@ const snapshotPolicy = Effect.fn("BrowserRunInteractive.snapshotPolicy")(functio
|
|
|
1084
1185
|
"Cloudflare Browser Run cannot enforce the PublicWeb network policy for all session traffic",
|
|
1085
1186
|
});
|
|
1086
1187
|
}
|
|
1188
|
+
|
|
1087
1189
|
return Object.freeze({
|
|
1088
1190
|
network:
|
|
1089
1191
|
decoded.network._tag === "ExactHosts"
|
|
@@ -1102,6 +1204,7 @@ const hostAllowed = (policy: InteractiveBrowserPolicySnapshot, value: string): b
|
|
|
1102
1204
|
if (policy.network._tag === "Unrestricted") return true;
|
|
1103
1205
|
try {
|
|
1104
1206
|
const url = new URL(value);
|
|
1207
|
+
|
|
1105
1208
|
return (
|
|
1106
1209
|
url.protocol === "https:" &&
|
|
1107
1210
|
url.username === "" &&
|
|
@@ -1122,6 +1225,7 @@ const closeLateAcquisition = async <A>(
|
|
|
1122
1225
|
close: (acquired: A) => Promise<void>,
|
|
1123
1226
|
): Promise<A> => {
|
|
1124
1227
|
const acquired = await acquire();
|
|
1228
|
+
|
|
1125
1229
|
if (!signal.aborted) return acquired;
|
|
1126
1230
|
|
|
1127
1231
|
// The SDK does not accept AbortSignal. If an acquisition settles after Effect
|
|
@@ -1152,6 +1256,7 @@ const deadlineError = Effect.fn("BrowserRunInteractive.deadlineError")(function*
|
|
|
1152
1256
|
startedAt: number,
|
|
1153
1257
|
) {
|
|
1154
1258
|
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
1259
|
+
|
|
1155
1260
|
return yield* InteractiveBrowserLimitError.make({
|
|
1156
1261
|
implementation: browserRunInteractiveImplementation,
|
|
1157
1262
|
limit: "elapsed",
|
|
@@ -1170,7 +1275,9 @@ const withinDeadline = Effect.fn("BrowserRunInteractive.withinDeadline")(functio
|
|
|
1170
1275
|
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
1171
1276
|
const elapsed = Math.max(0, now - startedAt);
|
|
1172
1277
|
const remaining = policy.maxElapsedMillis - elapsed;
|
|
1278
|
+
|
|
1173
1279
|
if (remaining <= 0) return yield* deadlineError(policy, startedAt);
|
|
1280
|
+
|
|
1174
1281
|
return yield* effect.pipe(
|
|
1175
1282
|
Effect.timeoutOrElse({
|
|
1176
1283
|
duration: Duration.millis(remaining),
|
|
@@ -1193,6 +1300,7 @@ const stateFailure = (state: HandleState): BrowserFailure | undefined => {
|
|
|
1193
1300
|
if (state.closed.value || state.disconnected.value || state.uncertain.value) {
|
|
1194
1301
|
return expiredError();
|
|
1195
1302
|
}
|
|
1303
|
+
|
|
1196
1304
|
return undefined;
|
|
1197
1305
|
};
|
|
1198
1306
|
|
|
@@ -1226,6 +1334,7 @@ class BrowserRunRemoteFailure {
|
|
|
1226
1334
|
const awaitPendingRequests = (state: HandleState): Effect.Effect<void> =>
|
|
1227
1335
|
Effect.suspend(() => {
|
|
1228
1336
|
const pending = [...state.pendingRequests];
|
|
1337
|
+
|
|
1229
1338
|
return pending.length === 0
|
|
1230
1339
|
? Effect.void
|
|
1231
1340
|
: Effect.promise(() => Promise.all(pending)).pipe(
|
|
@@ -1242,6 +1351,7 @@ const decodeNavigationResult = Effect.fn("BrowserRunInteractive.decodeNavigation
|
|
|
1242
1351
|
try: page.url,
|
|
1243
1352
|
catch: (cause) => protocolError("Reading the browser navigation URL failed", cause),
|
|
1244
1353
|
});
|
|
1354
|
+
|
|
1245
1355
|
const result = yield* Schema.decodeUnknownEffect(BrowserNavigationResult)({
|
|
1246
1356
|
url,
|
|
1247
1357
|
}).pipe(
|
|
@@ -1249,9 +1359,11 @@ const decodeNavigationResult = Effect.fn("BrowserRunInteractive.decodeNavigation
|
|
|
1249
1359
|
protocolError("The browser returned a malformed navigation URL", cause),
|
|
1250
1360
|
),
|
|
1251
1361
|
);
|
|
1362
|
+
|
|
1252
1363
|
if (!hostAllowed(policy, result.url)) {
|
|
1253
1364
|
return yield* policyError("The browser returned an off-policy page URL");
|
|
1254
1365
|
}
|
|
1366
|
+
|
|
1255
1367
|
return result;
|
|
1256
1368
|
});
|
|
1257
1369
|
|
|
@@ -1263,12 +1375,15 @@ const decodeActionResult = Effect.fn("BrowserRunInteractive.decodeActionResult")
|
|
|
1263
1375
|
try: page.url,
|
|
1264
1376
|
catch: (cause) => protocolError("Reading the browser page URL failed", cause),
|
|
1265
1377
|
});
|
|
1378
|
+
|
|
1266
1379
|
const result = yield* Schema.decodeUnknownEffect(BrowserActionResult)({ url }).pipe(
|
|
1267
1380
|
Effect.mapError((cause) => protocolError("The browser returned a malformed page URL", cause)),
|
|
1268
1381
|
);
|
|
1382
|
+
|
|
1269
1383
|
if (!hostAllowed(policy, result.url)) {
|
|
1270
1384
|
return yield* policyError("The browser returned an off-policy page URL");
|
|
1271
1385
|
}
|
|
1386
|
+
|
|
1272
1387
|
return result;
|
|
1273
1388
|
});
|
|
1274
1389
|
|
|
@@ -1279,6 +1394,7 @@ const makeRequestListener =
|
|
|
1279
1394
|
): BrowserRunInteractiveRequestListener =>
|
|
1280
1395
|
(request) => {
|
|
1281
1396
|
let allowed = false;
|
|
1397
|
+
|
|
1282
1398
|
try {
|
|
1283
1399
|
allowed = hostAllowed(policy, request.url());
|
|
1284
1400
|
} catch {
|
|
@@ -1289,12 +1405,15 @@ const makeRequestListener =
|
|
|
1289
1405
|
}
|
|
1290
1406
|
|
|
1291
1407
|
let settlement: Promise<void>;
|
|
1408
|
+
|
|
1292
1409
|
try {
|
|
1293
1410
|
settlement = allowed ? request.continue() : request.abort();
|
|
1294
1411
|
} catch {
|
|
1295
1412
|
state.violation.value = protocolError("Resolving an intercepted browser request failed");
|
|
1413
|
+
|
|
1296
1414
|
return;
|
|
1297
1415
|
}
|
|
1416
|
+
|
|
1298
1417
|
const observed = settlement
|
|
1299
1418
|
.catch(() => {
|
|
1300
1419
|
state.violation.value = protocolError("Resolving an intercepted browser request failed");
|
|
@@ -1302,6 +1421,7 @@ const makeRequestListener =
|
|
|
1302
1421
|
.finally(() => {
|
|
1303
1422
|
state.pendingRequests.delete(observed);
|
|
1304
1423
|
});
|
|
1424
|
+
|
|
1305
1425
|
state.pendingRequests.add(observed);
|
|
1306
1426
|
};
|
|
1307
1427
|
|
|
@@ -1325,6 +1445,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1325
1445
|
failure,
|
|
1326
1446
|
): Effect.Effect<never, InteractiveBrowserActionError | InteractiveBrowserExpiredError> => {
|
|
1327
1447
|
const cause = failure.cause;
|
|
1448
|
+
|
|
1328
1449
|
if (cause instanceof BrowserRunActionUndispatched) {
|
|
1329
1450
|
return Effect.logInfo("Browser interactive action was not dispatched").pipe(
|
|
1330
1451
|
Effect.annotateLogs({
|
|
@@ -1336,8 +1457,10 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1336
1457
|
}
|
|
1337
1458
|
if (state.disconnected.value || isRemoteClosure(cause)) {
|
|
1338
1459
|
state.disconnected.value = true;
|
|
1460
|
+
|
|
1339
1461
|
return Effect.fail(expiredError());
|
|
1340
1462
|
}
|
|
1463
|
+
|
|
1341
1464
|
return Effect.fail(actionError(operation, cause));
|
|
1342
1465
|
},
|
|
1343
1466
|
),
|
|
@@ -1350,10 +1473,12 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1350
1473
|
Effect.suspend(() => {
|
|
1351
1474
|
let dispatched = false;
|
|
1352
1475
|
let pending: Promise<unknown> | undefined;
|
|
1476
|
+
|
|
1353
1477
|
return remote(operation, (signal) => {
|
|
1354
1478
|
pending = evaluate(signal, () => {
|
|
1355
1479
|
dispatched = true;
|
|
1356
1480
|
});
|
|
1481
|
+
|
|
1357
1482
|
return pending;
|
|
1358
1483
|
}).pipe(
|
|
1359
1484
|
Effect.onInterrupt(() =>
|
|
@@ -1369,6 +1494,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1369
1494
|
}),
|
|
1370
1495
|
);
|
|
1371
1496
|
const completion = pending;
|
|
1497
|
+
|
|
1372
1498
|
if (completion !== undefined) {
|
|
1373
1499
|
yield* Effect.promise(() => boundedBestEffort(completion, 500));
|
|
1374
1500
|
}
|
|
@@ -1455,6 +1581,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1455
1581
|
.withPermitsIfAvailable(1)(
|
|
1456
1582
|
Effect.gen(function* () {
|
|
1457
1583
|
const unavailable = stateFailure(state);
|
|
1584
|
+
|
|
1458
1585
|
if (unavailable !== undefined) return yield* unavailable;
|
|
1459
1586
|
yield* preflight;
|
|
1460
1587
|
|
|
@@ -1464,6 +1591,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1464
1591
|
? [{ allowed: false, observed: count + 1 }, count]
|
|
1465
1592
|
: [{ allowed: true, observed: count + 1 }, count + 1],
|
|
1466
1593
|
);
|
|
1594
|
+
|
|
1467
1595
|
if (!admitted.allowed) {
|
|
1468
1596
|
return yield* InteractiveBrowserLimitError.make({
|
|
1469
1597
|
implementation: browserRunInteractiveImplementation,
|
|
@@ -1478,17 +1606,20 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1478
1606
|
const completed = effect.pipe(
|
|
1479
1607
|
Effect.catch((error) => {
|
|
1480
1608
|
const failure = stateFailure(state);
|
|
1609
|
+
|
|
1481
1610
|
return Effect.fail(failure ?? error);
|
|
1482
1611
|
}),
|
|
1483
1612
|
Effect.flatMap((result) =>
|
|
1484
1613
|
awaitPendingRequests(state).pipe(
|
|
1485
1614
|
Effect.flatMap(() => {
|
|
1486
1615
|
const failure = stateFailure(state);
|
|
1616
|
+
|
|
1487
1617
|
return failure === undefined ? Effect.succeed(result) : Effect.fail(failure);
|
|
1488
1618
|
}),
|
|
1489
1619
|
),
|
|
1490
1620
|
),
|
|
1491
1621
|
);
|
|
1622
|
+
|
|
1492
1623
|
return yield* withinDeadline(completed, policy, startedAt, () => {
|
|
1493
1624
|
state.uncertain.value = true;
|
|
1494
1625
|
}).pipe(
|
|
@@ -1502,12 +1633,14 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1502
1633
|
return Effect.fail(error);
|
|
1503
1634
|
}
|
|
1504
1635
|
const failure = stateFailure(state);
|
|
1636
|
+
|
|
1505
1637
|
if (
|
|
1506
1638
|
Schema.is(InteractiveBrowserActionError)(error) &&
|
|
1507
1639
|
!isBrowserRunUndispatchedActionError(error)
|
|
1508
1640
|
) {
|
|
1509
1641
|
state.uncertain.value = true;
|
|
1510
1642
|
}
|
|
1643
|
+
|
|
1511
1644
|
return Effect.fail(failure ?? error);
|
|
1512
1645
|
}),
|
|
1513
1646
|
);
|
|
@@ -1531,6 +1664,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1531
1664
|
run(
|
|
1532
1665
|
Effect.gen(function* () {
|
|
1533
1666
|
yield* remote("navigate", () => page.goto(request.url));
|
|
1667
|
+
|
|
1534
1668
|
return yield* decodeNavigationResult(page, policy);
|
|
1535
1669
|
}),
|
|
1536
1670
|
Effect.suspend(() =>
|
|
@@ -1545,11 +1679,13 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1545
1679
|
const raw = yield* remote("read-text", () =>
|
|
1546
1680
|
page.readText(request.selector, policy.maxReturnedBytes),
|
|
1547
1681
|
);
|
|
1682
|
+
|
|
1548
1683
|
const observation = yield* Schema.decodeUnknownEffect(TextObservation)(raw).pipe(
|
|
1549
1684
|
Effect.mapError((cause) =>
|
|
1550
1685
|
protocolError("The browser returned a malformed text observation", cause),
|
|
1551
1686
|
),
|
|
1552
1687
|
);
|
|
1688
|
+
|
|
1553
1689
|
if (observation._tag === "MissingElement") {
|
|
1554
1690
|
return yield* actionError("read-text");
|
|
1555
1691
|
}
|
|
@@ -1563,6 +1699,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1563
1699
|
});
|
|
1564
1700
|
}
|
|
1565
1701
|
const observed = new TextEncoder().encode(observation.text).byteLength;
|
|
1702
|
+
|
|
1566
1703
|
if (observed > policy.maxReturnedBytes) {
|
|
1567
1704
|
return yield* InteractiveBrowserLimitError.make({
|
|
1568
1705
|
implementation: browserRunInteractiveImplementation,
|
|
@@ -1572,6 +1709,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1572
1709
|
message: "The browser returned-text limit was reached",
|
|
1573
1710
|
});
|
|
1574
1711
|
}
|
|
1712
|
+
|
|
1575
1713
|
return yield* Schema.decodeUnknownEffect(BrowserTextResult)({
|
|
1576
1714
|
text: observation.text,
|
|
1577
1715
|
}).pipe(
|
|
@@ -1587,7 +1725,9 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1587
1725
|
const observation = yield* observedAction("fill", (signal, onDispatch) =>
|
|
1588
1726
|
page.fill(request.selector, request.value, signal, onDispatch),
|
|
1589
1727
|
).pipe(Effect.flatMap(decodeActionObservation));
|
|
1728
|
+
|
|
1590
1729
|
yield* logActionObservation("fill", observation);
|
|
1730
|
+
|
|
1591
1731
|
return yield* decodeActionResult(page, policy);
|
|
1592
1732
|
}),
|
|
1593
1733
|
),
|
|
@@ -1597,7 +1737,9 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1597
1737
|
const observation = yield* observedAction("click", (signal, onDispatch) =>
|
|
1598
1738
|
page.click(request.selector, signal, onDispatch),
|
|
1599
1739
|
).pipe(Effect.flatMap(decodeActionObservation));
|
|
1740
|
+
|
|
1600
1741
|
yield* logActionObservation("click", observation);
|
|
1742
|
+
|
|
1601
1743
|
return yield* decodeActionResult(page, policy);
|
|
1602
1744
|
}),
|
|
1603
1745
|
),
|
|
@@ -1608,11 +1750,13 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1608
1750
|
run(
|
|
1609
1751
|
Effect.gen(function* () {
|
|
1610
1752
|
const raw = yield* remote("screenshot", () => page.screenshot(decoded.fullPage));
|
|
1753
|
+
|
|
1611
1754
|
const bytes = yield* Schema.decodeUnknownEffect(PngBytes)(raw).pipe(
|
|
1612
1755
|
Effect.mapError(() =>
|
|
1613
1756
|
protocolError("The browser returned a malformed PNG screenshot"),
|
|
1614
1757
|
),
|
|
1615
1758
|
);
|
|
1759
|
+
|
|
1616
1760
|
if (bytes.length > policy.maxReturnedBytes) {
|
|
1617
1761
|
return yield* InteractiveBrowserLimitError.make({
|
|
1618
1762
|
implementation: browserRunInteractiveImplementation,
|
|
@@ -1622,6 +1766,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1622
1766
|
message: "The browser screenshot byte limit was reached",
|
|
1623
1767
|
});
|
|
1624
1768
|
}
|
|
1769
|
+
|
|
1625
1770
|
return yield* Schema.decodeUnknownEffect(PageScreenshotResult)({
|
|
1626
1771
|
implementation: browserRunInteractiveImplementation,
|
|
1627
1772
|
mediaType: "image/png",
|
|
@@ -1650,6 +1795,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1650
1795
|
),
|
|
1651
1796
|
close,
|
|
1652
1797
|
};
|
|
1798
|
+
|
|
1653
1799
|
return { handle, run };
|
|
1654
1800
|
});
|
|
1655
1801
|
|
|
@@ -1658,6 +1804,7 @@ const remainingMillis = Effect.fn("BrowserRunInteractive.remainingMillis")(funct
|
|
|
1658
1804
|
startedAt: number,
|
|
1659
1805
|
) {
|
|
1660
1806
|
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
1807
|
+
|
|
1661
1808
|
return Math.max(0, policy.maxElapsedMillis - Math.max(0, now - startedAt));
|
|
1662
1809
|
});
|
|
1663
1810
|
|
|
@@ -1720,6 +1867,7 @@ const cdpCommand = <A>(
|
|
|
1720
1867
|
),
|
|
1721
1868
|
{ interruptible: true },
|
|
1722
1869
|
);
|
|
1870
|
+
|
|
1723
1871
|
const raw = yield* Effect.tryPromise({
|
|
1724
1872
|
try: () => cdp.send(command, parameters),
|
|
1725
1873
|
catch: (cause) =>
|
|
@@ -1727,6 +1875,7 @@ const cdpCommand = <A>(
|
|
|
1727
1875
|
? expiredError()
|
|
1728
1876
|
: protocolError("The Cloudflare browser control command failed", cause),
|
|
1729
1877
|
});
|
|
1878
|
+
|
|
1730
1879
|
return yield* Schema.decodeUnknownEffect(output)(raw).pipe(
|
|
1731
1880
|
Effect.mapError(() => protocolError(malformedMessage)),
|
|
1732
1881
|
);
|
|
@@ -1737,11 +1886,13 @@ const makeHostService = (
|
|
|
1737
1886
|
binding: BrowserRunInteractiveBinding["Service"],
|
|
1738
1887
|
): BrowserRunInteractiveHost["Service"] => {
|
|
1739
1888
|
const closeSession = binding.closeSession;
|
|
1889
|
+
|
|
1740
1890
|
const terminate = Effect.fn("BrowserRunInteractiveHost.terminate")(function* (
|
|
1741
1891
|
sessionId: Redacted.Redacted<string>,
|
|
1742
1892
|
entries: ReadonlyArray<CloseEntry>,
|
|
1743
1893
|
) {
|
|
1744
1894
|
const deadline = (yield* Clock.currentTimeMillis) + 10_000;
|
|
1895
|
+
|
|
1745
1896
|
yield* closeSession(sessionId).pipe(
|
|
1746
1897
|
Effect.interruptible,
|
|
1747
1898
|
Effect.timeoutOrElse({
|
|
@@ -1750,6 +1901,7 @@ const makeHostService = (
|
|
|
1750
1901
|
}),
|
|
1751
1902
|
);
|
|
1752
1903
|
const remaining = deadline - (yield* Clock.currentTimeMillis);
|
|
1904
|
+
|
|
1753
1905
|
if (remaining <= 0)
|
|
1754
1906
|
return yield* Effect.logWarning(
|
|
1755
1907
|
"Local browser teardown skipped after confirmed termination deadline",
|
|
@@ -1768,6 +1920,7 @@ const makeHostService = (
|
|
|
1768
1920
|
),
|
|
1769
1921
|
);
|
|
1770
1922
|
});
|
|
1923
|
+
|
|
1771
1924
|
const closeAcquired = Effect.fn("BrowserRunInteractiveHost.closeAcquired")(function* (
|
|
1772
1925
|
browser: BrowserRunInteractiveBrowser,
|
|
1773
1926
|
) {
|
|
@@ -1781,15 +1934,18 @@ const makeHostService = (
|
|
|
1781
1934
|
closeWithWarning(browser.close, "Closing an unidentified browser failed"),
|
|
1782
1935
|
),
|
|
1783
1936
|
);
|
|
1937
|
+
|
|
1784
1938
|
yield* terminate(Redacted.make(sessionId), [
|
|
1785
1939
|
closeEntry(browser.close, "Closing the local browser connection failed"),
|
|
1786
1940
|
]);
|
|
1787
1941
|
});
|
|
1942
|
+
|
|
1788
1943
|
const open = Effect.fn("BrowserRunInteractiveHost.open")(function* (
|
|
1789
1944
|
policy: InteractiveBrowserPolicy,
|
|
1790
1945
|
): Effect.fn.Return<BrowserRunInteractiveSession, InteractiveBrowserError, Scope.Scope> {
|
|
1791
1946
|
const fixedPolicy = yield* snapshotPolicy(policy);
|
|
1792
1947
|
const startedAt = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
1948
|
+
|
|
1793
1949
|
const state: HandleState = {
|
|
1794
1950
|
closed: { value: false },
|
|
1795
1951
|
disconnected: { value: false },
|
|
@@ -1797,10 +1953,13 @@ const makeHostService = (
|
|
|
1797
1953
|
violation: { value: undefined },
|
|
1798
1954
|
pendingRequests: new Set(),
|
|
1799
1955
|
};
|
|
1956
|
+
|
|
1800
1957
|
const lifecycle = {
|
|
1801
1958
|
managedTeardownInstalled: false,
|
|
1802
1959
|
};
|
|
1960
|
+
|
|
1803
1961
|
const closers: Array<CloseEntry> = [];
|
|
1962
|
+
|
|
1804
1963
|
const releaseBeforeManaged = (entry: CloseEntry): Effect.Effect<void> =>
|
|
1805
1964
|
Effect.suspend(() =>
|
|
1806
1965
|
lifecycle.managedTeardownInstalled
|
|
@@ -1830,6 +1989,7 @@ const makeHostService = (
|
|
|
1830
1989
|
),
|
|
1831
1990
|
(acquired) => {
|
|
1832
1991
|
state.disconnected.value = true;
|
|
1992
|
+
|
|
1833
1993
|
return lifecycle.managedTeardownInstalled
|
|
1834
1994
|
? Effect.void
|
|
1835
1995
|
: closeAcquired(acquired).pipe(
|
|
@@ -1838,11 +1998,13 @@ const makeHostService = (
|
|
|
1838
1998
|
},
|
|
1839
1999
|
{ interruptible: true },
|
|
1840
2000
|
);
|
|
2001
|
+
|
|
1841
2002
|
closers.push(closeEntry(browser.close, "Closing the interactive browser failed"));
|
|
1842
2003
|
|
|
1843
2004
|
const disconnected = () => {
|
|
1844
2005
|
state.disconnected.value = true;
|
|
1845
2006
|
};
|
|
2007
|
+
|
|
1846
2008
|
yield* Effect.acquireRelease(
|
|
1847
2009
|
Effect.try({
|
|
1848
2010
|
try: () => browser.onDisconnected(disconnected),
|
|
@@ -1862,12 +2024,15 @@ const makeHostService = (
|
|
|
1862
2024
|
"Removing the browser disconnect listener failed",
|
|
1863
2025
|
),
|
|
1864
2026
|
);
|
|
2027
|
+
|
|
1865
2028
|
const connected = yield* Effect.try({
|
|
1866
2029
|
try: browser.isConnected,
|
|
1867
2030
|
catch: (cause) => protocolError("Reading the Browser Run connection state failed", cause),
|
|
1868
2031
|
});
|
|
2032
|
+
|
|
1869
2033
|
if (!connected) {
|
|
1870
2034
|
state.disconnected.value = true;
|
|
2035
|
+
|
|
1871
2036
|
return yield* expiredError();
|
|
1872
2037
|
}
|
|
1873
2038
|
|
|
@@ -1901,7 +2066,9 @@ const makeHostService = (
|
|
|
1901
2066
|
),
|
|
1902
2067
|
{ interruptible: true },
|
|
1903
2068
|
);
|
|
2069
|
+
|
|
1904
2070
|
closers.push(closeEntry(context.close, "Closing the interactive browser context failed"));
|
|
2071
|
+
|
|
1905
2072
|
const page = yield* Effect.acquireRelease(
|
|
1906
2073
|
withinDeadline(
|
|
1907
2074
|
Effect.tryPromise({
|
|
@@ -1921,6 +2088,7 @@ const makeHostService = (
|
|
|
1921
2088
|
),
|
|
1922
2089
|
{ interruptible: true },
|
|
1923
2090
|
);
|
|
2091
|
+
|
|
1924
2092
|
closers.push(closeEntry(page.close, "Closing the interactive browser page failed"));
|
|
1925
2093
|
|
|
1926
2094
|
yield* withinDeadline(
|
|
@@ -1933,6 +2101,7 @@ const makeHostService = (
|
|
|
1933
2101
|
);
|
|
1934
2102
|
|
|
1935
2103
|
const requestListener = makeRequestListener(fixedPolicy, state);
|
|
2104
|
+
|
|
1936
2105
|
yield* Effect.acquireRelease(
|
|
1937
2106
|
Effect.try({
|
|
1938
2107
|
try: () => page.onRequest(requestListener),
|
|
@@ -1964,11 +2133,13 @@ const makeHostService = (
|
|
|
1964
2133
|
|
|
1965
2134
|
yield* withinDeadline(awaitPendingRequests(state), fixedPolicy, startedAt);
|
|
1966
2135
|
const setupFailure = stateFailure(state);
|
|
2136
|
+
|
|
1967
2137
|
if (setupFailure !== undefined) return yield* setupFailure;
|
|
1968
2138
|
|
|
1969
2139
|
const teardown = yield* Effect.uninterruptible(
|
|
1970
2140
|
Effect.gen(function* () {
|
|
1971
2141
|
const cached = yield* Effect.cached(terminate(Redacted.make(sessionIdValue), closers));
|
|
2142
|
+
|
|
1972
2143
|
lifecycle.managedTeardownInstalled = true;
|
|
1973
2144
|
yield* Effect.addFinalizer(() =>
|
|
1974
2145
|
Effect.uninterruptible(
|
|
@@ -1981,6 +2152,7 @@ const makeHostService = (
|
|
|
1981
2152
|
),
|
|
1982
2153
|
),
|
|
1983
2154
|
);
|
|
2155
|
+
|
|
1984
2156
|
return cached;
|
|
1985
2157
|
}),
|
|
1986
2158
|
);
|
|
@@ -1994,6 +2166,7 @@ const makeHostService = (
|
|
|
1994
2166
|
|
|
1995
2167
|
const runtime = yield* makeHandle(page, fixedPolicy, startedAt, state, close);
|
|
1996
2168
|
const currentPagePreflight = decodeActionResult(page, fixedPolicy).pipe(Effect.asVoid);
|
|
2169
|
+
|
|
1997
2170
|
const requestFitsSession = (requestedMillis: number): Effect.Effect<void, BrowserFailure> =>
|
|
1998
2171
|
remainingMillis(fixedPolicy, startedAt).pipe(
|
|
1999
2172
|
Effect.flatMap((remaining) =>
|
|
@@ -2017,8 +2190,10 @@ const makeHostService = (
|
|
|
2017
2190
|
catch: (cause) => {
|
|
2018
2191
|
if (state.disconnected.value || isRemoteClosure(cause)) {
|
|
2019
2192
|
state.disconnected.value = true;
|
|
2193
|
+
|
|
2020
2194
|
return expiredError();
|
|
2021
2195
|
}
|
|
2196
|
+
|
|
2022
2197
|
return protocolError("Resizing the browser viewport failed", cause);
|
|
2023
2198
|
},
|
|
2024
2199
|
}),
|
|
@@ -2143,6 +2318,7 @@ export const browserRunInteractiveLayer = (): Layer.Layer<
|
|
|
2143
2318
|
Effect.gen(function* () {
|
|
2144
2319
|
const binding = yield* BrowserRunInteractiveBinding;
|
|
2145
2320
|
const host = makeHostService(binding);
|
|
2321
|
+
|
|
2146
2322
|
return InteractiveBrowser.of({
|
|
2147
2323
|
open: (policy) => host.open(policy).pipe(Effect.map((session) => session.handle)),
|
|
2148
2324
|
});
|