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

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.
@@ -1,5 +1,23 @@
1
1
  import { Context, Effect, Layer, Redacted, Schema, Scope } from "effect";
2
2
  import { BrowserHandle, InteractiveBrowser, InteractiveBrowserError, InteractiveBrowserPolicy, InteractiveBrowserPolicyDeniedError, SandboxImplementation } from "@effect-agent/sandbox";
3
+ import { HttpClient } from "effect/unstable/http";
4
+ //#region src/browser-session-lifecycle.d.ts
5
+ declare const BrowserRunCleanupError_base: Schema.Class<BrowserRunCleanupError, Schema.TaggedStruct<"BrowserRunCleanupError", {
6
+ readonly reason: Schema.Literals<readonly ["configuration", "authorization", "rate-limited", "provider", "malformed", "timeout", "pending"]>;
7
+ readonly status: Schema.optionalKey<Schema.Int>;
8
+ }>, import("effect/Cause").YieldableError>;
9
+ declare class BrowserRunCleanupError extends BrowserRunCleanupError_base {}
10
+ interface BrowserRunLifecycleOptions {
11
+ readonly accountId: string;
12
+ readonly apiToken: Redacted.Redacted<string>;
13
+ }
14
+ declare const BrowserRunSessionLifecycle_base: Context.ServiceClass<BrowserRunSessionLifecycle, "@effect-agent/platform-cloudflare/BrowserRunSessionLifecycle", {
15
+ readonly close: (sessionId: Redacted.Redacted<string>) => Effect.Effect<void, BrowserRunCleanupError>;
16
+ }>;
17
+ declare class BrowserRunSessionLifecycle extends BrowserRunSessionLifecycle_base {
18
+ static layer(options: BrowserRunLifecycleOptions): Layer.Layer<BrowserRunSessionLifecycle, BrowserRunCleanupError, HttpClient.HttpClient>;
19
+ }
20
+ //#endregion
3
21
  //#region src/interactive-browser.d.ts
4
22
  declare const browserRunInteractiveImplementation: SandboxImplementation;
5
23
  declare const BrowserRunViewport_base: Schema.Class<BrowserRunViewport, Schema.Struct<{
@@ -83,14 +101,15 @@ interface BrowserRunInteractiveBrowser {
83
101
  }
84
102
  declare const BrowserRunInteractiveBinding_base: Context.ServiceClass<BrowserRunInteractiveBinding, "@effect-agent/platform-cloudflare/BrowserRunInteractiveBinding", {
85
103
  readonly launch: (keepAliveMillis: number) => Promise<BrowserRunInteractiveBrowser>;
86
- readonly connect: (sessionId: string) => Promise<BrowserRunInteractiveBrowser>;
104
+ /** Success proves whole-browser termination or exact-session absence. */
105
+ readonly closeSession: (sessionId: Redacted.Redacted<string>) => Effect.Effect<void, InteractiveBrowserError>;
87
106
  }>;
88
107
  /** Host-supplied Browser Run binding projected into one fakeable launch operation. */
89
108
  declare class BrowserRunInteractiveBinding extends BrowserRunInteractiveBinding_base {
90
109
  static layer(options: {
91
110
  readonly browser: BrowserRun;
92
111
  readonly viewport?: BrowserRunViewport;
93
- }): Layer.Layer<BrowserRunInteractiveBinding, InteractiveBrowserPolicyDeniedError>;
112
+ }): Layer.Layer<BrowserRunInteractiveBinding, InteractiveBrowserPolicyDeniedError, BrowserRunSessionLifecycle>;
94
113
  }
95
114
  interface BrowserRunInteractiveSession {
96
115
  readonly handle: BrowserHandle;
@@ -108,6 +127,7 @@ interface BrowserRunInteractiveSession {
108
127
  readonly close: Effect.Effect<void, InteractiveBrowserError>;
109
128
  }
110
129
  declare const BrowserRunInteractiveHost_base: Context.ServiceClass<BrowserRunInteractiveHost, "@effect-agent/platform-cloudflare/BrowserRunInteractiveHost", {
130
+ readonly cleanupSemantics?: "confirmed-terminal";
111
131
  readonly open: (policy: InteractiveBrowserPolicy) => Effect.Effect<BrowserRunInteractiveSession, InteractiveBrowserError, Scope.Scope>;
112
132
  readonly closeSession: (sessionId: Redacted.Redacted<string>) => Effect.Effect<void, InteractiveBrowserError>;
113
133
  }>;
@@ -120,5 +140,5 @@ declare const browserRunInteractiveHostLayer: () => Layer.Layer<BrowserRunIntera
120
140
  /** Worker-only generic adapter; Cloudflare identity and controls remain host-only. */
121
141
  declare const browserRunInteractiveLayer: () => Layer.Layer<InteractiveBrowser, never, BrowserRunInteractiveBinding>;
122
142
  //#endregion
123
- export { BrowserRunCloudflareCommand, BrowserRunHandoffRequest, BrowserRunHandoffResult, BrowserRunHandoffState, BrowserRunInteractiveBinding, BrowserRunInteractiveBrowser, BrowserRunInteractiveCdpSession, BrowserRunInteractiveContext, BrowserRunInteractiveHost, BrowserRunInteractivePage, BrowserRunInteractiveRequest, BrowserRunInteractiveRequestListener, BrowserRunInteractiveSession, BrowserRunLiveViewRequest, BrowserRunLiveViewResult, BrowserRunViewport, browserRunInteractiveHostLayer, browserRunInteractiveImplementation, browserRunInteractiveLayer, isBrowserRunUndispatchedActionError };
143
+ export { BrowserRunCleanupError, BrowserRunCloudflareCommand, BrowserRunHandoffRequest, BrowserRunHandoffResult, BrowserRunHandoffState, BrowserRunInteractiveBinding, BrowserRunInteractiveBrowser, BrowserRunInteractiveCdpSession, BrowserRunInteractiveContext, BrowserRunInteractiveHost, BrowserRunInteractivePage, BrowserRunInteractiveRequest, BrowserRunInteractiveRequestListener, BrowserRunInteractiveSession, BrowserRunLiveViewRequest, BrowserRunLiveViewResult, BrowserRunSessionLifecycle, BrowserRunViewport, browserRunInteractiveHostLayer, browserRunInteractiveImplementation, browserRunInteractiveLayer, isBrowserRunUndispatchedActionError };
124
144
  //# sourceMappingURL=interactive-browser.d.mts.map
@@ -1,6 +1,86 @@
1
- import { Context, Duration, Effect, Layer, Option, Redacted, Ref, Schema, Semaphore } from "effect";
1
+ import { Clock, Context, Duration, Effect, Layer, Option, Redacted, Ref, Schema, Semaphore, Stream } from "effect";
2
2
  import { BrowserActionResult, BrowserNavigationResult, BrowserScreenshotRequest, BrowserScrollRequest, BrowserTextResult, InteractiveBrowser, InteractiveBrowserActionError, InteractiveBrowserBusyError, InteractiveBrowserCapacityError, InteractiveBrowserExpiredError, InteractiveBrowserLimitError, InteractiveBrowserPolicy, InteractiveBrowserPolicyDeniedError, InteractiveBrowserProtocolError, InteractiveBrowserTargetUrl, InteractiveBrowserUnsupportedError, PageScreenshotResult, SandboxImplementation } from "@effect-agent/sandbox";
3
3
  import puppeteer from "@cloudflare/puppeteer";
4
+ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
5
+ //#region src/browser-session-lifecycle.ts
6
+ var BrowserRunCleanupError = class extends Schema.TaggedError()("BrowserRunCleanupError", {
7
+ reason: Schema.Literals([
8
+ "configuration",
9
+ "authorization",
10
+ "rate-limited",
11
+ "provider",
12
+ "malformed",
13
+ "timeout",
14
+ "pending"
15
+ ]),
16
+ status: Schema.optionalKey(Schema.Int)
17
+ }) {};
18
+ const Identity = Schema.String.check(Schema.isUUID());
19
+ const Account = Schema.String.check(Schema.isPattern(/^[a-f0-9]{32}$/));
20
+ const Closed = Schema.Struct({ status: Schema.Literals(["closing", "closed"]) });
21
+ const Metadata = Schema.Struct({
22
+ sessionId: Identity,
23
+ endTime: Schema.optionalKey(Schema.Finite)
24
+ });
25
+ const Absent = Schema.Struct({ error: Schema.Literal("Session not found") });
26
+ var BrowserRunSessionLifecycle = class extends Context.Service()("@effect-agent/platform-cloudflare/BrowserRunSessionLifecycle") {
27
+ static layer(options) {
28
+ return Layer.effect(this, Effect.gen(function* () {
29
+ if (!Schema.is(Account)(options.accountId) || Redacted.value(options.apiToken).length === 0) return yield* new BrowserRunCleanupError({ reason: "configuration" });
30
+ const client = yield* HttpClient.HttpClient;
31
+ const request = Effect.fn("BrowserRunSessionLifecycle.request")(function* (method, sessionId) {
32
+ const path = method === "DELETE" ? "browser" : "session";
33
+ const response = yield* client.execute(HttpClientRequest.make(method)(`https://api.cloudflare.com/client/v4/accounts/${options.accountId}/browser-rendering/devtools/${path}/${sessionId}`).pipe(HttpClientRequest.bearerToken(options.apiToken))).pipe(Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }), Effect.mapError(() => new BrowserRunCleanupError({ reason: "provider" })));
34
+ if (response.status === 401 || response.status === 403) return yield* new BrowserRunCleanupError({
35
+ reason: "authorization",
36
+ status: response.status
37
+ });
38
+ if (response.status === 429) return yield* new BrowserRunCleanupError({
39
+ reason: "rate-limited",
40
+ status: response.status
41
+ });
42
+ if (response.status !== 200 && response.status !== 404) return yield* new BrowserRunCleanupError({
43
+ reason: "provider",
44
+ status: response.status
45
+ });
46
+ if (response.headers["content-type"]?.split(";", 1)[0]?.trim() !== "application/json") return yield* new BrowserRunCleanupError({ reason: "malformed" });
47
+ const bytes = yield* Stream.runFoldEffect(response.stream, () => /* @__PURE__ */ new Uint8Array(), (body, chunk) => {
48
+ if (body.byteLength + chunk.byteLength > 16384) return Effect.fail(new BrowserRunCleanupError({ reason: "malformed" }));
49
+ const combined = new Uint8Array(body.byteLength + chunk.byteLength);
50
+ combined.set(body);
51
+ combined.set(chunk, body.byteLength);
52
+ return Effect.succeed(combined);
53
+ }).pipe(Effect.mapError(() => new BrowserRunCleanupError({ reason: "malformed" })));
54
+ const body = yield* Effect.try({
55
+ try: () => new TextDecoder("utf-8", {
56
+ fatal: true,
57
+ ignoreBOM: false
58
+ }).decode(bytes),
59
+ catch: () => new BrowserRunCleanupError({ reason: "malformed" })
60
+ });
61
+ if (response.status === 404) {
62
+ const absent = Schema.decodeUnknownOption(Schema.fromJsonString(Absent))(body, { onExcessProperty: "error" });
63
+ if (Option.isSome(absent)) return true;
64
+ return yield* new BrowserRunCleanupError({ reason: "malformed" });
65
+ }
66
+ if (method === "DELETE") return (yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Closed))(body).pipe(Effect.mapError(() => new BrowserRunCleanupError({ reason: "malformed" })))).status === "closed";
67
+ const result = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Metadata))(body).pipe(Effect.mapError(() => new BrowserRunCleanupError({ reason: "malformed" })));
68
+ if (result.sessionId !== sessionId) return yield* new BrowserRunCleanupError({ reason: "malformed" });
69
+ return result.endTime !== void 0 && result.endTime > 0;
70
+ });
71
+ return { close: Effect.fn("BrowserRunSessionLifecycle.close")(function* (sessionId) {
72
+ const id = yield* Schema.decodeUnknownEffect(Identity)(Redacted.value(sessionId)).pipe(Effect.mapError(() => new BrowserRunCleanupError({ reason: "configuration" })));
73
+ if (yield* request("DELETE", id)) return;
74
+ for (let read = 0; read < 2; read++) if (yield* request("GET", id)) return;
75
+ return yield* new BrowserRunCleanupError({ reason: "pending" });
76
+ }, Effect.timeoutOrElse({
77
+ duration: "10 seconds",
78
+ orElse: () => Effect.fail(new BrowserRunCleanupError({ reason: "timeout" }))
79
+ }), Effect.withTracerEnabled(false)) };
80
+ }));
81
+ }
82
+ };
83
+ //#endregion
4
84
  //#region src/interactive-browser.ts
5
85
  const browserRunInteractiveImplementation = SandboxImplementation.make({
6
86
  isolation: "isolated",
@@ -15,7 +95,6 @@ const MAX_LIVE_VIEW_EXPIRY_MILLIS = 60 * 6e4;
15
95
  const MAX_HANDOFF_TIMEOUT_MILLIS = 30 * 6e4;
16
96
  const MAX_HOST_TEXT_LENGTH = 8 * 1024;
17
97
  const CLEANUP_STEP_TIMEOUT_MILLIS = 1e4;
18
- const CLOSE_SESSION_TIMEOUT_MILLIS = 1e4;
19
98
  const ACTION_NETWORK_QUIET_MILLIS = 200;
20
99
  const ACTION_NETWORK_SETTLE_MILLIS = 2e3;
21
100
  const ACTION_POST_STATE_MILLIS = 250;
@@ -149,13 +228,14 @@ var BrowserRunHandoffState = class extends Schema.Class("BrowserRunHandoffState"
149
228
  var BrowserRunInteractiveBinding = class BrowserRunInteractiveBinding extends Context.Service()("@effect-agent/platform-cloudflare/BrowserRunInteractiveBinding") {
150
229
  static layer(options) {
151
230
  return Layer.effect(BrowserRunInteractiveBinding)(Effect.gen(function* () {
231
+ const lifecycle = yield* BrowserRunSessionLifecycle;
152
232
  const viewport = options.viewport === void 0 ? void 0 : yield* decodeViewport(options.viewport);
153
233
  return {
154
234
  launch: async (keepAliveMillis) => makeProductionBrowser(await puppeteer.launch(options.browser, {
155
235
  keep_alive: keepAliveMillis,
156
236
  ...viewport === void 0 ? {} : { defaultViewport: { ...viewport } }
157
237
  })),
158
- connect: async (sessionId) => makeProductionBrowser(await puppeteer.connect(options.browser, sessionId))
238
+ closeSession: (sessionId) => lifecycle.close(sessionId).pipe(Effect.mapError((cause) => actionError("close", cause)))
159
239
  };
160
240
  }));
161
241
  }
@@ -919,6 +999,24 @@ const cdpCommand = (page, state, command, parameters, output, malformedMessage)
919
999
  return yield* Schema.decodeUnknownEffect(output)(raw).pipe(Effect.mapError(() => protocolError(malformedMessage)));
920
1000
  }));
921
1001
  const makeHostService = (binding) => {
1002
+ const closeSession = binding.closeSession;
1003
+ const terminate = Effect.fn("BrowserRunInteractiveHost.terminate")(function* (sessionId, entries) {
1004
+ const deadline = (yield* Clock.currentTimeMillis) + 1e4;
1005
+ yield* closeSession(sessionId).pipe(Effect.interruptible, Effect.timeoutOrElse({
1006
+ duration: "10 seconds",
1007
+ orElse: () => Effect.fail(actionError("close"))
1008
+ }));
1009
+ const remaining = deadline - (yield* Clock.currentTimeMillis);
1010
+ if (remaining <= 0) return yield* Effect.logWarning("Local browser teardown skipped after confirmed termination deadline");
1011
+ yield* runTeardown(entries).pipe(Effect.flatMap((failures) => Effect.forEach(failures, (failure) => Effect.logWarning(failure.warning), { discard: true })), Effect.interruptible, Effect.timeout(`${remaining} millis`), Effect.catchCause(() => Effect.logWarning("Local browser teardown incomplete after confirmed termination")));
1012
+ });
1013
+ const closeAcquired = Effect.fn("BrowserRunInteractiveHost.closeAcquired")(function* (browser) {
1014
+ const sessionId = yield* Effect.try({
1015
+ try: browser.sessionId,
1016
+ catch: () => actionError("close")
1017
+ }).pipe(Effect.flatMap(Schema.decodeUnknownEffect(BrowserRunSessionId)), Effect.mapError(() => actionError("close")), Effect.onError(() => closeWithWarning(browser.close, "Closing an unidentified browser failed")));
1018
+ yield* terminate(Redacted.make(sessionId), [closeEntry(browser.close, "Closing the local browser connection failed")]);
1019
+ });
922
1020
  const open = Effect.fn("BrowserRunInteractiveHost.open")(function* (policy) {
923
1021
  const fixedPolicy = yield* snapshotPolicy(policy);
924
1022
  const startedAt = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
@@ -929,21 +1027,18 @@ const makeHostService = (binding) => {
929
1027
  violation: { value: void 0 },
930
1028
  pendingRequests: /* @__PURE__ */ new Set()
931
1029
  };
932
- const lifecycle = {
933
- managedTeardownInstalled: false,
934
- explicitCloseInvoked: false
935
- };
1030
+ const lifecycle = { managedTeardownInstalled: false };
936
1031
  const closers = [];
937
1032
  const releaseBeforeManaged = (entry) => Effect.suspend(() => lifecycle.managedTeardownInstalled ? Effect.void : entry.close.pipe(Effect.catchCause(() => Effect.logWarning(entry.warning))));
938
1033
  const browser = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
939
- try: (signal) => closeLateAcquisition(signal, () => binding.launch(keepAliveMillis(fixedPolicy)), (acquired) => acquired.close()),
1034
+ try: (signal) => closeLateAcquisition(signal, () => binding.launch(keepAliveMillis(fixedPolicy)), (acquired) => Effect.runPromise(closeAcquired(acquired))),
940
1035
  catch: (cause) => isCapacityRefusal(cause) ? InteractiveBrowserCapacityError.make({
941
1036
  implementation: browserRunInteractiveImplementation,
942
1037
  message: "Browser Run has no capacity for a new browser session"
943
1038
  }) : protocolError("Launching the Browser Run session failed", cause)
944
1039
  }), fixedPolicy, startedAt), (acquired) => {
945
1040
  state.disconnected.value = true;
946
- return releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser failed"));
1041
+ return lifecycle.managedTeardownInstalled ? Effect.void : closeAcquired(acquired).pipe(Effect.catch(() => Effect.logWarning("Whole-browser cleanup remains unconfirmed")));
947
1042
  }, { interruptible: true });
948
1043
  closers.push(closeEntry(browser.close, "Closing the interactive browser failed"));
949
1044
  const disconnected = () => {
@@ -993,19 +1088,18 @@ const makeHostService = (binding) => {
993
1088
  const setupFailure = stateFailure(state);
994
1089
  if (setupFailure !== void 0) return yield* setupFailure;
995
1090
  const teardown = yield* Effect.uninterruptible(Effect.gen(function* () {
996
- const cached = yield* Effect.cached(runTeardown(closers));
1091
+ const cached = yield* Effect.cached(terminate(Redacted.make(sessionIdValue), closers));
997
1092
  lifecycle.managedTeardownInstalled = true;
998
1093
  yield* Effect.addFinalizer(() => Effect.uninterruptible(Effect.sync(() => {
999
1094
  state.closed.value = true;
1000
1095
  state.disconnected.value = true;
1001
- }).pipe(Effect.andThen(cached), Effect.flatMap((failures) => lifecycle.explicitCloseInvoked ? Effect.void : Effect.forEach(failures, (failure) => Effect.logWarning(failure.warning)).pipe(Effect.asVoid)))));
1096
+ }).pipe(Effect.andThen(cached), Effect.catch(() => Effect.logWarning("Whole-browser cleanup remains unconfirmed")))));
1002
1097
  return cached;
1003
1098
  }));
1004
1099
  const close = Effect.uninterruptible(Effect.sync(() => {
1005
- lifecycle.explicitCloseInvoked = true;
1006
1100
  state.closed.value = true;
1007
1101
  state.disconnected.value = true;
1008
- }).pipe(Effect.andThen(teardown), Effect.flatMap((failures) => failures[0] === void 0 ? Effect.void : Effect.fail(failures[0].error))));
1102
+ }).pipe(Effect.andThen(teardown)));
1009
1103
  const runtime = yield* makeHandle(page, fixedPolicy, startedAt, state, close);
1010
1104
  const currentPagePreflight = decodeActionResult(page, fixedPolicy).pipe(Effect.asVoid);
1011
1105
  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"))));
@@ -1038,29 +1132,10 @@ const makeHostService = (binding) => {
1038
1132
  close
1039
1133
  };
1040
1134
  });
1041
- const closeSession = Effect.fn("BrowserRunInteractiveHost.closeSession")(function* (sessionId) {
1042
- const decoded = yield* Schema.decodeUnknownEffect(Schema.Redacted(BrowserRunSessionId))(sessionId).pipe(Effect.mapError(() => policyError("The Browser Run cleanup session identity is malformed")));
1043
- return yield* Effect.scoped(Effect.gen(function* () {
1044
- const closeAttempted = { value: false };
1045
- const browser = yield* Effect.acquireRelease(Effect.tryPromise({
1046
- try: (signal) => closeLateAcquisition(signal, () => binding.connect(Redacted.value(decoded)), (acquired) => acquired.close()),
1047
- catch: (cause) => actionError("close", cause)
1048
- }), (acquired) => closeAttempted.value ? Effect.void : closeWithWarning(acquired.close, "Closing the leaked Browser Run session failed"), { interruptible: true });
1049
- return yield* Effect.tryPromise({
1050
- try: () => {
1051
- closeAttempted.value = true;
1052
- return browser.close();
1053
- },
1054
- catch: (cause) => actionError("close", cause)
1055
- });
1056
- })).pipe(Effect.timeoutOrElse({
1057
- duration: Duration.millis(CLOSE_SESSION_TIMEOUT_MILLIS),
1058
- orElse: () => Effect.fail(actionError("close"))
1059
- }));
1060
- });
1061
1135
  return BrowserRunInteractiveHost.of({
1062
1136
  open,
1063
- closeSession
1137
+ closeSession,
1138
+ cleanupSemantics: "confirmed-terminal"
1064
1139
  });
1065
1140
  };
1066
1141
  /** Cloudflare host controls and private session identity for one scoped Browser Run pass. */
@@ -1074,6 +1149,6 @@ const browserRunInteractiveLayer = () => Layer.effect(InteractiveBrowser, Effect
1074
1149
  return InteractiveBrowser.of({ open: (policy) => host.open(policy).pipe(Effect.map((session) => session.handle)) });
1075
1150
  }));
1076
1151
  //#endregion
1077
- export { BrowserRunHandoffRequest, BrowserRunHandoffResult, BrowserRunHandoffState, BrowserRunInteractiveBinding, BrowserRunInteractiveHost, BrowserRunLiveViewRequest, BrowserRunLiveViewResult, BrowserRunViewport, browserRunInteractiveHostLayer, browserRunInteractiveImplementation, browserRunInteractiveLayer, isBrowserRunUndispatchedActionError };
1152
+ export { BrowserRunCleanupError, BrowserRunHandoffRequest, BrowserRunHandoffResult, BrowserRunHandoffState, BrowserRunInteractiveBinding, BrowserRunInteractiveHost, BrowserRunLiveViewRequest, BrowserRunLiveViewResult, BrowserRunSessionLifecycle, BrowserRunViewport, browserRunInteractiveHostLayer, browserRunInteractiveImplementation, browserRunInteractiveLayer, isBrowserRunUndispatchedActionError };
1078
1153
 
1079
1154
  //# sourceMappingURL=interactive-browser.mjs.map