@effect-agent/platform-cloudflare 0.1.0-beta.36 → 0.1.0-beta.38
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/index.d.mts +14 -108
- package/dist/index.mjs +8 -824
- package/dist/index.mjs.map +1 -1
- package/dist/interactive-browser-BRo-Bfvb.d.mts +144 -0
- package/dist/interactive-browser-Cy5q-ldl.mjs +1154 -0
- package/dist/interactive-browser-Cy5q-ldl.mjs.map +1 -0
- package/dist/interactive-browser.d.mts +2 -102
- package/dist/interactive-browser.mjs +2 -618
- package/dist/scheduling-B-OFqoS9.mjs +1189 -0
- package/dist/scheduling-B-OFqoS9.mjs.map +1 -0
- package/dist/scheduling-BJs_kHTx.d.mts +142 -0
- package/dist/scheduling.d.mts +2 -0
- package/dist/scheduling.mjs +2 -0
- package/package.json +11 -10
- package/src/browser-session-lifecycle.ts +142 -0
- package/src/index.ts +1 -0
- package/src/interactive-browser.ts +921 -142
- package/src/layers.ts +1 -1
- package/src/scheduling.ts +676 -0
- package/dist/interactive-browser.mjs.map +0 -1
|
@@ -1,618 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import puppeteer from "@cloudflare/puppeteer";
|
|
4
|
-
//#region src/interactive-browser.ts
|
|
5
|
-
const browserRunInteractiveImplementation = SandboxImplementation.make({
|
|
6
|
-
isolation: "isolated",
|
|
7
|
-
identity: "cloudflare-browser-run-interactive"
|
|
8
|
-
});
|
|
9
|
-
const MIN_KEEP_ALIVE_MILLIS = 1e4;
|
|
10
|
-
const MAX_KEEP_ALIVE_MILLIS = 6e5;
|
|
11
|
-
const MAX_TEXT_LENGTH = 8 * 1024 * 1024;
|
|
12
|
-
const MAX_SCREENSHOT_BYTES = 8 * 1024 * 1024;
|
|
13
|
-
const MIN_LIVE_VIEW_EXPIRY_MILLIS = 6e4;
|
|
14
|
-
const MAX_LIVE_VIEW_EXPIRY_MILLIS = 60 * 6e4;
|
|
15
|
-
const MAX_HANDOFF_TIMEOUT_MILLIS = 30 * 6e4;
|
|
16
|
-
const MAX_HOST_TEXT_LENGTH = 8 * 1024;
|
|
17
|
-
const CLEANUP_STEP_TIMEOUT_MILLIS = 1e4;
|
|
18
|
-
const CLOSE_SESSION_TIMEOUT_MILLIS = 1e4;
|
|
19
|
-
const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0));
|
|
20
|
-
const BoundedHostText = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(MAX_HOST_TEXT_LENGTH));
|
|
21
|
-
const BoundedRemoteText = Schema.String.check(Schema.isMaxLength(MAX_TEXT_LENGTH));
|
|
22
|
-
const TextObservation = Schema.Union([
|
|
23
|
-
Schema.Struct({
|
|
24
|
-
_tag: Schema.Literal("Text"),
|
|
25
|
-
text: BoundedRemoteText
|
|
26
|
-
}),
|
|
27
|
-
Schema.Struct({ _tag: Schema.Literal("MissingElement") }),
|
|
28
|
-
Schema.Struct({
|
|
29
|
-
_tag: Schema.Literal("OverLimit"),
|
|
30
|
-
observed: Schema.Natural
|
|
31
|
-
})
|
|
32
|
-
]);
|
|
33
|
-
const PngBytes = Schema.Uint8Array.check(Schema.isMaxLength(MAX_SCREENSHOT_BYTES), Schema.makeFilter((bytes) => bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10, { title: "PNG bytes" }));
|
|
34
|
-
const BrowserRunSessionId = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256), Schema.makeFilter((value) => /^[A-Za-z0-9_-]+$/.test(value), { title: "a Browser Run session identifier" }));
|
|
35
|
-
const LiveViewUrl = Schema.String.check(Schema.isMaxLength(MAX_HOST_TEXT_LENGTH), Schema.makeFilter((value) => {
|
|
36
|
-
try {
|
|
37
|
-
const url = new URL(value);
|
|
38
|
-
return url.protocol === "https:" && url.host === "live.browser.run" && url.username === "" && url.password === "" && url.pathname === "/ui/view" && url.searchParams.get("mode") === "tab" && (url.searchParams.get("wss") ?? "").startsWith("live.browser.run/api/devtools/browser/");
|
|
39
|
-
} catch {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
}, { title: "a Cloudflare Live View HTTPS URL" }));
|
|
43
|
-
const LiveViewObservation = Schema.Struct({ devtoolsFrontendUrl: LiveViewUrl });
|
|
44
|
-
const HandoffObservation = Schema.Struct({ handoffId: BoundedHostText });
|
|
45
|
-
const HandoffDuration = Schema.Natural.check(Schema.isLessThanOrEqualTo(MAX_HANDOFF_TIMEOUT_MILLIS));
|
|
46
|
-
const HandoffStateObservation = Schema.Union([Schema.Struct({
|
|
47
|
-
active: Schema.Literal(true),
|
|
48
|
-
handoffId: BoundedHostText,
|
|
49
|
-
durationMs: HandoffDuration
|
|
50
|
-
}), Schema.Struct({
|
|
51
|
-
active: Schema.Literal(false),
|
|
52
|
-
handoffId: Schema.optionalKey(BoundedHostText),
|
|
53
|
-
durationMs: Schema.optionalKey(HandoffDuration)
|
|
54
|
-
})]);
|
|
55
|
-
/** Host-only request for a redacted Cloudflare Live View URL. */
|
|
56
|
-
var BrowserRunLiveViewRequest = class extends Schema.Class("BrowserRunLiveViewRequest")({
|
|
57
|
-
mode: Schema.Literal("tab"),
|
|
58
|
-
expiresInMs: PositiveInt.check(Schema.isBetween({
|
|
59
|
-
minimum: MIN_LIVE_VIEW_EXPIRY_MILLIS,
|
|
60
|
-
maximum: MAX_LIVE_VIEW_EXPIRY_MILLIS
|
|
61
|
-
}))
|
|
62
|
-
}) {};
|
|
63
|
-
var BrowserRunLiveViewResult = class extends Schema.Class("BrowserRunLiveViewResult")({ devtoolsFrontendUrl: Schema.Redacted(LiveViewUrl) }) {};
|
|
64
|
-
/** Start one bounded handoff; controller ownership remains a consumer concern. */
|
|
65
|
-
var BrowserRunHandoffRequest = class extends Schema.Class("BrowserRunHandoffRequest")({
|
|
66
|
-
instructions: BoundedHostText.check(Schema.isMaxLength(1024)),
|
|
67
|
-
timeout: PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_HANDOFF_TIMEOUT_MILLIS))
|
|
68
|
-
}) {};
|
|
69
|
-
var BrowserRunHandoffResult = class extends Schema.Class("BrowserRunHandoffResult")({ handoffId: Schema.Redacted(BoundedHostText) }) {};
|
|
70
|
-
var BrowserRunHandoffState = class extends Schema.Class("BrowserRunHandoffState")({
|
|
71
|
-
active: Schema.Boolean,
|
|
72
|
-
handoffId: Schema.optionalKey(Schema.Redacted(BoundedHostText)),
|
|
73
|
-
durationMs: Schema.optionalKey(HandoffDuration)
|
|
74
|
-
}) {};
|
|
75
|
-
/** Host-supplied Browser Run binding projected into one fakeable launch operation. */
|
|
76
|
-
var BrowserRunInteractiveBinding = class BrowserRunInteractiveBinding extends Context.Service()("@effect-agent/platform-cloudflare/BrowserRunInteractiveBinding") {
|
|
77
|
-
static layer(options) {
|
|
78
|
-
return Layer.succeed(BrowserRunInteractiveBinding)({
|
|
79
|
-
launch: async (keepAliveMillis) => makeProductionBrowser(await puppeteer.launch(options.browser, { keep_alive: keepAliveMillis })),
|
|
80
|
-
connect: async (sessionId) => makeProductionBrowser(await puppeteer.connect(options.browser, sessionId))
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
/** Cloudflare host authority kept separate from the provider-neutral browser handle. */
|
|
85
|
-
var BrowserRunInteractiveHost = class extends Context.Service()("@effect-agent/platform-cloudflare/BrowserRunInteractiveHost") {};
|
|
86
|
-
const makeProductionRequest = (request) => ({
|
|
87
|
-
url: () => request.url(),
|
|
88
|
-
abort: () => request.abort("blockedbyclient"),
|
|
89
|
-
continue: () => request.continue()
|
|
90
|
-
});
|
|
91
|
-
const makeProductionCdpSession = (session) => ({
|
|
92
|
-
send: async (command, parameters) => {
|
|
93
|
-
const send = Reflect.get(session, "send");
|
|
94
|
-
return await Reflect.apply(send, session, [command, parameters]);
|
|
95
|
-
},
|
|
96
|
-
detach: () => session.detach()
|
|
97
|
-
});
|
|
98
|
-
const makeProductionPage = (page) => {
|
|
99
|
-
const listeners = /* @__PURE__ */ new Map();
|
|
100
|
-
return {
|
|
101
|
-
close: () => page.close(),
|
|
102
|
-
setBypassServiceWorker: (enabled) => page.setBypassServiceWorker(enabled),
|
|
103
|
-
setRequestInterception: (enabled) => page.setRequestInterception(enabled),
|
|
104
|
-
onRequest: (listener) => {
|
|
105
|
-
const sdkListener = (request) => listener(makeProductionRequest(request));
|
|
106
|
-
listeners.set(listener, sdkListener);
|
|
107
|
-
page.on("request", sdkListener);
|
|
108
|
-
},
|
|
109
|
-
offRequest: (listener) => {
|
|
110
|
-
const sdkListener = listeners.get(listener);
|
|
111
|
-
if (sdkListener !== void 0) {
|
|
112
|
-
page.off("request", sdkListener);
|
|
113
|
-
listeners.delete(listener);
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
goto: async (url) => {
|
|
117
|
-
await page.goto(url, {
|
|
118
|
-
waitUntil: "networkidle0",
|
|
119
|
-
timeout: 0
|
|
120
|
-
});
|
|
121
|
-
},
|
|
122
|
-
url: () => page.url(),
|
|
123
|
-
readText: (selector, maximumBytes) => page.evaluate((requestedSelector, maximum) => {
|
|
124
|
-
const pageDocument = Reflect.get(globalThis, "document");
|
|
125
|
-
const element = requestedSelector === void 0 ? Reflect.get(pageDocument, "body") : Reflect.apply(Reflect.get(pageDocument, "querySelector"), pageDocument, [requestedSelector]);
|
|
126
|
-
if (element === null) return { _tag: "MissingElement" };
|
|
127
|
-
const innerText = Reflect.get(element, "innerText");
|
|
128
|
-
const textContent = Reflect.get(element, "textContent");
|
|
129
|
-
const text = typeof innerText === "string" ? innerText : typeof textContent === "string" ? textContent : "";
|
|
130
|
-
const observed = new TextEncoder().encode(text).byteLength;
|
|
131
|
-
return observed > maximum ? {
|
|
132
|
-
_tag: "OverLimit",
|
|
133
|
-
observed
|
|
134
|
-
} : {
|
|
135
|
-
_tag: "Text",
|
|
136
|
-
text
|
|
137
|
-
};
|
|
138
|
-
}, selector, maximumBytes),
|
|
139
|
-
fill: async (selector, value) => {
|
|
140
|
-
await page.$eval(selector, (element, nextValue) => {
|
|
141
|
-
let prototype = Reflect.getPrototypeOf(element);
|
|
142
|
-
let setValue;
|
|
143
|
-
while (prototype !== null) {
|
|
144
|
-
const setter = Reflect.getOwnPropertyDescriptor(prototype, "value")?.set;
|
|
145
|
-
if (typeof setter === "function") {
|
|
146
|
-
setValue = setter;
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
prototype = Reflect.getPrototypeOf(prototype);
|
|
150
|
-
}
|
|
151
|
-
if (setValue === void 0) throw new Error("The selector did not resolve to a fillable field");
|
|
152
|
-
const focus = Reflect.get(element, "focus");
|
|
153
|
-
if (typeof focus === "function") Reflect.apply(focus, element, []);
|
|
154
|
-
Reflect.apply(setValue, element, [nextValue]);
|
|
155
|
-
const dispatchEvent = Reflect.get(element, "dispatchEvent");
|
|
156
|
-
if (typeof dispatchEvent === "function") {
|
|
157
|
-
Reflect.apply(dispatchEvent, element, [new Event("input", { bubbles: true })]);
|
|
158
|
-
Reflect.apply(dispatchEvent, element, [new Event("change", { bubbles: true })]);
|
|
159
|
-
}
|
|
160
|
-
}, value);
|
|
161
|
-
},
|
|
162
|
-
click: (selector) => page.click(selector),
|
|
163
|
-
screenshot: (fullPage) => page.screenshot({
|
|
164
|
-
type: "png",
|
|
165
|
-
fullPage
|
|
166
|
-
}),
|
|
167
|
-
scroll: (deltaX, deltaY) => page.evaluate((x, y) => {
|
|
168
|
-
const scrollBy = Reflect.get(globalThis, "scrollBy");
|
|
169
|
-
Reflect.apply(scrollBy, globalThis, [{
|
|
170
|
-
left: x,
|
|
171
|
-
top: y,
|
|
172
|
-
behavior: "instant"
|
|
173
|
-
}]);
|
|
174
|
-
}, deltaX, deltaY),
|
|
175
|
-
createCdpSession: async () => makeProductionCdpSession(await page.createCDPSession())
|
|
176
|
-
};
|
|
177
|
-
};
|
|
178
|
-
const makeProductionContext = (context) => ({
|
|
179
|
-
newPage: async () => makeProductionPage(await context.newPage()),
|
|
180
|
-
close: () => context.close()
|
|
181
|
-
});
|
|
182
|
-
const makeProductionBrowser = (browser) => ({
|
|
183
|
-
createContext: async () => makeProductionContext(await browser.createBrowserContext()),
|
|
184
|
-
close: () => browser.close(),
|
|
185
|
-
sessionId: () => browser.sessionId(),
|
|
186
|
-
isConnected: () => browser.isConnected(),
|
|
187
|
-
onDisconnected: (listener) => {
|
|
188
|
-
browser.on("disconnected", listener);
|
|
189
|
-
},
|
|
190
|
-
offDisconnected: (listener) => {
|
|
191
|
-
browser.off("disconnected", listener);
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
const protocolError = (message, cause) => InteractiveBrowserProtocolError.make({
|
|
195
|
-
implementation: browserRunInteractiveImplementation,
|
|
196
|
-
message,
|
|
197
|
-
...cause === void 0 ? {} : { cause }
|
|
198
|
-
});
|
|
199
|
-
const actionError = (operation, cause) => InteractiveBrowserActionError.make({
|
|
200
|
-
implementation: browserRunInteractiveImplementation,
|
|
201
|
-
operation,
|
|
202
|
-
message: `The interactive browser ${operation} operation failed`,
|
|
203
|
-
...cause === void 0 ? {} : { cause }
|
|
204
|
-
});
|
|
205
|
-
const policyError = (message) => InteractiveBrowserPolicyDeniedError.make({
|
|
206
|
-
implementation: browserRunInteractiveImplementation,
|
|
207
|
-
message
|
|
208
|
-
});
|
|
209
|
-
const expiredError = () => InteractiveBrowserExpiredError.make({
|
|
210
|
-
implementation: browserRunInteractiveImplementation,
|
|
211
|
-
message: "The remote browser is no longer usable"
|
|
212
|
-
});
|
|
213
|
-
const causeText = (cause) => {
|
|
214
|
-
if (cause instanceof Error) return cause.message.slice(0, 8e3);
|
|
215
|
-
return String(cause).slice(0, 8e3);
|
|
216
|
-
};
|
|
217
|
-
const isCapacityRefusal = (cause) => /(^|\D)429(\D|$)|browser time limit|capacity|too many concurrent/i.test(causeText(cause));
|
|
218
|
-
const isRemoteClosure = (cause) => /target closed|browser.*closed|session.*closed|connection.*closed|not connected|websocket.*closed/i.test(causeText(cause));
|
|
219
|
-
const snapshotPolicy = Effect.fn("BrowserRunInteractive.snapshotPolicy")(function* (input) {
|
|
220
|
-
const decoded = yield* Schema.decodeUnknownEffect(InteractiveBrowserPolicy)(input).pipe(Effect.mapError(() => policyError("The interactive browser policy is malformed")));
|
|
221
|
-
if (decoded.network._tag === "PublicWeb") return yield* InteractiveBrowserUnsupportedError.make({
|
|
222
|
-
implementation: browserRunInteractiveImplementation,
|
|
223
|
-
feature: "policy",
|
|
224
|
-
message: "Cloudflare Browser Run cannot enforce the PublicWeb network policy for all session traffic"
|
|
225
|
-
});
|
|
226
|
-
return Object.freeze({
|
|
227
|
-
network: decoded.network._tag === "ExactHosts" ? Object.freeze({
|
|
228
|
-
_tag: decoded.network._tag,
|
|
229
|
-
allowedHosts: Object.freeze([...decoded.network.allowedHosts])
|
|
230
|
-
}) : Object.freeze({ _tag: decoded.network._tag }),
|
|
231
|
-
maxActions: decoded.maxActions,
|
|
232
|
-
maxElapsedMillis: decoded.maxElapsedMillis,
|
|
233
|
-
maxReturnedBytes: decoded.maxReturnedBytes
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
const hostAllowed = (policy, value) => {
|
|
237
|
-
if (policy.network._tag === "Unrestricted") return true;
|
|
238
|
-
try {
|
|
239
|
-
const url = new URL(value);
|
|
240
|
-
return url.protocol === "https:" && url.username === "" && url.password === "" && policy.network.allowedHosts.some((host) => host === url.host);
|
|
241
|
-
} catch {
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
const keepAliveMillis = (policy) => Math.max(MIN_KEEP_ALIVE_MILLIS, Math.min(MAX_KEEP_ALIVE_MILLIS, policy.maxElapsedMillis));
|
|
246
|
-
const closeLateAcquisition = async (signal, acquire, close) => {
|
|
247
|
-
const acquired = await acquire();
|
|
248
|
-
if (!signal.aborted) return acquired;
|
|
249
|
-
try {
|
|
250
|
-
await close(acquired);
|
|
251
|
-
} catch {}
|
|
252
|
-
throw new Error("The interrupted browser acquisition completed late");
|
|
253
|
-
};
|
|
254
|
-
const closeWithWarning = (close, warning) => Effect.tryPromise({
|
|
255
|
-
try: close,
|
|
256
|
-
catch: () => protocolError(warning)
|
|
257
|
-
}).pipe(Effect.timeoutOrElse({
|
|
258
|
-
duration: Duration.millis(CLEANUP_STEP_TIMEOUT_MILLIS),
|
|
259
|
-
orElse: () => Effect.fail(protocolError(warning))
|
|
260
|
-
}), Effect.catchCause(() => Effect.logWarning(warning)));
|
|
261
|
-
const deadlineError = Effect.fn("BrowserRunInteractive.deadlineError")(function* (policy, startedAt) {
|
|
262
|
-
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
263
|
-
return yield* InteractiveBrowserLimitError.make({
|
|
264
|
-
implementation: browserRunInteractiveImplementation,
|
|
265
|
-
limit: "elapsed",
|
|
266
|
-
maximum: policy.maxElapsedMillis,
|
|
267
|
-
observed: Math.max(policy.maxElapsedMillis, now - startedAt),
|
|
268
|
-
message: "The browser elapsed-time limit was reached"
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
const withinDeadline = Effect.fn("BrowserRunInteractive.withinDeadline")(function* (effect, policy, startedAt, onTimeout) {
|
|
272
|
-
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
273
|
-
const elapsed = Math.max(0, now - startedAt);
|
|
274
|
-
const remaining = policy.maxElapsedMillis - elapsed;
|
|
275
|
-
if (remaining <= 0) return yield* deadlineError(policy, startedAt);
|
|
276
|
-
return yield* effect.pipe(Effect.timeoutOrElse({
|
|
277
|
-
duration: Duration.millis(remaining),
|
|
278
|
-
orElse: () => Effect.sync(() => onTimeout?.()).pipe(Effect.andThen(deadlineError(policy, startedAt)))
|
|
279
|
-
}));
|
|
280
|
-
});
|
|
281
|
-
const stateFailure = (state) => {
|
|
282
|
-
if (state.violation.value !== void 0) return state.violation.value;
|
|
283
|
-
if (state.closed.value || state.disconnected.value || state.uncertain.value) return expiredError();
|
|
284
|
-
};
|
|
285
|
-
const awaitPendingRequests = (state) => Effect.suspend(() => {
|
|
286
|
-
const pending = [...state.pendingRequests];
|
|
287
|
-
return pending.length === 0 ? Effect.void : Effect.promise(() => Promise.all(pending)).pipe(Effect.asVoid, Effect.andThen(awaitPendingRequests(state)));
|
|
288
|
-
});
|
|
289
|
-
const decodeNavigationResult = Effect.fn("BrowserRunInteractive.decodeNavigationResult")(function* (page, policy) {
|
|
290
|
-
const url = yield* Effect.try({
|
|
291
|
-
try: page.url,
|
|
292
|
-
catch: (cause) => protocolError("Reading the browser navigation URL failed", cause)
|
|
293
|
-
});
|
|
294
|
-
const result = yield* Schema.decodeUnknownEffect(BrowserNavigationResult)({ url }).pipe(Effect.mapError((cause) => protocolError("The browser returned a malformed navigation URL", cause)));
|
|
295
|
-
if (!hostAllowed(policy, result.url)) return yield* policyError("The browser returned an off-policy page URL");
|
|
296
|
-
return result;
|
|
297
|
-
});
|
|
298
|
-
const decodeActionResult = Effect.fn("BrowserRunInteractive.decodeActionResult")(function* (page, policy) {
|
|
299
|
-
const url = yield* Effect.try({
|
|
300
|
-
try: page.url,
|
|
301
|
-
catch: (cause) => protocolError("Reading the browser page URL failed", cause)
|
|
302
|
-
});
|
|
303
|
-
const result = yield* Schema.decodeUnknownEffect(BrowserActionResult)({ url }).pipe(Effect.mapError((cause) => protocolError("The browser returned a malformed page URL", cause)));
|
|
304
|
-
if (!hostAllowed(policy, result.url)) return yield* policyError("The browser returned an off-policy page URL");
|
|
305
|
-
return result;
|
|
306
|
-
});
|
|
307
|
-
const makeRequestListener = (policy, state) => (request) => {
|
|
308
|
-
let allowed = false;
|
|
309
|
-
try {
|
|
310
|
-
allowed = hostAllowed(policy, request.url());
|
|
311
|
-
} catch {
|
|
312
|
-
allowed = false;
|
|
313
|
-
}
|
|
314
|
-
if (!allowed) state.violation.value = policyError("The browser requested an off-policy URL");
|
|
315
|
-
let settlement;
|
|
316
|
-
try {
|
|
317
|
-
settlement = allowed ? request.continue() : request.abort();
|
|
318
|
-
} catch {
|
|
319
|
-
state.violation.value = protocolError("Resolving an intercepted browser request failed");
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
const observed = settlement.catch(() => {
|
|
323
|
-
state.violation.value = protocolError("Resolving an intercepted browser request failed");
|
|
324
|
-
}).finally(() => {
|
|
325
|
-
state.pendingRequests.delete(observed);
|
|
326
|
-
});
|
|
327
|
-
state.pendingRequests.add(observed);
|
|
328
|
-
};
|
|
329
|
-
const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page, policy, startedAt, state, close) {
|
|
330
|
-
const permits = yield* Semaphore.make(1);
|
|
331
|
-
const actions = yield* Ref.make(0);
|
|
332
|
-
const remote = (operation, evaluate) => Effect.tryPromise({
|
|
333
|
-
try: evaluate,
|
|
334
|
-
catch: (cause) => {
|
|
335
|
-
if (state.disconnected.value || isRemoteClosure(cause)) {
|
|
336
|
-
state.disconnected.value = true;
|
|
337
|
-
return expiredError();
|
|
338
|
-
}
|
|
339
|
-
return actionError(operation, cause);
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
const run = (effect, preflight = Effect.void) => permits.withPermitsIfAvailable(1)(Effect.gen(function* () {
|
|
343
|
-
const unavailable = stateFailure(state);
|
|
344
|
-
if (unavailable !== void 0) return yield* unavailable;
|
|
345
|
-
yield* preflight;
|
|
346
|
-
const admitted = yield* Ref.modify(actions, (count) => count >= policy.maxActions ? [{
|
|
347
|
-
allowed: false,
|
|
348
|
-
observed: count + 1
|
|
349
|
-
}, count] : [{
|
|
350
|
-
allowed: true,
|
|
351
|
-
observed: count + 1
|
|
352
|
-
}, count + 1]);
|
|
353
|
-
if (!admitted.allowed) return yield* InteractiveBrowserLimitError.make({
|
|
354
|
-
implementation: browserRunInteractiveImplementation,
|
|
355
|
-
limit: "actions",
|
|
356
|
-
maximum: policy.maxActions,
|
|
357
|
-
observed: admitted.observed,
|
|
358
|
-
message: "The browser action limit was reached"
|
|
359
|
-
});
|
|
360
|
-
const completed = effect.pipe(Effect.catch((error) => {
|
|
361
|
-
const failure = stateFailure(state);
|
|
362
|
-
return Effect.fail(failure ?? error);
|
|
363
|
-
}), Effect.flatMap((result) => awaitPendingRequests(state).pipe(Effect.flatMap(() => {
|
|
364
|
-
const failure = stateFailure(state);
|
|
365
|
-
return failure === void 0 ? Effect.succeed(result) : Effect.fail(failure);
|
|
366
|
-
}))));
|
|
367
|
-
return yield* withinDeadline(completed, policy, startedAt, () => {
|
|
368
|
-
state.uncertain.value = true;
|
|
369
|
-
}).pipe(Effect.onInterrupt(() => Effect.sync(() => {
|
|
370
|
-
state.uncertain.value = true;
|
|
371
|
-
})), Effect.catch((error) => {
|
|
372
|
-
if (Schema.is(InteractiveBrowserLimitError)(error) && error.limit === "elapsed") return Effect.fail(error);
|
|
373
|
-
const failure = stateFailure(state);
|
|
374
|
-
return Effect.fail(failure ?? error);
|
|
375
|
-
}));
|
|
376
|
-
})).pipe(Effect.flatMap((result) => Option.isSome(result) ? Effect.succeed(result.value) : Effect.fail(InteractiveBrowserBusyError.make({
|
|
377
|
-
implementation: browserRunInteractiveImplementation,
|
|
378
|
-
message: "The browser handle already has an operation in flight"
|
|
379
|
-
}))));
|
|
380
|
-
return {
|
|
381
|
-
handle: {
|
|
382
|
-
navigate: (request) => run(Effect.gen(function* () {
|
|
383
|
-
yield* remote("navigate", () => page.goto(request.url));
|
|
384
|
-
return yield* decodeNavigationResult(page, policy);
|
|
385
|
-
}), Effect.suspend(() => Schema.is(InteractiveBrowserTargetUrl)(request.url) && hostAllowed(policy, request.url) ? Effect.void : Effect.fail(policyError("The navigation URL is outside the browser policy")))),
|
|
386
|
-
readText: (request) => run(Effect.gen(function* () {
|
|
387
|
-
const raw = yield* remote("read-text", () => page.readText(request.selector, policy.maxReturnedBytes));
|
|
388
|
-
const observation = yield* Schema.decodeUnknownEffect(TextObservation)(raw).pipe(Effect.mapError((cause) => protocolError("The browser returned a malformed text observation", cause)));
|
|
389
|
-
if (observation._tag === "MissingElement") return yield* actionError("read-text");
|
|
390
|
-
if (observation._tag === "OverLimit") return yield* InteractiveBrowserLimitError.make({
|
|
391
|
-
implementation: browserRunInteractiveImplementation,
|
|
392
|
-
limit: "returned-bytes",
|
|
393
|
-
maximum: policy.maxReturnedBytes,
|
|
394
|
-
observed: observation.observed,
|
|
395
|
-
message: "The browser returned-text limit was reached"
|
|
396
|
-
});
|
|
397
|
-
const observed = new TextEncoder().encode(observation.text).byteLength;
|
|
398
|
-
if (observed > policy.maxReturnedBytes) return yield* InteractiveBrowserLimitError.make({
|
|
399
|
-
implementation: browserRunInteractiveImplementation,
|
|
400
|
-
limit: "returned-bytes",
|
|
401
|
-
maximum: policy.maxReturnedBytes,
|
|
402
|
-
observed,
|
|
403
|
-
message: "The browser returned-text limit was reached"
|
|
404
|
-
});
|
|
405
|
-
return yield* Schema.decodeUnknownEffect(BrowserTextResult)({ text: observation.text }).pipe(Effect.mapError((cause) => protocolError("The browser returned malformed page text", cause)));
|
|
406
|
-
})),
|
|
407
|
-
fill: (request) => run(remote("fill", () => page.fill(request.selector, request.value)).pipe(Effect.andThen(decodeActionResult(page, policy)))),
|
|
408
|
-
click: (request) => run(remote("click", () => page.click(request.selector)).pipe(Effect.andThen(decodeActionResult(page, policy)))),
|
|
409
|
-
screenshot: (request) => Schema.decodeUnknownEffect(BrowserScreenshotRequest)(request).pipe(Effect.mapError(() => policyError("The browser screenshot request is malformed")), Effect.flatMap((decoded) => run(Effect.gen(function* () {
|
|
410
|
-
const raw = yield* remote("screenshot", () => page.screenshot(decoded.fullPage));
|
|
411
|
-
const bytes = yield* Schema.decodeUnknownEffect(PngBytes)(raw).pipe(Effect.mapError(() => protocolError("The browser returned a malformed PNG screenshot")));
|
|
412
|
-
if (bytes.length > policy.maxReturnedBytes) return yield* InteractiveBrowserLimitError.make({
|
|
413
|
-
implementation: browserRunInteractiveImplementation,
|
|
414
|
-
limit: "returned-bytes",
|
|
415
|
-
maximum: policy.maxReturnedBytes,
|
|
416
|
-
observed: bytes.length,
|
|
417
|
-
message: "The browser screenshot byte limit was reached"
|
|
418
|
-
});
|
|
419
|
-
return yield* Schema.decodeUnknownEffect(PageScreenshotResult)({
|
|
420
|
-
implementation: browserRunInteractiveImplementation,
|
|
421
|
-
mediaType: "image/png",
|
|
422
|
-
bytes: new Uint8Array(bytes)
|
|
423
|
-
}).pipe(Effect.mapError(() => protocolError("The browser returned a malformed PNG screenshot")));
|
|
424
|
-
}), decodeActionResult(page, policy).pipe(Effect.asVoid)))),
|
|
425
|
-
scroll: (request) => Schema.decodeUnknownEffect(BrowserScrollRequest)(request).pipe(Effect.mapError(() => policyError("The browser scroll request is malformed")), Effect.flatMap((decoded) => run(remote("scroll", () => page.scroll(decoded.deltaX, decoded.deltaY)).pipe(Effect.andThen(decodeActionResult(page, policy))), decodeActionResult(page, policy).pipe(Effect.asVoid)))),
|
|
426
|
-
close
|
|
427
|
-
},
|
|
428
|
-
run
|
|
429
|
-
};
|
|
430
|
-
});
|
|
431
|
-
const remainingMillis = Effect.fn("BrowserRunInteractive.remainingMillis")(function* (policy, startedAt) {
|
|
432
|
-
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
433
|
-
return Math.max(0, policy.maxElapsedMillis - Math.max(0, now - startedAt));
|
|
434
|
-
});
|
|
435
|
-
const closeEntry = (close, warning) => ({
|
|
436
|
-
close: Effect.tryPromise({
|
|
437
|
-
try: close,
|
|
438
|
-
catch: (cause) => actionError("close", cause)
|
|
439
|
-
}).pipe(Effect.timeoutOrElse({
|
|
440
|
-
duration: Duration.millis(CLEANUP_STEP_TIMEOUT_MILLIS),
|
|
441
|
-
orElse: () => Effect.fail(actionError("close"))
|
|
442
|
-
})),
|
|
443
|
-
warning
|
|
444
|
-
});
|
|
445
|
-
const syncCloseEntry = (close, warning) => ({
|
|
446
|
-
close: Effect.try({
|
|
447
|
-
try: close,
|
|
448
|
-
catch: (cause) => actionError("close", cause)
|
|
449
|
-
}),
|
|
450
|
-
warning
|
|
451
|
-
});
|
|
452
|
-
const runTeardown = (entries) => Effect.forEach([...entries].reverse(), (entry) => entry.close.pipe(Effect.match({
|
|
453
|
-
onFailure: (error) => ({
|
|
454
|
-
error,
|
|
455
|
-
warning: entry.warning
|
|
456
|
-
}),
|
|
457
|
-
onSuccess: () => void 0
|
|
458
|
-
}))).pipe(Effect.map((failures) => failures.filter((failure) => failure !== void 0)));
|
|
459
|
-
const cdpCommand = (page, state, command, parameters, output, malformedMessage) => Effect.scoped(Effect.gen(function* () {
|
|
460
|
-
const cdp = yield* Effect.acquireRelease(Effect.tryPromise({
|
|
461
|
-
try: (signal) => closeLateAcquisition(signal, page.createCdpSession, (acquired) => acquired.detach()),
|
|
462
|
-
catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the Cloudflare browser control session failed", cause)
|
|
463
|
-
}), (acquired) => closeWithWarning(acquired.detach, "Detaching the Cloudflare browser control session failed"), { interruptible: true });
|
|
464
|
-
const raw = yield* Effect.tryPromise({
|
|
465
|
-
try: () => cdp.send(command, parameters),
|
|
466
|
-
catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("The Cloudflare browser control command failed", cause)
|
|
467
|
-
});
|
|
468
|
-
return yield* Schema.decodeUnknownEffect(output)(raw).pipe(Effect.mapError(() => protocolError(malformedMessage)));
|
|
469
|
-
}));
|
|
470
|
-
const makeHostService = (binding) => {
|
|
471
|
-
const open = Effect.fn("BrowserRunInteractiveHost.open")(function* (policy) {
|
|
472
|
-
const fixedPolicy = yield* snapshotPolicy(policy);
|
|
473
|
-
const startedAt = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
474
|
-
const state = {
|
|
475
|
-
closed: { value: false },
|
|
476
|
-
disconnected: { value: false },
|
|
477
|
-
uncertain: { value: false },
|
|
478
|
-
violation: { value: void 0 },
|
|
479
|
-
pendingRequests: /* @__PURE__ */ new Set()
|
|
480
|
-
};
|
|
481
|
-
const lifecycle = {
|
|
482
|
-
managedTeardownInstalled: false,
|
|
483
|
-
explicitCloseInvoked: false
|
|
484
|
-
};
|
|
485
|
-
const closers = [];
|
|
486
|
-
const releaseBeforeManaged = (entry) => Effect.suspend(() => lifecycle.managedTeardownInstalled ? Effect.void : entry.close.pipe(Effect.catchCause(() => Effect.logWarning(entry.warning))));
|
|
487
|
-
const browser = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
|
|
488
|
-
try: (signal) => closeLateAcquisition(signal, () => binding.launch(keepAliveMillis(fixedPolicy)), (acquired) => acquired.close()),
|
|
489
|
-
catch: (cause) => isCapacityRefusal(cause) ? InteractiveBrowserCapacityError.make({
|
|
490
|
-
implementation: browserRunInteractiveImplementation,
|
|
491
|
-
message: "Browser Run has no capacity for a new browser session"
|
|
492
|
-
}) : protocolError("Launching the Browser Run session failed", cause)
|
|
493
|
-
}), fixedPolicy, startedAt), (acquired) => {
|
|
494
|
-
state.disconnected.value = true;
|
|
495
|
-
return releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser failed"));
|
|
496
|
-
}, { interruptible: true });
|
|
497
|
-
closers.push(closeEntry(browser.close, "Closing the interactive browser failed"));
|
|
498
|
-
const disconnected = () => {
|
|
499
|
-
state.disconnected.value = true;
|
|
500
|
-
};
|
|
501
|
-
yield* Effect.acquireRelease(Effect.try({
|
|
502
|
-
try: () => browser.onDisconnected(disconnected),
|
|
503
|
-
catch: (cause) => protocolError("Installing the browser disconnect listener failed", cause)
|
|
504
|
-
}), () => releaseBeforeManaged(syncCloseEntry(() => browser.offDisconnected(disconnected), "Removing the browser disconnect listener failed")));
|
|
505
|
-
closers.push(syncCloseEntry(() => browser.offDisconnected(disconnected), "Removing the browser disconnect listener failed"));
|
|
506
|
-
if (!(yield* Effect.try({
|
|
507
|
-
try: browser.isConnected,
|
|
508
|
-
catch: (cause) => protocolError("Reading the Browser Run connection state failed", cause)
|
|
509
|
-
}))) {
|
|
510
|
-
state.disconnected.value = true;
|
|
511
|
-
return yield* expiredError();
|
|
512
|
-
}
|
|
513
|
-
const sessionIdValue = yield* Effect.try({
|
|
514
|
-
try: browser.sessionId,
|
|
515
|
-
catch: (cause) => protocolError("Reading the Browser Run session identity failed", cause)
|
|
516
|
-
}).pipe(Effect.flatMap((value) => Schema.decodeUnknownEffect(BrowserRunSessionId)(value).pipe(Effect.mapError(() => protocolError("The Browser Run session identity was malformed")))));
|
|
517
|
-
const context = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
|
|
518
|
-
try: (signal) => closeLateAcquisition(signal, browser.createContext, (acquired) => acquired.close()),
|
|
519
|
-
catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the browser context failed", cause)
|
|
520
|
-
}), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser context failed")), { interruptible: true });
|
|
521
|
-
closers.push(closeEntry(context.close, "Closing the interactive browser context failed"));
|
|
522
|
-
const page = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
|
|
523
|
-
try: (signal) => closeLateAcquisition(signal, context.newPage, (acquired) => acquired.close()),
|
|
524
|
-
catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the browser page failed", cause)
|
|
525
|
-
}), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser page failed")), { interruptible: true });
|
|
526
|
-
closers.push(closeEntry(page.close, "Closing the interactive browser page failed"));
|
|
527
|
-
yield* withinDeadline(Effect.tryPromise({
|
|
528
|
-
try: () => page.setBypassServiceWorker(true),
|
|
529
|
-
catch: (cause) => protocolError("Bypassing browser service workers failed", cause)
|
|
530
|
-
}), fixedPolicy, startedAt);
|
|
531
|
-
const requestListener = makeRequestListener(fixedPolicy, state);
|
|
532
|
-
yield* Effect.acquireRelease(Effect.try({
|
|
533
|
-
try: () => page.onRequest(requestListener),
|
|
534
|
-
catch: (cause) => protocolError("Installing the browser request listener failed", cause)
|
|
535
|
-
}), () => releaseBeforeManaged(syncCloseEntry(() => page.offRequest(requestListener), "Removing the browser request policy failed")));
|
|
536
|
-
closers.push(syncCloseEntry(() => page.offRequest(requestListener), "Removing the browser request policy failed"));
|
|
537
|
-
yield* withinDeadline(Effect.tryPromise({
|
|
538
|
-
try: () => page.setRequestInterception(true),
|
|
539
|
-
catch: (cause) => protocolError("Installing the browser request policy failed", cause)
|
|
540
|
-
}), fixedPolicy, startedAt);
|
|
541
|
-
yield* withinDeadline(awaitPendingRequests(state), fixedPolicy, startedAt);
|
|
542
|
-
const setupFailure = stateFailure(state);
|
|
543
|
-
if (setupFailure !== void 0) return yield* setupFailure;
|
|
544
|
-
const teardown = yield* Effect.uninterruptible(Effect.gen(function* () {
|
|
545
|
-
const cached = yield* Effect.cached(runTeardown(closers));
|
|
546
|
-
lifecycle.managedTeardownInstalled = true;
|
|
547
|
-
yield* Effect.addFinalizer(() => Effect.uninterruptible(Effect.sync(() => {
|
|
548
|
-
state.closed.value = true;
|
|
549
|
-
state.disconnected.value = true;
|
|
550
|
-
}).pipe(Effect.andThen(cached), Effect.flatMap((failures) => lifecycle.explicitCloseInvoked ? Effect.void : Effect.forEach(failures, (failure) => Effect.logWarning(failure.warning)).pipe(Effect.asVoid)))));
|
|
551
|
-
return cached;
|
|
552
|
-
}));
|
|
553
|
-
const close = Effect.uninterruptible(Effect.sync(() => {
|
|
554
|
-
lifecycle.explicitCloseInvoked = true;
|
|
555
|
-
state.closed.value = true;
|
|
556
|
-
state.disconnected.value = true;
|
|
557
|
-
}).pipe(Effect.andThen(teardown), Effect.flatMap((failures) => failures[0] === void 0 ? Effect.void : Effect.fail(failures[0].error))));
|
|
558
|
-
const runtime = yield* makeHandle(page, fixedPolicy, startedAt, state, close);
|
|
559
|
-
const currentPagePreflight = decodeActionResult(page, fixedPolicy).pipe(Effect.asVoid);
|
|
560
|
-
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"))));
|
|
561
|
-
return {
|
|
562
|
-
handle: runtime.handle,
|
|
563
|
-
sessionId: Redacted.make(sessionIdValue),
|
|
564
|
-
getLiveView: (request) => Schema.decodeUnknownEffect(BrowserRunLiveViewRequest)(request).pipe(Effect.mapError(() => policyError("The Live View request is malformed")), Effect.flatMap((decoded) => runtime.run(cdpCommand(page, state, "Cloudflare.getLiveView", {
|
|
565
|
-
mode: decoded.mode,
|
|
566
|
-
expiresInMs: decoded.expiresInMs
|
|
567
|
-
}, LiveViewObservation, "Cloudflare returned a malformed Live View response").pipe(Effect.flatMap((observation) => Schema.decodeUnknownEffect(BrowserRunLiveViewResult)({ devtoolsFrontendUrl: Redacted.make(observation.devtoolsFrontendUrl) }).pipe(Effect.mapError(() => protocolError("Cloudflare returned a malformed Live View response"))))), currentPagePreflight.pipe(Effect.andThen(requestFitsSession(decoded.expiresInMs)))))),
|
|
568
|
-
handoff: (request) => Schema.decodeUnknownEffect(BrowserRunHandoffRequest)(request).pipe(Effect.mapError(() => policyError("The browser handoff request is malformed")), Effect.flatMap((decoded) => runtime.run(cdpCommand(page, state, "Cloudflare.handoff", {
|
|
569
|
-
instructions: decoded.instructions,
|
|
570
|
-
timeout: decoded.timeout
|
|
571
|
-
}, HandoffObservation, "Cloudflare returned a malformed browser handoff response").pipe(Effect.flatMap((observation) => Schema.decodeUnknownEffect(BrowserRunHandoffResult)({ handoffId: Redacted.make(observation.handoffId) }).pipe(Effect.mapError(() => protocolError("Cloudflare returned a malformed browser handoff response"))))), currentPagePreflight.pipe(Effect.andThen(requestFitsSession(decoded.timeout)))))),
|
|
572
|
-
getHandoffState: runtime.run(cdpCommand(page, state, "Cloudflare.getHandoffState", {}, HandoffStateObservation, "Cloudflare returned a malformed browser handoff state").pipe(Effect.flatMap((observation) => Schema.decodeUnknownEffect(BrowserRunHandoffState)({
|
|
573
|
-
active: observation.active,
|
|
574
|
-
...observation.handoffId === void 0 ? {} : { handoffId: Redacted.make(observation.handoffId) },
|
|
575
|
-
...observation.durationMs === void 0 ? {} : { durationMs: observation.durationMs }
|
|
576
|
-
}).pipe(Effect.mapError(() => protocolError("Cloudflare returned a malformed browser handoff state"))))), currentPagePreflight),
|
|
577
|
-
close
|
|
578
|
-
};
|
|
579
|
-
});
|
|
580
|
-
const closeSession = Effect.fn("BrowserRunInteractiveHost.closeSession")(function* (sessionId) {
|
|
581
|
-
const decoded = yield* Schema.decodeUnknownEffect(Schema.Redacted(BrowserRunSessionId))(sessionId).pipe(Effect.mapError(() => policyError("The Browser Run cleanup session identity is malformed")));
|
|
582
|
-
return yield* Effect.scoped(Effect.gen(function* () {
|
|
583
|
-
const closeAttempted = { value: false };
|
|
584
|
-
const browser = yield* Effect.acquireRelease(Effect.tryPromise({
|
|
585
|
-
try: (signal) => closeLateAcquisition(signal, () => binding.connect(Redacted.value(decoded)), (acquired) => acquired.close()),
|
|
586
|
-
catch: (cause) => actionError("close", cause)
|
|
587
|
-
}), (acquired) => closeAttempted.value ? Effect.void : closeWithWarning(acquired.close, "Closing the leaked Browser Run session failed"), { interruptible: true });
|
|
588
|
-
return yield* Effect.tryPromise({
|
|
589
|
-
try: () => {
|
|
590
|
-
closeAttempted.value = true;
|
|
591
|
-
return browser.close();
|
|
592
|
-
},
|
|
593
|
-
catch: (cause) => actionError("close", cause)
|
|
594
|
-
});
|
|
595
|
-
})).pipe(Effect.timeoutOrElse({
|
|
596
|
-
duration: Duration.millis(CLOSE_SESSION_TIMEOUT_MILLIS),
|
|
597
|
-
orElse: () => Effect.fail(actionError("close"))
|
|
598
|
-
}));
|
|
599
|
-
});
|
|
600
|
-
return BrowserRunInteractiveHost.of({
|
|
601
|
-
open,
|
|
602
|
-
closeSession
|
|
603
|
-
});
|
|
604
|
-
};
|
|
605
|
-
/** Cloudflare host controls and private session identity for one scoped Browser Run pass. */
|
|
606
|
-
const browserRunInteractiveHostLayer = () => Layer.effect(BrowserRunInteractiveHost, Effect.gen(function* () {
|
|
607
|
-
return makeHostService(yield* BrowserRunInteractiveBinding);
|
|
608
|
-
}));
|
|
609
|
-
/** Worker-only generic adapter; Cloudflare identity and controls remain host-only. */
|
|
610
|
-
const browserRunInteractiveLayer = () => Layer.effect(InteractiveBrowser, Effect.gen(function* () {
|
|
611
|
-
const binding = yield* BrowserRunInteractiveBinding;
|
|
612
|
-
const host = makeHostService(binding);
|
|
613
|
-
return InteractiveBrowser.of({ open: (policy) => host.open(policy).pipe(Effect.map((session) => session.handle)) });
|
|
614
|
-
}));
|
|
615
|
-
//#endregion
|
|
616
|
-
export { BrowserRunHandoffRequest, BrowserRunHandoffResult, BrowserRunHandoffState, BrowserRunInteractiveBinding, BrowserRunInteractiveHost, BrowserRunLiveViewRequest, BrowserRunLiveViewResult, browserRunInteractiveHostLayer, browserRunInteractiveImplementation, browserRunInteractiveLayer };
|
|
617
|
-
|
|
618
|
-
//# sourceMappingURL=interactive-browser.mjs.map
|
|
1
|
+
import { a as BrowserRunInteractiveHost, c as BrowserRunViewport, d as browserRunInteractiveLayer, f as isBrowserRunUndispatchedActionError, i as BrowserRunInteractiveBinding, l as browserRunInteractiveHostLayer, m as BrowserRunSessionLifecycle, n as BrowserRunHandoffResult, o as BrowserRunLiveViewRequest, p as BrowserRunCleanupError, r as BrowserRunHandoffState, s as BrowserRunLiveViewResult, t as BrowserRunHandoffRequest, u as browserRunInteractiveImplementation } from "./interactive-browser-Cy5q-ldl.mjs";
|
|
2
|
+
export { BrowserRunCleanupError, BrowserRunHandoffRequest, BrowserRunHandoffResult, BrowserRunHandoffState, BrowserRunInteractiveBinding, BrowserRunInteractiveHost, BrowserRunLiveViewRequest, BrowserRunLiveViewResult, BrowserRunSessionLifecycle, BrowserRunViewport, browserRunInteractiveHostLayer, browserRunInteractiveImplementation, browserRunInteractiveLayer, isBrowserRunUndispatchedActionError };
|