@effect-agent/platform-cloudflare 0.1.0-beta.41 → 0.1.0-beta.44

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 (44) hide show
  1. package/dist/browser-quick-action.mjs.map +1 -1
  2. package/dist/browser-rest-capture.mjs.map +1 -1
  3. package/dist/browser-rest-crawl.mjs.map +1 -1
  4. package/dist/browser-session-lifecycle-DqntvG-Y.d.mts +21 -0
  5. package/dist/browser-session-lifecycle-ZGgb3pnK.mjs +84 -0
  6. package/dist/browser-session-lifecycle-ZGgb3pnK.mjs.map +1 -0
  7. package/dist/index.d.mts +85 -65
  8. package/dist/index.mjs +7 -2
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/interactive-browser.d.mts +1 -18
  11. package/dist/interactive-browser.mjs +2 -81
  12. package/dist/interactive-browser.mjs.map +1 -1
  13. package/dist/prepared-admission-BKp_Upw2.mjs.map +1 -1
  14. package/dist/protected-browser.d.mts +62 -0
  15. package/dist/protected-browser.mjs +714 -0
  16. package/dist/protected-browser.mjs.map +1 -0
  17. package/dist/scheduling.mjs.map +1 -1
  18. package/dist/subscriptions.mjs.map +1 -1
  19. package/package.json +1 -81
  20. package/src/alarm.ts +41 -0
  21. package/src/bindings.ts +5 -0
  22. package/src/boundary.ts +4 -0
  23. package/src/browser-quick-action.ts +52 -0
  24. package/src/browser-rest-capture.ts +30 -0
  25. package/src/browser-rest-crawl.ts +43 -0
  26. package/src/browser-session-lifecycle.ts +18 -0
  27. package/src/client.ts +40 -0
  28. package/src/code-mode-executor.ts +50 -0
  29. package/src/index.ts +1 -1
  30. package/src/interactive-browser.ts +176 -0
  31. package/src/layers.ts +16 -1
  32. package/src/memory.ts +42 -3
  33. package/src/prepared-admission.ts +1 -0
  34. package/src/progress-wait.ts +14 -0
  35. package/src/protected-browser/binding.ts +185 -0
  36. package/src/protected-browser/inspect-frame.ts +82 -0
  37. package/src/protected-browser/native.ts +384 -0
  38. package/src/protected-browser/policy.ts +594 -0
  39. package/src/protected-browser.ts +2 -0
  40. package/src/scheduling.ts +34 -0
  41. package/src/subscriptions.ts +50 -0
  42. package/src/thread-object.ts +55 -1
  43. package/src/transport.ts +1 -0
  44. package/src/wake-scheduler.ts +1 -0
package/src/alarm.ts CHANGED
@@ -98,6 +98,7 @@ export class DurableAlarmService extends Context.Service<
98
98
  * on top of the already-committed pre-armed alarm.
99
99
  */
100
100
  const runningPasses = yield* Ref.make(0);
101
+
101
102
  const scheduled = Effect.tryPromise({
102
103
  try: () => ctx.storage.getAlarm(),
103
104
  catch: alarmFailure("get alarm"),
@@ -106,11 +107,13 @@ export class DurableAlarmService extends Context.Service<
106
107
  deadline === null ? Option.none<number>() : Option.some(deadline),
107
108
  ),
108
109
  );
110
+
109
111
  const scheduleAt = (epochMillis: number) =>
110
112
  Effect.tryPromise({
111
113
  try: () => ctx.storage.setAlarm(epochMillis),
112
114
  catch: alarmFailure("set alarm"),
113
115
  });
116
+
114
117
  const ensureScheduledBy = (epochMillis: number) =>
115
118
  scheduled.pipe(
116
119
  Effect.flatMap((existing) =>
@@ -119,21 +122,26 @@ export class DurableAlarmService extends Context.Service<
119
122
  : scheduleAt(epochMillis),
120
123
  ),
121
124
  );
125
+
122
126
  const armNow = Clock.currentTimeMillis.pipe(
123
127
  Effect.flatMap((now) => ensureScheduledBy(now)),
124
128
  );
129
+
125
130
  const scheduleNow = Ref.get(runningPasses).pipe(
126
131
  Effect.flatMap((passes) => (passes > 0 ? Effect.void : armNow)),
127
132
  );
133
+
128
134
  const withWakesDeferred = <A, E, R>(body: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
129
135
  Ref.update(runningPasses, (passes) => passes + 1).pipe(
130
136
  Effect.andThen(body),
131
137
  Effect.ensuring(Ref.update(runningPasses, (passes) => passes - 1)),
132
138
  );
139
+
133
140
  const cancel = Effect.tryPromise({
134
141
  try: () => ctx.storage.deleteAlarm(),
135
142
  catch: alarmFailure("delete alarm"),
136
143
  });
144
+
137
145
  return DurableAlarmService.of({
138
146
  scheduled,
139
147
  scheduleAt,
@@ -221,6 +229,7 @@ const readMaintenanceState = async (
221
229
  transaction: DurableObjectTransaction,
222
230
  ): Promise<{ readonly state: ThreadMaintenanceState; readonly initialized: boolean }> => {
223
231
  const encoded = await transaction.get(MAINTENANCE_STATE_KEY);
232
+
224
233
  return encoded === undefined
225
234
  ? { state: initialMaintenanceState(), initialized: false }
226
235
  : { state: decodeMaintenanceState(encoded), initialized: true };
@@ -231,6 +240,7 @@ const ensureTransactionAlarmBy = async (
231
240
  deadline: number,
232
241
  ): Promise<void> => {
233
242
  const scheduled = await transaction.getAlarm();
243
+
234
244
  if (scheduled === null || scheduled > deadline) {
235
245
  await transaction.setAlarm(deadline);
236
246
  }
@@ -241,6 +251,7 @@ const stableExternalWait = (
241
251
  reports: ReadonlyMap<string, RecoveryReport>,
242
252
  ): boolean => {
243
253
  const decision = reports.get(snapshot.submissionId)?.decision._tag;
254
+
244
255
  // An accepted abort still owes cleanup/settlement even if its claim was deferred this pass.
245
256
  if (decision === "SettleAborted") return false;
246
257
  switch (snapshot.state) {
@@ -353,13 +364,16 @@ export class ThreadMaintenance extends Context.Service<
353
364
  const beginMutation = Effect.fn("ThreadMaintenance.beginMutation")(function* () {
354
365
  yield* failpoint.hit("maintenance:dirty:before");
355
366
  const now = yield* Clock.currentTimeMillis;
367
+
356
368
  yield* runTransaction("advance maintenance generation", () =>
357
369
  ctx.storage.transaction(async (transaction) => {
358
370
  const { state } = await readMaintenanceState(transaction);
371
+
359
372
  const next = ThreadMaintenanceState.make({
360
373
  ...state,
361
374
  dirty: state.dirty + 1n,
362
375
  });
376
+
363
377
  await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
364
378
  // The earliest configured retry bounds a newly actionable mutation without relying
365
379
  // on its best-effort immediate wake hint.
@@ -390,9 +404,11 @@ export class ThreadMaintenance extends Context.Service<
390
404
  const ensureAlarm = Effect.fn("ThreadMaintenance.ensureAlarm")(function* () {
391
405
  yield* failpoint.hit("maintenance:ensure:before");
392
406
  const now = yield* Clock.currentTimeMillis;
407
+
393
408
  yield* runTransaction("ensure maintenance alarm", () =>
394
409
  ctx.storage.transaction(async (transaction) => {
395
410
  const { state, initialized } = await readMaintenanceState(transaction);
411
+
396
412
  if (!initialized) {
397
413
  await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
398
414
  }
@@ -407,23 +423,29 @@ export class ThreadMaintenance extends Context.Service<
407
423
  const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* () {
408
424
  yield* failpoint.hit("maintenance:begin:before");
409
425
  const now = yield* Clock.currentTimeMillis;
426
+
410
427
  const result = yield* runTransaction("begin maintenance pass", () =>
411
428
  ctx.storage.transaction(async (transaction) => {
412
429
  const { state, initialized } = await readMaintenanceState(transaction);
430
+
413
431
  if (!initialized) {
414
432
  await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
415
433
  }
416
434
  if (state.processed >= state.dirty) {
417
435
  await transaction.deleteAlarm();
436
+
418
437
  return { _tag: "CaughtUp" as const, nonterminal: state.nonterminal };
419
438
  }
420
439
  // Pre-arm the earliest retry before recovery. A successful finish may move this slot
421
440
  // LATER to its bounded backoff, which does not cancel the running handler.
422
441
  await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
442
+
423
443
  return { _tag: "Actionable" as const, generation: state.dirty };
424
444
  }),
425
445
  );
446
+
426
447
  yield* failpoint.hit("maintenance:begin:after");
448
+
427
449
  return result;
428
450
  });
429
451
 
@@ -433,6 +455,7 @@ export class ThreadMaintenance extends Context.Service<
433
455
  const priorStalls = yield* Ref.getAndUpdate(stalls, (count) =>
434
456
  progressed ? 0 : count + 1,
435
457
  );
458
+
436
459
  if (progressed) return config.alarmBackoffBase;
437
460
  const exponent = Math.min(priorStalls, 30);
438
461
  const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** exponent);
@@ -440,6 +463,7 @@ export class ThreadMaintenance extends Context.Service<
440
463
  // Full jitter over [backoff/2, backoff]: desynchronizes retry storms without ever
441
464
  // waiting longer than the deterministic bound.
442
465
  const jittered = Math.ceil(backoff / 2 + (backoff / 2) * jitter);
466
+
443
467
  return Math.min(jittered, config.wakeScanInterval);
444
468
  });
445
469
 
@@ -460,9 +484,11 @@ export class ThreadMaintenance extends Context.Service<
460
484
  Effect.gen(function* () {
461
485
  const activeAtStart = yield* Ref.get(activeMutations);
462
486
  const generation = yield* beginPass();
487
+
463
488
  return { ...generation, activeAtStart };
464
489
  }),
465
490
  );
491
+
466
492
  if (started._tag === "CaughtUp") {
467
493
  return yield* annotate(
468
494
  MaintenancePassReport.make({
@@ -483,6 +509,7 @@ export class ThreadMaintenance extends Context.Service<
483
509
  const reports = new Map(recovered.map((report) => [report.submissionId, report]));
484
510
  const head = remaining[0];
485
511
  const headWaiting = head !== undefined && stableExternalWait(head, reports);
512
+
486
513
  const autonomous = remaining.some((snapshot, index) => {
487
514
  // FIFO followers cannot execute through a stable external wait. Only plain queued
488
515
  // input is dormant here; admission repairs and accepted aborts still need a pass.
@@ -493,19 +520,26 @@ export class ThreadMaintenance extends Context.Service<
493
520
  reports.get(snapshot.submissionId)?.decision._tag === "ApplyInput"
494
521
  )
495
522
  return false;
523
+
496
524
  return !stableExternalWait(snapshot, reports);
497
525
  });
526
+
498
527
  const progressed =
499
528
  settlements.length > 0 || recovered.some((report) => report.disposition === "repaired");
529
+
500
530
  const delay = autonomous ? yield* rearmDelay(progressed) : 0;
501
531
  const now = yield* Clock.currentTimeMillis;
532
+
502
533
  yield* failpoint.hit("maintenance:finish:before");
534
+
503
535
  const alarmDisposition = yield* generationGate.withPermit(
504
536
  Effect.gen(function* () {
505
537
  const active = yield* Ref.get(activeMutations);
538
+
506
539
  return yield* runTransaction("finish maintenance pass", () =>
507
540
  ctx.storage.transaction(async (transaction) => {
508
541
  const { state } = await readMaintenanceState(transaction);
542
+
509
543
  // Autonomous work and in-flight mutations intentionally leave the observed
510
544
  // generation dirty. Otherwise acknowledge only the pass-start generation.
511
545
  const processed =
@@ -514,17 +548,20 @@ export class ThreadMaintenance extends Context.Service<
514
548
  : state.processed > started.generation
515
549
  ? state.processed
516
550
  : started.generation;
551
+
517
552
  const next = ThreadMaintenanceState.make({
518
553
  ...state,
519
554
  processed,
520
555
  nonterminal: remaining.length,
521
556
  });
557
+
522
558
  await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
523
559
  if (autonomous) {
524
560
  // Replace the crash-fallback slot with this pass's bounded backoff. The target
525
561
  // is never earlier than the begin-pass fallback, so workerd does not cancel
526
562
  // this running alarm handler before its report/span can complete.
527
563
  await transaction.setAlarm(now + delay);
564
+
528
565
  return "rearmed" as const;
529
566
  }
530
567
  if (started.activeAtStart > 0 || active > 0 || next.dirty > next.processed) {
@@ -534,18 +571,22 @@ export class ThreadMaintenance extends Context.Service<
534
571
  // from inside the current handler: workerd cancels a running handler when it
535
572
  // writes an earlier slot.
536
573
  await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
574
+
537
575
  return "rearmed" as const;
538
576
  }
539
577
  await transaction.deleteAlarm();
578
+
540
579
  return "cleared" as const;
541
580
  }),
542
581
  );
543
582
  }),
544
583
  );
584
+
545
585
  yield* failpoint.hit("maintenance:finish:after");
546
586
  if (alarmDisposition === "cleared") {
547
587
  yield* Ref.set(stalls, 0);
548
588
  }
589
+
549
590
  return yield* annotate(
550
591
  MaintenancePassReport.make({
551
592
  phase: "actionable",
package/src/bindings.ts CHANGED
@@ -91,12 +91,15 @@ export const threadNamespaceFromEnv = Effect.fn("threadNamespaceFromEnv")(functi
91
91
  message: "The Worker environment is not an object; no bindings are available.",
92
92
  });
93
93
  }
94
+
94
95
  const candidate = yield* Effect.try({
95
96
  try: () => {
96
97
  const value: unknown = Reflect.get(env, binding);
98
+
97
99
  if (!Predicate.isObjectKeyword(value)) return undefined;
98
100
  const idFromName: unknown = Reflect.get(value, "idFromName");
99
101
  const get: unknown = Reflect.get(value, "get");
102
+
100
103
  return typeof idFromName === "function" && typeof get === "function" ? value : undefined;
101
104
  },
102
105
  catch: () =>
@@ -105,11 +108,13 @@ export const threadNamespaceFromEnv = Effect.fn("threadNamespaceFromEnv")(functi
105
108
  message: `env.${binding} could not be inspected as a DurableObjectNamespace binding.`,
106
109
  }),
107
110
  });
111
+
108
112
  if (candidate !== undefined) {
109
113
  // The structural probe above is the entire runtime contract this package relies on;
110
114
  // the assertion records that `idFromName`/`get` name a DurableObjectNamespace.
111
115
  return candidate as unknown as DurableObjectNamespace<ThreadObjectRpc>;
112
116
  }
117
+
113
118
  return yield* CloudflareBindingError.make({
114
119
  binding,
115
120
  message:
package/src/boundary.ts CHANGED
@@ -9,6 +9,7 @@ const boundForeignDiagnostic = (message: string): string =>
9
9
  export const safeCauseMessage = (cause: unknown, fallback: string): string => {
10
10
  try {
11
11
  const message = cause instanceof Error ? cause.message : cause;
12
+
12
13
  return boundForeignDiagnostic(typeof message === "string" ? message : String(message));
13
14
  } catch {
14
15
  return boundForeignDiagnostic(fallback);
@@ -38,9 +39,12 @@ export const cloudflareFailureSignals = (cause: unknown): CloudflareFailureSigna
38
39
  const retryableValue = Reflect.get(cause, "retryable");
39
40
  const overloadedValue = Reflect.get(cause, "overloaded");
40
41
  const resetValue = Reflect.get(cause, "durableObjectReset");
42
+
41
43
  const retryable =
42
44
  typeof retryableValue === "boolean" ? retryableValue : resetValue === true ? true : undefined;
45
+
43
46
  const overloaded = typeof overloadedValue === "boolean" ? overloadedValue : undefined;
47
+
44
48
  return {
45
49
  ...(retryable === undefined ? {} : { retryable }),
46
50
  ...(overloaded === undefined ? {} : { overloaded }),
@@ -97,6 +97,7 @@ export class BrowserQuickActionBrowserBinding extends Context.Service<
97
97
  options: BrowserQuickActionCaptureOptions,
98
98
  ): Layer.Layer<BrowserQuickActionBrowserBinding> {
99
99
  const browser = options.browser;
100
+
100
101
  const invoke = Effect.fn("BrowserQuickActionBrowserBinding.invoke")(function* (
101
102
  action: "screenshot" | "content" | "markdown" | "links" | "scrape" | "json",
102
103
  evaluate: () => Promise<Response>,
@@ -106,6 +107,7 @@ export class BrowserQuickActionBrowserBinding extends Context.Service<
106
107
  catch: (cause) => BrowserQuickActionRpcError.make({ action, cause }),
107
108
  });
108
109
  });
110
+
109
111
  return Layer.succeed(BrowserQuickActionBrowserBinding)({
110
112
  screenshot: (request) =>
111
113
  invoke("screenshot", () => browser.quickAction("screenshot", request)),
@@ -175,8 +177,10 @@ const decodeEnvelope = Schema.decodeUnknownOption(Schema.fromJsonString(QuickAct
175
177
  const quickActionCommonOptions = (request: PageCaptureRequest): BrowserRunCommonOptions => {
176
178
  const options: BrowserRunBaseOptions = {};
177
179
  const navigation = request.navigation;
180
+
178
181
  if (navigation !== undefined) {
179
182
  const goto: NonNullable<BrowserRunBaseOptions["gotoOptions"]> = {};
183
+
180
184
  if (navigation.waitUntil !== undefined) goto.waitUntil = navigation.waitUntil;
181
185
  if (navigation.timeoutMillis !== undefined) goto.timeout = navigation.timeoutMillis;
182
186
  if (Object.keys(goto).length > 0) options.gotoOptions = goto;
@@ -200,6 +204,7 @@ const quickActionCommonOptions = (request: PageCaptureRequest): BrowserRunCommon
200
204
  options.allowRequestPattern = [...request.resourcePolicy.allowRequestPatterns];
201
205
  }
202
206
  }
207
+
203
208
  return request.target._tag === "PageUrlTarget"
204
209
  ? { ...options, url: request.target.url }
205
210
  : { ...options, html: request.target.html };
@@ -211,6 +216,7 @@ const executeQuickAction = (
211
216
  request: PageCaptureRequest,
212
217
  ): Effect.Effect<Response, BrowserQuickActionRpcError> => {
213
218
  const options = quickActionCommonOptions(request);
219
+
214
220
  switch (request.action._tag) {
215
221
  case "CapturePageContent": {
216
222
  return browser.content(options);
@@ -284,6 +290,7 @@ const readBoundedResponse = Effect.fn("BrowserQuickActionCapture.readResponse")(
284
290
  request: PageCaptureRequest,
285
291
  ) {
286
292
  const body = response.body;
293
+
287
294
  if (body === null) return "";
288
295
 
289
296
  const reader = yield* Effect.acquireRelease(
@@ -293,6 +300,7 @@ const readBoundedResponse = Effect.fn("BrowserQuickActionCapture.readResponse")(
293
300
  }),
294
301
  releaseResponseReader,
295
302
  );
303
+
296
304
  const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
297
305
  let observedBytes = 0;
298
306
  let bodyText = "";
@@ -302,6 +310,7 @@ const readBoundedResponse = Effect.fn("BrowserQuickActionCapture.readResponse")(
302
310
  try: () => reader.read(),
303
311
  catch: (cause) => protocolError("Reading the Quick Action response failed", cause),
304
312
  });
313
+
305
314
  if (chunk.done) break;
306
315
 
307
316
  observedBytes += chunk.value.byteLength;
@@ -334,25 +343,32 @@ const readBoundedResponse = Effect.fn("BrowserQuickActionCapture.readResponse")(
334
343
  */
335
344
  const retryAfterMillis = (response: Response): number | undefined => {
336
345
  const header = response.headers.get("Retry-After");
346
+
337
347
  if (header === null) return undefined;
338
348
  const seconds = Number(header);
349
+
339
350
  if (!Number.isSafeInteger(seconds) || seconds < 0) return undefined;
340
351
  const millis = seconds * 1_000;
352
+
341
353
  return Number.isSafeInteger(millis) ? millis : undefined;
342
354
  };
343
355
 
344
356
  const browserMillis = (response: Response): number | undefined => {
345
357
  const header = response.headers.get("X-Browser-Ms-Used");
358
+
346
359
  if (header === null) return undefined;
347
360
  const millis = Number(header);
361
+
348
362
  return Number.isSafeInteger(millis) && millis >= 0 ? millis : undefined;
349
363
  };
350
364
 
351
365
  /** Only trusted response metadata chooses transport framing; page text never does. */
352
366
  const isJsonResponse = (response: Response): boolean => {
353
367
  const contentType = response.headers.get("Content-Type");
368
+
354
369
  if (contentType === null) return false;
355
370
  const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase();
371
+
356
372
  return mediaType === "application/json" || mediaType?.endsWith("+json") === true;
357
373
  };
358
374
 
@@ -368,6 +384,7 @@ const parseOutput = (
368
384
  );
369
385
  }
370
386
  const envelope = decodeEnvelope(bodyText);
387
+
371
388
  if (Option.isNone(envelope)) {
372
389
  return protocolError(
373
390
  "The JSON Quick Action response did not carry a valid response envelope",
@@ -386,6 +403,7 @@ const parseOutput = (
386
403
  if (typeof envelope.value.result !== "string") {
387
404
  return protocolError("The Quick Action envelope carried a non-text result");
388
405
  }
406
+
389
407
  return action._tag === "CapturePageContent"
390
408
  ? PageContentCaptured.make({ html: envelope.value.result })
391
409
  : PageMarkdownCaptured.make({ markdown: envelope.value.result });
@@ -395,9 +413,11 @@ const parseOutput = (
395
413
  _tag: "PageLinksCaptured",
396
414
  links: envelope.value.result,
397
415
  });
416
+
398
417
  if (Option.isNone(decoded)) {
399
418
  return protocolError("The links Quick Action did not return a bounded array of valid URLs");
400
419
  }
420
+
401
421
  return decoded.value;
402
422
  }
403
423
  case "CapturePageScrape": {
@@ -405,11 +425,13 @@ const parseOutput = (
405
425
  _tag: "PageScrapeCaptured",
406
426
  groups: envelope.value.result,
407
427
  });
428
+
408
429
  if (Option.isNone(decoded)) {
409
430
  return protocolError(
410
431
  "The scrape Quick Action did not return bounded grouped element records",
411
432
  );
412
433
  }
434
+
413
435
  return decoded.value;
414
436
  }
415
437
  case "CapturePageStructured": {
@@ -437,6 +459,7 @@ const makeCapture = (
437
459
  }
438
460
 
439
461
  const usesWorkersAi = request.action._tag === "CapturePageStructured";
462
+
440
463
  if (usesWorkersAi) {
441
464
  if (workersAi === undefined) {
442
465
  return yield* PageCaptureUnsupportedError.make({
@@ -467,11 +490,14 @@ const makeCapture = (
467
490
  protocolError("The browser binding rejected the Quick Action", error.cause),
468
491
  ),
469
492
  );
493
+
470
494
  const bodyText = yield* readBoundedResponse(response, request);
495
+
471
496
  if (response.status === 429) {
472
497
  const retryAfter = retryAfterMillis(response);
473
498
  const reason = isQuotaMessage(bodyText) ? "quota" : "rate";
474
499
  const cause = privateResponseCause(bodyText);
500
+
475
501
  return yield* PageCaptureRateLimitedError.make({
476
502
  implementation: browserQuickActionImplementation,
477
503
  reason,
@@ -486,12 +512,15 @@ const makeCapture = (
486
512
  if (!response.ok) {
487
513
  const message = `The Quick Action answered HTTP ${response.status}`;
488
514
  const cause = privateResponseCause(bodyText);
515
+
489
516
  if (response.status >= 500) {
490
517
  return yield* protocolError(message, cause);
491
518
  }
519
+
492
520
  return yield* navigationError(message, cause);
493
521
  }
494
522
  const output = parseOutput(request.action, bodyText, response);
523
+
495
524
  if (
496
525
  output._tag === "PageCaptureNavigationError" ||
497
526
  output._tag === "PageCaptureProtocolError"
@@ -499,6 +528,7 @@ const makeCapture = (
499
528
  return yield* output;
500
529
  }
501
530
  const millis = browserMillis(response);
531
+
502
532
  return PageCaptureResult.make({
503
533
  implementation: browserQuickActionImplementation,
504
534
  output,
@@ -543,6 +573,7 @@ export const browserQuickActionWorkersAiCaptureLayer = (): Layer.Layer<
543
573
  Effect.gen(function* () {
544
574
  const browser = yield* BrowserQuickActionBrowserBinding;
545
575
  const workersAi = yield* BrowserQuickActionWorkersAi;
576
+
546
577
  return PageCapture.of({ capture: makeCapture(browser, workersAi) });
547
578
  }),
548
579
  );
@@ -580,6 +611,7 @@ export const CloudflareBrowser = {
580
611
 
581
612
  const screenshotOptions = (request: PageScreenshotRequest): BrowserRunScreenshotOptions => {
582
613
  const options: BrowserRunBaseOptions = {};
614
+
583
615
  if (
584
616
  request.navigation?.waitUntil !== undefined ||
585
617
  request.navigation?.timeoutMillis !== undefined
@@ -610,6 +642,7 @@ const screenshotOptions = (request: PageScreenshotRequest): BrowserRunScreenshot
610
642
  if (request.resourcePolicy?.allowRequestPatterns !== undefined) {
611
643
  options.allowRequestPattern = [...request.resourcePolicy.allowRequestPatterns];
612
644
  }
645
+
613
646
  return {
614
647
  ...options,
615
648
  ...(request.target._tag === "PageUrlTarget"
@@ -643,8 +676,10 @@ const pngResponse = (response: Response): boolean =>
643
676
 
644
677
  const declaredLength = (response: Response): number | undefined => {
645
678
  const raw = response.headers.get("Content-Length");
679
+
646
680
  if (raw === null || !/^(0|[1-9][0-9]*)$/.test(raw)) return undefined;
647
681
  const length = Number(raw);
682
+
648
683
  return Number.isSafeInteger(length) ? length : undefined;
649
684
  };
650
685
 
@@ -653,22 +688,27 @@ const readScreenshot = Effect.fn("BrowserQuickActionScreenshot.read")(function*
653
688
  request: PageScreenshotRequest,
654
689
  ) {
655
690
  const body = response.body;
691
+
656
692
  if (body === null) {
657
693
  return yield* protocolError("The screenshot response had no body");
658
694
  }
659
695
  if (!pngResponse(response)) {
660
696
  yield* cancelBody(body);
697
+
661
698
  return yield* protocolError("The screenshot response was not image/png");
662
699
  }
663
700
  const length = declaredLength(response);
701
+
664
702
  if (length !== undefined && length > request.limits.maxOutputBytes) {
665
703
  yield* cancelBody(body);
704
+
666
705
  return yield* PageScreenshotOutputLimitError.make({
667
706
  implementation: browserQuickActionImplementation,
668
707
  limit: request.limits.maxOutputBytes,
669
708
  observed: length,
670
709
  });
671
710
  }
711
+
672
712
  const reader = yield* Effect.acquireRelease(
673
713
  Effect.try({
674
714
  try: () => body.getReader(),
@@ -676,13 +716,16 @@ const readScreenshot = Effect.fn("BrowserQuickActionScreenshot.read")(function*
676
716
  }),
677
717
  releaseScreenshotReader,
678
718
  );
719
+
679
720
  const chunks: Array<Uint8Array> = [];
680
721
  let observed = 0;
722
+
681
723
  while (true) {
682
724
  const next = yield* Effect.tryPromise({
683
725
  try: () => reader.read(),
684
726
  catch: (cause) => protocolError("Reading the screenshot response failed", cause),
685
727
  });
728
+
686
729
  if (next.done) break;
687
730
  observed += next.value.byteLength;
688
731
  if (observed > request.limits.maxOutputBytes) {
@@ -696,10 +739,12 @@ const readScreenshot = Effect.fn("BrowserQuickActionScreenshot.read")(function*
696
739
  }
697
740
  const bytes = new Uint8Array(observed);
698
741
  let offset = 0;
742
+
699
743
  for (const chunk of chunks) {
700
744
  bytes.set(chunk, offset);
701
745
  offset += chunk.byteLength;
702
746
  }
747
+
703
748
  return bytes;
704
749
  }, Effect.scoped);
705
750
 
@@ -714,6 +759,7 @@ const makeScreenshot = (browser: BrowserQuickActionClient): PageScreenshotCaptur
714
759
  message: "The browser binding's screenshot action exposes no engine selector",
715
760
  });
716
761
  }
762
+
717
763
  const response = yield* browser
718
764
  .screenshot(screenshotOptions(request))
719
765
  .pipe(
@@ -721,9 +767,12 @@ const makeScreenshot = (browser: BrowserQuickActionClient): PageScreenshotCaptur
721
767
  protocolError("The browser binding rejected the screenshot", error.cause),
722
768
  ),
723
769
  );
770
+
724
771
  if (response.status === 429) {
725
772
  const body = response.body;
773
+
726
774
  if (body !== null) yield* cancelBody(body);
775
+
727
776
  return yield* PageCaptureRateLimitedError.make({
728
777
  implementation: browserQuickActionImplementation,
729
778
  reason: "rate",
@@ -735,11 +784,14 @@ const makeScreenshot = (browser: BrowserQuickActionClient): PageScreenshotCaptur
735
784
  }
736
785
  if (!response.ok) {
737
786
  const body = response.body;
787
+
738
788
  if (body !== null) yield* cancelBody(body);
739
789
  const message = `The screenshot Quick Action answered HTTP ${response.status}`;
790
+
740
791
  return yield* response.status >= 500 ? protocolError(message) : navigationError(message);
741
792
  }
742
793
  const bytes = yield* readScreenshot(response, request);
794
+
743
795
  return PageScreenshotResult.make({
744
796
  implementation: browserQuickActionImplementation,
745
797
  mediaType: "image/png",