@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
@@ -51,6 +51,7 @@ const RestSuccessEnvelope = Schema.Struct({
51
51
  errors: Schema.optionalKey(Schema.Array(Schema.Unknown)),
52
52
  meta: Schema.optionalKey(Schema.Unknown),
53
53
  });
54
+
54
55
  const RestErrorEnvelope = Schema.Struct({
55
56
  success: Schema.Literal(false),
56
57
  errors: Schema.Array(
@@ -62,6 +63,7 @@ const RestErrorEnvelope = Schema.Struct({
62
63
  result: Schema.optionalKey(Schema.Json),
63
64
  meta: Schema.optionalKey(Schema.Unknown),
64
65
  });
66
+
65
67
  const RestEnvelope = Schema.Union([RestSuccessEnvelope, RestErrorEnvelope]);
66
68
  const decodeEnvelope = Schema.decodeUnknownOption(Schema.fromJsonString(RestEnvelope));
67
69
 
@@ -86,6 +88,7 @@ const privateResponseCause = (
86
88
  if (bodyText.length === 0) return undefined;
87
89
  const token = Redacted.value(apiToken);
88
90
  const diagnostic = boundedDiagnostic(bodyText);
91
+
89
92
  return new Error(token.length === 0 ? diagnostic : diagnostic.replaceAll(token, "[REDACTED]"));
90
93
  };
91
94
 
@@ -94,8 +97,10 @@ const requestBody = (request: PageCaptureRequest): Record<string, unknown> => {
94
97
  request.target._tag === "PageUrlTarget"
95
98
  ? { url: request.target.url }
96
99
  : { html: request.target.html };
100
+
97
101
  if (request.navigation !== undefined) {
98
102
  const goto: Record<string, unknown> = {};
103
+
99
104
  if (request.navigation.waitUntil !== undefined) goto.waitUntil = request.navigation.waitUntil;
100
105
  if (request.navigation.timeoutMillis !== undefined)
101
106
  goto.timeout = request.navigation.timeoutMillis;
@@ -116,6 +121,7 @@ const requestBody = (request: PageCaptureRequest): Record<string, unknown> => {
116
121
  if (request.resourcePolicy?.allowRequestPatterns !== undefined) {
117
122
  body.allowRequestPattern = [...request.resourcePolicy.allowRequestPatterns];
118
123
  }
124
+
119
125
  return body;
120
126
  };
121
127
 
@@ -138,6 +144,7 @@ const actionName = (
138
144
 
139
145
  const actionBody = (request: PageCaptureRequest): Record<string, unknown> => {
140
146
  const body = requestBody(request);
147
+
141
148
  switch (request.action._tag) {
142
149
  case "CapturePageContent":
143
150
  case "CapturePageMarkdown":
@@ -167,8 +174,10 @@ const retryAfterMillis = (
167
174
  headers: Readonly<Record<string, string | undefined>>,
168
175
  ): number | undefined => {
169
176
  const seconds = Number(headers["retry-after"]);
177
+
170
178
  if (!Number.isSafeInteger(seconds) || seconds < 0) return undefined;
171
179
  const millis = seconds * 1_000;
180
+
172
181
  return Number.isSafeInteger(millis) ? millis : undefined;
173
182
  };
174
183
 
@@ -176,14 +185,17 @@ const browserMillis = (
176
185
  headers: Readonly<Record<string, string | undefined>>,
177
186
  ): number | undefined => {
178
187
  const millis = Number(headers["x-browser-ms-used"]);
188
+
179
189
  return Number.isSafeInteger(millis) && millis >= 0 ? millis : undefined;
180
190
  };
181
191
 
182
192
  /** Response framing is provider metadata, never content controlled by the rendered page. */
183
193
  const isJsonResponse = (headers: Readonly<Record<string, string | undefined>>): boolean => {
184
194
  const contentType = headers["content-type"];
195
+
185
196
  if (contentType === undefined) return false;
186
197
  const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase();
198
+
187
199
  return mediaType === "application/json" || mediaType?.endsWith("+json") === true;
188
200
  };
189
201
 
@@ -192,6 +204,7 @@ const readBoundedResponse = Effect.fn("BrowserRestCapture.readResponse")(functio
192
204
  request: PageCaptureRequest,
193
205
  ): Effect.fn.Return<string, PageCaptureError> {
194
206
  const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
207
+
195
208
  const chunks = yield* Stream.runFoldEffect<
196
209
  Uint8Array,
197
210
  HttpClientError.HttpClientError,
@@ -204,6 +217,7 @@ const readBoundedResponse = Effect.fn("BrowserRestCapture.readResponse")(functio
204
217
  () => ({ observed: 0, text: "" }),
205
218
  (state, chunk) => {
206
219
  const observed = state.observed + chunk.byteLength;
220
+
207
221
  if (observed > request.limits.maxOutputBytes) {
208
222
  return Effect.fail(
209
223
  PageCaptureOutputLimitError.make({
@@ -213,6 +227,7 @@ const readBoundedResponse = Effect.fn("BrowserRestCapture.readResponse")(functio
213
227
  }),
214
228
  );
215
229
  }
230
+
216
231
  return Effect.try({
217
232
  try: () => ({ observed, text: state.text + decoder.decode(chunk, { stream: true }) }),
218
233
  catch: (cause) => protocolError("Decoding the Browser Run response failed", cause),
@@ -225,6 +240,7 @@ const readBoundedResponse = Effect.fn("BrowserRestCapture.readResponse")(functio
225
240
  : protocolError("Reading the Browser Run response failed", error),
226
241
  ),
227
242
  );
243
+
228
244
  return yield* Effect.try({
229
245
  try: () => chunks.text + decoder.decode(),
230
246
  catch: (cause) => protocolError("Decoding the Browser Run response failed", cause),
@@ -237,6 +253,7 @@ const parseOutput = (
237
253
  apiToken: Redacted.Redacted<string>,
238
254
  ): PageCaptureOutput | PageCaptureNavigationError | PageCaptureProtocolError => {
239
255
  const envelope = decodeEnvelope(bodyText);
256
+
240
257
  if (Option.isNone(envelope)) {
241
258
  return protocolError(
242
259
  "The Browser Run response did not carry a valid response envelope",
@@ -255,6 +272,7 @@ const parseOutput = (
255
272
  if (typeof envelope.value.result !== "string") {
256
273
  return protocolError("The Browser Run response envelope carried a non-text result");
257
274
  }
275
+
258
276
  return action._tag === "CapturePageContent"
259
277
  ? PageContentCaptured.make({ html: envelope.value.result })
260
278
  : PageMarkdownCaptured.make({ markdown: envelope.value.result });
@@ -263,6 +281,7 @@ const parseOutput = (
263
281
  _tag: "PageLinksCaptured",
264
282
  links: envelope.value.result,
265
283
  });
284
+
266
285
  return Option.isSome(decoded)
267
286
  ? decoded.value
268
287
  : protocolError("Browser Run links did not return a bounded array of valid URLs");
@@ -272,6 +291,7 @@ const parseOutput = (
272
291
  _tag: "PageScrapeCaptured",
273
292
  groups: envelope.value.result,
274
293
  });
294
+
275
295
  return Option.isSome(decoded)
276
296
  ? decoded.value
277
297
  : protocolError("Browser Run scrape did not return bounded grouped element records");
@@ -292,6 +312,7 @@ const makeCapture = (
292
312
  request: PageCaptureRequest,
293
313
  ): Effect.fn.Return<PageCaptureResult, PageCaptureError> {
294
314
  const usesWorkersAi = request.action._tag === "CapturePageStructured";
315
+
295
316
  if (usesWorkersAi) {
296
317
  if (workersAi === undefined) {
297
318
  return yield* PageCaptureUnsupportedError.make({
@@ -318,6 +339,7 @@ const makeCapture = (
318
339
  }
319
340
  const action = actionName(request.action);
320
341
  const path = `${API_ORIGIN}/client/v4/accounts/${encodeURIComponent(options.accountId)}/browser-rendering/${action}`;
342
+
321
343
  const requestWithBody = yield* HttpClientRequest.post(path).pipe(
322
344
  request.engine === "kitesurf"
323
345
  ? HttpClientRequest.setUrlParam("browser", "kitesurf")
@@ -327,14 +349,18 @@ const makeCapture = (
327
349
  HttpClientRequest.bodyJson(actionBody(request)),
328
350
  Effect.mapError((cause) => protocolError("Encoding the Browser Run request failed", cause)),
329
351
  );
352
+
330
353
  const response = yield* client
331
354
  .execute(requestWithBody)
332
355
  .pipe(
333
356
  Effect.mapError((cause) => protocolError("Calling the Browser Run REST API failed", cause)),
334
357
  );
358
+
335
359
  const bodyText = yield* readBoundedResponse(response, request);
360
+
336
361
  if (response.status === 429) {
337
362
  const reason = isQuotaMessage(bodyText) ? "quota" : "rate";
363
+
338
364
  return yield* PageCaptureRateLimitedError.make({
339
365
  implementation: browserRestCaptureImplementation,
340
366
  reason,
@@ -361,6 +387,7 @@ const makeCapture = (
361
387
  `Browser Run answered HTTP ${String(response.status)}`,
362
388
  privateResponseCause(bodyText, options.apiToken),
363
389
  );
390
+
364
391
  return yield* error;
365
392
  }
366
393
  if (!isJsonResponse(response.headers)) {
@@ -370,12 +397,14 @@ const makeCapture = (
370
397
  );
371
398
  }
372
399
  const output = parseOutput(request.action, bodyText, options.apiToken);
400
+
373
401
  if (
374
402
  output._tag === "PageCaptureNavigationError" ||
375
403
  output._tag === "PageCaptureProtocolError"
376
404
  ) {
377
405
  return yield* output;
378
406
  }
407
+
379
408
  return PageCaptureResult.make({
380
409
  implementation: browserRestCaptureImplementation,
381
410
  output,
@@ -415,6 +444,7 @@ export const browserRestWorkersAiCaptureLayer = (
415
444
  Effect.gen(function* () {
416
445
  const client = yield* HttpClient.HttpClient;
417
446
  const workersAi = yield* BrowserQuickActionWorkersAi;
447
+
418
448
  return PageCapture.of({ capture: makeCapture(client, options, workersAi) });
419
449
  }),
420
450
  );
@@ -47,6 +47,7 @@ const BoundedJobId = Schema.NonEmptyString.check(Schema.isMaxLength(256));
47
47
  const BoundedCursorString = Schema.NonEmptyString.check(Schema.isMaxLength(MAX_CURSOR_LENGTH));
48
48
  // The generated API response Schema says string; the current product example returns a number.
49
49
  const ProviderCursor = Schema.Union([BoundedCursorString, Schema.Natural]);
50
+
50
51
  const ProviderJobStatus = Schema.Literals([
51
52
  "running",
52
53
  "completed",
@@ -55,6 +56,7 @@ const ProviderJobStatus = Schema.Literals([
55
56
  "cancelled_due_to_timeout",
56
57
  "cancelled_due_to_limits",
57
58
  ]);
59
+
58
60
  const ProviderResult = Schema.Struct({
59
61
  id: BoundedJobId,
60
62
  status: ProviderJobStatus,
@@ -65,14 +67,17 @@ const ProviderResult = Schema.Struct({
65
67
  records: Schema.Array(PageCrawlRecord).check(Schema.isMaxLength(MAX_RECORDS_PER_RESPONSE)),
66
68
  cursor: Schema.optionalKey(ProviderCursor),
67
69
  });
70
+
68
71
  const CreateEnvelope = Schema.Struct({
69
72
  success: Schema.Literal(true),
70
73
  result: BoundedJobId,
71
74
  });
75
+
72
76
  const ResultEnvelope = Schema.Struct({
73
77
  success: Schema.Literal(true),
74
78
  result: ProviderResult,
75
79
  });
80
+
76
81
  const DeleteEnvelope = Schema.Struct({
77
82
  success: Schema.Literal(true),
78
83
  result: Schema.Struct({
@@ -91,8 +96,10 @@ const boundedDiagnostic = (message: string): string => message.slice(0, MAX_DIAG
91
96
 
92
97
  const privateCause = (value: unknown, apiToken: Redacted.Redacted<string>): Error | undefined => {
93
98
  const raw = boundedDiagnostic(String(value));
99
+
94
100
  if (raw.length === 0) return undefined;
95
101
  const token = Redacted.value(apiToken);
102
+
96
103
  return new Error(token.length === 0 ? raw : raw.replaceAll(token, "[REDACTED]"));
97
104
  };
98
105
 
@@ -116,6 +123,7 @@ const limitError = (
116
123
  : limit === "total-bytes"
117
124
  ? request.limits.maxTotalBytes
118
125
  : request.limits.deadlineMillis;
126
+
119
127
  return PageCrawlLimitError.make({
120
128
  implementation: browserRestCrawlImplementation,
121
129
  limit,
@@ -129,15 +137,19 @@ const retryAfterMillis = (
129
137
  headers: Readonly<Record<string, string | undefined>>,
130
138
  ): number | undefined => {
131
139
  const seconds = Number(headers["retry-after"]);
140
+
132
141
  if (!Number.isSafeInteger(seconds) || seconds < 0) return undefined;
133
142
  const millis = seconds * 1_000;
143
+
134
144
  return Number.isSafeInteger(millis) ? millis : undefined;
135
145
  };
136
146
 
137
147
  const isJsonResponse = (headers: Readonly<Record<string, string | undefined>>): boolean => {
138
148
  const contentType = headers["content-type"];
149
+
139
150
  if (contentType === undefined) return false;
140
151
  const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase();
152
+
141
153
  return mediaType === "application/json" || mediaType?.endsWith("+json") === true;
142
154
  };
143
155
 
@@ -153,6 +165,7 @@ const readBoundedResponse = Effect.fn("BrowserRestCrawl.readBoundedResponse")(fu
153
165
  maximum: number,
154
166
  ): Effect.fn.Return<string, PageCrawlProtocolError> {
155
167
  const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
168
+
156
169
  const state = yield* Stream.runFoldEffect<
157
170
  Uint8Array,
158
171
  HttpClientError.HttpClientError,
@@ -165,6 +178,7 @@ const readBoundedResponse = Effect.fn("BrowserRestCrawl.readBoundedResponse")(fu
165
178
  () => ({ observed: 0, text: "" }),
166
179
  (current, chunk) => {
167
180
  const observed = current.observed + chunk.byteLength;
181
+
168
182
  if (observed > maximum) {
169
183
  return Effect.fail(
170
184
  protocolError(
@@ -172,6 +186,7 @@ const readBoundedResponse = Effect.fn("BrowserRestCrawl.readBoundedResponse")(fu
172
186
  ),
173
187
  );
174
188
  }
189
+
175
190
  return Effect.try({
176
191
  try: () => ({ observed, text: current.text + decoder.decode(chunk, { stream: true }) }),
177
192
  catch: (cause) => protocolError("Decoding the Browser Run crawl response failed", cause),
@@ -184,6 +199,7 @@ const readBoundedResponse = Effect.fn("BrowserRestCrawl.readBoundedResponse")(fu
184
199
  : protocolError("Reading the Browser Run crawl response failed", cause),
185
200
  ),
186
201
  );
202
+
187
203
  return yield* Effect.try({
188
204
  try: () => state.text + decoder.decode(),
189
205
  catch: (cause) => protocolError("Decoding the Browser Run crawl response failed", cause),
@@ -195,6 +211,7 @@ const deadlineFailure = Effect.fn("BrowserRestCrawl.deadlineFailure")(function*
195
211
  startedAt: number,
196
212
  ) {
197
213
  const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
214
+
198
215
  return yield* limitError(request, "deadline", Math.max(0, now - startedAt));
199
216
  });
200
217
 
@@ -206,7 +223,9 @@ const withinDeadline = Effect.fn("BrowserRestCrawl.withinDeadline")(function* <A
206
223
  const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
207
224
  const elapsed = Math.max(0, now - startedAt);
208
225
  const remaining = request.limits.deadlineMillis - elapsed;
226
+
209
227
  if (remaining <= 0) return yield* limitError(request, "deadline", elapsed);
228
+
210
229
  return yield* effect.pipe(
211
230
  Effect.timeoutOrElse({
212
231
  duration: Duration.millis(remaining),
@@ -221,6 +240,7 @@ const checkDeadline = Effect.fn("BrowserRestCrawl.checkDeadline")(function* (
221
240
  ) {
222
241
  const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
223
242
  const elapsed = Math.max(0, now - startedAt);
243
+
224
244
  if (elapsed >= request.limits.deadlineMillis) {
225
245
  return yield* limitError(request, "deadline", elapsed);
226
246
  }
@@ -254,6 +274,7 @@ const makeCrawl =
254
274
  const authorized = HttpClientRequest.bearerToken(request, options.apiToken).pipe(
255
275
  HttpClientRequest.acceptJson,
256
276
  );
277
+
257
278
  const response = yield* client
258
279
  .execute(authorized)
259
280
  .pipe(
@@ -264,9 +285,12 @@ const makeCrawl =
264
285
  ),
265
286
  ),
266
287
  );
288
+
267
289
  const bodyText = yield* readBoundedResponse(response, maximum);
290
+
268
291
  if (response.status === 429) {
269
292
  const reason = isQuotaMessage(bodyText) ? "quota" : "rate";
293
+
270
294
  return yield* PageCrawlRateLimitedError.make({
271
295
  implementation: browserRestCrawlImplementation,
272
296
  reason,
@@ -294,6 +318,7 @@ const makeCrawl =
294
318
  privateCause(bodyText, options.apiToken),
295
319
  );
296
320
  }
321
+
297
322
  return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(bodyText).pipe(
298
323
  Effect.mapError((cause) =>
299
324
  protocolError(
@@ -322,11 +347,13 @@ const makeCrawl =
322
347
  protocolError("Encoding the Browser Run crawl request failed", cause),
323
348
  ),
324
349
  );
350
+
325
351
  const created = yield* withinDeadline(
326
352
  executeJson(requestWithBody, CreateEnvelope, MAX_CONTROL_RESPONSE_BYTES),
327
353
  input,
328
354
  startedAt,
329
355
  );
356
+
330
357
  return created.result;
331
358
  });
332
359
 
@@ -344,6 +371,7 @@ const makeCrawl =
344
371
  ).pipe(
345
372
  Effect.flatMap((shouldCancel) => {
346
373
  if (!shouldCancel) return Effect.void;
374
+
347
375
  const cancellation = executeJson(
348
376
  HttpClientRequest.delete(endpoint(options, id)),
349
377
  DeleteEnvelope,
@@ -355,6 +383,7 @@ const makeCrawl =
355
383
  : protocolError("Browser Run cancelled a different crawl job"),
356
384
  ),
357
385
  );
386
+
358
387
  return cancellation.pipe(
359
388
  Effect.timeoutOrElse({
360
389
  duration: CANCEL_TIMEOUT,
@@ -373,6 +402,7 @@ const makeCrawl =
373
402
  ),
374
403
  ),
375
404
  );
405
+
376
406
  return { id, state } as const;
377
407
  }),
378
408
  ),
@@ -384,19 +414,23 @@ const makeCrawl =
384
414
  statusOnly: boolean,
385
415
  ) {
386
416
  let request = HttpClientRequest.get(endpoint(options, job.id));
417
+
387
418
  if (statusOnly) request = HttpClientRequest.setUrlParam(request, "limit", "1");
388
419
  if (Option.isSome(cursor)) {
389
420
  request = HttpClientRequest.setUrlParam(request, "cursor", cursor.value);
390
421
  }
422
+
391
423
  // `limit=1` still permits one complete record, so poll GETs use the bounded result cap.
392
424
  const envelope = yield* withinDeadline(
393
425
  executeJson(request, ResultEnvelope, MAX_RESULTS_RESPONSE_BYTES),
394
426
  input,
395
427
  startedAt,
396
428
  );
429
+
397
430
  if (envelope.result.id !== job.id) {
398
431
  return yield* protocolError("Browser Run returned a different crawl job identity");
399
432
  }
433
+
400
434
  return envelope.result;
401
435
  });
402
436
 
@@ -412,6 +446,7 @@ const makeCrawl =
412
446
  ),
413
447
  ).pipe(Effect.tap(() => Ref.set(job.state, "terminal"))),
414
448
  );
449
+
415
450
  if (terminal.status === "running") {
416
451
  return yield* protocolError("Browser Run polling stopped before a terminal status");
417
452
  }
@@ -428,6 +463,7 @@ const makeCrawl =
428
463
  (state) =>
429
464
  Effect.gen(function* () {
430
465
  const result = yield* fetchResult(state.cursor, false);
466
+
431
467
  if (result.status !== "completed") {
432
468
  return yield* protocolError(
433
469
  "Browser Run changed crawl status during result pagination",
@@ -437,12 +473,14 @@ const makeCrawl =
437
473
  return [result.records, Option.none()] as const;
438
474
  }
439
475
  const cursor = normalizedCursor(result.cursor);
476
+
440
477
  if (state.seen.includes(cursor)) {
441
478
  return yield* protocolError("Browser Run repeated a crawl result cursor");
442
479
  }
443
480
  if (state.seen.length >= input.limits.maxPages) {
444
481
  return yield* protocolError("Browser Run returned too many crawl result cursors");
445
482
  }
483
+
446
484
  return [
447
485
  result.records,
448
486
  Option.some({ cursor: Option.some(cursor), seen: [...state.seen, cursor] }),
@@ -458,6 +496,7 @@ const makeCrawl =
458
496
  Effect.gen(function* () {
459
497
  yield* checkDeadline(input, startedAt);
460
498
  const pages = state.pages + 1;
499
+
461
500
  if (pages > input.limits.maxPages) {
462
501
  return yield* limitError(input, "pages", pages);
463
502
  }
@@ -467,17 +506,21 @@ const makeCrawl =
467
506
  if (record.metadata !== undefined && !sameHost(startHost, record.metadata.url)) {
468
507
  return yield* protocolError("Browser Run returned off-host crawl metadata");
469
508
  }
509
+
470
510
  const pageBytes =
471
511
  record.markdown === undefined
472
512
  ? 0
473
513
  : new TextEncoder().encode(record.markdown).byteLength;
514
+
474
515
  if (pageBytes > input.limits.maxPageBytes) {
475
516
  return yield* limitError(input, "page-bytes", pageBytes);
476
517
  }
477
518
  const totalBytes = state.totalBytes + pageBytes;
519
+
478
520
  if (totalBytes > input.limits.maxTotalBytes) {
479
521
  return yield* limitError(input, "total-bytes", totalBytes);
480
522
  }
523
+
481
524
  return [{ pages, totalBytes }, [record]] as const;
482
525
  }),
483
526
  ),
@@ -49,11 +49,13 @@ export class BrowserRunSessionLifecycle extends Context.Service<
49
49
  return yield* new BrowserRunCleanupError({ reason: "configuration" });
50
50
  }
51
51
  const client = yield* HttpClient.HttpClient;
52
+
52
53
  const request = Effect.fn("BrowserRunSessionLifecycle.request")(function* (
53
54
  method: "GET" | "DELETE",
54
55
  sessionId: string,
55
56
  ) {
56
57
  const path = method === "DELETE" ? "browser" : "session";
58
+
57
59
  const response = yield* client
58
60
  .execute(
59
61
  HttpClientRequest.make(method)(
@@ -65,6 +67,7 @@ export class BrowserRunSessionLifecycle extends Context.Service<
65
67
  Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }),
66
68
  Effect.mapError(() => new BrowserRunCleanupError({ reason: "provider" })),
67
69
  );
70
+
68
71
  if (response.status === 401 || response.status === 403)
69
72
  return yield* new BrowserRunCleanupError({
70
73
  reason: "authorization",
@@ -82,6 +85,7 @@ export class BrowserRunSessionLifecycle extends Context.Service<
82
85
  });
83
86
  if (response.headers["content-type"]?.split(";", 1)[0]?.trim() !== "application/json")
84
87
  return yield* new BrowserRunCleanupError({ reason: "malformed" });
88
+
85
89
  const bytes = yield* Stream.runFoldEffect(
86
90
  response.stream,
87
91
  () => new Uint8Array(),
@@ -89,44 +93,57 @@ export class BrowserRunSessionLifecycle extends Context.Service<
89
93
  if (body.byteLength + chunk.byteLength > 16_384)
90
94
  return Effect.fail(new BrowserRunCleanupError({ reason: "malformed" }));
91
95
  const combined = new Uint8Array(body.byteLength + chunk.byteLength);
96
+
92
97
  combined.set(body);
93
98
  combined.set(chunk, body.byteLength);
99
+
94
100
  return Effect.succeed(combined);
95
101
  },
96
102
  ).pipe(Effect.mapError(() => new BrowserRunCleanupError({ reason: "malformed" })));
103
+
97
104
  const body = yield* Effect.try({
98
105
  try: () => new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes),
99
106
  catch: () => new BrowserRunCleanupError({ reason: "malformed" }),
100
107
  });
108
+
101
109
  if (response.status === 404) {
102
110
  const absent = Schema.decodeUnknownOption(Schema.fromJsonString(Absent))(body, {
103
111
  onExcessProperty: "error",
104
112
  });
113
+
105
114
  if (Option.isSome(absent)) return true;
115
+
106
116
  return yield* new BrowserRunCleanupError({ reason: "malformed" });
107
117
  }
108
118
  if (method === "DELETE") {
109
119
  const result = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Closed))(
110
120
  body,
111
121
  ).pipe(Effect.mapError(() => new BrowserRunCleanupError({ reason: "malformed" })));
122
+
112
123
  return result.status === "closed";
113
124
  }
125
+
114
126
  const result = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Metadata))(
115
127
  body,
116
128
  ).pipe(Effect.mapError(() => new BrowserRunCleanupError({ reason: "malformed" })));
129
+
117
130
  if (result.sessionId !== sessionId)
118
131
  return yield* new BrowserRunCleanupError({ reason: "malformed" });
132
+
119
133
  return result.endTime !== undefined && result.endTime > 0;
120
134
  });
135
+
121
136
  const close = Effect.fn("BrowserRunSessionLifecycle.close")(
122
137
  function* (sessionId: Redacted.Redacted<string>) {
123
138
  const id = yield* Schema.decodeUnknownEffect(Identity)(Redacted.value(sessionId)).pipe(
124
139
  Effect.mapError(() => new BrowserRunCleanupError({ reason: "configuration" })),
125
140
  );
141
+
126
142
  if (yield* request("DELETE", id)) return;
127
143
  for (let read = 0; read < 2; read++) {
128
144
  if (yield* request("GET", id)) return;
129
145
  }
146
+
130
147
  return yield* new BrowserRunCleanupError({ reason: "pending" });
131
148
  },
132
149
  Effect.timeoutOrElse({
@@ -135,6 +152,7 @@ export class BrowserRunSessionLifecycle extends Context.Service<
135
152
  }),
136
153
  Effect.withTracerEnabled(false),
137
154
  );
155
+
138
156
  return { close };
139
157
  }),
140
158
  );