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

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