@effect-agent/platform-cloudflare 0.1.0-beta.45 → 0.1.0-beta.47
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/Alarm.d.mts +119 -0
- package/dist/Alarm.mjs +317 -0
- package/dist/Alarm.mjs.map +1 -0
- package/dist/{browser-rest-capture.d.mts → BrowserRestCapture.d.mts} +5 -4
- package/dist/{browser-rest-capture.mjs → BrowserRestCapture.mjs} +5 -4
- package/dist/BrowserRestCapture.mjs.map +1 -0
- package/dist/{browser-rest-crawl.d.mts → BrowserRestCrawl.d.mts} +4 -3
- package/dist/{browser-rest-crawl.mjs → BrowserRestCrawl.mjs} +4 -3
- package/dist/BrowserRestCrawl.mjs.map +1 -0
- package/dist/{bindings-BrVEOkfe.d.mts → CloudflareBindings.d.mts} +7 -5
- package/dist/CloudflareBindings.mjs +98 -0
- package/dist/CloudflareBindings.mjs.map +1 -0
- package/dist/{browser-quick-action.mjs → CloudflareBrowser-1jeZx3_g.mjs} +19 -4
- package/dist/CloudflareBrowser-1jeZx3_g.mjs.map +1 -0
- package/dist/{browser-quick-action.d.mts → CloudflareBrowser-DJq53niJ.d.mts} +9 -18
- package/dist/CloudflareBrowser.d.mts +2 -0
- package/dist/CloudflareBrowser.mjs +2 -0
- package/dist/CloudflareCodeMode.d.mts +44 -0
- package/dist/CloudflareCodeMode.mjs +618 -0
- package/dist/CloudflareCodeMode.mjs.map +1 -0
- package/dist/CloudflareConfig-f3CqTel1.d.mts +101 -0
- package/dist/CloudflareConfig.d.mts +2 -0
- package/dist/CloudflareConfig.mjs +122 -0
- package/dist/CloudflareConfig.mjs.map +1 -0
- package/dist/CloudflareMemory.d.mts +140 -0
- package/dist/CloudflareMemory.mjs +168 -0
- package/dist/CloudflareMemory.mjs.map +1 -0
- package/dist/{scheduling.d.mts → CloudflareScheduling.d.mts} +9 -6
- package/dist/{scheduling.mjs → CloudflareScheduling.mjs} +22 -8
- package/dist/CloudflareScheduling.mjs.map +1 -0
- package/dist/{subscriptions.d.mts → CloudflareSubscriptions.d.mts} +11 -6
- package/dist/{subscriptions.mjs → CloudflareSubscriptions.mjs} +26 -7
- package/dist/CloudflareSubscriptions.mjs.map +1 -0
- package/dist/CloudflareThreadClient.d.mts +715 -0
- package/dist/CloudflareThreadClient.mjs +364 -0
- package/dist/CloudflareThreadClient.mjs.map +1 -0
- package/dist/{interactive-browser.d.mts → InteractiveBrowser.d.mts} +5 -4
- package/dist/{interactive-browser.mjs → InteractiveBrowser.mjs} +6 -4
- package/dist/InteractiveBrowser.mjs.map +1 -0
- package/dist/{protected-browser.d.mts → ProtectedBrowser.d.mts} +4 -3
- package/dist/{protected-browser.mjs → ProtectedBrowser.mjs} +4 -3
- package/dist/ProtectedBrowser.mjs.map +1 -0
- package/dist/ThreadObject-IrGuIO4a.mjs +575 -0
- package/dist/ThreadObject-IrGuIO4a.mjs.map +1 -0
- package/dist/ThreadObject-TEgOO0YM.d.mts +735 -0
- package/dist/ThreadObject.d.mts +2 -0
- package/dist/ThreadObject.mjs +3 -0
- package/dist/WakeScheduler.d.mts +23 -0
- package/dist/WakeScheduler.mjs +60 -0
- package/dist/WakeScheduler.mjs.map +1 -0
- package/dist/boundary-BguazVkh.mjs +42 -0
- package/dist/boundary-BguazVkh.mjs.map +1 -0
- package/dist/{browser-session-lifecycle-ZGgb3pnK.mjs → browser-session-lifecycle-CUbVfEgP.mjs} +2 -2
- package/dist/browser-session-lifecycle-CUbVfEgP.mjs.map +1 -0
- package/dist/{browser-session-lifecycle-DqntvG-Y.d.mts → browser-session-lifecycle-DJpqLXy_.d.mts} +2 -2
- package/dist/index.d.mts +12 -1833
- package/dist/index.mjs +12 -1357
- package/dist/prepared-admission-DRBhl2Sp.mjs +63 -0
- package/dist/prepared-admission-DRBhl2Sp.mjs.map +1 -0
- package/package.json +1 -1
- package/src/{alarm.ts → Alarm.ts} +6 -7
- package/src/{browser-rest-capture.ts → BrowserRestCapture.ts} +3 -3
- package/src/{browser-rest-crawl.ts → BrowserRestCrawl.ts} +2 -2
- package/src/{bindings.ts → CloudflareBindings.ts} +2 -2
- package/src/CloudflareBrowser.ts +15 -0
- package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +3 -3
- package/src/{config.ts → CloudflareConfig.ts} +3 -2
- package/src/{memory.ts → CloudflareMemory.ts} +15 -12
- package/src/{scheduling.ts → CloudflareScheduling.ts} +12 -11
- package/src/{subscriptions.ts → CloudflareSubscriptions.ts} +14 -12
- package/src/{client.ts → CloudflareThreadClient.ts} +27 -20
- package/src/{interactive-browser.ts → InteractiveBrowser.ts} +9 -5
- package/src/{thread-object.ts → ThreadObject.ts} +37 -29
- package/src/{wake-scheduler.ts → WakeScheduler.ts} +5 -5
- package/src/index.ts +11 -30
- package/src/{browser-quick-action.ts → internal/browser-quick-action.ts} +10 -8
- package/src/{layers.ts → internal/layers.ts} +39 -30
- package/src/{prepared-admission.ts → internal/prepared-admission.ts} +7 -7
- package/src/{transport.ts → internal/transport.ts} +5 -2
- package/src/protected-browser/binding.ts +3 -2
- package/src/protected-browser/native.ts +2 -2
- package/src/protected-browser/policy.ts +2 -2
- package/dist/browser-quick-action.mjs.map +0 -1
- package/dist/browser-rest-capture.mjs.map +0 -1
- package/dist/browser-rest-crawl.mjs.map +0 -1
- package/dist/browser-session-lifecycle-ZGgb3pnK.mjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/interactive-browser.mjs.map +0 -1
- package/dist/prepared-admission-BhW2eT_a.mjs +0 -890
- package/dist/prepared-admission-BhW2eT_a.mjs.map +0 -1
- package/dist/protected-browser.mjs.map +0 -1
- package/dist/scheduling.mjs.map +0 -1
- package/dist/subscriptions.mjs.map +0 -1
- /package/src/{protected-browser.ts → ProtectedBrowser.ts} +0 -0
- /package/src/{boundary.ts → internal/boundary.ts} +0 -0
- /package/src/{browser-session-lifecycle.ts → internal/browser-session-lifecycle.ts} +0 -0
- /package/src/{progress-wait.ts → internal/progress-wait.ts} +0 -0
package/dist/Alarm.d.mts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { DurableObjectContext, ThreadObjectIdentity } from "./CloudflareBindings.mjs";
|
|
2
|
+
import { o as CloudflareDurableRuntimeConfig } from "./CloudflareConfig-f3CqTel1.mjs";
|
|
3
|
+
import { DurableBindingFailure } from "@effect-agent/thread/AgentRegistration";
|
|
4
|
+
import { DurableAgentRuntime, DurableWorkerFailure } from "@effect-agent/thread/DurableAgentRuntime";
|
|
5
|
+
import { SubmissionLedger } from "@effect-agent/thread/SubmissionLedger";
|
|
6
|
+
import { Context, Effect, Layer, Option, Schema } from "effect";
|
|
7
|
+
declare namespace Alarm_d_exports {
|
|
8
|
+
export { DurableAlarmError, DurableAlarmService, MaintenancePassFailure, MaintenancePassReport, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMaintenanceFailpointHandler, ThreadMaintenanceFailpointLocation };
|
|
9
|
+
}
|
|
10
|
+
declare const DurableAlarmError_base: Schema.Class<DurableAlarmError, Schema.TaggedStruct<"DurableAlarmError", {
|
|
11
|
+
readonly operation: Schema.String;
|
|
12
|
+
readonly message: Schema.String;
|
|
13
|
+
readonly cause: Schema.optionalKey<Schema.Defect>;
|
|
14
|
+
}>, import("effect/Cause").YieldableError>;
|
|
15
|
+
/**
|
|
16
|
+
* The single multiplexed Durable Object alarm (decision D-P6-2). A Durable Object has ONE
|
|
17
|
+
* alarm slot; every cadence the Node host ran on fibers (wake scan, lease expiry, settlement
|
|
18
|
+
* and abort re-checks, retry backoff) multiplexes into one idempotent maintenance pass, and
|
|
19
|
+
* the slot always holds the EARLIEST deadline any caller asked for.
|
|
20
|
+
*
|
|
21
|
+
* The alarm invariant (plan §1.4): every committed actionable mutation carries a newer durable
|
|
22
|
+
* maintenance generation and a committed alarm. Stable externally-driven waits may be
|
|
23
|
+
* nonterminal without retaining an alarm; their resolving mutation advances the generation and
|
|
24
|
+
* restores the alarm atomically.
|
|
25
|
+
*/
|
|
26
|
+
/** The Durable Object alarm API failed; surfaces on host entry points as a typed refusal. */
|
|
27
|
+
declare class DurableAlarmError extends DurableAlarmError_base {}
|
|
28
|
+
declare const DurableAlarmService_base: Context.ServiceClass<DurableAlarmService, "@effect-agent/platform-cloudflare/DurableAlarmService", {
|
|
29
|
+
/** The scheduled deadline in epoch milliseconds, if any. */
|
|
30
|
+
readonly scheduled: Effect.Effect<Option.Option<number>, DurableAlarmError>;
|
|
31
|
+
/** Replace the slot with this deadline. */
|
|
32
|
+
readonly scheduleAt: (epochMillis: number) => Effect.Effect<void, DurableAlarmError>;
|
|
33
|
+
/** Keep the EARLIER of the existing deadline and this one (the multiplexing rule). */
|
|
34
|
+
readonly ensureScheduledBy: (epochMillis: number) => Effect.Effect<void, DurableAlarmError>;
|
|
35
|
+
/**
|
|
36
|
+
* Arm an immediate alarm (the durable, coalescing local wake) — DEFERRED while a
|
|
37
|
+
* maintenance pass is executing. Workerd cancels an in-flight alarm handler when a new
|
|
38
|
+
* EARLIER deadline is written during its execution (`requestScheduledAlarm`), and the
|
|
39
|
+
* maintenance pass runs INSIDE the alarm handler: an immediate wake landing mid-pass
|
|
40
|
+
* (a routed port mutation, a sibling's `wake()`, the coordinator's own local notify)
|
|
41
|
+
* would kill the running Attempt — manufacturing an ownership loss no real eviction
|
|
42
|
+
* caused, and routing open uncertain-class Tool Calls into spurious Unknown Outcomes.
|
|
43
|
+
* Deferral is contract-safe: wakes are droppable hints, every mutating entry point
|
|
44
|
+
* pre-arms BEFORE its first durable mutation (the alarm invariant never rests on this
|
|
45
|
+
* call). The pass's durable generation check observes any racing mutation, so the
|
|
46
|
+
* in-memory hint does not need to be flushed after a stable wait is acknowledged.
|
|
47
|
+
*/
|
|
48
|
+
readonly scheduleNow: Effect.Effect<void, DurableAlarmError>;
|
|
49
|
+
/**
|
|
50
|
+
* Run one maintenance pass with wake deferral (see `scheduleNow`). Calls made while `body`
|
|
51
|
+
* executes are droppable promptness hints; correctness rests on the durable generation.
|
|
52
|
+
*/
|
|
53
|
+
readonly withWakesDeferred: <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
54
|
+
/** Clear the slot; correctness-sensitive clears live in maintenance generation transactions. */
|
|
55
|
+
readonly cancel: Effect.Effect<void, DurableAlarmError>;
|
|
56
|
+
}>;
|
|
57
|
+
/** `ctx.storage` alarm slot as an Effect service; storage is truth, never a memory field. */
|
|
58
|
+
declare class DurableAlarmService extends DurableAlarmService_base {
|
|
59
|
+
static readonly layer: Layer.Layer<DurableAlarmService, never, DurableObjectContext>;
|
|
60
|
+
}
|
|
61
|
+
declare const MaintenancePassReport_base: Schema.Class<MaintenancePassReport, Schema.Struct<{
|
|
62
|
+
/** `caught-up` is generation-only; `actionable` ran recovery and one bounded drain. */
|
|
63
|
+
readonly phase: Schema.Literals<readonly ["caught-up", "actionable"]>;
|
|
64
|
+
/** Recovery decisions executed (or deferred) BEFORE any new claim in this pass. */
|
|
65
|
+
readonly recovered: Schema.Int;
|
|
66
|
+
/** Settlements the drain pass finalized. */
|
|
67
|
+
readonly settled: Schema.Int;
|
|
68
|
+
/** Submissions still nonterminal after the pass (suspended/unknown lanes stay honest). */
|
|
69
|
+
readonly nonterminal: Schema.Int;
|
|
70
|
+
/** `rearmed` for dirty/autonomous work, `cleared` for stable waits or settlement. */
|
|
71
|
+
readonly alarm: Schema.Literals<readonly ["rearmed", "cleared"]>;
|
|
72
|
+
}>, {}>;
|
|
73
|
+
/** What one maintenance pass did — auditable evidence mirroring `NodeDurableHost`'s report. */
|
|
74
|
+
declare class MaintenancePassReport extends MaintenancePassReport_base {}
|
|
75
|
+
/** Fault boundaries around every maintenance-owned durable mutation. */
|
|
76
|
+
type ThreadMaintenanceFailpointLocation = "maintenance:dirty:before" | "maintenance:dirty:after" | "maintenance:mutation:armed" | "maintenance:mutation:finished" | "maintenance:ensure:before" | "maintenance:ensure:after" | "maintenance:begin:before" | "maintenance:begin:after" | "maintenance:finish:before" | "maintenance:finish:after";
|
|
77
|
+
type ThreadMaintenanceFailpointHandler = (location: ThreadMaintenanceFailpointLocation) => Effect.Effect<void>;
|
|
78
|
+
declare const ThreadMaintenanceFailpoint_base: Context.ServiceClass<ThreadMaintenanceFailpoint, "@effect-agent/platform-cloudflare/ThreadMaintenanceFailpoint", {
|
|
79
|
+
readonly hit: ThreadMaintenanceFailpointHandler;
|
|
80
|
+
}>;
|
|
81
|
+
/** Test-only fault authority; production uses the inert layer. */
|
|
82
|
+
declare class ThreadMaintenanceFailpoint extends ThreadMaintenanceFailpoint_base {
|
|
83
|
+
static readonly layer: Layer.Layer<ThreadMaintenanceFailpoint, never, never>;
|
|
84
|
+
}
|
|
85
|
+
type MaintenancePassFailure = DurableWorkerFailure | DurableBindingFailure | DurableAlarmError;
|
|
86
|
+
declare const ThreadMaintenance_base: Context.ServiceClass<ThreadMaintenance, "@effect-agent/platform-cloudflare/ThreadMaintenance", {
|
|
87
|
+
/** One idempotent maintenance pass; failures propagate so workerd retries the alarm. */
|
|
88
|
+
readonly pass: Effect.Effect<MaintenancePassReport, MaintenancePassFailure>;
|
|
89
|
+
/**
|
|
90
|
+
* Constructor gate: initialize/inspect only the O(1) maintenance record and ensure a dirty
|
|
91
|
+
* generation has an alarm. It never scans the ledger or canonical history.
|
|
92
|
+
*/
|
|
93
|
+
readonly ensureAlarm: Effect.Effect<void, MaintenancePassFailure>;
|
|
94
|
+
/**
|
|
95
|
+
* Serialize the pre-arm boundary with pass acknowledgement, advance the durable dirty
|
|
96
|
+
* generation and arm the alarm in one transaction BEFORE running the caller's mutation.
|
|
97
|
+
* A pass cannot acknowledge while that mutation remains in flight.
|
|
98
|
+
*/
|
|
99
|
+
readonly withMutation: <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E | DurableAlarmError, R>;
|
|
100
|
+
}>;
|
|
101
|
+
/**
|
|
102
|
+
* Incremental, quiescent maintenance over a durable dirty/processed generation (issue #93).
|
|
103
|
+
*
|
|
104
|
+
* `pass` = generation snapshot/pre-arm → recovery → bounded drain → generation acknowledgement:
|
|
105
|
+
*
|
|
106
|
+
* 1. One storage transaction reads dirty/processed and re-arms before work. A caught-up forced
|
|
107
|
+
* alarm takes an O(1) path without recovery, ledger scans, or canonical-history reads.
|
|
108
|
+
* 2. Recovery still strictly precedes a new claim, and one bounded drain advances the lane.
|
|
109
|
+
* 3. The final transaction acknowledges only the generation observed at pass start. A racing
|
|
110
|
+
* mutation therefore remains `dirty > processed` and retains its atomically-established alarm.
|
|
111
|
+
* 4. Stable external waits acknowledge and clear. Autonomous retry, indeterminate, and lease
|
|
112
|
+
* recovery states leave their generation dirty and retain bounded backoff rearming.
|
|
113
|
+
*/
|
|
114
|
+
declare class ThreadMaintenance extends ThreadMaintenance_base {
|
|
115
|
+
static readonly layer: Layer.Layer<ThreadMaintenance, never, DurableAgentRuntime | SubmissionLedger | DurableAlarmService | ThreadMaintenanceFailpoint | CloudflareDurableRuntimeConfig | ThreadObjectIdentity | DurableObjectContext>;
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
export { DurableAlarmError, DurableAlarmService, MaintenancePassFailure, MaintenancePassReport, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMaintenanceFailpointHandler, ThreadMaintenanceFailpointLocation, Alarm_d_exports as t };
|
|
119
|
+
//# sourceMappingURL=Alarm.d.mts.map
|
package/dist/Alarm.mjs
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { DurableObjectContext, ThreadObjectIdentity } from "./CloudflareBindings.mjs";
|
|
3
|
+
import { CloudflareDurableRuntimeConfig } from "./CloudflareConfig.mjs";
|
|
4
|
+
import { r as safeCauseMessage } from "./boundary-BguazVkh.mjs";
|
|
5
|
+
import "@effect-agent/thread/AgentRegistration";
|
|
6
|
+
import { DurableAgentRuntime } from "@effect-agent/thread/DurableAgentRuntime";
|
|
7
|
+
import { SubmissionLedger } from "@effect-agent/thread/SubmissionLedger";
|
|
8
|
+
import { Clock, Context, Effect, Layer, Option, Random, Ref, Schema, Semaphore, Stream } from "effect";
|
|
9
|
+
//#region src/Alarm.ts
|
|
10
|
+
var Alarm_exports = /* @__PURE__ */ __exportAll({
|
|
11
|
+
DurableAlarmError: () => DurableAlarmError,
|
|
12
|
+
DurableAlarmService: () => DurableAlarmService,
|
|
13
|
+
MaintenancePassReport: () => MaintenancePassReport,
|
|
14
|
+
ThreadMaintenance: () => ThreadMaintenance,
|
|
15
|
+
ThreadMaintenanceFailpoint: () => ThreadMaintenanceFailpoint
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* The single multiplexed Durable Object alarm (decision D-P6-2). A Durable Object has ONE
|
|
19
|
+
* alarm slot; every cadence the Node host ran on fibers (wake scan, lease expiry, settlement
|
|
20
|
+
* and abort re-checks, retry backoff) multiplexes into one idempotent maintenance pass, and
|
|
21
|
+
* the slot always holds the EARLIEST deadline any caller asked for.
|
|
22
|
+
*
|
|
23
|
+
* The alarm invariant (plan §1.4): every committed actionable mutation carries a newer durable
|
|
24
|
+
* maintenance generation and a committed alarm. Stable externally-driven waits may be
|
|
25
|
+
* nonterminal without retaining an alarm; their resolving mutation advances the generation and
|
|
26
|
+
* restores the alarm atomically.
|
|
27
|
+
*/
|
|
28
|
+
/** The Durable Object alarm API failed; surfaces on host entry points as a typed refusal. */
|
|
29
|
+
var DurableAlarmError = class extends Schema.TaggedError()("DurableAlarmError", {
|
|
30
|
+
operation: Schema.String,
|
|
31
|
+
message: Schema.String,
|
|
32
|
+
cause: Schema.optionalKey(Schema.Defect())
|
|
33
|
+
}) {};
|
|
34
|
+
const alarmFailure = (operation) => (cause) => DurableAlarmError.make({
|
|
35
|
+
operation,
|
|
36
|
+
message: safeCauseMessage(cause, "The Cloudflare alarm API failed without a diagnostic"),
|
|
37
|
+
cause
|
|
38
|
+
});
|
|
39
|
+
/** `ctx.storage` alarm slot as an Effect service; storage is truth, never a memory field. */
|
|
40
|
+
var DurableAlarmService = class DurableAlarmService extends Context.Service()("@effect-agent/platform-cloudflare/DurableAlarmService") {
|
|
41
|
+
static layer = Layer.effect(DurableAlarmService)(Effect.gen(function* () {
|
|
42
|
+
const { ctx } = yield* DurableObjectContext;
|
|
43
|
+
/**
|
|
44
|
+
* In-memory pass bookkeeping — a pure CACHE, never state: a fresh incarnation has no
|
|
45
|
+
* running pass, and a deferred wake lost to eviction was only ever a promptness hint
|
|
46
|
+
* on top of the already-committed pre-armed alarm.
|
|
47
|
+
*/
|
|
48
|
+
const runningPasses = yield* Ref.make(0);
|
|
49
|
+
const scheduled = Effect.tryPromise({
|
|
50
|
+
try: () => ctx.storage.getAlarm(),
|
|
51
|
+
catch: alarmFailure("get alarm")
|
|
52
|
+
}).pipe(Effect.map((deadline) => deadline === null ? Option.none() : Option.some(deadline)));
|
|
53
|
+
const scheduleAt = (epochMillis) => Effect.tryPromise({
|
|
54
|
+
try: () => ctx.storage.setAlarm(epochMillis),
|
|
55
|
+
catch: alarmFailure("set alarm")
|
|
56
|
+
});
|
|
57
|
+
const ensureScheduledBy = (epochMillis) => scheduled.pipe(Effect.flatMap((existing) => Option.isSome(existing) && existing.value <= epochMillis ? Effect.void : scheduleAt(epochMillis)));
|
|
58
|
+
const armNow = Clock.currentTimeMillis.pipe(Effect.flatMap((now) => ensureScheduledBy(now)));
|
|
59
|
+
const scheduleNow = Ref.get(runningPasses).pipe(Effect.flatMap((passes) => passes > 0 ? Effect.void : armNow));
|
|
60
|
+
const withWakesDeferred = (body) => Ref.update(runningPasses, (passes) => passes + 1).pipe(Effect.andThen(body), Effect.ensuring(Ref.update(runningPasses, (passes) => passes - 1)));
|
|
61
|
+
const cancel = Effect.tryPromise({
|
|
62
|
+
try: () => ctx.storage.deleteAlarm(),
|
|
63
|
+
catch: alarmFailure("delete alarm")
|
|
64
|
+
});
|
|
65
|
+
return DurableAlarmService.of({
|
|
66
|
+
scheduled,
|
|
67
|
+
scheduleAt,
|
|
68
|
+
ensureScheduledBy,
|
|
69
|
+
scheduleNow,
|
|
70
|
+
withWakesDeferred,
|
|
71
|
+
cancel
|
|
72
|
+
});
|
|
73
|
+
}));
|
|
74
|
+
};
|
|
75
|
+
/** What one maintenance pass did — auditable evidence mirroring `NodeDurableHost`'s report. */
|
|
76
|
+
var MaintenancePassReport = class extends Schema.Class("@effect-agent/platform-cloudflare/MaintenancePassReport")({
|
|
77
|
+
/** `caught-up` is generation-only; `actionable` ran recovery and one bounded drain. */
|
|
78
|
+
phase: Schema.Literals(["caught-up", "actionable"]),
|
|
79
|
+
/** Recovery decisions executed (or deferred) BEFORE any new claim in this pass. */
|
|
80
|
+
recovered: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
81
|
+
/** Settlements the drain pass finalized. */
|
|
82
|
+
settled: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
83
|
+
/** Submissions still nonterminal after the pass (suspended/unknown lanes stay honest). */
|
|
84
|
+
nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
85
|
+
/** `rearmed` for dirty/autonomous work, `cleared` for stable waits or settlement. */
|
|
86
|
+
alarm: Schema.Literals(["rearmed", "cleared"])
|
|
87
|
+
}) {};
|
|
88
|
+
/** Test-only fault authority; production uses the inert layer. */
|
|
89
|
+
var ThreadMaintenanceFailpoint = class extends Context.Service()("@effect-agent/platform-cloudflare/ThreadMaintenanceFailpoint") {
|
|
90
|
+
static layer = Layer.succeed(this)({ hit: () => Effect.void });
|
|
91
|
+
};
|
|
92
|
+
const MaintenanceGeneration = Schema.BigIntFromString.check(Schema.isGreaterThanOrEqualToBigInt(0n));
|
|
93
|
+
/** Versioned, platform-private maintenance state stored through Durable Object KV. */
|
|
94
|
+
var ThreadMaintenanceState = class extends Schema.Class("@effect-agent/platform-cloudflare/ThreadMaintenanceState")({
|
|
95
|
+
schemaVersion: Schema.Literal(1),
|
|
96
|
+
dirty: MaintenanceGeneration,
|
|
97
|
+
processed: MaintenanceGeneration,
|
|
98
|
+
nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
|
|
99
|
+
}) {};
|
|
100
|
+
const MAINTENANCE_STATE_KEY = "effect-agent:thread-maintenance:v1";
|
|
101
|
+
const decodeMaintenanceState = Schema.decodeUnknownSync(ThreadMaintenanceState);
|
|
102
|
+
const encodeMaintenanceState = Schema.encodeSync(ThreadMaintenanceState);
|
|
103
|
+
const initialMaintenanceState = () => ThreadMaintenanceState.make({
|
|
104
|
+
schemaVersion: 1,
|
|
105
|
+
dirty: 1n,
|
|
106
|
+
processed: 0n,
|
|
107
|
+
nonterminal: 0
|
|
108
|
+
});
|
|
109
|
+
const readMaintenanceState = async (transaction) => {
|
|
110
|
+
const encoded = await transaction.get(MAINTENANCE_STATE_KEY);
|
|
111
|
+
return encoded === void 0 ? {
|
|
112
|
+
state: initialMaintenanceState(),
|
|
113
|
+
initialized: false
|
|
114
|
+
} : {
|
|
115
|
+
state: decodeMaintenanceState(encoded),
|
|
116
|
+
initialized: true
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
const ensureTransactionAlarmBy = async (transaction, deadline) => {
|
|
120
|
+
const scheduled = await transaction.getAlarm();
|
|
121
|
+
if (scheduled === null || scheduled > deadline) await transaction.setAlarm(deadline);
|
|
122
|
+
};
|
|
123
|
+
const stableExternalWait = (snapshot, reports) => {
|
|
124
|
+
const decision = reports.get(snapshot.submissionId)?.decision._tag;
|
|
125
|
+
if (decision === "SettleAborted") return false;
|
|
126
|
+
switch (snapshot.state) {
|
|
127
|
+
case "suspended":
|
|
128
|
+
case "joined": return true;
|
|
129
|
+
case "unknown": return decision === "AwaitUnknownResolution" || decision === "MarkUnknown";
|
|
130
|
+
case "admitted": return reports.get(snapshot.submissionId)?.decision._tag === "AwaitParentEstablishment";
|
|
131
|
+
case "input-applied":
|
|
132
|
+
case "joining":
|
|
133
|
+
case "ready":
|
|
134
|
+
case "running":
|
|
135
|
+
case "settled":
|
|
136
|
+
case "terminalizing": return false;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Incremental, quiescent maintenance over a durable dirty/processed generation (issue #93).
|
|
141
|
+
*
|
|
142
|
+
* `pass` = generation snapshot/pre-arm → recovery → bounded drain → generation acknowledgement:
|
|
143
|
+
*
|
|
144
|
+
* 1. One storage transaction reads dirty/processed and re-arms before work. A caught-up forced
|
|
145
|
+
* alarm takes an O(1) path without recovery, ledger scans, or canonical-history reads.
|
|
146
|
+
* 2. Recovery still strictly precedes a new claim, and one bounded drain advances the lane.
|
|
147
|
+
* 3. The final transaction acknowledges only the generation observed at pass start. A racing
|
|
148
|
+
* mutation therefore remains `dirty > processed` and retains its atomically-established alarm.
|
|
149
|
+
* 4. Stable external waits acknowledge and clear. Autonomous retry, indeterminate, and lease
|
|
150
|
+
* recovery states leave their generation dirty and retain bounded backoff rearming.
|
|
151
|
+
*/
|
|
152
|
+
var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effect-agent/platform-cloudflare/ThreadMaintenance") {
|
|
153
|
+
static layer = Layer.effect(ThreadMaintenance)(Effect.gen(function* () {
|
|
154
|
+
const runtime = yield* DurableAgentRuntime;
|
|
155
|
+
const ledger = yield* SubmissionLedger;
|
|
156
|
+
const alarm = yield* DurableAlarmService;
|
|
157
|
+
const config = yield* CloudflareDurableRuntimeConfig;
|
|
158
|
+
const identity = yield* ThreadObjectIdentity;
|
|
159
|
+
const { ctx } = yield* DurableObjectContext;
|
|
160
|
+
const failpoint = yield* ThreadMaintenanceFailpoint;
|
|
161
|
+
/**
|
|
162
|
+
* Consecutive no-progress passes — an in-memory CACHE, not state: a fresh incarnation
|
|
163
|
+
* restarts at zero and merely re-arms sooner than a long-lived one would have.
|
|
164
|
+
*/
|
|
165
|
+
const stalls = yield* Ref.make(0);
|
|
166
|
+
/**
|
|
167
|
+
* Incarnation-local mutation count guarded with the generation transactions below. It is
|
|
168
|
+
* deliberately not durable: after eviction every begun mutation has stopped, while its
|
|
169
|
+
* pre-armed dirty generation remains durable for recovery. The short gate never spans the
|
|
170
|
+
* caller's mutation or cross-Object I/O.
|
|
171
|
+
*/
|
|
172
|
+
const activeMutations = yield* Ref.make(0);
|
|
173
|
+
const generationGate = yield* Semaphore.make(1);
|
|
174
|
+
const maintenancePassGate = yield* Semaphore.make(1);
|
|
175
|
+
const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
|
|
176
|
+
const runTransaction = (operation, transaction) => Effect.tryPromise({
|
|
177
|
+
try: transaction,
|
|
178
|
+
catch: alarmFailure(operation)
|
|
179
|
+
});
|
|
180
|
+
const beginMutation = Effect.fn("ThreadMaintenance.beginMutation")(function* () {
|
|
181
|
+
yield* failpoint.hit("maintenance:dirty:before");
|
|
182
|
+
const now = yield* Clock.currentTimeMillis;
|
|
183
|
+
yield* runTransaction("advance maintenance generation", () => ctx.storage.transaction(async (transaction) => {
|
|
184
|
+
const { state } = await readMaintenanceState(transaction);
|
|
185
|
+
const next = ThreadMaintenanceState.make({
|
|
186
|
+
...state,
|
|
187
|
+
dirty: state.dirty + 1n
|
|
188
|
+
});
|
|
189
|
+
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
|
|
190
|
+
await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
|
|
191
|
+
}));
|
|
192
|
+
yield* failpoint.hit("maintenance:dirty:after");
|
|
193
|
+
yield* Ref.update(activeMutations, (active) => active + 1);
|
|
194
|
+
});
|
|
195
|
+
const endMutation = generationGate.withPermit(Ref.update(activeMutations, (active) => Math.max(0, active - 1)));
|
|
196
|
+
const withMutation = (body) => Effect.acquireUseRelease(generationGate.withPermit(beginMutation()), () => failpoint.hit("maintenance:mutation:armed").pipe(Effect.andThen(body), Effect.tap(() => failpoint.hit("maintenance:mutation:finished"))), () => endMutation);
|
|
197
|
+
const ensureAlarm = Effect.fn("ThreadMaintenance.ensureAlarm")(function* () {
|
|
198
|
+
yield* failpoint.hit("maintenance:ensure:before");
|
|
199
|
+
const now = yield* Clock.currentTimeMillis;
|
|
200
|
+
yield* runTransaction("ensure maintenance alarm", () => ctx.storage.transaction(async (transaction) => {
|
|
201
|
+
const { state, initialized } = await readMaintenanceState(transaction);
|
|
202
|
+
if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
|
|
203
|
+
if (state.dirty > state.processed) await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
|
|
204
|
+
}));
|
|
205
|
+
yield* failpoint.hit("maintenance:ensure:after");
|
|
206
|
+
});
|
|
207
|
+
const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* () {
|
|
208
|
+
yield* failpoint.hit("maintenance:begin:before");
|
|
209
|
+
const now = yield* Clock.currentTimeMillis;
|
|
210
|
+
const result = yield* runTransaction("begin maintenance pass", () => ctx.storage.transaction(async (transaction) => {
|
|
211
|
+
const { state, initialized } = await readMaintenanceState(transaction);
|
|
212
|
+
if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
|
|
213
|
+
if (state.processed >= state.dirty) {
|
|
214
|
+
await transaction.deleteAlarm();
|
|
215
|
+
return {
|
|
216
|
+
_tag: "CaughtUp",
|
|
217
|
+
nonterminal: state.nonterminal
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
|
|
221
|
+
return {
|
|
222
|
+
_tag: "Actionable",
|
|
223
|
+
generation: state.dirty
|
|
224
|
+
};
|
|
225
|
+
}));
|
|
226
|
+
yield* failpoint.hit("maintenance:begin:after");
|
|
227
|
+
return result;
|
|
228
|
+
});
|
|
229
|
+
const rearmDelay = Effect.fn("ThreadMaintenance.rearmDelay")(function* (progressed) {
|
|
230
|
+
const priorStalls = yield* Ref.getAndUpdate(stalls, (count) => progressed ? 0 : count + 1);
|
|
231
|
+
if (progressed) return config.alarmBackoffBase;
|
|
232
|
+
const exponent = Math.min(priorStalls, 30);
|
|
233
|
+
const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** exponent);
|
|
234
|
+
const jitter = yield* Random.next;
|
|
235
|
+
const jittered = Math.ceil(backoff / 2 + backoff / 2 * jitter);
|
|
236
|
+
return Math.min(jittered, config.wakeScanInterval);
|
|
237
|
+
});
|
|
238
|
+
const pass = Effect.fn("ThreadMaintenance.pass")(function* () {
|
|
239
|
+
const annotate = (report) => Effect.annotateCurrentSpan({
|
|
240
|
+
phase: report.phase,
|
|
241
|
+
recovered: report.recovered,
|
|
242
|
+
settled: report.settled,
|
|
243
|
+
nonterminal: report.nonterminal,
|
|
244
|
+
alarm: report.alarm
|
|
245
|
+
}).pipe(Effect.as(report));
|
|
246
|
+
const started = yield* generationGate.withPermit(Effect.gen(function* () {
|
|
247
|
+
const activeAtStart = yield* Ref.get(activeMutations);
|
|
248
|
+
return {
|
|
249
|
+
...yield* beginPass(),
|
|
250
|
+
activeAtStart
|
|
251
|
+
};
|
|
252
|
+
}));
|
|
253
|
+
if (started._tag === "CaughtUp") return yield* annotate(MaintenancePassReport.make({
|
|
254
|
+
phase: "caught-up",
|
|
255
|
+
recovered: 0,
|
|
256
|
+
settled: 0,
|
|
257
|
+
nonterminal: started.nonterminal,
|
|
258
|
+
alarm: "cleared"
|
|
259
|
+
}));
|
|
260
|
+
const recovered = yield* runtime.runRecovery;
|
|
261
|
+
const settlements = yield* runtime.processThreadResolved(identity.threadId);
|
|
262
|
+
const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
263
|
+
const reports = new Map(recovered.map((report) => [report.submissionId, report]));
|
|
264
|
+
const head = remaining[0];
|
|
265
|
+
const headWaiting = head !== void 0 && stableExternalWait(head, reports);
|
|
266
|
+
const autonomous = remaining.some((snapshot, index) => {
|
|
267
|
+
if (index > 0 && headWaiting && snapshot.state === "ready" && reports.get(snapshot.submissionId)?.decision._tag === "ApplyInput") return false;
|
|
268
|
+
return !stableExternalWait(snapshot, reports);
|
|
269
|
+
});
|
|
270
|
+
const progressed = settlements.length > 0 || recovered.some((report) => report.disposition === "repaired");
|
|
271
|
+
const delay = autonomous ? yield* rearmDelay(progressed) : 0;
|
|
272
|
+
const now = yield* Clock.currentTimeMillis;
|
|
273
|
+
yield* failpoint.hit("maintenance:finish:before");
|
|
274
|
+
const alarmDisposition = yield* generationGate.withPermit(Effect.gen(function* () {
|
|
275
|
+
const active = yield* Ref.get(activeMutations);
|
|
276
|
+
return yield* runTransaction("finish maintenance pass", () => ctx.storage.transaction(async (transaction) => {
|
|
277
|
+
const { state } = await readMaintenanceState(transaction);
|
|
278
|
+
const processed = autonomous || started.activeAtStart > 0 || active > 0 ? state.processed : state.processed > started.generation ? state.processed : started.generation;
|
|
279
|
+
const next = ThreadMaintenanceState.make({
|
|
280
|
+
...state,
|
|
281
|
+
processed,
|
|
282
|
+
nonterminal: remaining.length
|
|
283
|
+
});
|
|
284
|
+
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
|
|
285
|
+
if (autonomous) {
|
|
286
|
+
await transaction.setAlarm(now + delay);
|
|
287
|
+
return "rearmed";
|
|
288
|
+
}
|
|
289
|
+
if (started.activeAtStart > 0 || active > 0 || next.dirty > next.processed) {
|
|
290
|
+
await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
|
|
291
|
+
return "rearmed";
|
|
292
|
+
}
|
|
293
|
+
await transaction.deleteAlarm();
|
|
294
|
+
return "cleared";
|
|
295
|
+
}));
|
|
296
|
+
}));
|
|
297
|
+
yield* failpoint.hit("maintenance:finish:after");
|
|
298
|
+
if (alarmDisposition === "cleared") yield* Ref.set(stalls, 0);
|
|
299
|
+
return yield* annotate(MaintenancePassReport.make({
|
|
300
|
+
phase: "actionable",
|
|
301
|
+
recovered: recovered.length,
|
|
302
|
+
settled: settlements.length,
|
|
303
|
+
nonterminal: remaining.length,
|
|
304
|
+
alarm: alarmDisposition
|
|
305
|
+
}));
|
|
306
|
+
});
|
|
307
|
+
return ThreadMaintenance.of({
|
|
308
|
+
pass: alarm.withWakesDeferred(maintenancePassGate.withPermit(pass())),
|
|
309
|
+
ensureAlarm: ensureAlarm(),
|
|
310
|
+
withMutation
|
|
311
|
+
});
|
|
312
|
+
}));
|
|
313
|
+
};
|
|
314
|
+
//#endregion
|
|
315
|
+
export { DurableAlarmError, DurableAlarmService, MaintenancePassReport, ThreadMaintenance, ThreadMaintenanceFailpoint, Alarm_exports as t };
|
|
316
|
+
|
|
317
|
+
//# sourceMappingURL=Alarm.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Alarm.mjs","names":[],"sources":["../src/Alarm.ts"],"sourcesContent":["import { type DurableBindingFailure } from \"@effect-agent/thread/AgentRegistration\";\nimport {\n DurableAgentRuntime,\n type DurableWorkerFailure,\n type RecoveryReport,\n} from \"@effect-agent/thread/DurableAgentRuntime\";\nimport { SubmissionLedger, type SubmissionSnapshot } from \"@effect-agent/thread/SubmissionLedger\";\nimport {\n Clock,\n Context,\n Effect,\n Layer,\n Option,\n Random,\n Ref,\n Schema,\n Semaphore,\n Stream,\n} from \"effect\";\n\nimport { ThreadObjectIdentity, DurableObjectContext } from \"./CloudflareBindings.ts\";\nimport { CloudflareDurableRuntimeConfig } from \"./CloudflareConfig.ts\";\nimport { safeCauseMessage } from \"./internal/boundary.ts\";\n\n/**\n * The single multiplexed Durable Object alarm (decision D-P6-2). A Durable Object has ONE\n * alarm slot; every cadence the Node host ran on fibers (wake scan, lease expiry, settlement\n * and abort re-checks, retry backoff) multiplexes into one idempotent maintenance pass, and\n * the slot always holds the EARLIEST deadline any caller asked for.\n *\n * The alarm invariant (plan §1.4): every committed actionable mutation carries a newer durable\n * maintenance generation and a committed alarm. Stable externally-driven waits may be\n * nonterminal without retaining an alarm; their resolving mutation advances the generation and\n * restores the alarm atomically.\n */\n\n/** The Durable Object alarm API failed; surfaces on host entry points as a typed refusal. */\nexport class DurableAlarmError extends Schema.TaggedError<DurableAlarmError>()(\n \"DurableAlarmError\",\n {\n operation: Schema.String,\n message: Schema.String,\n cause: Schema.optionalKey(Schema.Defect()),\n },\n) {}\n\nconst alarmFailure =\n (operation: string) =>\n (cause: unknown): DurableAlarmError =>\n DurableAlarmError.make({\n operation,\n message: safeCauseMessage(cause, \"The Cloudflare alarm API failed without a diagnostic\"),\n cause,\n });\n\n/** `ctx.storage` alarm slot as an Effect service; storage is truth, never a memory field. */\nexport class DurableAlarmService extends Context.Service<\n DurableAlarmService,\n {\n /** The scheduled deadline in epoch milliseconds, if any. */\n readonly scheduled: Effect.Effect<Option.Option<number>, DurableAlarmError>;\n /** Replace the slot with this deadline. */\n readonly scheduleAt: (epochMillis: number) => Effect.Effect<void, DurableAlarmError>;\n /** Keep the EARLIER of the existing deadline and this one (the multiplexing rule). */\n readonly ensureScheduledBy: (epochMillis: number) => Effect.Effect<void, DurableAlarmError>;\n /**\n * Arm an immediate alarm (the durable, coalescing local wake) — DEFERRED while a\n * maintenance pass is executing. Workerd cancels an in-flight alarm handler when a new\n * EARLIER deadline is written during its execution (`requestScheduledAlarm`), and the\n * maintenance pass runs INSIDE the alarm handler: an immediate wake landing mid-pass\n * (a routed port mutation, a sibling's `wake()`, the coordinator's own local notify)\n * would kill the running Attempt — manufacturing an ownership loss no real eviction\n * caused, and routing open uncertain-class Tool Calls into spurious Unknown Outcomes.\n * Deferral is contract-safe: wakes are droppable hints, every mutating entry point\n * pre-arms BEFORE its first durable mutation (the alarm invariant never rests on this\n * call). The pass's durable generation check observes any racing mutation, so the\n * in-memory hint does not need to be flushed after a stable wait is acknowledged.\n */\n readonly scheduleNow: Effect.Effect<void, DurableAlarmError>;\n /**\n * Run one maintenance pass with wake deferral (see `scheduleNow`). Calls made while `body`\n * executes are droppable promptness hints; correctness rests on the durable generation.\n */\n readonly withWakesDeferred: <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;\n /** Clear the slot; correctness-sensitive clears live in maintenance generation transactions. */\n readonly cancel: Effect.Effect<void, DurableAlarmError>;\n }\n>()(\"@effect-agent/platform-cloudflare/DurableAlarmService\") {\n static readonly layer: Layer.Layer<DurableAlarmService, never, DurableObjectContext> =\n Layer.effect(DurableAlarmService)(\n Effect.gen(function* () {\n const { ctx } = yield* DurableObjectContext;\n /**\n * In-memory pass bookkeeping — a pure CACHE, never state: a fresh incarnation has no\n * running pass, and a deferred wake lost to eviction was only ever a promptness hint\n * on top of the already-committed pre-armed alarm.\n */\n const runningPasses = yield* Ref.make(0);\n\n const scheduled = Effect.tryPromise({\n try: () => ctx.storage.getAlarm(),\n catch: alarmFailure(\"get alarm\"),\n }).pipe(\n Effect.map((deadline) =>\n deadline === null ? Option.none<number>() : Option.some(deadline),\n ),\n );\n\n const scheduleAt = (epochMillis: number) =>\n Effect.tryPromise({\n try: () => ctx.storage.setAlarm(epochMillis),\n catch: alarmFailure(\"set alarm\"),\n });\n\n const ensureScheduledBy = (epochMillis: number) =>\n scheduled.pipe(\n Effect.flatMap((existing) =>\n Option.isSome(existing) && existing.value <= epochMillis\n ? Effect.void\n : scheduleAt(epochMillis),\n ),\n );\n\n const armNow = Clock.currentTimeMillis.pipe(\n Effect.flatMap((now) => ensureScheduledBy(now)),\n );\n\n const scheduleNow = Ref.get(runningPasses).pipe(\n Effect.flatMap((passes) => (passes > 0 ? Effect.void : armNow)),\n );\n\n const withWakesDeferred = <A, E, R>(body: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>\n Ref.update(runningPasses, (passes) => passes + 1).pipe(\n Effect.andThen(body),\n Effect.ensuring(Ref.update(runningPasses, (passes) => passes - 1)),\n );\n\n const cancel = Effect.tryPromise({\n try: () => ctx.storage.deleteAlarm(),\n catch: alarmFailure(\"delete alarm\"),\n });\n\n return DurableAlarmService.of({\n scheduled,\n scheduleAt,\n ensureScheduledBy,\n scheduleNow,\n withWakesDeferred,\n cancel,\n });\n }),\n );\n}\n\n/** What one maintenance pass did — auditable evidence mirroring `NodeDurableHost`'s report. */\nexport class MaintenancePassReport extends Schema.Class<MaintenancePassReport>(\n \"@effect-agent/platform-cloudflare/MaintenancePassReport\",\n)({\n /** `caught-up` is generation-only; `actionable` ran recovery and one bounded drain. */\n phase: Schema.Literals([\"caught-up\", \"actionable\"]),\n /** Recovery decisions executed (or deferred) BEFORE any new claim in this pass. */\n recovered: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),\n /** Settlements the drain pass finalized. */\n settled: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),\n /** Submissions still nonterminal after the pass (suspended/unknown lanes stay honest). */\n nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),\n /** `rearmed` for dirty/autonomous work, `cleared` for stable waits or settlement. */\n alarm: Schema.Literals([\"rearmed\", \"cleared\"]),\n}) {}\n\n/** Fault boundaries around every maintenance-owned durable mutation. */\nexport type ThreadMaintenanceFailpointLocation =\n | \"maintenance:dirty:before\"\n | \"maintenance:dirty:after\"\n | \"maintenance:mutation:armed\"\n | \"maintenance:mutation:finished\"\n | \"maintenance:ensure:before\"\n | \"maintenance:ensure:after\"\n | \"maintenance:begin:before\"\n | \"maintenance:begin:after\"\n | \"maintenance:finish:before\"\n | \"maintenance:finish:after\";\n\nexport type ThreadMaintenanceFailpointHandler = (\n location: ThreadMaintenanceFailpointLocation,\n) => Effect.Effect<void>;\n\n/** Test-only fault authority; production uses the inert layer. */\nexport class ThreadMaintenanceFailpoint extends Context.Service<\n ThreadMaintenanceFailpoint,\n {\n readonly hit: ThreadMaintenanceFailpointHandler;\n }\n>()(\"@effect-agent/platform-cloudflare/ThreadMaintenanceFailpoint\") {\n static readonly layer = Layer.succeed(this)({ hit: () => Effect.void });\n}\n\nconst MaintenanceGeneration = Schema.BigIntFromString.check(\n Schema.isGreaterThanOrEqualToBigInt(0n),\n);\n\n/** Versioned, platform-private maintenance state stored through Durable Object KV. */\nclass ThreadMaintenanceState extends Schema.Class<ThreadMaintenanceState>(\n \"@effect-agent/platform-cloudflare/ThreadMaintenanceState\",\n)({\n schemaVersion: Schema.Literal(1),\n dirty: MaintenanceGeneration,\n processed: MaintenanceGeneration,\n nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),\n}) {}\n\nconst MAINTENANCE_STATE_KEY = \"effect-agent:thread-maintenance:v1\";\nconst decodeMaintenanceState = Schema.decodeUnknownSync(ThreadMaintenanceState);\nconst encodeMaintenanceState = Schema.encodeSync(ThreadMaintenanceState);\n\nconst initialMaintenanceState = (): ThreadMaintenanceState =>\n ThreadMaintenanceState.make({\n schemaVersion: 1,\n // Bootstrap Objects created by the pre-generation release without scanning the ledger in\n // the constructor. One useful pass classifies and acknowledges any existing obligation.\n dirty: 1n,\n processed: 0n,\n nonterminal: 0,\n });\n\nconst readMaintenanceState = async (\n transaction: DurableObjectTransaction,\n): Promise<{ readonly state: ThreadMaintenanceState; readonly initialized: boolean }> => {\n const encoded = await transaction.get(MAINTENANCE_STATE_KEY);\n\n return encoded === undefined\n ? { state: initialMaintenanceState(), initialized: false }\n : { state: decodeMaintenanceState(encoded), initialized: true };\n};\n\nconst ensureTransactionAlarmBy = async (\n transaction: DurableObjectTransaction,\n deadline: number,\n): Promise<void> => {\n const scheduled = await transaction.getAlarm();\n\n if (scheduled === null || scheduled > deadline) {\n await transaction.setAlarm(deadline);\n }\n};\n\nconst stableExternalWait = (\n snapshot: SubmissionSnapshot,\n reports: ReadonlyMap<string, RecoveryReport>,\n): boolean => {\n const decision = reports.get(snapshot.submissionId)?.decision._tag;\n\n // An accepted abort still owes cleanup/settlement even if its claim was deferred this pass.\n if (decision === \"SettleAborted\") return false;\n switch (snapshot.state) {\n case \"suspended\":\n case \"joined\":\n return true;\n case \"unknown\":\n return decision === \"AwaitUnknownResolution\" || decision === \"MarkUnknown\";\n case \"admitted\":\n return reports.get(snapshot.submissionId)?.decision._tag === \"AwaitParentEstablishment\";\n case \"input-applied\":\n case \"joining\":\n case \"ready\":\n case \"running\":\n case \"settled\":\n case \"terminalizing\":\n return false;\n }\n};\n\nexport type MaintenancePassFailure =\n | DurableWorkerFailure\n | DurableBindingFailure\n | DurableAlarmError;\n\n/**\n * Incremental, quiescent maintenance over a durable dirty/processed generation (issue #93).\n *\n * `pass` = generation snapshot/pre-arm → recovery → bounded drain → generation acknowledgement:\n *\n * 1. One storage transaction reads dirty/processed and re-arms before work. A caught-up forced\n * alarm takes an O(1) path without recovery, ledger scans, or canonical-history reads.\n * 2. Recovery still strictly precedes a new claim, and one bounded drain advances the lane.\n * 3. The final transaction acknowledges only the generation observed at pass start. A racing\n * mutation therefore remains `dirty > processed` and retains its atomically-established alarm.\n * 4. Stable external waits acknowledge and clear. Autonomous retry, indeterminate, and lease\n * recovery states leave their generation dirty and retain bounded backoff rearming.\n */\nexport class ThreadMaintenance extends Context.Service<\n ThreadMaintenance,\n {\n /** One idempotent maintenance pass; failures propagate so workerd retries the alarm. */\n readonly pass: Effect.Effect<MaintenancePassReport, MaintenancePassFailure>;\n /**\n * Constructor gate: initialize/inspect only the O(1) maintenance record and ensure a dirty\n * generation has an alarm. It never scans the ledger or canonical history.\n */\n readonly ensureAlarm: Effect.Effect<void, MaintenancePassFailure>;\n /**\n * Serialize the pre-arm boundary with pass acknowledgement, advance the durable dirty\n * generation and arm the alarm in one transaction BEFORE running the caller's mutation.\n * A pass cannot acknowledge while that mutation remains in flight.\n */\n readonly withMutation: <A, E, R>(\n body: Effect.Effect<A, E, R>,\n ) => Effect.Effect<A, E | DurableAlarmError, R>;\n }\n>()(\"@effect-agent/platform-cloudflare/ThreadMaintenance\") {\n static readonly layer: Layer.Layer<\n ThreadMaintenance,\n never,\n | DurableAgentRuntime\n | SubmissionLedger\n | DurableAlarmService\n | ThreadMaintenanceFailpoint\n | CloudflareDurableRuntimeConfig\n | ThreadObjectIdentity\n | DurableObjectContext\n > = Layer.effect(ThreadMaintenance)(\n Effect.gen(function* () {\n const runtime = yield* DurableAgentRuntime;\n const ledger = yield* SubmissionLedger;\n const alarm = yield* DurableAlarmService;\n const config = yield* CloudflareDurableRuntimeConfig;\n const identity = yield* ThreadObjectIdentity;\n const { ctx } = yield* DurableObjectContext;\n const failpoint = yield* ThreadMaintenanceFailpoint;\n\n /**\n * Consecutive no-progress passes — an in-memory CACHE, not state: a fresh incarnation\n * restarts at zero and merely re-arms sooner than a long-lived one would have.\n */\n const stalls = yield* Ref.make(0);\n /**\n * Incarnation-local mutation count guarded with the generation transactions below. It is\n * deliberately not durable: after eviction every begun mutation has stopped, while its\n * pre-armed dirty generation remains durable for recovery. The short gate never spans the\n * caller's mutation or cross-Object I/O.\n */\n const activeMutations = yield* Ref.make(0);\n const generationGate = yield* Semaphore.make(1);\n // At-least-once deliveries are idempotent, but overlapping pass bodies could otherwise\n // acknowledge state while a sibling pass is still mutating it. Port/RPC mutations do not\n // take this permit, so cross-Object I/O cannot deadlock the maintenance serialization.\n const maintenancePassGate = yield* Semaphore.make(1);\n const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));\n\n const runTransaction = <A>(\n operation: string,\n transaction: () => Promise<A>,\n ): Effect.Effect<A, DurableAlarmError> =>\n Effect.tryPromise({\n try: transaction,\n catch: alarmFailure(operation),\n });\n\n const beginMutation = Effect.fn(\"ThreadMaintenance.beginMutation\")(function* () {\n yield* failpoint.hit(\"maintenance:dirty:before\");\n const now = yield* Clock.currentTimeMillis;\n\n yield* runTransaction(\"advance maintenance generation\", () =>\n ctx.storage.transaction(async (transaction) => {\n const { state } = await readMaintenanceState(transaction);\n\n const next = ThreadMaintenanceState.make({\n ...state,\n dirty: state.dirty + 1n,\n });\n\n await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));\n // The earliest configured retry bounds a newly actionable mutation without relying\n // on its best-effort immediate wake hint.\n await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);\n }),\n );\n yield* failpoint.hit(\"maintenance:dirty:after\");\n yield* Ref.update(activeMutations, (active) => active + 1);\n });\n\n const endMutation = generationGate.withPermit(\n Ref.update(activeMutations, (active) => Math.max(0, active - 1)),\n );\n\n const withMutation = <A, E, R>(\n body: Effect.Effect<A, E, R>,\n ): Effect.Effect<A, E | DurableAlarmError, R> =>\n Effect.acquireUseRelease(\n generationGate.withPermit(beginMutation()),\n () =>\n failpoint.hit(\"maintenance:mutation:armed\").pipe(\n Effect.andThen(body),\n Effect.tap(() => failpoint.hit(\"maintenance:mutation:finished\")),\n ),\n () => endMutation,\n );\n\n const ensureAlarm = Effect.fn(\"ThreadMaintenance.ensureAlarm\")(function* () {\n yield* failpoint.hit(\"maintenance:ensure:before\");\n const now = yield* Clock.currentTimeMillis;\n\n yield* runTransaction(\"ensure maintenance alarm\", () =>\n ctx.storage.transaction(async (transaction) => {\n const { state, initialized } = await readMaintenanceState(transaction);\n\n if (!initialized) {\n await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));\n }\n if (state.dirty > state.processed) {\n await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);\n }\n }),\n );\n yield* failpoint.hit(\"maintenance:ensure:after\");\n });\n\n const beginPass = Effect.fn(\"ThreadMaintenance.beginPass\")(function* () {\n yield* failpoint.hit(\"maintenance:begin:before\");\n const now = yield* Clock.currentTimeMillis;\n\n const result = yield* runTransaction(\"begin maintenance pass\", () =>\n ctx.storage.transaction(async (transaction) => {\n const { state, initialized } = await readMaintenanceState(transaction);\n\n if (!initialized) {\n await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));\n }\n if (state.processed >= state.dirty) {\n await transaction.deleteAlarm();\n\n return { _tag: \"CaughtUp\" as const, nonterminal: state.nonterminal };\n }\n // Pre-arm the earliest retry before recovery. A successful finish may move this slot\n // LATER to its bounded backoff, which does not cancel the running handler.\n await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);\n\n return { _tag: \"Actionable\" as const, generation: state.dirty };\n }),\n );\n\n yield* failpoint.hit(\"maintenance:begin:after\");\n\n return result;\n });\n\n const rearmDelay = Effect.fn(\"ThreadMaintenance.rearmDelay\")(function* (progressed: boolean) {\n const priorStalls = yield* Ref.getAndUpdate(stalls, (count) =>\n progressed ? 0 : count + 1,\n );\n\n if (progressed) return config.alarmBackoffBase;\n const exponent = Math.min(priorStalls, 30);\n const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** exponent);\n const jitter = yield* Random.next;\n // Full jitter over [backoff/2, backoff]: desynchronizes retry storms without ever\n // waiting longer than the deterministic bound.\n const jittered = Math.ceil(backoff / 2 + (backoff / 2) * jitter);\n\n return Math.min(jittered, config.wakeScanInterval);\n });\n\n const pass = Effect.fn(\"ThreadMaintenance.pass\")(function* (): Effect.fn.Return<\n MaintenancePassReport,\n MaintenancePassFailure\n > {\n const annotate = (report: MaintenancePassReport) =>\n Effect.annotateCurrentSpan({\n phase: report.phase,\n recovered: report.recovered,\n settled: report.settled,\n nonterminal: report.nonterminal,\n alarm: report.alarm,\n }).pipe(Effect.as(report));\n\n const started = yield* generationGate.withPermit(\n Effect.gen(function* () {\n const activeAtStart = yield* Ref.get(activeMutations);\n const generation = yield* beginPass();\n\n return { ...generation, activeAtStart };\n }),\n );\n\n if (started._tag === \"CaughtUp\") {\n return yield* annotate(\n MaintenancePassReport.make({\n phase: \"caught-up\",\n recovered: 0,\n settled: 0,\n nonterminal: started.nonterminal,\n alarm: \"cleared\",\n }),\n );\n }\n // Step 2 — reconciliation strictly precedes new work in this pass (exit gate).\n const recovered: ReadonlyArray<RecoveryReport> = yield* runtime.runRecovery;\n // Step 3 — one bounded drain pass over this Object's own lane.\n const settlements = yield* runtime.processThreadResolved(identity.threadId);\n // Observe residual state before acknowledging this exact pass-start generation.\n const remaining = yield* Stream.runCollect(ledger.scanNonterminal);\n const reports = new Map(recovered.map((report) => [report.submissionId, report]));\n const head = remaining[0];\n const headWaiting = head !== undefined && stableExternalWait(head, reports);\n\n const autonomous = remaining.some((snapshot, index) => {\n // FIFO followers cannot execute through a stable external wait. Only plain queued\n // input is dormant here; admission repairs and accepted aborts still need a pass.\n if (\n index > 0 &&\n headWaiting &&\n snapshot.state === \"ready\" &&\n reports.get(snapshot.submissionId)?.decision._tag === \"ApplyInput\"\n )\n return false;\n\n return !stableExternalWait(snapshot, reports);\n });\n\n const progressed =\n settlements.length > 0 || recovered.some((report) => report.disposition === \"repaired\");\n\n const delay = autonomous ? yield* rearmDelay(progressed) : 0;\n const now = yield* Clock.currentTimeMillis;\n\n yield* failpoint.hit(\"maintenance:finish:before\");\n\n const alarmDisposition = yield* generationGate.withPermit(\n Effect.gen(function* () {\n const active = yield* Ref.get(activeMutations);\n\n return yield* runTransaction(\"finish maintenance pass\", () =>\n ctx.storage.transaction(async (transaction) => {\n const { state } = await readMaintenanceState(transaction);\n\n // Autonomous work and in-flight mutations intentionally leave the observed\n // generation dirty. Otherwise acknowledge only the pass-start generation.\n const processed =\n autonomous || started.activeAtStart > 0 || active > 0\n ? state.processed\n : state.processed > started.generation\n ? state.processed\n : started.generation;\n\n const next = ThreadMaintenanceState.make({\n ...state,\n processed,\n nonterminal: remaining.length,\n });\n\n await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));\n if (autonomous) {\n // Replace the crash-fallback slot with this pass's bounded backoff. The target\n // is never earlier than the begin-pass fallback, so workerd does not cancel\n // this running alarm handler before its report/span can complete.\n await transaction.setAlarm(now + delay);\n\n return \"rearmed\" as const;\n }\n if (started.activeAtStart > 0 || active > 0 || next.dirty > next.processed) {\n // A mutation overlapped this pass's observation window or raced\n // acknowledgement. It stays dirty and its pre-armed bounded alarm survives;\n // unseen effects are never acknowledged. Do not accelerate that future alarm\n // from inside the current handler: workerd cancels a running handler when it\n // writes an earlier slot.\n await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);\n\n return \"rearmed\" as const;\n }\n await transaction.deleteAlarm();\n\n return \"cleared\" as const;\n }),\n );\n }),\n );\n\n yield* failpoint.hit(\"maintenance:finish:after\");\n if (alarmDisposition === \"cleared\") {\n yield* Ref.set(stalls, 0);\n }\n\n return yield* annotate(\n MaintenancePassReport.make({\n phase: \"actionable\",\n recovered: recovered.length,\n settled: settlements.length,\n nonterminal: remaining.length,\n alarm: alarmDisposition,\n }),\n );\n });\n\n return ThreadMaintenance.of({\n // A mid-pass immediate hint is droppable; durable dirty state decides the final alarm.\n pass: alarm.withWakesDeferred(maintenancePassGate.withPermit(pass())),\n ensureAlarm: ensureAlarm(),\n withMutation,\n });\n }),\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { BrowserQuickActionWorkersAi, BrowserQuickActionWorkersAiPolicy } from "./
|
|
1
|
+
import { o as BrowserQuickActionWorkersAi, s as BrowserQuickActionWorkersAiPolicy } from "./CloudflareBrowser-DJq53niJ.mjs";
|
|
2
2
|
import { Layer, Redacted } from "effect";
|
|
3
|
-
import { PageCapture
|
|
3
|
+
import { PageCapture } from "@effect-agent/sandbox/PageCapture";
|
|
4
|
+
import { SandboxImplementation } from "@effect-agent/sandbox/Sandbox";
|
|
4
5
|
import { HttpClient } from "effect/unstable/http";
|
|
5
|
-
//#region src/
|
|
6
|
+
//#region src/BrowserRestCapture.d.ts
|
|
6
7
|
/** Node-safe REST PageCapture implementation; it never imports Worker runtime modules. */
|
|
7
8
|
declare const browserRestCaptureImplementation: SandboxImplementation;
|
|
8
9
|
/** Explicit construction inputs; credentials are only projected into the fixed-origin Authorization header. */
|
|
@@ -31,4 +32,4 @@ declare const CloudflareBrowserRest: {
|
|
|
31
32
|
};
|
|
32
33
|
//#endregion
|
|
33
34
|
export { BrowserRestCaptureOptions, CloudflareBrowserRest, CloudflareBrowserRestOptions, browserRestCaptureImplementation, browserRestCaptureLayer, browserRestWorkersAiCaptureLayer };
|
|
34
|
-
//# sourceMappingURL=
|
|
35
|
+
//# sourceMappingURL=BrowserRestCapture.d.mts.map
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { BrowserQuickActionWorkersAi } from "./
|
|
1
|
+
import { i as BrowserQuickActionWorkersAi } from "./CloudflareBrowser-1jeZx3_g.mjs";
|
|
2
2
|
import { Effect, Layer, Option, Redacted, Schema, Stream } from "effect";
|
|
3
|
-
import { PageCapture, PageCaptureInferencePolicyError, PageCaptureInferenceUse, PageCaptureNavigationError, PageCaptureOutputLimitError, PageCaptureProtocolError, PageCaptureRateLimitedError, PageCaptureResourceUse, PageCaptureResult, PageCaptureUnsupportedError, PageContentCaptured, PageLinksCaptured, PageMarkdownCaptured, PageScrapeCaptured, PageStructuredCaptured
|
|
3
|
+
import { PageCapture, PageCaptureInferencePolicyError, PageCaptureInferenceUse, PageCaptureNavigationError, PageCaptureOutputLimitError, PageCaptureProtocolError, PageCaptureRateLimitedError, PageCaptureResourceUse, PageCaptureResult, PageCaptureUnsupportedError, PageContentCaptured, PageLinksCaptured, PageMarkdownCaptured, PageScrapeCaptured, PageStructuredCaptured } from "@effect-agent/sandbox/PageCapture";
|
|
4
|
+
import { SandboxImplementation } from "@effect-agent/sandbox/Sandbox";
|
|
4
5
|
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
5
|
-
//#region src/
|
|
6
|
+
//#region src/BrowserRestCapture.ts
|
|
6
7
|
/** Node-safe REST PageCapture implementation; it never imports Worker runtime modules. */
|
|
7
8
|
const browserRestCaptureImplementation = SandboxImplementation.make({
|
|
8
9
|
isolation: "isolated",
|
|
@@ -234,4 +235,4 @@ layer: (definition, options) => {
|
|
|
234
235
|
//#endregion
|
|
235
236
|
export { CloudflareBrowserRest, browserRestCaptureImplementation, browserRestCaptureLayer, browserRestWorkersAiCaptureLayer };
|
|
236
237
|
|
|
237
|
-
//# sourceMappingURL=
|
|
238
|
+
//# sourceMappingURL=BrowserRestCapture.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BrowserRestCapture.mjs","names":[],"sources":["../src/BrowserRestCapture.ts"],"sourcesContent":["import {\n PageCapture,\n PageCaptureInferencePolicyError,\n PageCaptureInferenceUse,\n PageCaptureNavigationError,\n PageCaptureOutputLimitError,\n PageCaptureProtocolError,\n PageCaptureRateLimitedError,\n PageCaptureResourceUse,\n PageCaptureResult,\n PageContentCaptured,\n PageLinksCaptured,\n PageMarkdownCaptured,\n PageScrapeCaptured,\n PageStructuredCaptured,\n PageCaptureUnsupportedError,\n type PageCaptureAction,\n type PageCaptureCapture,\n type PageCaptureError,\n type PageCaptureOutput,\n type PageCaptureRequest,\n} from \"@effect-agent/sandbox/PageCapture\";\nimport { SandboxImplementation } from \"@effect-agent/sandbox/Sandbox\";\nimport { Effect, Layer, Option, Redacted, Schema, Stream } from \"effect\";\nimport { HttpClient, HttpClientRequest, type HttpClientError } from \"effect/unstable/http\";\n\nimport {\n BrowserQuickActionWorkersAi,\n type BrowserQuickActionWorkersAiPolicy,\n} from \"./CloudflareBrowser.ts\";\n\n/** Node-safe REST PageCapture implementation; it never imports Worker runtime modules. */\nexport const browserRestCaptureImplementation = SandboxImplementation.make({\n isolation: \"isolated\",\n identity: \"cloudflare-browser-rest\",\n});\n\nconst API_ORIGIN = \"https://api.cloudflare.com\";\nconst MAX_DIAGNOSTIC_LENGTH = 8_000;\nconst boundedDiagnostic = (message: string): string => message.slice(0, MAX_DIAGNOSTIC_LENGTH);\n\n/** Explicit construction inputs; credentials are only projected into the fixed-origin Authorization header. */\nexport interface BrowserRestCaptureOptions {\n readonly accountId: string;\n readonly apiToken: Redacted.Redacted<string>;\n}\n\nconst RestSuccessEnvelope = Schema.Struct({\n success: Schema.Literal(true),\n result: Schema.Json,\n errors: Schema.optionalKey(Schema.Array(Schema.Unknown)),\n meta: Schema.optionalKey(Schema.Unknown),\n});\n\nconst RestErrorEnvelope = Schema.Struct({\n success: Schema.Literal(false),\n errors: Schema.Array(\n Schema.Struct({\n message: Schema.String,\n code: Schema.optionalKey(Schema.Number),\n }),\n ),\n result: Schema.optionalKey(Schema.Json),\n meta: Schema.optionalKey(Schema.Unknown),\n});\n\nconst RestEnvelope = Schema.Union([RestSuccessEnvelope, RestErrorEnvelope]);\nconst decodeEnvelope = Schema.decodeUnknownOption(Schema.fromJsonString(RestEnvelope));\n\nconst protocolError = (message: string, cause?: unknown): PageCaptureProtocolError =>\n PageCaptureProtocolError.make({\n implementation: browserRestCaptureImplementation,\n message: boundedDiagnostic(message),\n ...(cause === undefined ? {} : { cause }),\n });\n\nconst navigationError = (message: string, cause?: unknown): PageCaptureNavigationError =>\n PageCaptureNavigationError.make({\n implementation: browserRestCaptureImplementation,\n message: boundedDiagnostic(message),\n ...(cause === undefined ? {} : { cause }),\n });\n\nconst privateResponseCause = (\n bodyText: string,\n apiToken: Redacted.Redacted<string>,\n): Error | undefined => {\n if (bodyText.length === 0) return undefined;\n const token = Redacted.value(apiToken);\n const diagnostic = boundedDiagnostic(bodyText);\n\n return new Error(token.length === 0 ? diagnostic : diagnostic.replaceAll(token, \"[REDACTED]\"));\n};\n\nconst requestBody = (request: PageCaptureRequest): Record<string, unknown> => {\n const body: Record<string, unknown> =\n request.target._tag === \"PageUrlTarget\"\n ? { url: request.target.url }\n : { html: request.target.html };\n\n if (request.navigation !== undefined) {\n const goto: Record<string, unknown> = {};\n\n if (request.navigation.waitUntil !== undefined) goto.waitUntil = request.navigation.waitUntil;\n if (request.navigation.timeoutMillis !== undefined)\n goto.timeout = request.navigation.timeoutMillis;\n if (Object.keys(goto).length > 0) body.gotoOptions = goto;\n if (request.navigation.waitForSelector !== undefined) {\n body.waitForSelector = {\n selector: request.navigation.waitForSelector.selector,\n ...(request.navigation.waitForSelector.timeoutMillis === undefined\n ? {}\n : { timeout: request.navigation.waitForSelector.timeoutMillis }),\n };\n }\n }\n if (request.viewport !== undefined) body.viewport = request.viewport;\n if (request.resourcePolicy?.rejectResourceTypes !== undefined) {\n body.rejectResourceTypes = [...request.resourcePolicy.rejectResourceTypes];\n }\n if (request.resourcePolicy?.allowRequestPatterns !== undefined) {\n body.allowRequestPattern = [...request.resourcePolicy.allowRequestPatterns];\n }\n\n return body;\n};\n\nconst actionName = (\n action: PageCaptureAction,\n): \"content\" | \"markdown\" | \"links\" | \"scrape\" | \"json\" => {\n switch (action._tag) {\n case \"CapturePageContent\":\n return \"content\";\n case \"CapturePageMarkdown\":\n return \"markdown\";\n case \"CapturePageLinks\":\n return \"links\";\n case \"CapturePageScrape\":\n return \"scrape\";\n case \"CapturePageStructured\":\n return \"json\";\n }\n};\n\nconst actionBody = (request: PageCaptureRequest): Record<string, unknown> => {\n const body = requestBody(request);\n\n switch (request.action._tag) {\n case \"CapturePageContent\":\n case \"CapturePageMarkdown\":\n return body;\n case \"CapturePageLinks\":\n return {\n ...body,\n ...(request.action.visibleLinksOnly === undefined\n ? {}\n : { visibleLinksOnly: request.action.visibleLinksOnly }),\n };\n case \"CapturePageScrape\":\n return {\n ...body,\n elements: request.action.selectors.map((selector) => ({ selector })),\n };\n case \"CapturePageStructured\":\n return {\n ...body,\n response_format: { type: \"json_schema\", json_schema: request.action.responseFormat },\n ...(request.action.prompt === undefined ? {} : { prompt: request.action.prompt }),\n };\n }\n};\n\nconst retryAfterMillis = (\n headers: Readonly<Record<string, string | undefined>>,\n): number | undefined => {\n const seconds = Number(headers[\"retry-after\"]);\n\n if (!Number.isSafeInteger(seconds) || seconds < 0) return undefined;\n const millis = seconds * 1_000;\n\n return Number.isSafeInteger(millis) ? millis : undefined;\n};\n\nconst browserMillis = (\n headers: Readonly<Record<string, string | undefined>>,\n): number | undefined => {\n const millis = Number(headers[\"x-browser-ms-used\"]);\n\n return Number.isSafeInteger(millis) && millis >= 0 ? millis : undefined;\n};\n\n/** Response framing is provider metadata, never content controlled by the rendered page. */\nconst isJsonResponse = (headers: Readonly<Record<string, string | undefined>>): boolean => {\n const contentType = headers[\"content-type\"];\n\n if (contentType === undefined) return false;\n const mediaType = contentType.split(\";\", 1)[0]?.trim().toLowerCase();\n\n return mediaType === \"application/json\" || mediaType?.endsWith(\"+json\") === true;\n};\n\nconst readBoundedResponse = Effect.fn(\"BrowserRestCapture.readResponse\")(function* (\n response: { readonly stream: Stream.Stream<Uint8Array, HttpClientError.HttpClientError> },\n request: PageCaptureRequest,\n): Effect.fn.Return<string, PageCaptureError> {\n const decoder = new TextDecoder(\"utf-8\", { fatal: true, ignoreBOM: false });\n\n const chunks = yield* Stream.runFoldEffect<\n Uint8Array,\n HttpClientError.HttpClientError,\n never,\n { readonly observed: number; readonly text: string },\n PageCaptureOutputLimitError | PageCaptureProtocolError,\n never\n >(\n response.stream,\n () => ({ observed: 0, text: \"\" }),\n (state, chunk) => {\n const observed = state.observed + chunk.byteLength;\n\n if (observed > request.limits.maxOutputBytes) {\n return Effect.fail(\n PageCaptureOutputLimitError.make({\n implementation: browserRestCaptureImplementation,\n limit: request.limits.maxOutputBytes,\n observed,\n }),\n );\n }\n\n return Effect.try({\n try: () => ({ observed, text: state.text + decoder.decode(chunk, { stream: true }) }),\n catch: (cause) => protocolError(\"Decoding the Browser Run response failed\", cause),\n });\n },\n ).pipe(\n Effect.mapError((error) =>\n Schema.is(PageCaptureOutputLimitError)(error) || Schema.is(PageCaptureProtocolError)(error)\n ? error\n : protocolError(\"Reading the Browser Run response failed\", error),\n ),\n );\n\n return yield* Effect.try({\n try: () => chunks.text + decoder.decode(),\n catch: (cause) => protocolError(\"Decoding the Browser Run response failed\", cause),\n });\n});\n\nconst parseOutput = (\n action: PageCaptureAction,\n bodyText: string,\n apiToken: Redacted.Redacted<string>,\n): PageCaptureOutput | PageCaptureNavigationError | PageCaptureProtocolError => {\n const envelope = decodeEnvelope(bodyText);\n\n if (Option.isNone(envelope)) {\n return protocolError(\n \"The Browser Run response did not carry a valid response envelope\",\n privateResponseCause(bodyText, apiToken),\n );\n }\n if (!envelope.value.success) {\n return navigationError(\n \"Browser Run reported a navigation failure\",\n privateResponseCause(bodyText, apiToken),\n );\n }\n switch (action._tag) {\n case \"CapturePageContent\":\n case \"CapturePageMarkdown\":\n if (typeof envelope.value.result !== \"string\") {\n return protocolError(\"The Browser Run response envelope carried a non-text result\");\n }\n\n return action._tag === \"CapturePageContent\"\n ? PageContentCaptured.make({ html: envelope.value.result })\n : PageMarkdownCaptured.make({ markdown: envelope.value.result });\n case \"CapturePageLinks\": {\n const decoded = Schema.decodeUnknownOption(PageLinksCaptured)({\n _tag: \"PageLinksCaptured\",\n links: envelope.value.result,\n });\n\n return Option.isSome(decoded)\n ? decoded.value\n : protocolError(\"Browser Run links did not return a bounded array of valid URLs\");\n }\n case \"CapturePageScrape\": {\n const decoded = Schema.decodeUnknownOption(PageScrapeCaptured)({\n _tag: \"PageScrapeCaptured\",\n groups: envelope.value.result,\n });\n\n return Option.isSome(decoded)\n ? decoded.value\n : protocolError(\"Browser Run scrape did not return bounded grouped element records\");\n }\n case \"CapturePageStructured\":\n return PageStructuredCaptured.make({ value: envelope.value.result });\n }\n};\n\nconst isQuotaMessage = (text: string): boolean => /time limit|daily|quota/i.test(text);\n\nconst makeCapture = (\n client: HttpClient.HttpClient,\n options: BrowserRestCaptureOptions,\n workersAi?: BrowserQuickActionWorkersAiPolicy,\n): PageCaptureCapture =>\n Effect.fn(\"BrowserRestCapture.capture\")(function* (\n request: PageCaptureRequest,\n ): Effect.fn.Return<PageCaptureResult, PageCaptureError> {\n const usesWorkersAi = request.action._tag === \"CapturePageStructured\";\n\n if (usesWorkersAi) {\n if (workersAi === undefined) {\n return yield* PageCaptureUnsupportedError.make({\n implementation: browserRestCaptureImplementation,\n feature: \"action\",\n message:\n \"Structured capture invokes separately billed Workers AI and requires an explicit authorization and accounting policy\",\n });\n }\n yield* workersAi.authorizeAndAccount(request).pipe(\n Effect.mapError((cause) =>\n PageCaptureInferencePolicyError.make({\n implementation: browserRestCaptureImplementation,\n provider: \"cloudflare-workers-ai\",\n reason: cause.reason,\n message:\n cause.reason === \"authorization\"\n ? \"Workers AI extraction was not authorized\"\n : \"Workers AI extraction could not be accounted for\",\n cause,\n }),\n ),\n );\n }\n const action = actionName(request.action);\n const path = `${API_ORIGIN}/client/v4/accounts/${encodeURIComponent(options.accountId)}/browser-rendering/${action}`;\n\n const requestWithBody = yield* HttpClientRequest.post(path).pipe(\n request.engine === \"kitesurf\"\n ? HttpClientRequest.setUrlParam(\"browser\", \"kitesurf\")\n : (value) => value,\n HttpClientRequest.bearerToken(options.apiToken),\n HttpClientRequest.acceptJson,\n HttpClientRequest.bodyJson(actionBody(request)),\n Effect.mapError((cause) => protocolError(\"Encoding the Browser Run request failed\", cause)),\n );\n\n const response = yield* client\n .execute(requestWithBody)\n .pipe(\n Effect.mapError((cause) => protocolError(\"Calling the Browser Run REST API failed\", cause)),\n );\n\n const bodyText = yield* readBoundedResponse(response, request);\n\n if (response.status === 429) {\n const reason = isQuotaMessage(bodyText) ? \"quota\" : \"rate\";\n\n return yield* PageCaptureRateLimitedError.make({\n implementation: browserRestCaptureImplementation,\n reason,\n ...(retryAfterMillis(response.headers) === undefined\n ? {}\n : { retryAfterMillis: retryAfterMillis(response.headers) }),\n ...(privateResponseCause(bodyText, options.apiToken) === undefined\n ? {}\n : { cause: privateResponseCause(bodyText, options.apiToken) }),\n message:\n reason === \"quota\"\n ? \"Browser Run exceeded its browser quota\"\n : \"Browser Run was rate limited\",\n });\n }\n if (response.status < 200 || response.status >= 300) {\n const error =\n response.status >= 500\n ? protocolError(\n `Browser Run answered HTTP ${String(response.status)}`,\n privateResponseCause(bodyText, options.apiToken),\n )\n : navigationError(\n `Browser Run answered HTTP ${String(response.status)}`,\n privateResponseCause(bodyText, options.apiToken),\n );\n\n return yield* error;\n }\n if (!isJsonResponse(response.headers)) {\n return yield* protocolError(\n \"The Browser Run success response was not a JSON response envelope\",\n privateResponseCause(bodyText, options.apiToken),\n );\n }\n const output = parseOutput(request.action, bodyText, options.apiToken);\n\n if (\n output._tag === \"PageCaptureNavigationError\" ||\n output._tag === \"PageCaptureProtocolError\"\n ) {\n return yield* output;\n }\n\n return PageCaptureResult.make({\n implementation: browserRestCaptureImplementation,\n output,\n resourceUse: PageCaptureResourceUse.make({\n ...(browserMillis(response.headers) === undefined\n ? {}\n : { browserMillis: browserMillis(response.headers) }),\n ...(usesWorkersAi\n ? {\n inference: PageCaptureInferenceUse.make({\n provider: \"cloudflare-workers-ai\",\n modelCalls: 1,\n }),\n }\n : {}),\n }),\n });\n });\n\n/** REST PageCapture layer for Node and other non-Worker composition roots. */\nexport const browserRestCaptureLayer = (\n options: BrowserRestCaptureOptions,\n): Layer.Layer<PageCapture, never, HttpClient.HttpClient> =>\n Layer.effect(\n PageCapture,\n Effect.map(HttpClient.HttpClient, (client) =>\n PageCapture.of({ capture: makeCapture(client, options) }),\n ),\n );\n\n/** REST structured extraction additionally requires explicit Workers AI authorization/accounting. */\nexport const browserRestWorkersAiCaptureLayer = (\n options: BrowserRestCaptureOptions,\n): Layer.Layer<PageCapture, never, HttpClient.HttpClient | BrowserQuickActionWorkersAi> =>\n Layer.effect(\n PageCapture,\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient;\n const workersAi = yield* BrowserQuickActionWorkersAi;\n\n return PageCapture.of({ capture: makeCapture(client, options, workersAi) });\n }),\n );\n\n/** Explicit REST credentials and optional authorization for separately billed extraction. */\nexport interface CloudflareBrowserRestOptions extends BrowserRestCaptureOptions {\n readonly workersAi?: BrowserQuickActionWorkersAiPolicy;\n}\n\n/** Node-safe WebCapture handler assembly; the application supplies its HttpClient. */\nexport const CloudflareBrowserRest = {\n /**\n * Supports capture, scrape, and extraction definitions. Only PageCapture is supplied;\n * handler errors and other services remain visible. Extraction fails closed without\n * workersAi. Existing host policies, response limits, and scoped cleanup are unchanged.\n */\n layer: <A, E, R>(\n definition: { readonly handlers: Layer.Layer<A, E, R> },\n options: CloudflareBrowserRestOptions,\n ): Layer.Layer<A, E, Exclude<R, PageCapture> | HttpClient.HttpClient> => {\n const capture =\n options.workersAi === undefined\n ? browserRestCaptureLayer(options)\n : browserRestWorkersAiCaptureLayer(options).pipe(\n Layer.provide(BrowserQuickActionWorkersAi.layer(options.workersAi)),\n );\n\n return definition.handlers.pipe(Layer.provide(capture));\n },\n};\n"],"mappings":"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"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Layer, Redacted } from "effect";
|
|
2
|
-
import {
|
|
2
|
+
import { SandboxImplementation } from "@effect-agent/sandbox/Sandbox";
|
|
3
3
|
import { HttpClient } from "effect/unstable/http";
|
|
4
|
-
|
|
4
|
+
import { PageCrawl } from "@effect-agent/sandbox/PageCrawl";
|
|
5
|
+
//#region src/BrowserRestCrawl.d.ts
|
|
5
6
|
/** Node-safe REST crawl implementation. It never exposes or persists provider job identity. */
|
|
6
7
|
declare const browserRestCrawlImplementation: SandboxImplementation;
|
|
7
8
|
/** Explicit credentials. The token is projected only into fixed-origin Authorization headers. */
|
|
@@ -13,4 +14,4 @@ interface BrowserRestCrawlOptions {
|
|
|
13
14
|
declare const browserRestCrawlLayer: (options: BrowserRestCrawlOptions) => Layer.Layer<PageCrawl, never, HttpClient.HttpClient>;
|
|
14
15
|
//#endregion
|
|
15
16
|
export { BrowserRestCrawlOptions, browserRestCrawlImplementation, browserRestCrawlLayer };
|
|
16
|
-
//# sourceMappingURL=
|
|
17
|
+
//# sourceMappingURL=BrowserRestCrawl.d.mts.map
|