@effect-agent/platform-cloudflare 0.1.0-beta.13 → 0.1.0-beta.131

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.
Files changed (106) hide show
  1. package/dist/Alarm.d.mts +244 -0
  2. package/dist/Alarm.mjs +947 -0
  3. package/dist/Alarm.mjs.map +1 -0
  4. package/dist/BrowserCredentials.d.mts +91 -0
  5. package/dist/BrowserCredentials.mjs +120 -0
  6. package/dist/BrowserCredentials.mjs.map +1 -0
  7. package/dist/BrowserRestCapture.d.mts +34 -0
  8. package/dist/BrowserRestCapture.mjs +238 -0
  9. package/dist/BrowserRestCapture.mjs.map +1 -0
  10. package/dist/BrowserRestCrawl.d.mts +16 -0
  11. package/dist/BrowserRestCrawl.mjs +244 -0
  12. package/dist/BrowserRestCrawl.mjs.map +1 -0
  13. package/dist/BrowserSession-DP6b-cpL.d.mts +125 -0
  14. package/dist/BrowserSession-DWoRC_iM.mjs +505 -0
  15. package/dist/BrowserSession-DWoRC_iM.mjs.map +1 -0
  16. package/dist/BrowserSession.d.mts +3 -0
  17. package/dist/BrowserSession.mjs +4 -0
  18. package/dist/CloudflareAiGateway.d.mts +64 -0
  19. package/dist/CloudflareAiGateway.mjs +70 -0
  20. package/dist/CloudflareAiGateway.mjs.map +1 -0
  21. package/dist/CloudflareBindings.d.mts +115 -0
  22. package/dist/CloudflareBindings.mjs +113 -0
  23. package/dist/CloudflareBindings.mjs.map +1 -0
  24. package/dist/CloudflareBrowser-DV6kya1H.mjs +495 -0
  25. package/dist/CloudflareBrowser-DV6kya1H.mjs.map +1 -0
  26. package/dist/CloudflareBrowser-Doe1M7R5.d.mts +80 -0
  27. package/dist/CloudflareBrowser.d.mts +2 -0
  28. package/dist/CloudflareBrowser.mjs +2 -0
  29. package/dist/CloudflareCodeMode.d.mts +44 -0
  30. package/dist/CloudflareCodeMode.mjs +616 -0
  31. package/dist/CloudflareCodeMode.mjs.map +1 -0
  32. package/dist/CloudflareConfig-DKXGo8L3.d.mts +106 -0
  33. package/dist/CloudflareConfig.d.mts +2 -0
  34. package/dist/CloudflareConfig.mjs +128 -0
  35. package/dist/CloudflareConfig.mjs.map +1 -0
  36. package/dist/CloudflareMemory.d.mts +142 -0
  37. package/dist/CloudflareMemory.mjs +202 -0
  38. package/dist/CloudflareMemory.mjs.map +1 -0
  39. package/dist/CloudflareScheduling.d.mts +52 -0
  40. package/dist/CloudflareScheduling.mjs +396 -0
  41. package/dist/CloudflareScheduling.mjs.map +1 -0
  42. package/dist/CloudflareSubscriptions.d.mts +87 -0
  43. package/dist/CloudflareSubscriptions.mjs +530 -0
  44. package/dist/CloudflareSubscriptions.mjs.map +1 -0
  45. package/dist/CloudflareThreadClient.d.mts +1752 -0
  46. package/dist/CloudflareThreadClient.mjs +382 -0
  47. package/dist/CloudflareThreadClient.mjs.map +1 -0
  48. package/dist/InteractiveBrowser-BnqY_Y-F.d.mts +228 -0
  49. package/dist/InteractiveBrowser-CxFT-Nwm.mjs +1923 -0
  50. package/dist/InteractiveBrowser-CxFT-Nwm.mjs.map +1 -0
  51. package/dist/InteractiveBrowser.d.mts +2 -0
  52. package/dist/InteractiveBrowser.mjs +2 -0
  53. package/dist/ThreadObject-CVhZC-7l.mjs +849 -0
  54. package/dist/ThreadObject-CVhZC-7l.mjs.map +1 -0
  55. package/dist/ThreadObject-D8GzJxJB.d.mts +839 -0
  56. package/dist/ThreadObject.d.mts +2 -0
  57. package/dist/ThreadObject.mjs +3 -0
  58. package/dist/WakeScheduler.d.mts +23 -0
  59. package/dist/WakeScheduler.mjs +57 -0
  60. package/dist/WakeScheduler.mjs.map +1 -0
  61. package/dist/boundary-BguazVkh.mjs +42 -0
  62. package/dist/boundary-BguazVkh.mjs.map +1 -0
  63. package/dist/index.d.mts +15 -1550
  64. package/dist/index.mjs +15 -1716
  65. package/dist/prepared-admission-DU0_T-ev.mjs +73 -0
  66. package/dist/prepared-admission-DU0_T-ev.mjs.map +1 -0
  67. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  68. package/package.json +1 -56
  69. package/src/Alarm.ts +1979 -0
  70. package/src/BrowserCredentials.ts +146 -0
  71. package/src/BrowserRestCapture.ts +477 -0
  72. package/src/BrowserRestCrawl.ts +540 -0
  73. package/src/BrowserSession.ts +624 -0
  74. package/src/CloudflareAiGateway.ts +170 -0
  75. package/src/CloudflareBindings.ts +199 -0
  76. package/src/CloudflareBrowser.ts +15 -0
  77. package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +308 -283
  78. package/src/{config.ts → CloudflareConfig.ts} +19 -12
  79. package/src/CloudflareMemory.ts +420 -0
  80. package/src/CloudflareScheduling.ts +747 -0
  81. package/src/CloudflareSubscriptions.ts +1041 -0
  82. package/src/CloudflareThreadClient.ts +809 -0
  83. package/src/InteractiveBrowser.ts +3246 -0
  84. package/src/ThreadObject.ts +1190 -0
  85. package/src/{wake-scheduler.ts → WakeScheduler.ts} +32 -26
  86. package/src/index.ts +14 -23
  87. package/src/internal/boundary.ts +55 -0
  88. package/src/internal/browser-binding.ts +322 -0
  89. package/src/internal/browser-credentials.ts +362 -0
  90. package/src/internal/browser-failure.ts +92 -0
  91. package/src/internal/browser-file-selection.ts +126 -0
  92. package/src/internal/browser-quick-action.ts +857 -0
  93. package/src/internal/browser-readonly-live-view.ts +111 -0
  94. package/src/internal/browser-session-lifecycle.ts +193 -0
  95. package/src/internal/layers.ts +773 -0
  96. package/src/internal/message-delivery.ts +131 -0
  97. package/src/internal/prepared-admission.ts +116 -0
  98. package/src/internal/progress-wait.ts +121 -0
  99. package/src/internal/transport.ts +44 -0
  100. package/dist/index.mjs.map +0 -1
  101. package/src/alarm.ts +0 -334
  102. package/src/bindings.ts +0 -145
  103. package/src/client.ts +0 -646
  104. package/src/conversation-object.ts +0 -768
  105. package/src/layers.ts +0 -376
  106. package/src/transport.ts +0 -38
@@ -0,0 +1,624 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+
3
+ import {
4
+ Cause,
5
+ Clock,
6
+ Context,
7
+ Effect,
8
+ Exit,
9
+ Layer,
10
+ Redacted,
11
+ Schema,
12
+ Semaphore,
13
+ Scope,
14
+ } from "effect";
15
+ import type { Page } from "puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js";
16
+
17
+ import {
18
+ CredentialFillError,
19
+ type BrowserCredentialAccess,
20
+ type CredentialFillResult,
21
+ type FillCredentialRequest,
22
+ } from "./BrowserCredentials.ts";
23
+ import {
24
+ BrowserRunHandoffRequest,
25
+ BrowserRunHandoffResult,
26
+ BrowserRunHandoffState,
27
+ BrowserRunLiveViewRequest,
28
+ BrowserRunLiveViewResult,
29
+ } from "./InteractiveBrowser.ts";
30
+ import { BrowserRunBinding, type BrowserRunAttachment } from "./internal/browser-binding.ts";
31
+ import { BrowserSessionPage, fillCredential } from "./internal/browser-credentials.ts";
32
+ import { reportBrowserCause, reportedBrowserError } from "./internal/browser-failure.ts";
33
+ import { BrowserRunReadonlyLiveView } from "./internal/browser-readonly-live-view.ts";
34
+ import {
35
+ BrowserRunSessionLifecycle,
36
+ type BrowserRunLifecycleOptions,
37
+ } from "./internal/browser-session-lifecycle.ts";
38
+
39
+ export {
40
+ BrowserRunHandoffRequest,
41
+ BrowserRunHandoffResult,
42
+ BrowserRunHandoffState,
43
+ BrowserRunLiveViewRequest,
44
+ BrowserRunLiveViewResult,
45
+ };
46
+
47
+ const PositiveMillis = Schema.Int.check(
48
+ Schema.isGreaterThan(0),
49
+ Schema.isLessThanOrEqualTo(Number.MAX_SAFE_INTEGER),
50
+ );
51
+
52
+ const SessionId = Schema.Redacted(Schema.String.check(Schema.isUUID()));
53
+ const ProviderId = Schema.Redacted(Schema.NonEmptyString.check(Schema.isMaxLength(256)));
54
+
55
+ /** Private host record, not a model capability. Persist together with the owner's controller state. */
56
+ export class BrowserSessionReference extends Schema.Class<BrowserSessionReference>(
57
+ "BrowserSessionReference",
58
+ )({
59
+ version: Schema.Literal(1),
60
+ sessionId: SessionId,
61
+ contextId: ProviderId,
62
+ targetId: ProviderId,
63
+ expiresAt: PositiveMillis,
64
+ commandTimeoutMillis: PositiveMillis,
65
+ }) {}
66
+
67
+ export class BrowserSessionOptions extends Schema.Class<BrowserSessionOptions>(
68
+ "BrowserSessionOptions",
69
+ )({
70
+ maxElapsedMillis: PositiveMillis,
71
+ keepAliveMillis: Schema.optionalKey(PositiveMillis.check(Schema.isLessThanOrEqualTo(600_000))),
72
+ commandTimeoutMillis: Schema.optionalKey(PositiveMillis),
73
+ }) {}
74
+
75
+ /** No SDK text, selectors, page data, or credential values are retained in public failures. */
76
+ export class BrowserSessionError extends Schema.TaggedError<BrowserSessionError>()(
77
+ "BrowserSessionError",
78
+ {
79
+ reason: Schema.Literals([
80
+ "invalid",
81
+ "expired",
82
+ "missing-page",
83
+ "busy",
84
+ "closed",
85
+ "provider",
86
+ "timeout",
87
+ "cleanup",
88
+ ]),
89
+ dispatch: Schema.Literals(["not-dispatched", "possibly-dispatched"]),
90
+ cleanup: Schema.Literals(["not-requested", "confirmed", "unconfirmed"]),
91
+ },
92
+ ) {}
93
+
94
+ /**
95
+ * One local attachment to an application-owned browser. Scope release disconnects; it never
96
+ * transfers ownership or closes a healthy remote session. The owner serializes attachments,
97
+ * fences old attempts, authorizes human access, and calls BrowserSessions.close on expiry/stop.
98
+ * Native callbacks are trusted host code: never retain SDK handles or start unawaited work.
99
+ */
100
+ export interface BrowserSession {
101
+ readonly reference: BrowserSessionReference;
102
+ /**
103
+ * Check current authority under the lock before native dispatch. A settled SDK rejection leaves
104
+ * the session available for inspection, but may have changed the website; never retry blindly.
105
+ * Timeout/interruption fences outstanding SDK work and terminates the exact browser.
106
+ */
107
+ readonly run: <A, E, R>(
108
+ authorize: Effect.Effect<void, E, R>,
109
+ action: (page: Page) => Promise<A>,
110
+ ) => Effect.Effect<A, E | BrowserSessionError, R>;
111
+ /**
112
+ * Resolves host-owned material under fresh credential grants. Never submits or retries a write.
113
+ * Credential timeouts retain acknowledged writes and dispatch evidence alongside cleanup.
114
+ */
115
+ readonly fillCredential: (
116
+ request: FillCredentialRequest,
117
+ ) => Effect.Effect<
118
+ CredentialFillResult,
119
+ CredentialFillError | BrowserSessionError,
120
+ BrowserCredentialAccess
121
+ >;
122
+ readonly handoff: <E, R>(
123
+ authorize: Effect.Effect<void, E, R>,
124
+ request: BrowserRunHandoffRequest,
125
+ ) => Effect.Effect<BrowserRunHandoffResult, E | BrowserSessionError, R>;
126
+ readonly getLiveView: <E, R>(
127
+ authorize: Effect.Effect<void, E, R>,
128
+ request: BrowserRunLiveViewRequest,
129
+ ) => Effect.Effect<BrowserRunLiveViewResult, E | BrowserSessionError, R>;
130
+ /**
131
+ * Mint a provider-enforced read-only connection to this exact page. Requires REST credentials;
132
+ * missing read-only confirmation fails closed, never falling back to an interactive URL.
133
+ * Expiry limits new connections, not established connections or access to visible page data.
134
+ */
135
+ readonly getReadOnlyLiveView: <E, R>(
136
+ authorize: Effect.Effect<void, E, R>,
137
+ request: BrowserRunLiveViewRequest,
138
+ ) => Effect.Effect<BrowserRunLiveViewResult, E | BrowserSessionError, R>;
139
+ readonly getHandoffState: <E, R>(
140
+ authorize: Effect.Effect<void, E, R>,
141
+ ) => Effect.Effect<BrowserRunHandoffState, E | BrowserSessionError, R>;
142
+ }
143
+
144
+ const failure = (
145
+ reason: BrowserSessionError["reason"],
146
+ dispatch: BrowserSessionError["dispatch"] = "not-dispatched",
147
+ cleanup: BrowserSessionError["cleanup"] = "not-requested",
148
+ ) => new BrowserSessionError({ reason, dispatch, cleanup });
149
+
150
+ const decode = <A>(schema: Schema.Codec<A>, input: unknown) =>
151
+ Schema.decodeUnknownEffect(schema, { onExcessProperty: "error" })(input).pipe(
152
+ Effect.mapError(() => failure("invalid")),
153
+ );
154
+
155
+ // Catch inside the Promise before workerd can report an unhandled foreign diagnostic.
156
+ const native = Effect.fnUntraced(function* <A>(
157
+ operation: string,
158
+ action: (signal: AbortSignal) => Promise<A>,
159
+ ) {
160
+ const runReport = Effect.runPromiseWith(yield* Effect.context<never>());
161
+
162
+ return yield* Effect.tryPromise({
163
+ try: async (signal) => {
164
+ try {
165
+ return { ok: true as const, value: await action(signal) };
166
+ } catch (cause) {
167
+ if (!signal.aborted || cause !== signal.reason)
168
+ await runReport(reportBrowserCause(operation, Cause.fail(cause)));
169
+
170
+ return { ok: false as const };
171
+ }
172
+ },
173
+ catch: () => failure("provider"),
174
+ }).pipe(
175
+ Effect.flatMap((result) =>
176
+ result.ok
177
+ ? Effect.succeed(result.value)
178
+ : Effect.fail(reportedBrowserError(failure("provider"))),
179
+ ),
180
+ Effect.withTracerEnabled(false),
181
+ );
182
+ });
183
+
184
+ const targetId = (page: Page) =>
185
+ native("session.target", async () => {
186
+ const cdp = await page.createCDPSession();
187
+
188
+ try {
189
+ return (await cdp.send("Target.getTargetInfo")).targetInfo.targetId;
190
+ } finally {
191
+ await cdp.detach();
192
+ }
193
+ });
194
+
195
+ export interface BrowserSessionsOptions extends BrowserRunLifecycleOptions {
196
+ readonly browser: Pick<BrowserRun, "fetch">;
197
+ }
198
+
199
+ /** Native Cloudflare sessions; no framework browser workflow, checkpoint transfer, or observation policy. */
200
+ export class BrowserSessions extends Context.Service<
201
+ BrowserSessions,
202
+ {
203
+ /**
204
+ * Allocate once and commit its private reference in retain. Failed/uncommitted creation closes
205
+ * the allocation. After retain succeeds the application owns remote cleanup, independent of
206
+ * attempt Scopes. A lost allocation reply can remain indeterminate until provider idle expiry.
207
+ */
208
+ readonly create: <E, R>(
209
+ options: BrowserSessionOptions,
210
+ retain: (reference: BrowserSessionReference) => Effect.Effect<void, E, R>,
211
+ ) => Effect.Effect<BrowserSessionReference, E | BrowserSessionError, R>;
212
+ /** Attach only the exact saved context/page. Never recreate missing/expired state. */
213
+ readonly attach: (
214
+ reference: BrowserSessionReference,
215
+ ) => Effect.Effect<BrowserSession, BrowserSessionError, Scope.Scope>;
216
+ /** Refresh provider inactivity only; the owner must still enforce its expiresAt. */
217
+ readonly keepAlive: (
218
+ sessionId: Redacted.Redacted<string>,
219
+ ) => Effect.Effect<void, BrowserSessionError>;
220
+ readonly close: (
221
+ sessionId: Redacted.Redacted<string>,
222
+ ) => Effect.Effect<void, BrowserSessionError>;
223
+ }
224
+ >()("@effect-agent/platform-cloudflare/BrowserSessions") {
225
+ static readonly layerNoDeps = Layer.effect(
226
+ this,
227
+ Effect.gen(function* () {
228
+ const binding = yield* BrowserRunBinding;
229
+ const lifecycle = yield* BrowserRunSessionLifecycle;
230
+ const readonlyLiveView = yield* BrowserRunReadonlyLiveView;
231
+
232
+ const close = (sessionId: Redacted.Redacted<string>) =>
233
+ decode(SessionId, sessionId).pipe(
234
+ Effect.flatMap((id) =>
235
+ lifecycle
236
+ .close(id)
237
+ .pipe(Effect.mapError(() => failure("cleanup", "not-dispatched", "unconfirmed"))),
238
+ ),
239
+ );
240
+
241
+ const cleanup = (sessionId: Redacted.Redacted<string>) =>
242
+ close(sessionId).pipe(
243
+ Effect.as("confirmed" as const),
244
+ Effect.catchCause((cause) =>
245
+ reportBrowserCause("session.close", cause).pipe(Effect.as("unconfirmed" as const)),
246
+ ),
247
+ );
248
+
249
+ const connect = Effect.fnUntraced(function* (sessionId: Redacted.Redacted<string>) {
250
+ let attachment: BrowserRunAttachment | undefined;
251
+
252
+ const retire = Effect.suspend(() =>
253
+ attachment === undefined
254
+ ? Effect.void
255
+ : attachment.retire.pipe(
256
+ // Dispatch is already fenced; give the socket close handshake the same
257
+ // bounded window as keepalive instead of failing a settled command after 1s.
258
+ Effect.timeoutOrElse({
259
+ duration: "10 seconds",
260
+ orElse: () => Effect.fail(failure("cleanup")),
261
+ }),
262
+ Effect.catchCause((cause) =>
263
+ reportBrowserCause("session.disconnect", cause).pipe(
264
+ Effect.andThen(Effect.die(failure("cleanup", "not-dispatched", "unconfirmed"))),
265
+ ),
266
+ ),
267
+ ),
268
+ );
269
+
270
+ yield* Effect.addFinalizer(() => retire);
271
+
272
+ const browser = yield* native("session.connect", (signal) => {
273
+ attachment = binding.connect(Redacted.value(sessionId), "session.connect", signal);
274
+
275
+ return attachment.browser;
276
+ });
277
+
278
+ return { browser, retire };
279
+ });
280
+
281
+ const create = <E, R>(
282
+ input: BrowserSessionOptions,
283
+ retain: (reference: BrowserSessionReference) => Effect.Effect<void, E, R>,
284
+ ) =>
285
+ Effect.gen(function* () {
286
+ const options = yield* decode(BrowserSessionOptions, input);
287
+ const expiresAt = (yield* Clock.currentTimeMillis) + options.maxElapsedMillis;
288
+
289
+ if (!Number.isSafeInteger(expiresAt)) return yield* failure("invalid");
290
+ let sessionId: Redacted.Redacted<string> | undefined;
291
+ let retained = false;
292
+ const runCleanup = Effect.runPromiseWith(yield* Effect.context<never>());
293
+
294
+ yield* Effect.addFinalizer(() =>
295
+ sessionId === undefined || retained ? Effect.void : cleanup(sessionId),
296
+ );
297
+ sessionId = yield* native("session.acquire", async (signal) => {
298
+ const id = Redacted.make(
299
+ await binding.acquire(options.keepAliveMillis ?? 600_000, "session.acquire"),
300
+ );
301
+
302
+ if (signal.aborted) {
303
+ await runCleanup(cleanup(id));
304
+ signal.throwIfAborted();
305
+ }
306
+
307
+ return id;
308
+ });
309
+ const { browser } = yield* connect(sessionId);
310
+
311
+ const page = yield* native("session.page", async () => {
312
+ const context = await browser.createBrowserContext();
313
+
314
+ return await context.newPage();
315
+ });
316
+
317
+ const reference = yield* decode(BrowserSessionReference, {
318
+ version: 1,
319
+ sessionId,
320
+ contextId: Redacted.make(page.browserContext().id ?? ""),
321
+ targetId: Redacted.make(yield* targetId(page)),
322
+ expiresAt,
323
+ commandTimeoutMillis: options.commandTimeoutMillis ?? 30_000,
324
+ });
325
+
326
+ yield* Effect.uninterruptibleMask((restore) =>
327
+ restore(retain(reference)).pipe(
328
+ Effect.tap(() =>
329
+ Effect.sync(() => {
330
+ retained = true;
331
+ }),
332
+ ),
333
+ ),
334
+ );
335
+
336
+ return reference;
337
+ }).pipe(
338
+ Effect.scoped,
339
+ Effect.timeoutOrElse({
340
+ duration: "30 seconds",
341
+ orElse: () => Effect.fail(failure("timeout")),
342
+ }),
343
+ Effect.withTracerEnabled(false),
344
+ );
345
+
346
+ const attachPage = Effect.fnUntraced(function* (input: BrowserSessionReference) {
347
+ const reference = yield* decode(BrowserSessionReference, input);
348
+
349
+ if (reference.expiresAt <= (yield* Clock.currentTimeMillis))
350
+ return yield* failure("expired");
351
+ const scope = yield* Effect.scope;
352
+ const { browser, retire } = yield* connect(reference.sessionId);
353
+
354
+ const context = browser
355
+ .browserContexts()
356
+ .find((value) => value.id === Redacted.value(reference.contextId));
357
+
358
+ if (context === undefined) return yield* failure("missing-page");
359
+ let page: Page | undefined;
360
+
361
+ for (const candidate of yield* native("session.pages", () => context.pages())) {
362
+ if ((yield* targetId(candidate)) === Redacted.value(reference.targetId)) {
363
+ page = candidate;
364
+ break;
365
+ }
366
+ }
367
+ if (page === undefined) return yield* failure("missing-page");
368
+ const currentPage = page;
369
+ const lock = yield* Semaphore.make(1);
370
+ let invalid = false;
371
+
372
+ const terminate = Effect.suspend(() => {
373
+ invalid = true;
374
+
375
+ return retire.pipe(
376
+ Effect.catchCause((cause) => reportBrowserCause("session.disconnect", cause)),
377
+ Effect.andThen(cleanup(reference.sessionId)),
378
+ );
379
+ });
380
+
381
+ const runEffect = <A, E, R>(
382
+ action: (commandTimeoutMillis: number) => Effect.Effect<A, E, R>,
383
+ ) =>
384
+ lock
385
+ .withPermitsIfAvailable(1)(
386
+ Effect.gen(function* () {
387
+ if (invalid || scope.state._tag === "Closed" || !browser.isConnected())
388
+ return yield* failure("closed");
389
+ const remaining = reference.expiresAt - (yield* Clock.currentTimeMillis);
390
+
391
+ if (remaining <= 0) {
392
+ return yield* failure("expired", "not-dispatched", yield* terminate);
393
+ }
394
+
395
+ return yield* action(Math.min(remaining, reference.commandTimeoutMillis)).pipe(
396
+ Effect.catchDefect((defect) =>
397
+ reportBrowserCause("session.command", Cause.die(defect)).pipe(
398
+ Effect.andThen(terminate),
399
+ Effect.flatMap((closed) =>
400
+ Effect.fail(failure("provider", "possibly-dispatched", closed)),
401
+ ),
402
+ ),
403
+ ),
404
+ Effect.onInterrupt(() => terminate),
405
+ );
406
+ }),
407
+ )
408
+ .pipe(
409
+ Effect.flatMap(Effect.fromOption(() => failure("busy"))),
410
+ Effect.withTracerEnabled(false),
411
+ );
412
+
413
+ const run = <A, E, R>(
414
+ authorize: Effect.Effect<void, E, R>,
415
+ action: (page: Page) => Promise<A>,
416
+ terminateOnError = false,
417
+ ) =>
418
+ runEffect((commandTimeoutMillis) =>
419
+ authorize.pipe(
420
+ Effect.andThen(() =>
421
+ native("session.command", () => action(currentPage)).pipe(
422
+ Effect.catch((error) =>
423
+ Effect.gen(function* () {
424
+ const closed = terminateOnError ? yield* terminate : "not-requested";
425
+
426
+ return yield* reportedBrowserError(
427
+ failure(error.reason, "possibly-dispatched", closed),
428
+ );
429
+ }),
430
+ ),
431
+ ),
432
+ ),
433
+ Effect.timeoutOrElse({
434
+ duration: commandTimeoutMillis,
435
+ orElse: () =>
436
+ terminate.pipe(
437
+ Effect.flatMap((closed) =>
438
+ Effect.fail(failure("timeout", "possibly-dispatched", closed)),
439
+ ),
440
+ ),
441
+ }),
442
+ ),
443
+ );
444
+
445
+ const command = <A, E, R>(
446
+ authorize: Effect.Effect<void, E, R>,
447
+ method: string,
448
+ parameters: Record<string, string | number>,
449
+ schema: Schema.Codec<A>,
450
+ ) =>
451
+ run(
452
+ authorize,
453
+ async (page) => {
454
+ const cdp = await page.createCDPSession();
455
+
456
+ try {
457
+ const result: unknown = await Reflect.apply(cdp.send, cdp, [method, parameters]);
458
+
459
+ return Schema.decodeUnknownSync(schema)(result);
460
+ } finally {
461
+ await cdp.detach();
462
+ }
463
+ },
464
+ method === "Cloudflare.handoff",
465
+ );
466
+
467
+ const session: BrowserSession = {
468
+ reference,
469
+ run,
470
+ fillCredential: (request) =>
471
+ runEffect((commandTimeoutMillis) =>
472
+ fillCredential(request).pipe(
473
+ Effect.provideService(BrowserSessionPage, {
474
+ page: currentPage,
475
+ commandTimeoutMillis,
476
+ }),
477
+ Effect.catchIf(
478
+ (error) => error.reason === "timeout" || error.dispatch === "possibly-dispatched",
479
+ (error) =>
480
+ terminate.pipe(
481
+ Effect.flatMap((cleanup) =>
482
+ Effect.fail(CredentialFillError.make({ ...error, cleanup })),
483
+ ),
484
+ ),
485
+ ),
486
+ ),
487
+ ),
488
+ handoff: (authorize, request) =>
489
+ decode(BrowserRunHandoffRequest, request).pipe(
490
+ Effect.flatMap((value) =>
491
+ command(
492
+ authorize,
493
+ "Cloudflare.handoff",
494
+ { instructions: value.instructions, timeout: value.timeout },
495
+ Schema.Struct({
496
+ handoffId: Schema.NonEmptyString.check(Schema.isMaxLength(1024)),
497
+ }),
498
+ ),
499
+ ),
500
+ Effect.map((value) =>
501
+ BrowserRunHandoffResult.make({ handoffId: Redacted.make(value.handoffId) }),
502
+ ),
503
+ ),
504
+ getLiveView: (authorize, request) =>
505
+ decode(BrowserRunLiveViewRequest, request).pipe(
506
+ Effect.flatMap((value) =>
507
+ command(
508
+ authorize,
509
+ "Cloudflare.getLiveView",
510
+ { mode: value.mode, expiresInMs: value.expiresInMs },
511
+ Schema.Struct({ devtoolsFrontendUrl: Schema.String }),
512
+ ),
513
+ ),
514
+ Effect.flatMap((value) =>
515
+ decode(BrowserRunLiveViewResult, {
516
+ devtoolsFrontendUrl: Redacted.make(value.devtoolsFrontendUrl),
517
+ }),
518
+ ),
519
+ ),
520
+ getReadOnlyLiveView: (authorize, request) =>
521
+ decode(BrowserRunLiveViewRequest, request).pipe(
522
+ Effect.flatMap((value) =>
523
+ runEffect((commandTimeoutMillis) =>
524
+ authorize.pipe(
525
+ Effect.andThen(() =>
526
+ readonlyLiveView.mint(reference.sessionId, reference.targetId, value).pipe(
527
+ Effect.tapCause((cause) =>
528
+ reportBrowserCause("session.readonlyLiveView", cause),
529
+ ),
530
+ Effect.mapError(() => reportedBrowserError(failure("provider"))),
531
+ ),
532
+ ),
533
+ Effect.timeoutOrElse({
534
+ duration: commandTimeoutMillis,
535
+ orElse: () => Effect.fail(failure("timeout")),
536
+ }),
537
+ ),
538
+ ),
539
+ ),
540
+ ),
541
+ getHandoffState: (authorize) =>
542
+ command(
543
+ authorize,
544
+ "Cloudflare.getHandoffState",
545
+ {},
546
+ Schema.Struct({
547
+ active: Schema.Boolean,
548
+ handoffId: Schema.optionalKey(
549
+ Schema.NonEmptyString.check(Schema.isMaxLength(1024)),
550
+ ),
551
+ durationMs: Schema.optionalKey(Schema.Natural),
552
+ }),
553
+ ).pipe(
554
+ Effect.map((value) =>
555
+ BrowserRunHandoffState.make({
556
+ active: value.active,
557
+ ...(value.handoffId === undefined
558
+ ? {}
559
+ : { handoffId: Redacted.make(value.handoffId) }),
560
+ ...(value.durationMs === undefined ? {} : { durationMs: value.durationMs }),
561
+ }),
562
+ ),
563
+ ),
564
+ };
565
+
566
+ return session;
567
+ });
568
+
569
+ const attach = Effect.fnUntraced(function* (input: BrowserSessionReference) {
570
+ const reference = yield* decode(BrowserSessionReference, input);
571
+ const scope = yield* Scope.fork(yield* Effect.scope);
572
+
573
+ return yield* attachPage(reference).pipe(
574
+ Effect.provideService(Scope.Scope, scope),
575
+ Effect.timeoutOrElse({
576
+ duration: reference.commandTimeoutMillis,
577
+ orElse: () =>
578
+ reportBrowserCause("session.connect", Cause.fail(failure("timeout"))).pipe(
579
+ Effect.andThen(Effect.fail(reportedBrowserError(failure("timeout")))),
580
+ ),
581
+ }),
582
+ Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(scope, exit) : Effect.void)),
583
+ );
584
+ });
585
+
586
+ return BrowserSessions.of({
587
+ create,
588
+ attach,
589
+ close,
590
+ keepAlive: (sessionId) =>
591
+ decode(SessionId, sessionId).pipe(
592
+ Effect.flatMap((id) =>
593
+ binding.keepAlive(Redacted.value(id)).pipe(
594
+ Effect.timeoutOrElse({
595
+ duration: "10 seconds",
596
+ orElse: () => Effect.fail(failure("timeout")),
597
+ }),
598
+ Effect.catch((error) =>
599
+ reportBrowserCause("session.keepAlive", Cause.fail(error)).pipe(
600
+ Effect.andThen(
601
+ Effect.fail(
602
+ reportedBrowserError(
603
+ failure(error.reason === "timeout" ? "timeout" : "provider"),
604
+ ),
605
+ ),
606
+ ),
607
+ ),
608
+ ),
609
+ ),
610
+ ),
611
+ Effect.withTracerEnabled(false),
612
+ ),
613
+ });
614
+ }),
615
+ );
616
+
617
+ static layer(options: BrowserSessionsOptions) {
618
+ return this.layerNoDeps.pipe(
619
+ Layer.provide(BrowserRunBinding.layer(options.browser)),
620
+ Layer.provide(BrowserRunSessionLifecycle.layer(options)),
621
+ Layer.provide(BrowserRunReadonlyLiveView.layer(options)),
622
+ );
623
+ }
624
+ }