@effect-agent/platform-cloudflare 0.1.0-beta.30 → 0.1.0-beta.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-quick-action.d.mts +2 -1
- package/dist/browser-quick-action.mjs +15 -1
- package/dist/browser-quick-action.mjs.map +1 -1
- package/dist/browser-rest-capture.mjs +13 -1
- package/dist/browser-rest-capture.mjs.map +1 -1
- package/dist/index.d.mts +10 -10
- package/dist/index.mjs +2 -2
- package/dist/interactive-browser.d.mts +57 -4
- package/dist/interactive-browser.mjs +277 -57
- package/dist/interactive-browser.mjs.map +1 -1
- package/package.json +8 -8
- package/src/browser-quick-action.ts +25 -2
- package/src/browser-rest-capture.ts +20 -1
- package/src/interactive-browser.ts +741 -152
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Context, Duration, Effect, Layer, Option, Ref, Schema, Semaphore } from "effect";
|
|
2
|
-
import { BrowserActionResult, BrowserNavigationResult, BrowserTextResult, InteractiveBrowser, InteractiveBrowserActionError, InteractiveBrowserBusyError, InteractiveBrowserCapacityError, InteractiveBrowserExpiredError, InteractiveBrowserLimitError, InteractiveBrowserPolicy, InteractiveBrowserPolicyDeniedError, InteractiveBrowserProtocolError, SandboxImplementation } from "@effect-agent/sandbox";
|
|
1
|
+
import { Context, Duration, Effect, Layer, Option, Redacted, Ref, Schema, Semaphore } from "effect";
|
|
2
|
+
import { BrowserActionResult, BrowserNavigationResult, BrowserScreenshotRequest, BrowserScrollRequest, BrowserTextResult, InteractiveBrowser, InteractiveBrowserActionError, InteractiveBrowserBusyError, InteractiveBrowserCapacityError, InteractiveBrowserExpiredError, InteractiveBrowserLimitError, InteractiveBrowserPolicy, InteractiveBrowserPolicyDeniedError, InteractiveBrowserProtocolError, PageScreenshotResult, SandboxImplementation } from "@effect-agent/sandbox";
|
|
3
3
|
import puppeteer from "@cloudflare/puppeteer";
|
|
4
4
|
//#region src/interactive-browser.ts
|
|
5
5
|
const browserRunInteractiveImplementation = SandboxImplementation.make({
|
|
@@ -8,7 +8,17 @@ const browserRunInteractiveImplementation = SandboxImplementation.make({
|
|
|
8
8
|
});
|
|
9
9
|
const MIN_KEEP_ALIVE_MILLIS = 1e4;
|
|
10
10
|
const MAX_KEEP_ALIVE_MILLIS = 6e5;
|
|
11
|
-
const
|
|
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));
|
|
12
22
|
const TextObservation = Schema.Union([
|
|
13
23
|
Schema.Struct({
|
|
14
24
|
_tag: Schema.Literal("Text"),
|
|
@@ -20,17 +30,71 @@ const TextObservation = Schema.Union([
|
|
|
20
30
|
observed: Schema.Natural
|
|
21
31
|
})
|
|
22
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
|
+
}) {};
|
|
23
75
|
/** Host-supplied Browser Run binding projected into one fakeable launch operation. */
|
|
24
76
|
var BrowserRunInteractiveBinding = class BrowserRunInteractiveBinding extends Context.Service()("@effect-agent/platform-cloudflare/BrowserRunInteractiveBinding") {
|
|
25
77
|
static layer(options) {
|
|
26
|
-
return Layer.succeed(BrowserRunInteractiveBinding)({
|
|
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
|
+
});
|
|
27
82
|
}
|
|
28
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") {};
|
|
29
86
|
const makeProductionRequest = (request) => ({
|
|
30
87
|
url: () => request.url(),
|
|
31
88
|
abort: () => request.abort("blockedbyclient"),
|
|
32
89
|
continue: () => request.continue()
|
|
33
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
|
+
});
|
|
34
98
|
const makeProductionPage = (page) => {
|
|
35
99
|
const listeners = /* @__PURE__ */ new Map();
|
|
36
100
|
return {
|
|
@@ -85,7 +149,20 @@ const makeProductionPage = (page) => {
|
|
|
85
149
|
}
|
|
86
150
|
}, value);
|
|
87
151
|
},
|
|
88
|
-
click: (selector) => page.click(selector)
|
|
152
|
+
click: (selector) => page.click(selector),
|
|
153
|
+
screenshot: (fullPage) => page.screenshot({
|
|
154
|
+
type: "png",
|
|
155
|
+
fullPage
|
|
156
|
+
}),
|
|
157
|
+
scroll: (deltaX, deltaY) => page.evaluate((x, y) => {
|
|
158
|
+
const scrollBy = Reflect.get(globalThis, "scrollBy");
|
|
159
|
+
Reflect.apply(scrollBy, globalThis, [{
|
|
160
|
+
left: x,
|
|
161
|
+
top: y,
|
|
162
|
+
behavior: "instant"
|
|
163
|
+
}]);
|
|
164
|
+
}, deltaX, deltaY),
|
|
165
|
+
createCdpSession: async () => makeProductionCdpSession(await page.createCDPSession())
|
|
89
166
|
};
|
|
90
167
|
};
|
|
91
168
|
const makeProductionContext = (context) => ({
|
|
@@ -95,6 +172,7 @@ const makeProductionContext = (context) => ({
|
|
|
95
172
|
const makeProductionBrowser = (browser) => ({
|
|
96
173
|
createContext: async () => makeProductionContext(await browser.createBrowserContext()),
|
|
97
174
|
close: () => browser.close(),
|
|
175
|
+
sessionId: () => browser.sessionId(),
|
|
98
176
|
isConnected: () => browser.isConnected(),
|
|
99
177
|
onDisconnected: (listener) => {
|
|
100
178
|
browser.on("disconnected", listener);
|
|
@@ -143,18 +221,21 @@ const hostAllowed = (policy, value) => {
|
|
|
143
221
|
}
|
|
144
222
|
};
|
|
145
223
|
const keepAliveMillis = (policy) => Math.max(MIN_KEEP_ALIVE_MILLIS, Math.min(MAX_KEEP_ALIVE_MILLIS, policy.maxElapsedMillis));
|
|
146
|
-
const closeLateAcquisition = async (signal, acquire) => {
|
|
224
|
+
const closeLateAcquisition = async (signal, acquire, close) => {
|
|
147
225
|
const acquired = await acquire();
|
|
148
226
|
if (!signal.aborted) return acquired;
|
|
149
227
|
try {
|
|
150
|
-
await
|
|
228
|
+
await close(acquired);
|
|
151
229
|
} catch {}
|
|
152
230
|
throw new Error("The interrupted browser acquisition completed late");
|
|
153
231
|
};
|
|
154
232
|
const closeWithWarning = (close, warning) => Effect.tryPromise({
|
|
155
233
|
try: close,
|
|
156
234
|
catch: () => protocolError(warning)
|
|
157
|
-
}).pipe(Effect.
|
|
235
|
+
}).pipe(Effect.timeoutOrElse({
|
|
236
|
+
duration: Duration.millis(CLEANUP_STEP_TIMEOUT_MILLIS),
|
|
237
|
+
orElse: () => Effect.fail(protocolError(warning))
|
|
238
|
+
}), Effect.catchCause(() => Effect.logWarning(warning)));
|
|
158
239
|
const deadlineError = Effect.fn("BrowserRunInteractive.deadlineError")(function* (policy, startedAt) {
|
|
159
240
|
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
160
241
|
return yield* InteractiveBrowserLimitError.make({
|
|
@@ -177,7 +258,7 @@ const withinDeadline = Effect.fn("BrowserRunInteractive.withinDeadline")(functio
|
|
|
177
258
|
});
|
|
178
259
|
const stateFailure = (state) => {
|
|
179
260
|
if (state.violation.value !== void 0) return state.violation.value;
|
|
180
|
-
if (state.disconnected.value || state.uncertain.value) return expiredError();
|
|
261
|
+
if (state.closed.value || state.disconnected.value || state.uncertain.value) return expiredError();
|
|
181
262
|
};
|
|
182
263
|
const awaitPendingRequests = (state) => Effect.suspend(() => {
|
|
183
264
|
const pending = [...state.pendingRequests];
|
|
@@ -223,7 +304,7 @@ const makeRequestListener = (policy, state) => (request) => {
|
|
|
223
304
|
});
|
|
224
305
|
state.pendingRequests.add(observed);
|
|
225
306
|
};
|
|
226
|
-
const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page, policy, startedAt, state) {
|
|
307
|
+
const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page, policy, startedAt, state, close) {
|
|
227
308
|
const permits = yield* Semaphore.make(1);
|
|
228
309
|
const actions = yield* Ref.make(0);
|
|
229
310
|
const remote = (operation, evaluate) => Effect.tryPromise({
|
|
@@ -275,65 +356,131 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page
|
|
|
275
356
|
message: "The browser handle already has an operation in flight"
|
|
276
357
|
}))));
|
|
277
358
|
return {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
359
|
+
handle: {
|
|
360
|
+
navigate: (request) => run(Effect.gen(function* () {
|
|
361
|
+
yield* remote("navigate", () => page.goto(request.url));
|
|
362
|
+
return yield* decodeNavigationResult(page, policy);
|
|
363
|
+
}), Effect.suspend(() => hostAllowed(policy, request.url) ? Effect.void : Effect.fail(policyError("The navigation URL is outside the browser policy")))),
|
|
364
|
+
readText: (request) => run(Effect.gen(function* () {
|
|
365
|
+
const raw = yield* remote("read-text", () => page.readText(request.selector, policy.maxReturnedBytes));
|
|
366
|
+
const observation = yield* Schema.decodeUnknownEffect(TextObservation)(raw).pipe(Effect.mapError((cause) => protocolError("The browser returned a malformed text observation", cause)));
|
|
367
|
+
if (observation._tag === "MissingElement") return yield* actionError("read-text");
|
|
368
|
+
if (observation._tag === "OverLimit") return yield* InteractiveBrowserLimitError.make({
|
|
369
|
+
implementation: browserRunInteractiveImplementation,
|
|
370
|
+
limit: "returned-bytes",
|
|
371
|
+
maximum: policy.maxReturnedBytes,
|
|
372
|
+
observed: observation.observed,
|
|
373
|
+
message: "The browser returned-text limit was reached"
|
|
374
|
+
});
|
|
375
|
+
const observed = new TextEncoder().encode(observation.text).byteLength;
|
|
376
|
+
if (observed > policy.maxReturnedBytes) return yield* InteractiveBrowserLimitError.make({
|
|
377
|
+
implementation: browserRunInteractiveImplementation,
|
|
378
|
+
limit: "returned-bytes",
|
|
379
|
+
maximum: policy.maxReturnedBytes,
|
|
380
|
+
observed,
|
|
381
|
+
message: "The browser returned-text limit was reached"
|
|
382
|
+
});
|
|
383
|
+
return yield* Schema.decodeUnknownEffect(BrowserTextResult)({ text: observation.text }).pipe(Effect.mapError((cause) => protocolError("The browser returned malformed page text", cause)));
|
|
384
|
+
})),
|
|
385
|
+
fill: (request) => run(remote("fill", () => page.fill(request.selector, request.value)).pipe(Effect.andThen(decodeActionResult(page, policy)))),
|
|
386
|
+
click: (request) => run(remote("click", () => page.click(request.selector)).pipe(Effect.andThen(decodeActionResult(page, policy)))),
|
|
387
|
+
screenshot: (request) => Schema.decodeUnknownEffect(BrowserScreenshotRequest)(request).pipe(Effect.mapError(() => policyError("The browser screenshot request is malformed")), Effect.flatMap((decoded) => run(Effect.gen(function* () {
|
|
388
|
+
const raw = yield* remote("screenshot", () => page.screenshot(decoded.fullPage));
|
|
389
|
+
const bytes = yield* Schema.decodeUnknownEffect(PngBytes)(raw).pipe(Effect.mapError(() => protocolError("The browser returned a malformed PNG screenshot")));
|
|
390
|
+
if (bytes.length > policy.maxReturnedBytes) return yield* InteractiveBrowserLimitError.make({
|
|
391
|
+
implementation: browserRunInteractiveImplementation,
|
|
392
|
+
limit: "returned-bytes",
|
|
393
|
+
maximum: policy.maxReturnedBytes,
|
|
394
|
+
observed: bytes.length,
|
|
395
|
+
message: "The browser screenshot byte limit was reached"
|
|
396
|
+
});
|
|
397
|
+
return yield* Schema.decodeUnknownEffect(PageScreenshotResult)({
|
|
398
|
+
implementation: browserRunInteractiveImplementation,
|
|
399
|
+
mediaType: "image/png",
|
|
400
|
+
bytes: new Uint8Array(bytes)
|
|
401
|
+
}).pipe(Effect.mapError(() => protocolError("The browser returned a malformed PNG screenshot")));
|
|
402
|
+
}), decodeActionResult(page, policy).pipe(Effect.asVoid)))),
|
|
403
|
+
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)))),
|
|
404
|
+
close
|
|
405
|
+
},
|
|
406
|
+
run
|
|
305
407
|
};
|
|
306
408
|
});
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
409
|
+
const remainingMillis = Effect.fn("BrowserRunInteractive.remainingMillis")(function* (policy, startedAt) {
|
|
410
|
+
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
411
|
+
return Math.max(0, policy.maxElapsedMillis - Math.max(0, now - startedAt));
|
|
412
|
+
});
|
|
413
|
+
const closeEntry = (close, warning) => ({
|
|
414
|
+
close: Effect.tryPromise({
|
|
415
|
+
try: close,
|
|
416
|
+
catch: (cause) => actionError("close", cause)
|
|
417
|
+
}).pipe(Effect.timeoutOrElse({
|
|
418
|
+
duration: Duration.millis(CLEANUP_STEP_TIMEOUT_MILLIS),
|
|
419
|
+
orElse: () => Effect.fail(actionError("close"))
|
|
420
|
+
})),
|
|
421
|
+
warning
|
|
422
|
+
});
|
|
423
|
+
const syncCloseEntry = (close, warning) => ({
|
|
424
|
+
close: Effect.try({
|
|
425
|
+
try: close,
|
|
426
|
+
catch: (cause) => actionError("close", cause)
|
|
427
|
+
}),
|
|
428
|
+
warning
|
|
429
|
+
});
|
|
430
|
+
const runTeardown = (entries) => Effect.forEach([...entries].reverse(), (entry) => entry.close.pipe(Effect.match({
|
|
431
|
+
onFailure: (error) => ({
|
|
432
|
+
error,
|
|
433
|
+
warning: entry.warning
|
|
434
|
+
}),
|
|
435
|
+
onSuccess: () => void 0
|
|
436
|
+
}))).pipe(Effect.map((failures) => failures.filter((failure) => failure !== void 0)));
|
|
437
|
+
const cdpCommand = (page, state, command, parameters, output, malformedMessage) => Effect.scoped(Effect.gen(function* () {
|
|
438
|
+
const cdp = yield* Effect.acquireRelease(Effect.tryPromise({
|
|
439
|
+
try: (signal) => closeLateAcquisition(signal, page.createCdpSession, (acquired) => acquired.detach()),
|
|
440
|
+
catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the Cloudflare browser control session failed", cause)
|
|
441
|
+
}), (acquired) => closeWithWarning(acquired.detach, "Detaching the Cloudflare browser control session failed"), { interruptible: true });
|
|
442
|
+
const raw = yield* Effect.tryPromise({
|
|
443
|
+
try: () => cdp.send(command, parameters),
|
|
444
|
+
catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("The Cloudflare browser control command failed", cause)
|
|
445
|
+
});
|
|
446
|
+
return yield* Schema.decodeUnknownEffect(output)(raw).pipe(Effect.mapError(() => protocolError(malformedMessage)));
|
|
447
|
+
}));
|
|
448
|
+
const makeHostService = (binding) => {
|
|
449
|
+
const open = Effect.fn("BrowserRunInteractiveHost.open")(function* (policy) {
|
|
311
450
|
const fixedPolicy = yield* snapshotPolicy(policy);
|
|
312
451
|
const startedAt = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
313
452
|
const state = {
|
|
453
|
+
closed: { value: false },
|
|
314
454
|
disconnected: { value: false },
|
|
315
455
|
uncertain: { value: false },
|
|
316
456
|
violation: { value: void 0 },
|
|
317
457
|
pendingRequests: /* @__PURE__ */ new Set()
|
|
318
458
|
};
|
|
459
|
+
const lifecycle = {
|
|
460
|
+
managedTeardownInstalled: false,
|
|
461
|
+
explicitCloseInvoked: false
|
|
462
|
+
};
|
|
463
|
+
const closers = [];
|
|
464
|
+
const releaseBeforeManaged = (entry) => Effect.suspend(() => lifecycle.managedTeardownInstalled ? Effect.void : entry.close.pipe(Effect.catchCause(() => Effect.logWarning(entry.warning))));
|
|
319
465
|
const browser = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
|
|
320
|
-
try: (signal) => closeLateAcquisition(signal, () => binding.launch(keepAliveMillis(fixedPolicy))),
|
|
466
|
+
try: (signal) => closeLateAcquisition(signal, () => binding.launch(keepAliveMillis(fixedPolicy)), (acquired) => acquired.close()),
|
|
321
467
|
catch: (cause) => isCapacityRefusal(cause) ? InteractiveBrowserCapacityError.make({
|
|
322
468
|
implementation: browserRunInteractiveImplementation,
|
|
323
469
|
message: "Browser Run has no capacity for a new browser session"
|
|
324
470
|
}) : protocolError("Launching the Browser Run session failed", cause)
|
|
325
|
-
}), fixedPolicy, startedAt), (acquired) =>
|
|
471
|
+
}), fixedPolicy, startedAt), (acquired) => {
|
|
326
472
|
state.disconnected.value = true;
|
|
327
|
-
|
|
473
|
+
return releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser failed"));
|
|
474
|
+
}, { interruptible: true });
|
|
475
|
+
closers.push(closeEntry(browser.close, "Closing the interactive browser failed"));
|
|
328
476
|
const disconnected = () => {
|
|
329
477
|
state.disconnected.value = true;
|
|
330
478
|
};
|
|
331
479
|
yield* Effect.acquireRelease(Effect.try({
|
|
332
480
|
try: () => browser.onDisconnected(disconnected),
|
|
333
481
|
catch: (cause) => protocolError("Installing the browser disconnect listener failed", cause)
|
|
334
|
-
}), () =>
|
|
335
|
-
|
|
336
|
-
}).pipe(Effect.catchCause(() => Effect.logWarning("Removing the browser disconnect listener failed"))));
|
|
482
|
+
}), () => releaseBeforeManaged(syncCloseEntry(() => browser.offDisconnected(disconnected), "Removing the browser disconnect listener failed")));
|
|
483
|
+
closers.push(syncCloseEntry(() => browser.offDisconnected(disconnected), "Removing the browser disconnect listener failed"));
|
|
337
484
|
if (!(yield* Effect.try({
|
|
338
485
|
try: browser.isConnected,
|
|
339
486
|
catch: (cause) => protocolError("Reading the Browser Run connection state failed", cause)
|
|
@@ -341,14 +488,20 @@ const browserRunInteractiveLayer = () => Layer.effect(InteractiveBrowser, Effect
|
|
|
341
488
|
state.disconnected.value = true;
|
|
342
489
|
return yield* expiredError();
|
|
343
490
|
}
|
|
491
|
+
const sessionIdValue = yield* Effect.try({
|
|
492
|
+
try: browser.sessionId,
|
|
493
|
+
catch: (cause) => protocolError("Reading the Browser Run session identity failed", cause)
|
|
494
|
+
}).pipe(Effect.flatMap((value) => Schema.decodeUnknownEffect(BrowserRunSessionId)(value).pipe(Effect.mapError(() => protocolError("The Browser Run session identity was malformed")))));
|
|
344
495
|
const context = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
|
|
345
|
-
try: (signal) => closeLateAcquisition(signal, browser.createContext),
|
|
496
|
+
try: (signal) => closeLateAcquisition(signal, browser.createContext, (acquired) => acquired.close()),
|
|
346
497
|
catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the browser context failed", cause)
|
|
347
|
-
}), fixedPolicy, startedAt), (acquired) =>
|
|
498
|
+
}), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser context failed")), { interruptible: true });
|
|
499
|
+
closers.push(closeEntry(context.close, "Closing the interactive browser context failed"));
|
|
348
500
|
const page = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
|
|
349
|
-
try: (signal) => closeLateAcquisition(signal, context.newPage),
|
|
501
|
+
try: (signal) => closeLateAcquisition(signal, context.newPage, (acquired) => acquired.close()),
|
|
350
502
|
catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the browser page failed", cause)
|
|
351
|
-
}), fixedPolicy, startedAt), (acquired) =>
|
|
503
|
+
}), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser page failed")), { interruptible: true });
|
|
504
|
+
closers.push(closeEntry(page.close, "Closing the interactive browser page failed"));
|
|
352
505
|
yield* withinDeadline(Effect.tryPromise({
|
|
353
506
|
try: () => page.setBypassServiceWorker(true),
|
|
354
507
|
catch: (cause) => protocolError("Bypassing browser service workers failed", cause)
|
|
@@ -357,9 +510,8 @@ const browserRunInteractiveLayer = () => Layer.effect(InteractiveBrowser, Effect
|
|
|
357
510
|
yield* Effect.acquireRelease(Effect.try({
|
|
358
511
|
try: () => page.onRequest(requestListener),
|
|
359
512
|
catch: (cause) => protocolError("Installing the browser request listener failed", cause)
|
|
360
|
-
}), () =>
|
|
361
|
-
|
|
362
|
-
}).pipe(Effect.catchCause(() => Effect.logWarning("Removing the browser request policy failed"))));
|
|
513
|
+
}), () => releaseBeforeManaged(syncCloseEntry(() => page.offRequest(requestListener), "Removing the browser request policy failed")));
|
|
514
|
+
closers.push(syncCloseEntry(() => page.offRequest(requestListener), "Removing the browser request policy failed"));
|
|
363
515
|
yield* withinDeadline(Effect.tryPromise({
|
|
364
516
|
try: () => page.setRequestInterception(true),
|
|
365
517
|
catch: (cause) => protocolError("Installing the browser request policy failed", cause)
|
|
@@ -367,10 +519,78 @@ const browserRunInteractiveLayer = () => Layer.effect(InteractiveBrowser, Effect
|
|
|
367
519
|
yield* withinDeadline(awaitPendingRequests(state), fixedPolicy, startedAt);
|
|
368
520
|
const setupFailure = stateFailure(state);
|
|
369
521
|
if (setupFailure !== void 0) return yield* setupFailure;
|
|
370
|
-
|
|
371
|
-
|
|
522
|
+
const teardown = yield* Effect.uninterruptible(Effect.gen(function* () {
|
|
523
|
+
const cached = yield* Effect.cached(runTeardown(closers));
|
|
524
|
+
lifecycle.managedTeardownInstalled = true;
|
|
525
|
+
yield* Effect.addFinalizer(() => Effect.uninterruptible(Effect.sync(() => {
|
|
526
|
+
state.closed.value = true;
|
|
527
|
+
state.disconnected.value = true;
|
|
528
|
+
}).pipe(Effect.andThen(cached), Effect.flatMap((failures) => lifecycle.explicitCloseInvoked ? Effect.void : Effect.forEach(failures, (failure) => Effect.logWarning(failure.warning)).pipe(Effect.asVoid)))));
|
|
529
|
+
return cached;
|
|
530
|
+
}));
|
|
531
|
+
const close = Effect.uninterruptible(Effect.sync(() => {
|
|
532
|
+
lifecycle.explicitCloseInvoked = true;
|
|
533
|
+
state.closed.value = true;
|
|
534
|
+
state.disconnected.value = true;
|
|
535
|
+
}).pipe(Effect.andThen(teardown), Effect.flatMap((failures) => failures[0] === void 0 ? Effect.void : Effect.fail(failures[0].error))));
|
|
536
|
+
const runtime = yield* makeHandle(page, fixedPolicy, startedAt, state, close);
|
|
537
|
+
const currentPagePreflight = decodeActionResult(page, fixedPolicy).pipe(Effect.asVoid);
|
|
538
|
+
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"))));
|
|
539
|
+
return {
|
|
540
|
+
handle: runtime.handle,
|
|
541
|
+
sessionId: Redacted.make(sessionIdValue),
|
|
542
|
+
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", {
|
|
543
|
+
mode: decoded.mode,
|
|
544
|
+
expiresInMs: decoded.expiresInMs
|
|
545
|
+
}, 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)))))),
|
|
546
|
+
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", {
|
|
547
|
+
instructions: decoded.instructions,
|
|
548
|
+
timeout: decoded.timeout
|
|
549
|
+
}, 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)))))),
|
|
550
|
+
getHandoffState: runtime.run(cdpCommand(page, state, "Cloudflare.getHandoffState", {}, HandoffStateObservation, "Cloudflare returned a malformed browser handoff state").pipe(Effect.flatMap((observation) => Schema.decodeUnknownEffect(BrowserRunHandoffState)({
|
|
551
|
+
active: observation.active,
|
|
552
|
+
...observation.handoffId === void 0 ? {} : { handoffId: Redacted.make(observation.handoffId) },
|
|
553
|
+
...observation.durationMs === void 0 ? {} : { durationMs: observation.durationMs }
|
|
554
|
+
}).pipe(Effect.mapError(() => protocolError("Cloudflare returned a malformed browser handoff state"))))), currentPagePreflight),
|
|
555
|
+
close
|
|
556
|
+
};
|
|
557
|
+
});
|
|
558
|
+
const closeSession = Effect.fn("BrowserRunInteractiveHost.closeSession")(function* (sessionId) {
|
|
559
|
+
const decoded = yield* Schema.decodeUnknownEffect(Schema.Redacted(BrowserRunSessionId))(sessionId).pipe(Effect.mapError(() => policyError("The Browser Run cleanup session identity is malformed")));
|
|
560
|
+
return yield* Effect.scoped(Effect.gen(function* () {
|
|
561
|
+
const closeAttempted = { value: false };
|
|
562
|
+
const browser = yield* Effect.acquireRelease(Effect.tryPromise({
|
|
563
|
+
try: (signal) => closeLateAcquisition(signal, () => binding.connect(Redacted.value(decoded)), (acquired) => acquired.close()),
|
|
564
|
+
catch: (cause) => actionError("close", cause)
|
|
565
|
+
}), (acquired) => closeAttempted.value ? Effect.void : closeWithWarning(acquired.close, "Closing the leaked Browser Run session failed"), { interruptible: true });
|
|
566
|
+
return yield* Effect.tryPromise({
|
|
567
|
+
try: () => {
|
|
568
|
+
closeAttempted.value = true;
|
|
569
|
+
return browser.close();
|
|
570
|
+
},
|
|
571
|
+
catch: (cause) => actionError("close", cause)
|
|
572
|
+
});
|
|
573
|
+
})).pipe(Effect.timeoutOrElse({
|
|
574
|
+
duration: Duration.millis(CLOSE_SESSION_TIMEOUT_MILLIS),
|
|
575
|
+
orElse: () => Effect.fail(actionError("close"))
|
|
576
|
+
}));
|
|
577
|
+
});
|
|
578
|
+
return BrowserRunInteractiveHost.of({
|
|
579
|
+
open,
|
|
580
|
+
closeSession
|
|
581
|
+
});
|
|
582
|
+
};
|
|
583
|
+
/** Cloudflare host controls and private session identity for one scoped Browser Run pass. */
|
|
584
|
+
const browserRunInteractiveHostLayer = () => Layer.effect(BrowserRunInteractiveHost, Effect.gen(function* () {
|
|
585
|
+
return makeHostService(yield* BrowserRunInteractiveBinding);
|
|
586
|
+
}));
|
|
587
|
+
/** Worker-only generic adapter; Cloudflare identity and controls remain host-only. */
|
|
588
|
+
const browserRunInteractiveLayer = () => Layer.effect(InteractiveBrowser, Effect.gen(function* () {
|
|
589
|
+
const binding = yield* BrowserRunInteractiveBinding;
|
|
590
|
+
const host = makeHostService(binding);
|
|
591
|
+
return InteractiveBrowser.of({ open: (policy) => host.open(policy).pipe(Effect.map((session) => session.handle)) });
|
|
372
592
|
}));
|
|
373
593
|
//#endregion
|
|
374
|
-
export { BrowserRunInteractiveBinding, browserRunInteractiveImplementation, browserRunInteractiveLayer };
|
|
594
|
+
export { BrowserRunHandoffRequest, BrowserRunHandoffResult, BrowserRunHandoffState, BrowserRunInteractiveBinding, BrowserRunInteractiveHost, BrowserRunLiveViewRequest, BrowserRunLiveViewResult, browserRunInteractiveHostLayer, browserRunInteractiveImplementation, browserRunInteractiveLayer };
|
|
375
595
|
|
|
376
596
|
//# sourceMappingURL=interactive-browser.mjs.map
|