@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
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",
|