@effect-agent/platform-cloudflare 0.1.0-beta.120 → 0.1.0-beta.122
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/CloudflareThreadClient.d.mts +50 -50
- package/dist/{InteractiveBrowser-BQo7QKaP.d.mts → InteractiveBrowser-Cm4xgiGy.d.mts} +2 -2
- package/dist/{InteractiveBrowser-CBUFAECo.mjs → InteractiveBrowser-DXwmvWdA.mjs} +197 -25
- package/dist/InteractiveBrowser-DXwmvWdA.mjs.map +1 -0
- package/dist/InteractiveBrowser.d.mts +1 -1
- package/dist/InteractiveBrowser.mjs +1 -1
- package/dist/ProtectedBrowser.d.mts +15 -1
- package/dist/ProtectedBrowser.mjs +194 -153
- package/dist/ProtectedBrowser.mjs.map +1 -1
- package/dist/{ThreadObject-9IVibJM7.d.mts → ThreadObject-mt0uxvZZ.d.mts} +27 -27
- package/dist/ThreadObject.d.mts +1 -1
- package/dist/index.d.mts +1 -1
- package/package.json +1 -1
- package/src/InteractiveBrowser.ts +17 -12
- package/src/internal/browser-binding.ts +270 -32
- package/src/internal/browser-file-selection.ts +1 -1
- package/src/protected-browser/binding.ts +124 -33
- package/src/protected-browser/host.ts +16 -0
- package/src/protected-browser/native.ts +7 -9
- package/dist/InteractiveBrowser-CBUFAECo.mjs.map +0 -1
|
@@ -705,20 +705,17 @@ declare const encodeAdminResponse: (input: AdminFailed | ExplainedRecovery | Obl
|
|
|
705
705
|
} | {
|
|
706
706
|
readonly _tag: "AdminFailed";
|
|
707
707
|
readonly failure: {
|
|
708
|
-
readonly _tag: "
|
|
709
|
-
readonly threadId: string;
|
|
710
|
-
} | {
|
|
711
|
-
readonly _tag: "ThreadStoreError";
|
|
708
|
+
readonly _tag: "DurableAlarmError";
|
|
712
709
|
readonly operation: string;
|
|
713
710
|
readonly message: string;
|
|
714
711
|
readonly cause?: Schema.Json | undefined;
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
712
|
+
} | {
|
|
713
|
+
readonly _tag: "HostProtocolError";
|
|
714
|
+
readonly message: string;
|
|
715
|
+
} | {
|
|
716
|
+
readonly _tag: "DigestError";
|
|
717
|
+
readonly message: string;
|
|
718
|
+
readonly cause?: Schema.Json | undefined;
|
|
722
719
|
} | {
|
|
723
720
|
readonly _tag: "AdmissionPolicyError";
|
|
724
721
|
readonly reason: "occupied" | "refused" | "unavailable";
|
|
@@ -734,6 +731,21 @@ declare const encodeAdminResponse: (input: AdminFailed | ExplainedRecovery | Obl
|
|
|
734
731
|
readonly operation: string;
|
|
735
732
|
readonly message: string;
|
|
736
733
|
readonly cause?: import("effect-agent/failure-diagnostic").Diagnostic | undefined;
|
|
734
|
+
} | {
|
|
735
|
+
readonly _tag: "ThreadStoreError";
|
|
736
|
+
readonly operation: string;
|
|
737
|
+
readonly message: string;
|
|
738
|
+
readonly cause?: Schema.Json | undefined;
|
|
739
|
+
readonly diagnostic?: {
|
|
740
|
+
readonly causeTag: string;
|
|
741
|
+
readonly operation: string;
|
|
742
|
+
readonly decoder?: string | undefined;
|
|
743
|
+
readonly issueTag?: string | undefined;
|
|
744
|
+
readonly sequence?: number | undefined;
|
|
745
|
+
} | undefined;
|
|
746
|
+
} | {
|
|
747
|
+
readonly _tag: "ThreadNotMaterialized";
|
|
748
|
+
readonly threadId: string;
|
|
737
749
|
} | {
|
|
738
750
|
readonly _tag: "AppendConflict";
|
|
739
751
|
readonly threadId: string;
|
|
@@ -746,30 +758,18 @@ declare const encodeAdminResponse: (input: AdminFailed | ExplainedRecovery | Obl
|
|
|
746
758
|
readonly threadId: string;
|
|
747
759
|
readonly actualEpoch: number;
|
|
748
760
|
readonly attemptedEpoch: number;
|
|
749
|
-
} | {
|
|
750
|
-
readonly _tag: "DurableAlarmError";
|
|
751
|
-
readonly operation: string;
|
|
752
|
-
readonly message: string;
|
|
753
|
-
readonly cause?: Schema.Json | undefined;
|
|
754
|
-
} | {
|
|
755
|
-
readonly _tag: "HostProtocolError";
|
|
756
|
-
readonly message: string;
|
|
757
|
-
} | {
|
|
758
|
-
readonly _tag: "DigestError";
|
|
759
|
-
readonly message: string;
|
|
760
|
-
readonly cause?: Schema.Json | undefined;
|
|
761
761
|
} | {
|
|
762
762
|
readonly _tag: "DurableRuntimeFailpointError";
|
|
763
763
|
readonly location: "abort:after-intent" | "approval:after-request-append" | "approval:after-suspend" | "checkpoint:after-save" | "checkpoint:before-save" | "claim:after-claim" | "compaction:after-canonical-append" | "compaction:before-canonical-append" | "input:after-canonical-append" | "join:after-canonical-append" | "join:after-claim" | "policy:after-reservation-append" | "policy:before-reservation-append" | "resolve:after-intent" | "run:after-duration-append" | "run:after-start-append" | "run:before-duration-append" | "run:before-start-append" | "step:after-step-append" | "subagent:after-admit" | "subagent:after-child-abort-intent" | "subagent:after-child-ready" | "subagent:after-join-append" | "subagent:after-release" | "subagent:after-release-pending" | "subagent:after-request-append" | "subagent:after-reserve" | "subagent:after-sibling-settle" | "subagent:after-start-append" | "subagent:after-suspend" | "subagent:before-join-append" | "submit:after-admit" | "submit:after-materialize" | "terminalize:after-canonical-append" | "terminalize:after-reserve" | "tools:after-prepared-append" | "tools:after-unavailable-append" | "tools:before-prepared-append" | "tools:before-unavailable-append" | "turn:after-canonical-append" | "turn:after-response-append" | "turn:after-results-append" | "update:after-canonical-append" | "update:after-delivery-insert" | "update:before-canonical-append" | "update:before-delivery-insert" | "worker:after-completion-append" | "worker:after-origin-append" | "worker:after-report-append" | "worker:after-report-delivery" | "worker:after-source-append" | "worker:after-stop-append" | "worker:after-stop-seal" | "worker:after-subtree-append" | "worker:before-completion-append" | "worker:before-origin-append" | "worker:before-report-append" | "worker:before-report-delivery" | "worker:before-source-append" | "worker:before-stop-append" | "worker:before-stop-seal" | "worker:before-subtree-append";
|
|
764
|
-
} | {
|
|
765
|
-
readonly _tag: "ApprovalSuspensionError";
|
|
766
|
-
readonly cause: Schema.Json;
|
|
767
764
|
} | {
|
|
768
765
|
readonly _tag: "OperationDenied";
|
|
769
766
|
readonly operation: "abort" | "awaitSettlement" | "explain" | "observe" | "resolveApproval" | "resolveUnknown" | "retry" | "scanObligations" | "verify" | "wake";
|
|
770
767
|
readonly reason: string;
|
|
771
768
|
readonly threadId?: string | undefined;
|
|
772
769
|
readonly submissionId?: string | undefined;
|
|
770
|
+
} | {
|
|
771
|
+
readonly _tag: "ApprovalSuspensionError";
|
|
772
|
+
readonly cause: Schema.Json;
|
|
773
773
|
} | {
|
|
774
774
|
readonly _tag: "RetryRefused";
|
|
775
775
|
readonly submissionId: string;
|
|
@@ -836,4 +836,4 @@ interface Class<EventServices = never> {
|
|
|
836
836
|
declare const make: <ApplicationServices, ApplicationError, EventServices = never, EventLayerError = never>(applicationLayer: Layer.Layer<CloudflareDurableRuntimeServices | ApplicationServices, ApplicationError, CloudflareBootstrapServices | DurableObjectState$1.DurableObjectState | WorkerEnvironment | DurableObjectContext | ThreadObjectNamespace>, options: Options<ApplicationServices, EventServices, EventLayerError>) => Class<ApplicationServices | EventServices>;
|
|
837
837
|
//#endregion
|
|
838
838
|
export { ThreadPublicationOptions as A, portCall as C, CloudflareDurableRuntimeOptions as D, CloudflareDurableRuntimeInitializationError as E, layerConfig as M, layerHostConfig as N, CloudflareDurableRuntimeServices as O, layerInHost as P, make as S, CloudflareBootstrapServices as T, decodeAdminVerifyRequest as _, AdminVerifyRequest as a, encodeAdminResponse as b, Instance as c, RetryExecuted as d, ThreadObject_d_exports as f, decodeAdminResponse as g, decodeAdminExplainRequest as h, AdminResponse as i, layer as j, ThreadObjectPorts as k, ObligationsScanned as l, VerifiedIntegrity as m, AdminFailed as n, Class as o, ThreadRpcOperation as p, AdminFailure as r, ExplainedRecovery as s, AdminExplainRequest as t, Options as u, decodeObligationThresholds as v, submit as w, handleRpc as x, decodeRetryCommand as y };
|
|
839
|
-
//# sourceMappingURL=ThreadObject-
|
|
839
|
+
//# sourceMappingURL=ThreadObject-mt0uxvZZ.d.mts.map
|
package/dist/ThreadObject.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as ThreadPublicationOptions, C as portCall, D as CloudflareDurableRuntimeOptions, E as CloudflareDurableRuntimeInitializationError, M as layerConfig, N as layerHostConfig, O as CloudflareDurableRuntimeServices, P as layerInHost, S as make, T as CloudflareBootstrapServices, _ as decodeAdminVerifyRequest, a as AdminVerifyRequest, b as encodeAdminResponse, c as Instance, d as RetryExecuted, g as decodeAdminResponse, h as decodeAdminExplainRequest, i as AdminResponse, j as layer, k as ThreadObjectPorts, l as ObligationsScanned, m as VerifiedIntegrity, n as AdminFailed, o as Class, p as ThreadRpcOperation, r as AdminFailure, s as ExplainedRecovery, t as AdminExplainRequest, u as Options, v as decodeObligationThresholds, w as submit, x as handleRpc, y as decodeRetryCommand } from "./ThreadObject-
|
|
1
|
+
import { A as ThreadPublicationOptions, C as portCall, D as CloudflareDurableRuntimeOptions, E as CloudflareDurableRuntimeInitializationError, M as layerConfig, N as layerHostConfig, O as CloudflareDurableRuntimeServices, P as layerInHost, S as make, T as CloudflareBootstrapServices, _ as decodeAdminVerifyRequest, a as AdminVerifyRequest, b as encodeAdminResponse, c as Instance, d as RetryExecuted, g as decodeAdminResponse, h as decodeAdminExplainRequest, i as AdminResponse, j as layer, k as ThreadObjectPorts, l as ObligationsScanned, m as VerifiedIntegrity, n as AdminFailed, o as Class, p as ThreadRpcOperation, r as AdminFailure, s as ExplainedRecovery, t as AdminExplainRequest, u as Options, v as decodeObligationThresholds, w as submit, x as handleRpc, y as decodeRetryCommand } from "./ThreadObject-mt0uxvZZ.mjs";
|
|
2
2
|
export { AdminExplainRequest, AdminFailed, AdminFailure, AdminResponse, AdminVerifyRequest, type CloudflareBootstrapServices as BootstrapServices, Class, ExplainedRecovery, type CloudflareDurableRuntimeInitializationError as InitializationError, Instance, ObligationsScanned, Options, type ThreadPublicationOptions as PublicationOptions, RetryExecuted, type CloudflareDurableRuntimeOptions as RuntimeOptions, type CloudflareDurableRuntimeServices as Services, ThreadObjectPorts, ThreadRpcOperation, VerifiedIntegrity, decodeAdminExplainRequest, decodeAdminResponse, decodeAdminVerifyRequest, decodeObligationThresholds, decodeRetryCommand, encodeAdminResponse, handleRpc, layer, layerConfig, layerHostConfig, layerInHost, make, portCall, submit };
|
package/dist/index.d.mts
CHANGED
|
@@ -8,6 +8,6 @@ import { t as CloudflareMemory_d_exports } from "./CloudflareMemory.mjs";
|
|
|
8
8
|
import { t as CloudflareScheduling_d_exports } from "./CloudflareScheduling.mjs";
|
|
9
9
|
import { t as CloudflareSubscriptions_d_exports } from "./CloudflareSubscriptions.mjs";
|
|
10
10
|
import { t as CloudflareThreadClient_d_exports } from "./CloudflareThreadClient.mjs";
|
|
11
|
-
import { f as ThreadObject_d_exports } from "./ThreadObject-
|
|
11
|
+
import { f as ThreadObject_d_exports } from "./ThreadObject-mt0uxvZZ.mjs";
|
|
12
12
|
import { t as WakeScheduler_d_exports } from "./WakeScheduler.mjs";
|
|
13
13
|
export { Alarm_d_exports as Alarm, CloudflareAiGateway_d_exports as CloudflareAiGateway, CloudflareBindings_d_exports as CloudflareBindings, CloudflareBrowser_d_exports as CloudflareBrowser, CloudflareCodeMode_d_exports as CloudflareCodeMode, CloudflareConfig_d_exports as CloudflareConfig, CloudflareMemory_d_exports as CloudflareMemory, CloudflareScheduling_d_exports as CloudflareScheduling, CloudflareSubscriptions_d_exports as CloudflareSubscriptions, CloudflareThreadClient_d_exports as CloudflareThreadClient, ThreadObject_d_exports as ThreadObject, WakeScheduler_d_exports as WakeScheduler };
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@effect-agent/platform-cloudflare","version":"0.1.0-beta.
|
|
1
|
+
{"name":"@effect-agent/platform-cloudflare","version":"0.1.0-beta.122","dependencies":{"@effect-agent/storage-cloudflare":"0.1.0-beta.122","@effect/platform-browser":"4.0.0-rc.116","@effect/sql-sqlite-do":"4.0.0-rc.116","effect-agent":"0.1.0-beta.122","puppeteer-core":"24.29.0"},"devDependencies":{"@cloudflare/vitest-pool-workers":"0.21.3","@cloudflare/workers-types":"5.20260825.1","@effect-agent/testing":"0.1.0-beta.122","@effect/platform-node":"4.0.0-rc.116","@effect/sql-d1":"4.0.0-rc.116","@effect/vitest":"4.0.0-rc.116","effect":"4.0.0-rc.116","effect-cf":"0.44.1","esbuild":"0.28.1","miniflare":"5.20260811.1-alpha","typescript":"7.0.2","vite-plus":"0.3.2","vitest":"4.1.11"},"peerDependencies":{"effect":"^4.0.0-rc.116","effect-cf":"^0.44.1 || ^0.45.0"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./alarm":{"types":"./dist/Alarm.d.mts","default":"./dist/Alarm.mjs"},"./browser-rest-capture":{"types":"./dist/BrowserRestCapture.d.mts","default":"./dist/BrowserRestCapture.mjs"},"./browser-rest-crawl":{"types":"./dist/BrowserRestCrawl.d.mts","default":"./dist/BrowserRestCrawl.mjs"},"./cloudflare-bindings":{"types":"./dist/CloudflareBindings.d.mts","default":"./dist/CloudflareBindings.mjs"},"./cloudflare-browser":{"types":"./dist/CloudflareBrowser.d.mts","default":"./dist/CloudflareBrowser.mjs"},"./cloudflare-code-mode":{"types":"./dist/CloudflareCodeMode.d.mts","default":"./dist/CloudflareCodeMode.mjs"},"./cloudflare-config":{"types":"./dist/CloudflareConfig.d.mts","default":"./dist/CloudflareConfig.mjs"},"./cloudflare-memory":{"types":"./dist/CloudflareMemory.d.mts","default":"./dist/CloudflareMemory.mjs"},"./cloudflare-scheduling":{"types":"./dist/CloudflareScheduling.d.mts","default":"./dist/CloudflareScheduling.mjs"},"./cloudflare-subscriptions":{"types":"./dist/CloudflareSubscriptions.d.mts","default":"./dist/CloudflareSubscriptions.mjs"},"./cloudflare-thread-client":{"types":"./dist/CloudflareThreadClient.d.mts","default":"./dist/CloudflareThreadClient.mjs"},"./interactive-browser":{"types":"./dist/InteractiveBrowser.d.mts","default":"./dist/InteractiveBrowser.mjs"},"./protected-browser":{"types":"./dist/ProtectedBrowser.d.mts","default":"./dist/ProtectedBrowser.mjs"},"./thread-object":{"types":"./dist/ThreadObject.d.mts","default":"./dist/ThreadObject.mjs"},"./wake-scheduler":{"types":"./dist/WakeScheduler.d.mts","default":"./dist/WakeScheduler.mjs"},"./cloudflare-ai-gateway":{"types":"./dist/CloudflareAiGateway.d.mts","default":"./dist/CloudflareAiGateway.mjs"}},"description":"Cloudflare Layer assembly for Effect Agent: Durable Objects and Browser Run adapters.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/danieljvdm/effect-agent.git","directory":"packages/platform-cloudflare"},"files":["dist","src"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json"}}
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
type Browser,
|
|
5
|
-
type BrowserContext,
|
|
6
|
-
type CDPSession,
|
|
7
|
-
type HTTPRequest,
|
|
8
|
-
type Page,
|
|
9
|
-
} from "@cloudflare/puppeteer";
|
|
10
3
|
import {
|
|
11
4
|
Cause,
|
|
12
5
|
Context,
|
|
@@ -47,6 +40,13 @@ import {
|
|
|
47
40
|
} from "effect-agent/interactive-browser";
|
|
48
41
|
import { PageScreenshotResult } from "effect-agent/page-screenshot";
|
|
49
42
|
import { SandboxImplementation } from "effect-agent/sandbox";
|
|
43
|
+
import {
|
|
44
|
+
type Browser,
|
|
45
|
+
type BrowserContext,
|
|
46
|
+
type CDPSession,
|
|
47
|
+
type HTTPRequest,
|
|
48
|
+
type Page,
|
|
49
|
+
} from "puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js";
|
|
50
50
|
|
|
51
51
|
import { BrowserRunBinding } from "./internal/browser-binding.ts";
|
|
52
52
|
import {
|
|
@@ -380,7 +380,10 @@ export class BrowserRunInteractiveBinding extends Context.Service<
|
|
|
380
380
|
BrowserRunInteractiveBinding,
|
|
381
381
|
{
|
|
382
382
|
readonly acquire: (keepAliveMillis: number) => Promise<unknown>;
|
|
383
|
-
readonly connect: (
|
|
383
|
+
readonly connect: (
|
|
384
|
+
sessionId: string,
|
|
385
|
+
signal?: AbortSignal,
|
|
386
|
+
) => Promise<BrowserRunInteractiveBrowser>;
|
|
384
387
|
/** Success proves whole-browser termination or exact-session absence. */
|
|
385
388
|
readonly closeSession: (
|
|
386
389
|
sessionId: Redacted.Redacted<string>,
|
|
@@ -406,9 +409,10 @@ export class BrowserRunInteractiveBinding extends Context.Service<
|
|
|
406
409
|
return {
|
|
407
410
|
acquire: async (keepAliveMillis: number) =>
|
|
408
411
|
binding.acquire(keepAliveMillis, "interactive.acquire"),
|
|
409
|
-
connect: async (sessionId: string) =>
|
|
412
|
+
connect: async (sessionId: string, signal?: AbortSignal) =>
|
|
410
413
|
makeProductionBrowser(
|
|
411
|
-
await binding.connect(sessionId, "interactive.connect"),
|
|
414
|
+
await binding.connect(sessionId, "interactive.connect", signal).browser,
|
|
415
|
+
sessionId,
|
|
412
416
|
viewport,
|
|
413
417
|
),
|
|
414
418
|
closeSession: (sessionId: Redacted.Redacted<string>) =>
|
|
@@ -1385,6 +1389,7 @@ const makeProductionContext = (
|
|
|
1385
1389
|
|
|
1386
1390
|
const makeProductionBrowser = (
|
|
1387
1391
|
browser: Browser,
|
|
1392
|
+
sessionId: string,
|
|
1388
1393
|
viewport?: BrowserRunViewport,
|
|
1389
1394
|
): BrowserRunInteractiveBrowser => ({
|
|
1390
1395
|
detach: () => browser.disconnect(),
|
|
@@ -1417,7 +1422,7 @@ const makeProductionBrowser = (
|
|
|
1417
1422
|
if (!isRemoteClosure(cause)) throw cause;
|
|
1418
1423
|
}
|
|
1419
1424
|
},
|
|
1420
|
-
sessionId: () =>
|
|
1425
|
+
sessionId: () => sessionId,
|
|
1421
1426
|
isConnected: () => browser.isConnected(),
|
|
1422
1427
|
onDisconnected: (listener) => {
|
|
1423
1428
|
browser.on("disconnected", listener);
|
|
@@ -2673,7 +2678,7 @@ const makeHostService = (
|
|
|
2673
2678
|
signal,
|
|
2674
2679
|
closeLateAcquisition(
|
|
2675
2680
|
signal,
|
|
2676
|
-
() => binding.connect(sessionIdValue),
|
|
2681
|
+
() => binding.connect(sessionIdValue, signal),
|
|
2677
2682
|
(acquired) =>
|
|
2678
2683
|
runCleanup(
|
|
2679
2684
|
resume === undefined
|
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import { Cause, Context, Effect, Layer, Schema } from "effect";
|
|
3
|
+
import puppeteer, {
|
|
4
|
+
type Browser,
|
|
5
|
+
type ConnectionTransport,
|
|
6
|
+
} from "puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js";
|
|
4
7
|
|
|
5
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
browserFailure,
|
|
10
|
+
BrowserRunFailure,
|
|
11
|
+
reportBrowserCause,
|
|
12
|
+
reportedBrowserError,
|
|
13
|
+
} from "./browser-failure.ts";
|
|
6
14
|
|
|
7
15
|
const Acquired = Schema.Struct({ sessionId: Schema.String.check(Schema.isUUID()) });
|
|
8
16
|
|
|
17
|
+
const VersionReply = Schema.fromJsonString(
|
|
18
|
+
Schema.Struct({
|
|
19
|
+
id: Schema.optionalKey(Schema.Int),
|
|
20
|
+
result: Schema.optionalKey(Schema.Unknown),
|
|
21
|
+
error: Schema.optionalKey(Schema.Unknown),
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const Version = Schema.Struct({
|
|
26
|
+
product: Schema.NonEmptyString,
|
|
27
|
+
protocolVersion: Schema.NonEmptyString,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** Private attachment ownership is available before asynchronous SDK initialization completes. */
|
|
31
|
+
export interface BrowserRunAttachment {
|
|
32
|
+
readonly browser: Promise<Browser>;
|
|
33
|
+
/** Fence SDK dispatch/callbacks and await local socket closure; never terminate the provider. */
|
|
34
|
+
readonly retire: Effect.Effect<void, BrowserRunFailure>;
|
|
35
|
+
}
|
|
36
|
+
|
|
9
37
|
/** Native transport port shared by both scoped adapters; the Layer owns the foreign binding. */
|
|
10
38
|
export class BrowserRunBinding extends Context.Service<
|
|
11
39
|
BrowserRunBinding,
|
|
@@ -14,10 +42,42 @@ export class BrowserRunBinding extends Context.Service<
|
|
|
14
42
|
keepAliveMillis: number,
|
|
15
43
|
operation: "protected.acquire" | "interactive.acquire",
|
|
16
44
|
) => Promise<string>;
|
|
17
|
-
readonly connect: (
|
|
45
|
+
readonly connect: (
|
|
46
|
+
sessionId: string,
|
|
47
|
+
operation: string,
|
|
48
|
+
signal?: AbortSignal,
|
|
49
|
+
) => BrowserRunAttachment;
|
|
50
|
+
readonly keepAlive: (sessionId: string) => Effect.Effect<void, BrowserRunFailure>;
|
|
18
51
|
}
|
|
19
52
|
>()("@effect-agent/platform-cloudflare/internal/BrowserRunBinding") {
|
|
20
53
|
static layer(browser: Pick<BrowserRun, "fetch">) {
|
|
54
|
+
const upgrade = async (sessionId: string, operation: string, signal?: AbortSignal) => {
|
|
55
|
+
const response = await browser.fetch(
|
|
56
|
+
`https://browser-rendering.cloudflare.com/v1/devtools/browser/${sessionId}`,
|
|
57
|
+
{ headers: { Upgrade: "websocket" }, ...(signal === undefined ? {} : { signal }) },
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (response.status !== 101) {
|
|
61
|
+
const failure = new BrowserRunFailure({
|
|
62
|
+
operation,
|
|
63
|
+
reason: "provider",
|
|
64
|
+
status: response.status,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await response.body?.cancel();
|
|
69
|
+
} catch {
|
|
70
|
+
// Preserve the provider refusal when local response release also fails.
|
|
71
|
+
}
|
|
72
|
+
throw failure;
|
|
73
|
+
}
|
|
74
|
+
const socket = response.webSocket;
|
|
75
|
+
|
|
76
|
+
if (socket === null) throw new BrowserRunFailure({ operation, reason: "malformed" });
|
|
77
|
+
|
|
78
|
+
return socket;
|
|
79
|
+
};
|
|
80
|
+
|
|
21
81
|
return Layer.succeed(this)({
|
|
22
82
|
// One allocation path, with recording explicitly disabled for private sessions.
|
|
23
83
|
acquire: async (keepAliveMillis, operation) => {
|
|
@@ -43,40 +103,218 @@ export class BrowserRunBinding extends Context.Service<
|
|
|
43
103
|
|
|
44
104
|
return Schema.decodeUnknownSync(Acquired)(await response.json()).sessionId;
|
|
45
105
|
},
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
let
|
|
106
|
+
connect: (sessionId, operation, signal) => {
|
|
107
|
+
let socket: WebSocket | undefined;
|
|
108
|
+
let retired = false;
|
|
109
|
+
let closedByAbort = false;
|
|
110
|
+
let retirementFailure: BrowserRunFailure | undefined;
|
|
49
111
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
112
|
+
const disconnectOperation =
|
|
113
|
+
operation === "protected.connect" ? "protected.disconnect" : "interactive.disconnect";
|
|
114
|
+
|
|
115
|
+
const transport: ConnectionTransport = {
|
|
116
|
+
send: (message) => {
|
|
117
|
+
if (retired || socket === undefined)
|
|
118
|
+
throw new BrowserRunFailure({ operation, reason: "provider" });
|
|
119
|
+
socket.send(message);
|
|
120
|
+
},
|
|
121
|
+
close: () => retireNow(),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const onMessage = (event: MessageEvent) => {
|
|
125
|
+
if (!retired) transport.onmessage?.(event.data);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const fence = () => {
|
|
129
|
+
if (retired) return;
|
|
130
|
+
retired = true;
|
|
131
|
+
const notify = transport.onclose;
|
|
132
|
+
|
|
133
|
+
transport.onmessage = undefined;
|
|
134
|
+
transport.onclose = undefined;
|
|
135
|
+
// Notify the SDK before touching the socket: disposal must reject pending CDP calls
|
|
136
|
+
// even when the raw transport cannot close. The send fence also covers queued work.
|
|
137
|
+
try {
|
|
138
|
+
notify?.();
|
|
139
|
+
} finally {
|
|
140
|
+
socket?.removeEventListener("message", onMessage);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const retireNow = () => {
|
|
145
|
+
try {
|
|
146
|
+
fence();
|
|
147
|
+
} catch (cause) {
|
|
148
|
+
retirementFailure ??= browserFailure(disconnectOperation, cause);
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
if (socket !== undefined && socket.readyState < WebSocket.CLOSING) socket.close();
|
|
152
|
+
} catch (cause) {
|
|
153
|
+
retirementFailure ??= browserFailure(disconnectOperation, cause);
|
|
154
|
+
}
|
|
155
|
+
if (retirementFailure !== undefined) throw retirementFailure;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const abort = () => {
|
|
159
|
+
closedByAbort = socket?.readyState === WebSocket.OPEN;
|
|
160
|
+
try {
|
|
161
|
+
retireNow();
|
|
162
|
+
} catch {
|
|
163
|
+
// The attachment owner observes this separately from the initiating failure.
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
168
|
+
|
|
169
|
+
const initialized = (async () => {
|
|
170
|
+
try {
|
|
171
|
+
signal?.throwIfAborted();
|
|
172
|
+
socket = await upgrade(sessionId, operation, signal);
|
|
173
|
+
socket.accept();
|
|
174
|
+
if (retired || signal?.aborted) {
|
|
175
|
+
// No SDK command may escape a late upgrade. Preserve a late local-close
|
|
176
|
+
// failure independently of the cancellation already returned to the owner.
|
|
177
|
+
retireNow();
|
|
178
|
+
signal?.throwIfAborted();
|
|
179
|
+
throw new BrowserRunFailure({ operation, reason: "provider" });
|
|
180
|
+
}
|
|
181
|
+
socket.addEventListener("message", onMessage);
|
|
182
|
+
socket.addEventListener(
|
|
183
|
+
"close",
|
|
184
|
+
() => {
|
|
54
185
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (response.status !== 101) {
|
|
58
|
-
failure = new BrowserRunFailure({
|
|
59
|
-
operation,
|
|
60
|
-
reason: "provider",
|
|
61
|
-
status: response.status,
|
|
62
|
-
});
|
|
63
|
-
await response.body?.cancel();
|
|
64
|
-
throw failure;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return response;
|
|
186
|
+
fence();
|
|
68
187
|
} catch (cause) {
|
|
69
|
-
|
|
70
|
-
throw failure;
|
|
188
|
+
retirementFailure ??= browserFailure(disconnectOperation, cause);
|
|
71
189
|
}
|
|
72
190
|
},
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
191
|
+
{ once: true },
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return await puppeteer.connect({ transport });
|
|
195
|
+
} catch (cause) {
|
|
196
|
+
try {
|
|
197
|
+
retireNow();
|
|
198
|
+
} catch {
|
|
199
|
+
// Preserve both facts: initialization failed, and retirement may be unconfirmed.
|
|
200
|
+
}
|
|
201
|
+
const failure = browserFailure(operation, cause);
|
|
202
|
+
|
|
203
|
+
const localCancellation =
|
|
204
|
+
cause instanceof Error &&
|
|
205
|
+
signal?.aborted === true &&
|
|
206
|
+
(cause === signal.reason || (closedByAbort && cause.name === "TargetCloseError"));
|
|
207
|
+
|
|
208
|
+
throw localCancellation ? reportedBrowserError(failure) : failure;
|
|
209
|
+
} finally {
|
|
210
|
+
signal?.removeEventListener("abort", abort);
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
browser: initialized,
|
|
216
|
+
retire: Effect.callback<void, BrowserRunFailure>((resume) => {
|
|
217
|
+
const connected = socket;
|
|
218
|
+
const onClose = () => resume(Effect.void);
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
retireNow();
|
|
222
|
+
if (connected === undefined || connected.readyState === WebSocket.CLOSED)
|
|
223
|
+
resume(Effect.void);
|
|
224
|
+
else connected.addEventListener("close", onClose, { once: true });
|
|
225
|
+
} catch (cause) {
|
|
226
|
+
resume(Effect.fail(browserFailure(disconnectOperation, cause)));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return Effect.sync(() => connected?.removeEventListener("close", onClose));
|
|
230
|
+
}),
|
|
231
|
+
};
|
|
79
232
|
},
|
|
233
|
+
keepAlive: Effect.fnUntraced(function* (sessionId: string) {
|
|
234
|
+
const operation = "protected.keepAlive";
|
|
235
|
+
const runReport = Effect.runPromiseWith(yield* Effect.context<never>());
|
|
236
|
+
|
|
237
|
+
const release = (socket: WebSocket) =>
|
|
238
|
+
Effect.sync(() => socket.close()).pipe(
|
|
239
|
+
Effect.catchCause((cause) => reportBrowserCause(operation, cause)),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const socket = yield* Effect.acquireRelease(
|
|
243
|
+
Effect.tryPromise({
|
|
244
|
+
try: async (signal) => {
|
|
245
|
+
let acquired: WebSocket | undefined;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
acquired = await upgrade(sessionId, operation, signal);
|
|
249
|
+
acquired.accept();
|
|
250
|
+
// The binding may resolve an upgrade after the Effect was interrupted.
|
|
251
|
+
signal.throwIfAborted();
|
|
252
|
+
|
|
253
|
+
return acquired;
|
|
254
|
+
} catch (cause) {
|
|
255
|
+
if (acquired !== undefined) await runReport(release(acquired));
|
|
256
|
+
if (signal.aborted && cause !== signal.reason)
|
|
257
|
+
await runReport(reportBrowserCause(operation, Cause.fail(cause)));
|
|
258
|
+
throw cause;
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
catch: (cause) => browserFailure(operation, cause),
|
|
262
|
+
}),
|
|
263
|
+
release,
|
|
264
|
+
{ interruptible: true },
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
yield* Effect.callback<void, BrowserRunFailure>((resume) => {
|
|
268
|
+
const finish = (result: Effect.Effect<void, BrowserRunFailure>) => {
|
|
269
|
+
removeListeners();
|
|
270
|
+
resume(result);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const fail = (reason: BrowserRunFailure["reason"]) =>
|
|
274
|
+
finish(Effect.fail(new BrowserRunFailure({ operation, reason })));
|
|
275
|
+
|
|
276
|
+
const onMessage = (event: MessageEvent) => {
|
|
277
|
+
try {
|
|
278
|
+
if (typeof event.data !== "string" || event.data.length > 16_384) {
|
|
279
|
+
fail("malformed");
|
|
280
|
+
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const reply = Schema.decodeSync(VersionReply)(event.data);
|
|
284
|
+
|
|
285
|
+
if (reply.id !== 1) return;
|
|
286
|
+
if (reply.error !== undefined) {
|
|
287
|
+
fail("provider");
|
|
288
|
+
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
Schema.decodeUnknownSync(Version)(reply.result);
|
|
292
|
+
finish(Effect.void);
|
|
293
|
+
} catch {
|
|
294
|
+
fail("malformed");
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const onClose = () => fail("provider");
|
|
299
|
+
|
|
300
|
+
const removeListeners = () => {
|
|
301
|
+
socket.removeEventListener("message", onMessage);
|
|
302
|
+
socket.removeEventListener("close", onClose);
|
|
303
|
+
socket.removeEventListener("error", onClose);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
socket.addEventListener("message", onMessage);
|
|
307
|
+
socket.addEventListener("close", onClose);
|
|
308
|
+
socket.addEventListener("error", onClose);
|
|
309
|
+
try {
|
|
310
|
+
socket.send(JSON.stringify({ id: 1, method: "Browser.getVersion" }));
|
|
311
|
+
} catch (cause) {
|
|
312
|
+
finish(Effect.fail(browserFailure(operation, cause)));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return Effect.sync(removeListeners);
|
|
316
|
+
});
|
|
317
|
+
}, Effect.scoped),
|
|
80
318
|
});
|
|
81
319
|
}
|
|
82
320
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Page } from "@cloudflare/puppeteer";
|
|
2
1
|
import { Encoding } from "effect";
|
|
3
2
|
import type { BrowserSelectFileRequest } from "effect-agent/interactive-browser";
|
|
3
|
+
import type { Page } from "puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js";
|
|
4
4
|
|
|
5
5
|
type Element = NonNullable<Awaited<ReturnType<Page["$"]>>>;
|
|
6
6
|
|