@effect-agent/platform-cloudflare 0.1.0-beta.42 → 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.
- package/dist/browser-quick-action.mjs.map +1 -1
- package/dist/browser-rest-capture.mjs.map +1 -1
- package/dist/browser-rest-crawl.mjs.map +1 -1
- package/dist/browser-session-lifecycle-DqntvG-Y.d.mts +21 -0
- package/dist/browser-session-lifecycle-ZGgb3pnK.mjs +84 -0
- package/dist/browser-session-lifecycle-ZGgb3pnK.mjs.map +1 -0
- package/dist/index.d.mts +69 -56
- package/dist/index.mjs.map +1 -1
- package/dist/interactive-browser.d.mts +1 -18
- package/dist/interactive-browser.mjs +2 -81
- package/dist/interactive-browser.mjs.map +1 -1
- package/dist/prepared-admission-BKp_Upw2.mjs.map +1 -1
- package/dist/protected-browser.d.mts +62 -0
- package/dist/protected-browser.mjs +714 -0
- package/dist/protected-browser.mjs.map +1 -0
- package/dist/scheduling.mjs.map +1 -1
- package/dist/subscriptions.mjs.map +1 -1
- package/package.json +1 -81
- package/src/alarm.ts +41 -0
- package/src/bindings.ts +5 -0
- package/src/boundary.ts +4 -0
- package/src/browser-quick-action.ts +52 -0
- package/src/browser-rest-capture.ts +30 -0
- package/src/browser-rest-crawl.ts +43 -0
- package/src/browser-session-lifecycle.ts +18 -0
- package/src/client.ts +40 -0
- package/src/code-mode-executor.ts +50 -0
- package/src/interactive-browser.ts +176 -0
- package/src/layers.ts +10 -0
- package/src/memory.ts +42 -3
- package/src/prepared-admission.ts +1 -0
- package/src/progress-wait.ts +14 -0
- package/src/protected-browser/binding.ts +185 -0
- package/src/protected-browser/inspect-frame.ts +82 -0
- package/src/protected-browser/native.ts +384 -0
- package/src/protected-browser/policy.ts +594 -0
- package/src/protected-browser.ts +2 -0
- package/src/scheduling.ts +34 -0
- package/src/subscriptions.ts +50 -0
- package/src/thread-object.ts +55 -1
- package/src/transport.ts +1 -0
- 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
|
);
|