@effect-agent/platform-cloudflare 0.1.0-beta.106 → 0.1.0-beta.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CloudflareThreadClient.d.mts +47 -47
- package/dist/{InteractiveBrowser-BR16UPbF.d.mts → InteractiveBrowser-CKp6pqmw.d.mts} +4 -2
- package/dist/{InteractiveBrowser-BCS9g0tn.mjs → InteractiveBrowser-l6J4eP6T.mjs} +286 -39
- package/dist/InteractiveBrowser-l6J4eP6T.mjs.map +1 -0
- package/dist/InteractiveBrowser.d.mts +1 -1
- package/dist/InteractiveBrowser.mjs +1 -1
- package/dist/ProtectedBrowser.d.mts +3 -3
- package/dist/ProtectedBrowser.mjs +160 -135
- package/dist/ProtectedBrowser.mjs.map +1 -1
- package/dist/{ThreadObject-CLBMWEPn.d.mts → ThreadObject-D_vvj7Qx.d.mts} +31 -31
- package/dist/ThreadObject.d.mts +1 -1
- package/dist/index.d.mts +1 -1
- package/package.json +1 -1
- package/src/InteractiveBrowser.ts +242 -48
- package/src/internal/browser-binding.ts +82 -0
- package/src/internal/browser-failure.ts +92 -0
- package/src/internal/browser-file-selection.ts +126 -0
- package/src/internal/browser-session-lifecycle.ts +38 -5
- package/src/protected-browser/binding.ts +170 -129
- package/src/protected-browser/host.ts +52 -19
- package/src/protected-browser/native.ts +45 -17
- package/src/protected-browser/policy.ts +75 -39
- package/dist/InteractiveBrowser-BCS9g0tn.mjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
4
|
type Browser,
|
|
5
5
|
type BrowserContext,
|
|
6
6
|
type CDPSession,
|
|
@@ -8,6 +8,7 @@ import puppeteer, {
|
|
|
8
8
|
type Page,
|
|
9
9
|
} from "@cloudflare/puppeteer";
|
|
10
10
|
import {
|
|
11
|
+
Cause,
|
|
11
12
|
Context,
|
|
12
13
|
Clock,
|
|
13
14
|
Duration,
|
|
@@ -21,6 +22,8 @@ import {
|
|
|
21
22
|
} from "effect";
|
|
22
23
|
import {
|
|
23
24
|
BrowserActionResult,
|
|
25
|
+
BrowserSelectFileRequest,
|
|
26
|
+
BrowserFileSelectionResult,
|
|
24
27
|
BrowserScreenshotRequest,
|
|
25
28
|
BrowserScrollRequest,
|
|
26
29
|
BrowserNavigationResult,
|
|
@@ -43,6 +46,13 @@ import {
|
|
|
43
46
|
import { PageScreenshotResult } from "effect-agent/page-screenshot";
|
|
44
47
|
import { SandboxImplementation } from "effect-agent/sandbox";
|
|
45
48
|
|
|
49
|
+
import { BrowserRunBinding } from "./internal/browser-binding.ts";
|
|
50
|
+
import {
|
|
51
|
+
BrowserRunFailure,
|
|
52
|
+
reportBrowserCause,
|
|
53
|
+
reportedBrowserError,
|
|
54
|
+
} from "./internal/browser-failure.ts";
|
|
55
|
+
import { makeFileSelection } from "./internal/browser-file-selection.ts";
|
|
46
56
|
import {
|
|
47
57
|
BrowserRunSessionLifecycle,
|
|
48
58
|
type BrowserRunLifecycleOptions,
|
|
@@ -328,6 +338,11 @@ export interface BrowserRunInteractivePage {
|
|
|
328
338
|
signal: AbortSignal,
|
|
329
339
|
onDispatch: () => void,
|
|
330
340
|
) => Promise<unknown>;
|
|
341
|
+
readonly selectFile: (
|
|
342
|
+
request: BrowserSelectFileRequest,
|
|
343
|
+
signal: AbortSignal,
|
|
344
|
+
onDispatch: () => void,
|
|
345
|
+
) => Promise<unknown>;
|
|
331
346
|
readonly screenshot: (fullPage: boolean) => Promise<unknown>;
|
|
332
347
|
readonly scroll: (deltaX: number, deltaY: number) => Promise<void>;
|
|
333
348
|
readonly setViewport: (viewport: BrowserRunViewport) => Promise<void>;
|
|
@@ -371,22 +386,26 @@ export class BrowserRunInteractiveBinding extends Context.Service<
|
|
|
371
386
|
return Layer.effect(BrowserRunInteractiveBinding)(
|
|
372
387
|
Effect.gen(function* () {
|
|
373
388
|
const lifecycle = yield* BrowserRunSessionLifecycle;
|
|
389
|
+
const binding = yield* BrowserRunBinding;
|
|
374
390
|
|
|
375
391
|
const viewport =
|
|
376
392
|
options.viewport === undefined ? undefined : yield* decodeViewport(options.viewport);
|
|
377
393
|
|
|
378
394
|
return {
|
|
379
395
|
acquire: async (keepAliveMillis: number) =>
|
|
380
|
-
|
|
396
|
+
binding.acquire(keepAliveMillis, "interactive.acquire"),
|
|
381
397
|
connect: async (sessionId: string) =>
|
|
382
|
-
makeProductionBrowser(
|
|
398
|
+
makeProductionBrowser(
|
|
399
|
+
await binding.connect(sessionId, "interactive.connect"),
|
|
400
|
+
viewport,
|
|
401
|
+
),
|
|
383
402
|
closeSession: (sessionId: Redacted.Redacted<string>) =>
|
|
384
403
|
lifecycle
|
|
385
404
|
.close(sessionId)
|
|
386
405
|
.pipe(Effect.mapError((cause) => actionError("close", cause))),
|
|
387
406
|
};
|
|
388
407
|
}),
|
|
389
|
-
);
|
|
408
|
+
).pipe(Layer.provide(BrowserRunBinding.layer(options.browser)));
|
|
390
409
|
}
|
|
391
410
|
}
|
|
392
411
|
|
|
@@ -706,6 +725,7 @@ const runObservedPageAction = async (
|
|
|
706
725
|
signal: AbortSignal,
|
|
707
726
|
onDispatch: () => void,
|
|
708
727
|
action: (element: NonNullable<Awaited<ReturnType<Page["$"]>>>) => Promise<void>,
|
|
728
|
+
validate?: (element: NonNullable<Awaited<ReturnType<Page["$"]>>>) => Promise<boolean>,
|
|
709
729
|
): Promise<unknown> => {
|
|
710
730
|
if (signal.aborted) throw new BrowserRunActionUndispatched(0);
|
|
711
731
|
|
|
@@ -737,6 +757,8 @@ const runObservedPageAction = async (
|
|
|
737
757
|
|
|
738
758
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
739
759
|
try {
|
|
760
|
+
if (validate !== undefined && !(await validate(matches[0])))
|
|
761
|
+
throw new BrowserRunActionUndispatched(1);
|
|
740
762
|
tracker = makeActionRequestTracker(page, signal);
|
|
741
763
|
// No await between the final cancellation fence and SDK dispatch. Once
|
|
742
764
|
// dispatched, interruption is uncertain, even if the SDK later resolves.
|
|
@@ -761,10 +783,18 @@ const runObservedPageAction = async (
|
|
|
761
783
|
};
|
|
762
784
|
|
|
763
785
|
const makeProductionPage = (page: Page): BrowserRunInteractivePage => {
|
|
786
|
+
const prepareFileSelection = makeFileSelection(page);
|
|
764
787
|
const listeners = new Map<BrowserRunInteractiveRequestListener, (request: HTTPRequest) => void>();
|
|
765
788
|
|
|
766
789
|
return {
|
|
767
|
-
close: () =>
|
|
790
|
+
close: async () => {
|
|
791
|
+
if (!page.browser().isConnected()) return;
|
|
792
|
+
try {
|
|
793
|
+
await page.close();
|
|
794
|
+
} catch (cause) {
|
|
795
|
+
if (!isRemoteClosure(cause)) throw cause;
|
|
796
|
+
}
|
|
797
|
+
},
|
|
768
798
|
setBypassServiceWorker: (enabled) => page.setBypassServiceWorker(enabled),
|
|
769
799
|
setRequestInterception: (enabled) => page.setRequestInterception(enabled),
|
|
770
800
|
onRequest: (listener) => {
|
|
@@ -1087,6 +1117,22 @@ const makeProductionPage = (page: Page): BrowserRunInteractivePage => {
|
|
|
1087
1117
|
),
|
|
1088
1118
|
click: (selector, signal, onDispatch) =>
|
|
1089
1119
|
runObservedPageAction(page, selector, signal, onDispatch, (element) => element.click()),
|
|
1120
|
+
selectFile: async (request, signal, onDispatch) => {
|
|
1121
|
+
const selection = await prepareFileSelection(request, signal);
|
|
1122
|
+
|
|
1123
|
+
try {
|
|
1124
|
+
return await runObservedPageAction(
|
|
1125
|
+
page,
|
|
1126
|
+
request.selector,
|
|
1127
|
+
signal,
|
|
1128
|
+
onDispatch,
|
|
1129
|
+
selection.select,
|
|
1130
|
+
selection.validate,
|
|
1131
|
+
);
|
|
1132
|
+
} finally {
|
|
1133
|
+
await selection.close();
|
|
1134
|
+
}
|
|
1135
|
+
},
|
|
1090
1136
|
// Puppeteer materializes the complete image before returning. The adapter
|
|
1091
1137
|
// validates the 8 MiB Schema ceiling and pass limit immediately afterward.
|
|
1092
1138
|
screenshot: (fullPage) => page.screenshot({ type: "png", fullPage }),
|
|
@@ -1116,7 +1162,14 @@ const makeProductionContext = (
|
|
|
1116
1162
|
|
|
1117
1163
|
return makeProductionPage(page);
|
|
1118
1164
|
},
|
|
1119
|
-
close: () =>
|
|
1165
|
+
close: async () => {
|
|
1166
|
+
if (!context.browser().isConnected()) return;
|
|
1167
|
+
try {
|
|
1168
|
+
await context.close();
|
|
1169
|
+
} catch (cause) {
|
|
1170
|
+
if (!isRemoteClosure(cause)) throw cause;
|
|
1171
|
+
}
|
|
1172
|
+
},
|
|
1120
1173
|
});
|
|
1121
1174
|
|
|
1122
1175
|
const makeProductionBrowser = (
|
|
@@ -1124,7 +1177,15 @@ const makeProductionBrowser = (
|
|
|
1124
1177
|
viewport?: BrowserRunViewport,
|
|
1125
1178
|
): BrowserRunInteractiveBrowser => ({
|
|
1126
1179
|
createContext: async () => makeProductionContext(await browser.createBrowserContext(), viewport),
|
|
1127
|
-
|
|
1180
|
+
// Exact-session termination may already have closed the provider transport.
|
|
1181
|
+
close: async () => {
|
|
1182
|
+
if (!browser.isConnected()) return;
|
|
1183
|
+
try {
|
|
1184
|
+
await browser.close();
|
|
1185
|
+
} catch (cause) {
|
|
1186
|
+
if (!isRemoteClosure(cause)) throw cause;
|
|
1187
|
+
}
|
|
1188
|
+
},
|
|
1128
1189
|
sessionId: () => browser.sessionId(),
|
|
1129
1190
|
isConnected: () => browser.isConnected(),
|
|
1130
1191
|
onDisconnected: (listener) => {
|
|
@@ -1183,7 +1244,7 @@ const causeText = (cause: unknown): string => {
|
|
|
1183
1244
|
};
|
|
1184
1245
|
|
|
1185
1246
|
const isCapacityRefusal = (cause: unknown): boolean =>
|
|
1186
|
-
|
|
1247
|
+
cause instanceof BrowserRunFailure && cause.status === 429;
|
|
1187
1248
|
|
|
1188
1249
|
const isRemoteClosure = (cause: unknown): boolean =>
|
|
1189
1250
|
/target closed|browser.*closed|session.*closed|connection.*closed|not connected|websocket.*closed/i.test(
|
|
@@ -1242,6 +1303,8 @@ const hostAllowed = (policy: InteractiveBrowserPolicySnapshot, value: string): b
|
|
|
1242
1303
|
const keepAliveMillis = (policy: InteractiveBrowserPolicySnapshot): number =>
|
|
1243
1304
|
Math.max(MIN_KEEP_ALIVE_MILLIS, Math.min(MAX_KEEP_ALIVE_MILLIS, policy.maxElapsedMillis));
|
|
1244
1305
|
|
|
1306
|
+
class LateBrowserAcquisition extends Error {}
|
|
1307
|
+
|
|
1245
1308
|
const closeLateAcquisition = async <A>(
|
|
1246
1309
|
signal: AbortSignal,
|
|
1247
1310
|
acquire: () => Promise<A>,
|
|
@@ -1256,22 +1319,25 @@ const closeLateAcquisition = async <A>(
|
|
|
1256
1319
|
try {
|
|
1257
1320
|
await close(acquired);
|
|
1258
1321
|
} catch {
|
|
1259
|
-
//
|
|
1260
|
-
// must not escape through an unhandled rejection.
|
|
1322
|
+
// The captured runtime reports cleanup failures before this callback absorbs rejection.
|
|
1261
1323
|
}
|
|
1262
|
-
throw new
|
|
1324
|
+
throw new LateBrowserAcquisition("The interrupted browser acquisition completed late");
|
|
1263
1325
|
};
|
|
1264
1326
|
|
|
1265
1327
|
const closeWithWarning = (close: () => Promise<void>, warning: string): Effect.Effect<void> =>
|
|
1266
1328
|
Effect.tryPromise({
|
|
1267
1329
|
try: close,
|
|
1268
|
-
catch: () => protocolError(warning),
|
|
1330
|
+
catch: (cause) => protocolError(warning, cause),
|
|
1269
1331
|
}).pipe(
|
|
1270
1332
|
Effect.timeoutOrElse({
|
|
1271
1333
|
duration: Duration.millis(CLEANUP_STEP_TIMEOUT_MILLIS),
|
|
1272
1334
|
orElse: () => Effect.fail(protocolError(warning)),
|
|
1273
1335
|
}),
|
|
1274
|
-
Effect.catchCause(() =>
|
|
1336
|
+
Effect.catchCause((cause) =>
|
|
1337
|
+
reportBrowserCause("interactive.cleanup", cause).pipe(
|
|
1338
|
+
Effect.andThen(Effect.logWarning(warning)),
|
|
1339
|
+
),
|
|
1340
|
+
),
|
|
1275
1341
|
);
|
|
1276
1342
|
|
|
1277
1343
|
const deadlineError = Effect.fn("BrowserRunInteractive.deadlineError")(function* (
|
|
@@ -1431,15 +1497,21 @@ const makeRequestListener =
|
|
|
1431
1497
|
|
|
1432
1498
|
try {
|
|
1433
1499
|
settlement = allowed ? request.continue() : request.abort();
|
|
1434
|
-
} catch {
|
|
1435
|
-
state.violation.value = protocolError(
|
|
1500
|
+
} catch (cause) {
|
|
1501
|
+
state.violation.value = protocolError(
|
|
1502
|
+
"Resolving an intercepted browser request failed",
|
|
1503
|
+
cause,
|
|
1504
|
+
);
|
|
1436
1505
|
|
|
1437
1506
|
return;
|
|
1438
1507
|
}
|
|
1439
1508
|
|
|
1440
1509
|
const observed = settlement
|
|
1441
|
-
.catch(() => {
|
|
1442
|
-
state.violation.value = protocolError(
|
|
1510
|
+
.catch((cause) => {
|
|
1511
|
+
state.violation.value = protocolError(
|
|
1512
|
+
"Resolving an intercepted browser request failed",
|
|
1513
|
+
cause,
|
|
1514
|
+
);
|
|
1443
1515
|
})
|
|
1444
1516
|
.finally(() => {
|
|
1445
1517
|
state.pendingRequests.delete(observed);
|
|
@@ -1490,7 +1562,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1490
1562
|
);
|
|
1491
1563
|
|
|
1492
1564
|
const observedAction = (
|
|
1493
|
-
operation: "fill" | "click",
|
|
1565
|
+
operation: "fill" | "click" | "select-file",
|
|
1494
1566
|
evaluate: (signal: AbortSignal, onDispatch: () => void) => Promise<unknown>,
|
|
1495
1567
|
) =>
|
|
1496
1568
|
Effect.suspend(() => {
|
|
@@ -1537,7 +1609,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1537
1609
|
);
|
|
1538
1610
|
|
|
1539
1611
|
const logActionObservation = (
|
|
1540
|
-
operation: "fill" | "click",
|
|
1612
|
+
operation: "fill" | "click" | "select-file",
|
|
1541
1613
|
observation: typeof ActionObservation.Type,
|
|
1542
1614
|
) =>
|
|
1543
1615
|
Effect.logInfo("Browser interactive action observed").pipe(
|
|
@@ -1764,6 +1836,29 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
|
|
|
1764
1836
|
return yield* decodeActionResult(page, policy);
|
|
1765
1837
|
}),
|
|
1766
1838
|
),
|
|
1839
|
+
selectFile: (request) =>
|
|
1840
|
+
Schema.decodeEffect(BrowserSelectFileRequest)(request).pipe(
|
|
1841
|
+
Effect.mapError(() => policyError("The browser file selection request is malformed")),
|
|
1842
|
+
Effect.flatMap((decoded) =>
|
|
1843
|
+
run(
|
|
1844
|
+
Effect.gen(function* () {
|
|
1845
|
+
const observation = yield* observedAction("select-file", (signal, onDispatch) =>
|
|
1846
|
+
page.selectFile(decoded, signal, onDispatch),
|
|
1847
|
+
).pipe(Effect.flatMap(decodeActionObservation));
|
|
1848
|
+
|
|
1849
|
+
yield* logActionObservation("select-file", observation);
|
|
1850
|
+
const result = yield* decodeActionResult(page, policy);
|
|
1851
|
+
|
|
1852
|
+
return BrowserFileSelectionResult.make({
|
|
1853
|
+
url: result.url,
|
|
1854
|
+
fileName: decoded.fileName,
|
|
1855
|
+
mediaType: decoded.mediaType,
|
|
1856
|
+
size: decoded.bytes.length,
|
|
1857
|
+
});
|
|
1858
|
+
}),
|
|
1859
|
+
),
|
|
1860
|
+
),
|
|
1861
|
+
),
|
|
1767
1862
|
screenshot: (request) =>
|
|
1768
1863
|
Schema.decodeEffect(BrowserScreenshotRequest)(request).pipe(
|
|
1769
1864
|
Effect.mapError(() => policyError("The browser screenshot request is malformed")),
|
|
@@ -1872,10 +1967,31 @@ const cdpCommand = <A>(
|
|
|
1872
1967
|
): Effect.Effect<A, InteractiveBrowserError> =>
|
|
1873
1968
|
Effect.scoped(
|
|
1874
1969
|
Effect.gen(function* () {
|
|
1970
|
+
const cleanupContext = yield* Effect.context<never>();
|
|
1971
|
+
|
|
1875
1972
|
const cdp = yield* Effect.acquireRelease(
|
|
1876
1973
|
Effect.tryPromise({
|
|
1877
|
-
try: (signal) =>
|
|
1878
|
-
closeLateAcquisition(signal, page.createCdpSession, (acquired) =>
|
|
1974
|
+
try: (signal) => {
|
|
1975
|
+
const pending = closeLateAcquisition(signal, page.createCdpSession, (acquired) =>
|
|
1976
|
+
Effect.runPromiseWith(cleanupContext)(
|
|
1977
|
+
Effect.tryPromise({
|
|
1978
|
+
try: () => acquired.detach(),
|
|
1979
|
+
catch: (cause) => actionError("close", cause),
|
|
1980
|
+
}).pipe(
|
|
1981
|
+
Effect.tapCause((cause) => reportBrowserCause("interactive.lateDetach", cause)),
|
|
1982
|
+
),
|
|
1983
|
+
),
|
|
1984
|
+
);
|
|
1985
|
+
|
|
1986
|
+
void pending.then(undefined, (cause) => {
|
|
1987
|
+
if (signal.aborted && !(cause instanceof LateBrowserAcquisition))
|
|
1988
|
+
return Effect.runPromiseWith(cleanupContext)(
|
|
1989
|
+
reportBrowserCause("interactive.connect", Cause.fail(cause)),
|
|
1990
|
+
);
|
|
1991
|
+
});
|
|
1992
|
+
|
|
1993
|
+
return pending;
|
|
1994
|
+
},
|
|
1879
1995
|
catch: (cause) =>
|
|
1880
1996
|
state.disconnected.value || isRemoteClosure(cause)
|
|
1881
1997
|
? expiredError()
|
|
@@ -1898,7 +2014,11 @@ const cdpCommand = <A>(
|
|
|
1898
2014
|
});
|
|
1899
2015
|
|
|
1900
2016
|
return yield* Schema.decodeUnknownEffect(output)(raw).pipe(
|
|
1901
|
-
Effect.
|
|
2017
|
+
Effect.catch((error) =>
|
|
2018
|
+
reportBrowserCause("interactive.command.decode", Cause.fail(error)).pipe(
|
|
2019
|
+
Effect.andThen(Effect.fail(reportedBrowserError(protocolError(malformedMessage)))),
|
|
2020
|
+
),
|
|
2021
|
+
),
|
|
1902
2022
|
);
|
|
1903
2023
|
}),
|
|
1904
2024
|
);
|
|
@@ -1930,14 +2050,25 @@ const makeHostService = (
|
|
|
1930
2050
|
// Remote termination is authoritative. Local cleanup must not veto it or extend the deadline.
|
|
1931
2051
|
yield* runTeardown(entries).pipe(
|
|
1932
2052
|
Effect.flatMap((failures) =>
|
|
1933
|
-
Effect.forEach(
|
|
1934
|
-
|
|
1935
|
-
|
|
2053
|
+
Effect.forEach(
|
|
2054
|
+
failures,
|
|
2055
|
+
(failure) =>
|
|
2056
|
+
reportBrowserCause("interactive.cleanup", Cause.fail(failure.error)).pipe(
|
|
2057
|
+
Effect.andThen(Effect.logWarning(failure.warning)),
|
|
2058
|
+
),
|
|
2059
|
+
{
|
|
2060
|
+
discard: true,
|
|
2061
|
+
},
|
|
2062
|
+
),
|
|
1936
2063
|
),
|
|
1937
2064
|
Effect.interruptible,
|
|
1938
2065
|
Effect.timeout(`${remaining} millis`),
|
|
1939
|
-
Effect.catchCause(() =>
|
|
1940
|
-
|
|
2066
|
+
Effect.catchCause((cause) =>
|
|
2067
|
+
reportBrowserCause("interactive.cleanup", cause).pipe(
|
|
2068
|
+
Effect.andThen(
|
|
2069
|
+
Effect.logWarning("Local browser teardown incomplete after confirmed termination"),
|
|
2070
|
+
),
|
|
2071
|
+
),
|
|
1941
2072
|
),
|
|
1942
2073
|
);
|
|
1943
2074
|
});
|
|
@@ -1947,10 +2078,10 @@ const makeHostService = (
|
|
|
1947
2078
|
) {
|
|
1948
2079
|
const sessionId = yield* Effect.try({
|
|
1949
2080
|
try: browser.sessionId,
|
|
1950
|
-
catch: () => actionError("close"),
|
|
2081
|
+
catch: (cause) => actionError("close", cause),
|
|
1951
2082
|
}).pipe(
|
|
1952
2083
|
Effect.flatMap(Schema.decodeUnknownEffect(BrowserRunSessionId)),
|
|
1953
|
-
Effect.mapError(() => actionError("close")),
|
|
2084
|
+
Effect.mapError((cause) => actionError("close", cause)),
|
|
1954
2085
|
Effect.onError(() =>
|
|
1955
2086
|
closeWithWarning(browser.close, "Closing an unidentified browser failed"),
|
|
1956
2087
|
),
|
|
@@ -1967,8 +2098,26 @@ const makeHostService = (
|
|
|
1967
2098
|
const scope = yield* Scope.Scope;
|
|
1968
2099
|
const fixedPolicy = yield* snapshotPolicy(policy);
|
|
1969
2100
|
const startedAt = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
1970
|
-
|
|
1971
|
-
|
|
2101
|
+
|
|
2102
|
+
// Late SDK replies retain this pass's clock and private reporting scope.
|
|
2103
|
+
const runCleanup = <A, E>(effect: Effect.Effect<A, E>) =>
|
|
2104
|
+
Effect.runPromiseWith(cleanupContext)(
|
|
2105
|
+
effect.pipe(
|
|
2106
|
+
Effect.tapCause((cause) => reportBrowserCause("interactive.lateCleanup", cause)),
|
|
2107
|
+
),
|
|
2108
|
+
);
|
|
2109
|
+
|
|
2110
|
+
const cleanupContext = yield* Effect.context<never>();
|
|
2111
|
+
|
|
2112
|
+
const observeLate = <A>(signal: AbortSignal, pending: Promise<A>): Promise<A> => {
|
|
2113
|
+
// Observe the original Promise without creating another rejected continuation.
|
|
2114
|
+
void pending.then(undefined, (cause) => {
|
|
2115
|
+
if (signal.aborted && !(cause instanceof LateBrowserAcquisition))
|
|
2116
|
+
return runCleanup(reportBrowserCause("interactive.acquire", Cause.fail(cause)));
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
return pending;
|
|
2120
|
+
};
|
|
1972
2121
|
|
|
1973
2122
|
const state: HandleState = {
|
|
1974
2123
|
closed: { value: false },
|
|
@@ -1988,22 +2137,31 @@ const makeHostService = (
|
|
|
1988
2137
|
Effect.suspend(() =>
|
|
1989
2138
|
lifecycle.managedTeardownInstalled
|
|
1990
2139
|
? Effect.void
|
|
1991
|
-
: entry.close.pipe(
|
|
2140
|
+
: entry.close.pipe(
|
|
2141
|
+
Effect.catchCause((cause) =>
|
|
2142
|
+
reportBrowserCause("interactive.cleanup", cause).pipe(
|
|
2143
|
+
Effect.andThen(Effect.logWarning(entry.warning)),
|
|
2144
|
+
),
|
|
2145
|
+
),
|
|
2146
|
+
),
|
|
1992
2147
|
);
|
|
1993
2148
|
|
|
1994
2149
|
const sessionIdValue = yield* Effect.acquireRelease(
|
|
1995
2150
|
withinDeadline(
|
|
1996
2151
|
Effect.tryPromise({
|
|
1997
2152
|
try: (signal) =>
|
|
1998
|
-
|
|
2153
|
+
observeLate(
|
|
1999
2154
|
signal,
|
|
2000
|
-
(
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2155
|
+
closeLateAcquisition(
|
|
2156
|
+
signal,
|
|
2157
|
+
() => binding.acquire(keepAliveMillis(fixedPolicy)),
|
|
2158
|
+
(id) =>
|
|
2159
|
+
runCleanup(
|
|
2160
|
+
Schema.decodeUnknownEffect(BrowserRunSessionId)(id).pipe(
|
|
2161
|
+
Effect.flatMap((value) => terminate(Redacted.make(value), [])),
|
|
2162
|
+
),
|
|
2005
2163
|
),
|
|
2006
|
-
|
|
2164
|
+
),
|
|
2007
2165
|
),
|
|
2008
2166
|
catch: (cause) =>
|
|
2009
2167
|
isCapacityRefusal(cause)
|
|
@@ -2029,7 +2187,11 @@ const makeHostService = (
|
|
|
2029
2187
|
return lifecycle.managedTeardownInstalled
|
|
2030
2188
|
? Effect.void
|
|
2031
2189
|
: terminate(Redacted.make(id), []).pipe(
|
|
2032
|
-
Effect.
|
|
2190
|
+
Effect.catchCause((cause) =>
|
|
2191
|
+
reportBrowserCause("interactive.close", cause).pipe(
|
|
2192
|
+
Effect.andThen(Effect.logWarning("Whole-browser cleanup remains unconfirmed")),
|
|
2193
|
+
),
|
|
2194
|
+
),
|
|
2033
2195
|
);
|
|
2034
2196
|
}),
|
|
2035
2197
|
{ interruptible: true },
|
|
@@ -2045,10 +2207,13 @@ const makeHostService = (
|
|
|
2045
2207
|
withinDeadline(
|
|
2046
2208
|
Effect.tryPromise({
|
|
2047
2209
|
try: (signal) =>
|
|
2048
|
-
|
|
2210
|
+
observeLate(
|
|
2049
2211
|
signal,
|
|
2050
|
-
(
|
|
2051
|
-
|
|
2212
|
+
closeLateAcquisition(
|
|
2213
|
+
signal,
|
|
2214
|
+
() => binding.connect(sessionIdValue),
|
|
2215
|
+
(acquired) => runCleanup(closeAcquired(acquired)),
|
|
2216
|
+
),
|
|
2052
2217
|
),
|
|
2053
2218
|
catch: (cause) =>
|
|
2054
2219
|
protocolError("Connecting to the Browser Run session failed", cause),
|
|
@@ -2105,7 +2270,17 @@ const makeHostService = (
|
|
|
2105
2270
|
withinDeadline(
|
|
2106
2271
|
Effect.tryPromise({
|
|
2107
2272
|
try: (signal) =>
|
|
2108
|
-
|
|
2273
|
+
observeLate(
|
|
2274
|
+
signal,
|
|
2275
|
+
closeLateAcquisition(signal, browser.createContext, (acquired) =>
|
|
2276
|
+
runCleanup(
|
|
2277
|
+
Effect.tryPromise({
|
|
2278
|
+
try: () => acquired.close(),
|
|
2279
|
+
catch: (cause) => actionError("close", cause),
|
|
2280
|
+
}),
|
|
2281
|
+
),
|
|
2282
|
+
),
|
|
2283
|
+
),
|
|
2109
2284
|
catch: (cause) =>
|
|
2110
2285
|
state.disconnected.value || isRemoteClosure(cause)
|
|
2111
2286
|
? expiredError()
|
|
@@ -2127,7 +2302,17 @@ const makeHostService = (
|
|
|
2127
2302
|
withinDeadline(
|
|
2128
2303
|
Effect.tryPromise({
|
|
2129
2304
|
try: (signal) =>
|
|
2130
|
-
|
|
2305
|
+
observeLate(
|
|
2306
|
+
signal,
|
|
2307
|
+
closeLateAcquisition(signal, context.newPage, (acquired) =>
|
|
2308
|
+
runCleanup(
|
|
2309
|
+
Effect.tryPromise({
|
|
2310
|
+
try: () => acquired.close(),
|
|
2311
|
+
catch: (cause) => actionError("close", cause),
|
|
2312
|
+
}),
|
|
2313
|
+
),
|
|
2314
|
+
),
|
|
2315
|
+
),
|
|
2131
2316
|
catch: (cause) =>
|
|
2132
2317
|
state.disconnected.value || isRemoteClosure(cause)
|
|
2133
2318
|
? expiredError()
|
|
@@ -2193,7 +2378,12 @@ const makeHostService = (
|
|
|
2193
2378
|
|
|
2194
2379
|
const teardown = yield* Effect.uninterruptible(
|
|
2195
2380
|
Effect.gen(function* () {
|
|
2196
|
-
const cached = yield* Effect.cached(
|
|
2381
|
+
const cached = yield* Effect.cached(
|
|
2382
|
+
terminate(Redacted.make(sessionIdValue), closers).pipe(
|
|
2383
|
+
Effect.tapCause((cause) => reportBrowserCause("interactive.close", cause)),
|
|
2384
|
+
Effect.mapError(reportedBrowserError),
|
|
2385
|
+
),
|
|
2386
|
+
);
|
|
2197
2387
|
|
|
2198
2388
|
lifecycle.managedTeardownInstalled = true;
|
|
2199
2389
|
yield* Effect.addFinalizer(() =>
|
|
@@ -2203,8 +2393,12 @@ const makeHostService = (
|
|
|
2203
2393
|
state.disconnected.value = true;
|
|
2204
2394
|
}).pipe(
|
|
2205
2395
|
Effect.andThen(cached),
|
|
2206
|
-
Effect.
|
|
2207
|
-
|
|
2396
|
+
Effect.catchCause((cause) =>
|
|
2397
|
+
reportBrowserCause("interactive.close", cause).pipe(
|
|
2398
|
+
Effect.andThen(
|
|
2399
|
+
Effect.logWarning("Whole-browser cleanup remains unconfirmed"),
|
|
2400
|
+
),
|
|
2401
|
+
),
|
|
2208
2402
|
),
|
|
2209
2403
|
),
|
|
2210
2404
|
),
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
import puppeteer, { type Browser } from "@cloudflare/puppeteer";
|
|
3
|
+
import { Context, Layer, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
import { browserFailure, BrowserRunFailure } from "./browser-failure.ts";
|
|
6
|
+
|
|
7
|
+
const Acquired = Schema.Struct({ sessionId: Schema.String.check(Schema.isUUID()) });
|
|
8
|
+
|
|
9
|
+
/** Native transport port shared by both scoped adapters; the Layer owns the foreign binding. */
|
|
10
|
+
export class BrowserRunBinding extends Context.Service<
|
|
11
|
+
BrowserRunBinding,
|
|
12
|
+
{
|
|
13
|
+
readonly acquire: (
|
|
14
|
+
keepAliveMillis: number,
|
|
15
|
+
operation: "protected.acquire" | "interactive.acquire",
|
|
16
|
+
) => Promise<string>;
|
|
17
|
+
readonly connect: (sessionId: string, operation: string) => Promise<Browser>;
|
|
18
|
+
}
|
|
19
|
+
>()("@effect-agent/platform-cloudflare/internal/BrowserRunBinding") {
|
|
20
|
+
static layer(browser: Pick<BrowserRun, "fetch">) {
|
|
21
|
+
return Layer.succeed(this)({
|
|
22
|
+
// One allocation path, with recording explicitly disabled for private sessions.
|
|
23
|
+
acquire: async (keepAliveMillis, operation) => {
|
|
24
|
+
const response = await browser.fetch(
|
|
25
|
+
`https://browser-rendering.cloudflare.com/v1/devtools/browser?keep_alive=${keepAliveMillis}&recording=false`,
|
|
26
|
+
{ method: "POST" },
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (response.status !== 200) {
|
|
30
|
+
const failure = new BrowserRunFailure({
|
|
31
|
+
operation,
|
|
32
|
+
reason: "provider",
|
|
33
|
+
status: response.status,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await response.body?.cancel();
|
|
38
|
+
} catch {
|
|
39
|
+
// Body cancellation cannot replace the acquisition refusal.
|
|
40
|
+
}
|
|
41
|
+
throw failure;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return Schema.decodeUnknownSync(Acquired)(await response.json()).sessionId;
|
|
45
|
+
},
|
|
46
|
+
// The SDK embeds bodies and session IDs in errors; retain status before that loss.
|
|
47
|
+
connect: async (sessionId, operation) => {
|
|
48
|
+
let failure: BrowserRunFailure | undefined;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
return await puppeteer.connect(
|
|
52
|
+
{
|
|
53
|
+
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
54
|
+
try {
|
|
55
|
+
const response = await browser.fetch(input, init);
|
|
56
|
+
|
|
57
|
+
if (response.status !== 101) {
|
|
58
|
+
failure = new BrowserRunFailure({
|
|
59
|
+
operation,
|
|
60
|
+
reason: "provider",
|
|
61
|
+
status: response.status,
|
|
62
|
+
});
|
|
63
|
+
await response.body?.cancel();
|
|
64
|
+
throw failure;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return response;
|
|
68
|
+
} catch (cause) {
|
|
69
|
+
failure ??= browserFailure(operation, cause);
|
|
70
|
+
throw failure;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
sessionId,
|
|
75
|
+
);
|
|
76
|
+
} catch (cause) {
|
|
77
|
+
throw failure ?? browserFailure(operation, cause);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|