@effect-agent/platform-cloudflare 0.1.0-beta.9 → 0.1.0-beta.91
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 +188 -0
- package/dist/Alarm.mjs +451 -0
- package/dist/Alarm.mjs.map +1 -0
- package/dist/BrowserRestCapture.d.mts +35 -0
- package/dist/BrowserRestCapture.mjs +238 -0
- package/dist/BrowserRestCapture.mjs.map +1 -0
- package/dist/BrowserRestCrawl.d.mts +17 -0
- package/dist/BrowserRestCrawl.mjs +244 -0
- package/dist/BrowserRestCrawl.mjs.map +1 -0
- package/dist/CloudflareAiGateway.d.mts +64 -0
- package/dist/CloudflareAiGateway.mjs +70 -0
- package/dist/CloudflareAiGateway.mjs.map +1 -0
- package/dist/CloudflareBindings.d.mts +115 -0
- package/dist/CloudflareBindings.mjs +113 -0
- package/dist/CloudflareBindings.mjs.map +1 -0
- package/dist/CloudflareBrowser-DV6kya1H.mjs +495 -0
- package/dist/CloudflareBrowser-DV6kya1H.mjs.map +1 -0
- package/dist/CloudflareBrowser-Doe1M7R5.d.mts +80 -0
- package/dist/CloudflareBrowser.d.mts +2 -0
- package/dist/CloudflareBrowser.mjs +2 -0
- package/dist/CloudflareCodeMode.d.mts +44 -0
- package/dist/CloudflareCodeMode.mjs +616 -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 +142 -0
- package/dist/CloudflareMemory.mjs +202 -0
- package/dist/CloudflareMemory.mjs.map +1 -0
- package/dist/CloudflareScheduling.d.mts +52 -0
- package/dist/CloudflareScheduling.mjs +396 -0
- package/dist/CloudflareScheduling.mjs.map +1 -0
- package/dist/CloudflareSubscriptions.d.mts +87 -0
- package/dist/CloudflareSubscriptions.mjs +530 -0
- package/dist/CloudflareSubscriptions.mjs.map +1 -0
- package/dist/CloudflareThreadClient.d.mts +1496 -0
- package/dist/CloudflareThreadClient.mjs +382 -0
- package/dist/CloudflareThreadClient.mjs.map +1 -0
- package/dist/InteractiveBrowser-DUE_m12-.d.mts +161 -0
- package/dist/InteractiveBrowser-DupDtfw2.mjs +1169 -0
- package/dist/InteractiveBrowser-DupDtfw2.mjs.map +1 -0
- package/dist/InteractiveBrowser.d.mts +2 -0
- package/dist/InteractiveBrowser.mjs +2 -0
- package/dist/ProtectedBrowser.d.mts +123 -0
- package/dist/ProtectedBrowser.mjs +1135 -0
- package/dist/ProtectedBrowser.mjs.map +1 -0
- package/dist/ThreadObject-CIbmXZCY.d.mts +812 -0
- package/dist/ThreadObject-DvRG0dDz.mjs +835 -0
- package/dist/ThreadObject-DvRG0dDz.mjs.map +1 -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 +57 -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/index.d.mts +13 -1548
- package/dist/index.mjs +13 -1636
- package/dist/prepared-admission-DU0_T-ev.mjs +73 -0
- package/dist/prepared-admission-DU0_T-ev.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -53
- package/src/Alarm.ts +973 -0
- package/src/BrowserRestCapture.ts +477 -0
- package/src/BrowserRestCrawl.ts +540 -0
- package/src/CloudflareAiGateway.ts +170 -0
- package/src/CloudflareBindings.ts +199 -0
- package/src/CloudflareBrowser.ts +15 -0
- package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +372 -204
- package/src/{config.ts → CloudflareConfig.ts} +9 -8
- package/src/CloudflareMemory.ts +420 -0
- package/src/CloudflareScheduling.ts +747 -0
- package/src/CloudflareSubscriptions.ts +1041 -0
- package/src/CloudflareThreadClient.ts +809 -0
- package/src/InteractiveBrowser.ts +2354 -0
- package/src/ProtectedBrowser.ts +9 -0
- package/src/ThreadObject.ts +1179 -0
- package/src/{wake-scheduler.ts → WakeScheduler.ts} +32 -26
- package/src/index.ts +12 -23
- package/src/internal/boundary.ts +55 -0
- package/src/internal/browser-quick-action.ts +857 -0
- package/src/internal/browser-session-lifecycle.ts +160 -0
- package/src/internal/layers.ts +751 -0
- package/src/internal/message-delivery.ts +152 -0
- package/src/internal/prepared-admission.ts +116 -0
- package/src/internal/progress-wait.ts +121 -0
- package/src/internal/transport.ts +44 -0
- package/src/protected-browser/binding.ts +313 -0
- package/src/protected-browser/host.ts +344 -0
- package/src/protected-browser/inspect-frame.ts +125 -0
- package/src/protected-browser/native.ts +445 -0
- package/src/protected-browser/policy.ts +821 -0
- package/dist/index.mjs.map +0 -1
- package/src/alarm.ts +0 -334
- package/src/bindings.ts +0 -145
- package/src/client.ts +0 -646
- package/src/conversation-object.ts +0 -768
- package/src/layers.ts +0 -376
- package/src/transport.ts +0 -38
package/dist/Alarm.d.mts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { DurableObjectContext } from "./CloudflareBindings.mjs";
|
|
2
|
+
import { o as CloudflareDurableRuntimeConfig } from "./CloudflareConfig-f3CqTel1.mjs";
|
|
3
|
+
import { Cause, Context, Deferred, Effect, Layer, Option, Schema } from "effect";
|
|
4
|
+
import { DurableBindingFailure } from "effect-agent/agent-registration";
|
|
5
|
+
import { DurableAgentRuntime, DurableWorkerFailure } from "effect-agent/durable-agent-runtime";
|
|
6
|
+
import { SubmissionLedger } from "effect-agent/submission-ledger";
|
|
7
|
+
import { ThreadProjectionError, ThreadProjectionMaintenance } from "effect-agent/thread-projection-maintenance";
|
|
8
|
+
import { SqlClient } from "effect/unstable/sql/SqlClient";
|
|
9
|
+
declare namespace Alarm_d_exports {
|
|
10
|
+
export { DurableAlarmError, DurableAlarmService, MaintenancePassFailure, MaintenancePassReport, ThreadHostMaintenance, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMaintenanceFailpointHandler, ThreadMaintenanceFailpointLocation, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, ThreadPublicationService, publishCommitted };
|
|
11
|
+
}
|
|
12
|
+
declare const DurableAlarmError_base: Schema.Class<DurableAlarmError, Schema.TaggedStruct<"DurableAlarmError", {
|
|
13
|
+
readonly operation: Schema.String;
|
|
14
|
+
readonly message: Schema.String;
|
|
15
|
+
readonly cause: Schema.optionalKey<Schema.Defect>;
|
|
16
|
+
}>, Cause.YieldableError>;
|
|
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
|
+
declare class DurableAlarmError extends DurableAlarmError_base {}
|
|
30
|
+
declare const DurableAlarmService_base: Context.ServiceClass<DurableAlarmService, "@effect-agent/platform-cloudflare/DurableAlarmService", {
|
|
31
|
+
/** The scheduled deadline in epoch milliseconds, if any. */
|
|
32
|
+
readonly scheduled: Effect.Effect<Option.Option<number>, DurableAlarmError>;
|
|
33
|
+
/** Replace the slot with this deadline. */
|
|
34
|
+
readonly scheduleAt: (epochMillis: number) => Effect.Effect<void, DurableAlarmError>;
|
|
35
|
+
/** Keep the EARLIER of the existing deadline and this one (the multiplexing rule). */
|
|
36
|
+
readonly ensureScheduledBy: (epochMillis: number) => Effect.Effect<void, DurableAlarmError>;
|
|
37
|
+
/**
|
|
38
|
+
* Arm an immediate alarm (the durable, coalescing local wake) — DEFERRED while a
|
|
39
|
+
* maintenance pass is executing. Workerd cancels an in-flight alarm handler when a new
|
|
40
|
+
* EARLIER deadline is written during its execution (`requestScheduledAlarm`), and the
|
|
41
|
+
* maintenance pass runs INSIDE the alarm handler: an immediate wake landing mid-pass
|
|
42
|
+
* (a routed port mutation, a sibling's `wake()`, the coordinator's own local notify)
|
|
43
|
+
* would kill the running Attempt — manufacturing an ownership loss no real eviction
|
|
44
|
+
* caused, and routing open uncertain-class Tool Calls into spurious Unknown Outcomes.
|
|
45
|
+
* Deferral is contract-safe: wakes are droppable hints, every mutating entry point
|
|
46
|
+
* pre-arms BEFORE its first durable mutation (the alarm invariant never rests on this
|
|
47
|
+
* call). The pass's durable generation check observes any racing mutation, so the
|
|
48
|
+
* in-memory hint does not need to be flushed after a stable wait is acknowledged.
|
|
49
|
+
*/
|
|
50
|
+
readonly scheduleNow: Effect.Effect<void, DurableAlarmError>;
|
|
51
|
+
/**
|
|
52
|
+
* Run one maintenance pass with wake deferral (see `scheduleNow`). Calls made while `body`
|
|
53
|
+
* executes are droppable promptness hints; correctness rests on the durable generation.
|
|
54
|
+
*/
|
|
55
|
+
readonly withWakesDeferred: <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
56
|
+
/** Clear the slot; correctness-sensitive clears live in maintenance generation transactions. */
|
|
57
|
+
readonly cancel: Effect.Effect<void, DurableAlarmError>;
|
|
58
|
+
}>;
|
|
59
|
+
/** `ctx.storage` alarm slot as an Effect service; storage is truth, never a memory field. */
|
|
60
|
+
declare class DurableAlarmService extends DurableAlarmService_base {
|
|
61
|
+
static readonly layer: Layer.Layer<DurableAlarmService, never, DurableObjectContext | SqlClient>;
|
|
62
|
+
}
|
|
63
|
+
declare const MaintenancePassReport_base: Schema.Class<MaintenancePassReport, Schema.Struct<{
|
|
64
|
+
/** `caught-up` ran no runtime work (publication may be pending); `actionable` ran recovery. */
|
|
65
|
+
readonly phase: Schema.Literals<readonly ["caught-up", "actionable"]>;
|
|
66
|
+
/** Recovery decisions executed (or deferred) BEFORE any new claim in this pass. */
|
|
67
|
+
readonly recovered: Schema.Int;
|
|
68
|
+
/** Whether the head Attempt settled. Joined input may settle with that head. */
|
|
69
|
+
readonly settled: Schema.Int;
|
|
70
|
+
/** Submissions still nonterminal after the pass (suspended/unknown lanes stay honest). */
|
|
71
|
+
readonly nonterminal: Schema.Int;
|
|
72
|
+
/** `rearmed` for dirty/autonomous work, `cleared` for stable waits or settlement. */
|
|
73
|
+
readonly alarm: Schema.Literals<readonly ["rearmed", "cleared"]>;
|
|
74
|
+
}>, {}>;
|
|
75
|
+
/** What one maintenance pass did — auditable evidence mirroring `NodeDurableHost`'s report. */
|
|
76
|
+
declare class MaintenancePassReport extends MaintenancePassReport_base {}
|
|
77
|
+
/** Fault boundaries around every maintenance-owned durable mutation. */
|
|
78
|
+
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:select:before" | "maintenance:select:after" | "maintenance:finish:before" | "maintenance:finish:after";
|
|
79
|
+
type ThreadMaintenanceFailpointHandler = (location: ThreadMaintenanceFailpointLocation) => Effect.Effect<void>;
|
|
80
|
+
declare const ThreadMaintenanceFailpoint_base: Context.ServiceClass<ThreadMaintenanceFailpoint, "@effect-agent/platform-cloudflare/ThreadMaintenanceFailpoint", {
|
|
81
|
+
readonly hit: ThreadMaintenanceFailpointHandler;
|
|
82
|
+
}>;
|
|
83
|
+
/** Test-only fault authority; production uses the inert layer. */
|
|
84
|
+
declare class ThreadMaintenanceFailpoint extends ThreadMaintenanceFailpoint_base {
|
|
85
|
+
static readonly layer: Layer.Layer<ThreadMaintenanceFailpoint, never, never>;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Durable host publication of canonical records and ledger approval/abort/resolution intents.
|
|
89
|
+
* The host owns schema-versioned cursors, destination idempotency and acknowledgement. Delivery
|
|
90
|
+
* is at least once. Hooks must not write the alarm slot or mutate the supplied raw source ports.
|
|
91
|
+
*
|
|
92
|
+
* `invalidate`, `prepareGeneration` and `pendingDeadline` must be bounded local operations.
|
|
93
|
+
* `prepareGeneration` durably invalidates a scan only when its generation changes; repeated
|
|
94
|
+
* calls must preserve partial scan progress. It runs with no source mutation in flight.
|
|
95
|
+
* `drain` performs bounded delivery and persists retries before returning. A pending deadline
|
|
96
|
+
* defers runtime recovery/Attempts, allowing committed host publications to drain first.
|
|
97
|
+
* Unexpected hook failures leave the prearmed generation for retry. Hooks acquire per-call
|
|
98
|
+
* resources with Effect.scoped; Layer construction owns incarnation resources (eviction need
|
|
99
|
+
* not run finalizers). Do not hold a local hook behind network I/O or call back into producers.
|
|
100
|
+
*/
|
|
101
|
+
interface ThreadPublicationService {
|
|
102
|
+
readonly invalidate: Effect.Effect<void, DurableAlarmError>;
|
|
103
|
+
readonly prepareGeneration: (generation: bigint) => Effect.Effect<void, DurableAlarmError>;
|
|
104
|
+
readonly drain: Effect.Effect<void, DurableAlarmError>;
|
|
105
|
+
readonly pendingDeadline: Effect.Effect<Option.Option<number>, DurableAlarmError>;
|
|
106
|
+
}
|
|
107
|
+
declare const ThreadPublication_base: Context.ServiceClass<ThreadPublication, "@effect-agent/platform-cloudflare/ThreadPublication", ThreadPublicationService>;
|
|
108
|
+
/** Opt in with `ThreadObject.layer(registrations, { publication: Layer.effect(ThreadPublication)(...) })`. */
|
|
109
|
+
declare class ThreadPublication extends ThreadPublication_base {
|
|
110
|
+
static readonly layer: Layer.Layer<ThreadPublication, never, never>;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Host-assembled message recovery, supplied by ThreadObject.layer even when the application
|
|
114
|
+
* rebuilds ThreadMaintenance. These obligations outlive source Runs and never defer a ready
|
|
115
|
+
* source Attempt while a destination is processing an accepted message.
|
|
116
|
+
*/
|
|
117
|
+
declare const ThreadMessageDelivery: Context.Reference<{
|
|
118
|
+
readonly drain: Effect.Effect<void, DurableAlarmError>;
|
|
119
|
+
/** Drain inserts and due retries during source work, finishing the bounded wave on completion. */
|
|
120
|
+
readonly drainUntil?: (finished: Deferred.Deferred<void>) => Effect.Effect<void, DurableAlarmError>;
|
|
121
|
+
readonly pendingDeadline: Effect.Effect<Option.Option<number>, DurableAlarmError>;
|
|
122
|
+
}>;
|
|
123
|
+
/**
|
|
124
|
+
* Application obligations sharing this Object's alarm. The deadline read is local and
|
|
125
|
+
* read-only. Drain beside the native Attempt and always finish one initial bounded wave,
|
|
126
|
+
* even when `finished` was already signalled. Then stop starting new waves on that signal
|
|
127
|
+
* and finish the bounded current wave before returning. Native maintenance joins that work
|
|
128
|
+
* before acknowledging a generation. Mutations use the same ThreadMutationGate; hooks never
|
|
129
|
+
* write the raw alarm slot. Pending host work does not defer a ready model Attempt.
|
|
130
|
+
*/
|
|
131
|
+
declare const ThreadHostMaintenance: Context.Reference<{
|
|
132
|
+
readonly pendingDeadline: Effect.Effect<Option.Option<number>, DurableAlarmError>;
|
|
133
|
+
readonly drainUntil: (finished: Deferred.Deferred<void>) => Effect.Effect<void, DurableAlarmError>;
|
|
134
|
+
}>;
|
|
135
|
+
/** @internal A committed source operation must not become a failed operation because delivery failed. */
|
|
136
|
+
declare const publishCommitted: Effect.Effect<void, never, ThreadPublication>;
|
|
137
|
+
declare const ThreadMutationGate_base: Context.ServiceClass<ThreadMutationGate, "@effect-agent/platform-cloudflare/internal/ThreadMutationGate", {
|
|
138
|
+
readonly withMutation: <A, E, R>(body: Effect.Effect<A, E, R>,
|
|
139
|
+
/** Indexed host work has its own durable deadline and does not invalidate ledger recovery. */
|
|
140
|
+
options?: {
|
|
141
|
+
readonly invalidatesRecovery: boolean;
|
|
142
|
+
}) => Effect.Effect<A, E | DurableAlarmError, R>;
|
|
143
|
+
readonly withSnapshot: <A, E, R>(body: (active: number) => Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
144
|
+
}>;
|
|
145
|
+
/**
|
|
146
|
+
* Shared prearm/acknowledgement boundary for ingress and runtime-owned producers.
|
|
147
|
+
* `ThreadObject.layer` provides this same instance in its Services. Rebuilt runtime/maintenance
|
|
148
|
+
* Layers must reuse that instance; a second gate cannot observe the native producers' activity.
|
|
149
|
+
*/
|
|
150
|
+
declare class ThreadMutationGate extends ThreadMutationGate_base {
|
|
151
|
+
static readonly layer: Layer.Layer<ThreadMutationGate, never, CloudflareDurableRuntimeConfig | DurableObjectContext | SqlClient | ThreadMaintenanceFailpoint>;
|
|
152
|
+
}
|
|
153
|
+
type MaintenancePassFailure = DurableWorkerFailure | DurableBindingFailure | DurableAlarmError | ThreadProjectionError;
|
|
154
|
+
declare const ThreadMaintenance_base: Context.ServiceClass<ThreadMaintenance, "@effect-agent/platform-cloudflare/ThreadMaintenance", {
|
|
155
|
+
/** One idempotent maintenance pass; failures propagate so workerd retries the alarm. */
|
|
156
|
+
readonly pass: Effect.Effect<MaintenancePassReport, MaintenancePassFailure>;
|
|
157
|
+
/**
|
|
158
|
+
* Constructor gate: initialize/inspect only the O(1) maintenance record and ensure a dirty
|
|
159
|
+
* generation has an alarm. It never scans the ledger or canonical history.
|
|
160
|
+
*/
|
|
161
|
+
readonly ensureAlarm: Effect.Effect<void, MaintenancePassFailure>;
|
|
162
|
+
/**
|
|
163
|
+
* Serialize the pre-arm boundary with pass acknowledgement, advance the durable dirty
|
|
164
|
+
* generation and arm the alarm in one transaction BEFORE running the caller's mutation.
|
|
165
|
+
* A pass cannot acknowledge while that mutation remains in flight.
|
|
166
|
+
*/
|
|
167
|
+
readonly withMutation: <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E | DurableAlarmError, R>;
|
|
168
|
+
}>;
|
|
169
|
+
/**
|
|
170
|
+
* Incremental, quiescent maintenance over a durable dirty/processed generation (issue #93).
|
|
171
|
+
*
|
|
172
|
+
* `pass` = generation snapshot/pre-arm → recovery → one head Attempt → generation acknowledgement:
|
|
173
|
+
*
|
|
174
|
+
* 1. One storage transaction reads dirty/processed and re-arms before work. A caught-up forced
|
|
175
|
+
* alarm takes an O(1) path without recovery, ledger scans, or canonical-history reads.
|
|
176
|
+
* 2. Recovery strictly precedes a new claim. One head Attempt advances the lane and requests
|
|
177
|
+
* a safe yield after ten minutes. The whole event has a fourteen-minute cooperative timeout.
|
|
178
|
+
* 3. The final transaction acknowledges only the generation observed at pass start. A racing
|
|
179
|
+
* mutation therefore remains `dirty > processed` and retains its atomically-established alarm.
|
|
180
|
+
* 4. Stable external waits acknowledge and clear. Autonomous retry, indeterminate, and lease
|
|
181
|
+
* recovery states leave their generation dirty and retain bounded backoff rearming.
|
|
182
|
+
*/
|
|
183
|
+
declare class ThreadMaintenance extends ThreadMaintenance_base {
|
|
184
|
+
static readonly layer: Layer.Layer<ThreadMaintenance, never, ThreadMutationGate | ThreadPublication | ThreadProjectionMaintenance | DurableAgentRuntime | SubmissionLedger | DurableAlarmService | ThreadMaintenanceFailpoint | CloudflareDurableRuntimeConfig | DurableObjectContext | SqlClient>;
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
export { DurableAlarmError, DurableAlarmService, MaintenancePassFailure, MaintenancePassReport, ThreadHostMaintenance, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMaintenanceFailpointHandler, ThreadMaintenanceFailpointLocation, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, ThreadPublicationService, publishCommitted, Alarm_d_exports as t };
|
|
188
|
+
//# sourceMappingURL=Alarm.d.mts.map
|
package/dist/Alarm.mjs
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { DurableObjectContext } from "./CloudflareBindings.mjs";
|
|
3
|
+
import { CloudflareDurableRuntimeConfig } from "./CloudflareConfig.mjs";
|
|
4
|
+
import { r as safeCauseMessage } from "./boundary-BguazVkh.mjs";
|
|
5
|
+
import { Cause, Clock, Context, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Random, Ref, Schema, Semaphore, Stream } from "effect";
|
|
6
|
+
import "effect-agent/agent-registration";
|
|
7
|
+
import { DurableAgentRuntime } from "effect-agent/durable-agent-runtime";
|
|
8
|
+
import { ThreadId } from "effect-agent/identifiers";
|
|
9
|
+
import { SubmissionLedger } from "effect-agent/submission-ledger";
|
|
10
|
+
import { ThreadProjectionMaintenance, drainDue } from "effect-agent/thread-projection-maintenance";
|
|
11
|
+
import { SqlClient } from "effect/unstable/sql/SqlClient";
|
|
12
|
+
//#region src/Alarm.ts
|
|
13
|
+
var Alarm_exports = /* @__PURE__ */ __exportAll({
|
|
14
|
+
DurableAlarmError: () => DurableAlarmError,
|
|
15
|
+
DurableAlarmService: () => DurableAlarmService,
|
|
16
|
+
MaintenancePassReport: () => MaintenancePassReport,
|
|
17
|
+
ThreadHostMaintenance: () => ThreadHostMaintenance,
|
|
18
|
+
ThreadMaintenance: () => ThreadMaintenance,
|
|
19
|
+
ThreadMaintenanceFailpoint: () => ThreadMaintenanceFailpoint,
|
|
20
|
+
ThreadMessageDelivery: () => ThreadMessageDelivery,
|
|
21
|
+
ThreadMutationGate: () => ThreadMutationGate,
|
|
22
|
+
ThreadPublication: () => ThreadPublication,
|
|
23
|
+
publishCommitted: () => publishCommitted
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* The single multiplexed Durable Object alarm (decision D-P6-2). A Durable Object has ONE
|
|
27
|
+
* alarm slot; every cadence the Node host ran on fibers (wake scan, lease expiry, settlement
|
|
28
|
+
* and abort re-checks, retry backoff) multiplexes into one idempotent maintenance pass, and
|
|
29
|
+
* the slot always holds the EARLIEST deadline any caller asked for.
|
|
30
|
+
*
|
|
31
|
+
* The alarm invariant (plan §1.4): every committed actionable mutation carries a newer durable
|
|
32
|
+
* maintenance generation and a committed alarm. Stable externally-driven waits may be
|
|
33
|
+
* nonterminal without retaining an alarm; their resolving mutation advances the generation and
|
|
34
|
+
* restores the alarm atomically.
|
|
35
|
+
*/
|
|
36
|
+
/** The Durable Object alarm API failed; surfaces on host entry points as a typed refusal. */
|
|
37
|
+
var DurableAlarmError = class extends Schema.TaggedError()("DurableAlarmError", {
|
|
38
|
+
operation: Schema.String,
|
|
39
|
+
message: Schema.String,
|
|
40
|
+
cause: Schema.optionalKey(Schema.Defect())
|
|
41
|
+
}) {};
|
|
42
|
+
const alarmFailure = (operation) => (cause) => DurableAlarmError.make({
|
|
43
|
+
operation,
|
|
44
|
+
message: safeCauseMessage(cause, "The Cloudflare alarm API failed without a diagnostic"),
|
|
45
|
+
cause
|
|
46
|
+
});
|
|
47
|
+
const makeStorageOperation = Effect.map(SqlClient, (sql) => (operation, execute) => Effect.flatMap(Effect.serviceOption(sql.transactionService), (current) => {
|
|
48
|
+
const body = Effect.uninterruptible(Effect.tryPromise({
|
|
49
|
+
try: execute,
|
|
50
|
+
catch: alarmFailure(operation)
|
|
51
|
+
}));
|
|
52
|
+
return current._tag === "Some" ? body : Effect.scoped(Effect.andThen(sql.reserve.pipe(Effect.mapError(alarmFailure(operation))), body));
|
|
53
|
+
}));
|
|
54
|
+
/** `ctx.storage` alarm slot as an Effect service; storage is truth, never a memory field. */
|
|
55
|
+
var DurableAlarmService = class DurableAlarmService extends Context.Service()("@effect-agent/platform-cloudflare/DurableAlarmService") {
|
|
56
|
+
static layer = Layer.effect(DurableAlarmService)(Effect.gen(function* () {
|
|
57
|
+
const { ctx } = yield* DurableObjectContext;
|
|
58
|
+
/**
|
|
59
|
+
* In-memory pass bookkeeping — a pure CACHE, never state: a fresh incarnation has no
|
|
60
|
+
* running pass, and a deferred wake lost to eviction was only ever a promptness hint
|
|
61
|
+
* on top of the already-committed pre-armed alarm.
|
|
62
|
+
*/
|
|
63
|
+
const runningPasses = yield* Ref.make(0);
|
|
64
|
+
const storageOperation = yield* makeStorageOperation;
|
|
65
|
+
const scheduled = storageOperation("get alarm", () => ctx.storage.getAlarm()).pipe(Effect.map((deadline) => deadline === null ? Option.none() : Option.some(deadline)));
|
|
66
|
+
const scheduleAt = (epochMillis) => storageOperation("set alarm", () => ctx.storage.setAlarm(epochMillis));
|
|
67
|
+
const ensureScheduledBy = (epochMillis) => storageOperation("ensure alarm", () => ctx.storage.transaction(async (transaction) => {
|
|
68
|
+
const existing = await transaction.getAlarm();
|
|
69
|
+
if (existing === null || existing > epochMillis) await transaction.setAlarm(epochMillis);
|
|
70
|
+
}));
|
|
71
|
+
const armNow = Clock.currentTimeMillis.pipe(Effect.flatMap((now) => ensureScheduledBy(now)));
|
|
72
|
+
const scheduleNow = Ref.get(runningPasses).pipe(Effect.flatMap((passes) => passes > 0 ? Effect.void : armNow));
|
|
73
|
+
const withWakesDeferred = (body) => Ref.update(runningPasses, (passes) => passes + 1).pipe(Effect.andThen(body), Effect.ensuring(Ref.update(runningPasses, (passes) => passes - 1)));
|
|
74
|
+
const cancel = storageOperation("delete alarm", () => ctx.storage.deleteAlarm());
|
|
75
|
+
return DurableAlarmService.of({
|
|
76
|
+
scheduled,
|
|
77
|
+
scheduleAt,
|
|
78
|
+
ensureScheduledBy,
|
|
79
|
+
scheduleNow,
|
|
80
|
+
withWakesDeferred,
|
|
81
|
+
cancel
|
|
82
|
+
});
|
|
83
|
+
}));
|
|
84
|
+
};
|
|
85
|
+
/** What one maintenance pass did — auditable evidence mirroring `NodeDurableHost`'s report. */
|
|
86
|
+
var MaintenancePassReport = class extends Schema.Class("@effect-agent/platform-cloudflare/MaintenancePassReport")({
|
|
87
|
+
/** `caught-up` ran no runtime work (publication may be pending); `actionable` ran recovery. */
|
|
88
|
+
phase: Schema.Literals(["caught-up", "actionable"]),
|
|
89
|
+
/** Recovery decisions executed (or deferred) BEFORE any new claim in this pass. */
|
|
90
|
+
recovered: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
91
|
+
/** Whether the head Attempt settled. Joined input may settle with that head. */
|
|
92
|
+
settled: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
93
|
+
/** Submissions still nonterminal after the pass (suspended/unknown lanes stay honest). */
|
|
94
|
+
nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
95
|
+
/** `rearmed` for dirty/autonomous work, `cleared` for stable waits or settlement. */
|
|
96
|
+
alarm: Schema.Literals(["rearmed", "cleared"])
|
|
97
|
+
}) {};
|
|
98
|
+
/** Test-only fault authority; production uses the inert layer. */
|
|
99
|
+
var ThreadMaintenanceFailpoint = class extends Context.Service()("@effect-agent/platform-cloudflare/ThreadMaintenanceFailpoint") {
|
|
100
|
+
static layer = Layer.succeed(this)({ hit: () => Effect.void });
|
|
101
|
+
};
|
|
102
|
+
/** Opt in with `ThreadObject.layer(registrations, { publication: Layer.effect(ThreadPublication)(...) })`. */
|
|
103
|
+
var ThreadPublication = class extends Context.Service()("@effect-agent/platform-cloudflare/ThreadPublication") {
|
|
104
|
+
static layer = Layer.succeed(this)({
|
|
105
|
+
invalidate: Effect.void,
|
|
106
|
+
prepareGeneration: () => Effect.void,
|
|
107
|
+
drain: Effect.void,
|
|
108
|
+
pendingDeadline: Effect.succeed(Option.none())
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Host-assembled message recovery, supplied by ThreadObject.layer even when the application
|
|
113
|
+
* rebuilds ThreadMaintenance. These obligations outlive source Runs and never defer a ready
|
|
114
|
+
* source Attempt while a destination is processing an accepted message.
|
|
115
|
+
*/
|
|
116
|
+
const ThreadMessageDelivery = Context.Reference("@effect-agent/platform-cloudflare/ThreadMessageDelivery", { defaultValue: () => ({
|
|
117
|
+
drain: Effect.void,
|
|
118
|
+
pendingDeadline: Effect.succeed(Option.none())
|
|
119
|
+
}) });
|
|
120
|
+
/**
|
|
121
|
+
* Application obligations sharing this Object's alarm. The deadline read is local and
|
|
122
|
+
* read-only. Drain beside the native Attempt and always finish one initial bounded wave,
|
|
123
|
+
* even when `finished` was already signalled. Then stop starting new waves on that signal
|
|
124
|
+
* and finish the bounded current wave before returning. Native maintenance joins that work
|
|
125
|
+
* before acknowledging a generation. Mutations use the same ThreadMutationGate; hooks never
|
|
126
|
+
* write the raw alarm slot. Pending host work does not defer a ready model Attempt.
|
|
127
|
+
*/
|
|
128
|
+
const ThreadHostMaintenance = Context.Reference("@effect-agent/platform-cloudflare/ThreadHostMaintenance", { defaultValue: () => ({
|
|
129
|
+
pendingDeadline: Effect.succeed(Option.none()),
|
|
130
|
+
drainUntil: () => Effect.void
|
|
131
|
+
}) });
|
|
132
|
+
const earliestDeadline = (left, right) => Option.isSome(left) ? Option.isSome(right) ? Option.some(Math.min(left.value, right.value)) : left : right;
|
|
133
|
+
/** @internal A committed source operation must not become a failed operation because delivery failed. */
|
|
134
|
+
const publishCommitted = Effect.gen(function* () {
|
|
135
|
+
const publication = yield* ThreadPublication;
|
|
136
|
+
yield* publication.invalidate.pipe(Effect.andThen(publication.drain));
|
|
137
|
+
}).pipe(Effect.catchCause((cause) => Cause.hasInterrupts(cause) ? Effect.interrupt : Effect.logError("Thread publication deferred after source commit", cause)));
|
|
138
|
+
const MaintenanceGeneration = Schema.BigIntFromString.check(Schema.isGreaterThanOrEqualToBigInt(0n));
|
|
139
|
+
/** Versioned, platform-private maintenance state stored through Durable Object KV. */
|
|
140
|
+
var ThreadMaintenanceState = class extends Schema.Class("@effect-agent/platform-cloudflare/ThreadMaintenanceState")({
|
|
141
|
+
schemaVersion: Schema.Literal(1),
|
|
142
|
+
dirty: MaintenanceGeneration,
|
|
143
|
+
processed: MaintenanceGeneration,
|
|
144
|
+
nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
145
|
+
/** One physical-owner cursor; old single-lane records need no conversion. */
|
|
146
|
+
lastServedThreadId: Schema.optionalKey(ThreadId)
|
|
147
|
+
}) {};
|
|
148
|
+
const MAINTENANCE_STATE_KEY = "effect-agent:thread-maintenance:v1";
|
|
149
|
+
const decodeMaintenanceState = Schema.decodeUnknownSync(ThreadMaintenanceState);
|
|
150
|
+
const encodeMaintenanceState = Schema.encodeSync(ThreadMaintenanceState);
|
|
151
|
+
const initialMaintenanceState = () => ThreadMaintenanceState.make({
|
|
152
|
+
schemaVersion: 1,
|
|
153
|
+
dirty: 1n,
|
|
154
|
+
processed: 0n,
|
|
155
|
+
nonterminal: 0
|
|
156
|
+
});
|
|
157
|
+
const readMaintenanceState = async (transaction) => {
|
|
158
|
+
const encoded = await transaction.get(MAINTENANCE_STATE_KEY);
|
|
159
|
+
return encoded === void 0 ? {
|
|
160
|
+
state: initialMaintenanceState(),
|
|
161
|
+
initialized: false
|
|
162
|
+
} : {
|
|
163
|
+
state: decodeMaintenanceState(encoded),
|
|
164
|
+
initialized: true
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
const ensureTransactionAlarmBy = async (transaction, deadline) => {
|
|
168
|
+
const scheduled = await transaction.getAlarm();
|
|
169
|
+
if (scheduled === null || scheduled > deadline) await transaction.setAlarm(deadline);
|
|
170
|
+
};
|
|
171
|
+
const stableExternalWait = (snapshot, reports) => {
|
|
172
|
+
const decision = reports.get(snapshot.submissionId)?.decision._tag;
|
|
173
|
+
if (decision === "SettleAborted") return false;
|
|
174
|
+
switch (snapshot.state) {
|
|
175
|
+
case "suspended":
|
|
176
|
+
case "joined": return true;
|
|
177
|
+
case "unknown": return decision === "AwaitUnknownResolution" || decision === "MarkUnknown";
|
|
178
|
+
case "admitted": return reports.get(snapshot.submissionId)?.decision._tag === "AwaitParentEstablishment";
|
|
179
|
+
case "input-applied":
|
|
180
|
+
case "joining":
|
|
181
|
+
case "ready":
|
|
182
|
+
case "running":
|
|
183
|
+
case "settled":
|
|
184
|
+
case "terminalizing": return false;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Shared prearm/acknowledgement boundary for ingress and runtime-owned producers.
|
|
189
|
+
* `ThreadObject.layer` provides this same instance in its Services. Rebuilt runtime/maintenance
|
|
190
|
+
* Layers must reuse that instance; a second gate cannot observe the native producers' activity.
|
|
191
|
+
*/
|
|
192
|
+
var ThreadMutationGate = class ThreadMutationGate extends Context.Service()("@effect-agent/platform-cloudflare/internal/ThreadMutationGate") {
|
|
193
|
+
static layer = Layer.effect(this)(Effect.gen(function* () {
|
|
194
|
+
const { ctx } = yield* DurableObjectContext;
|
|
195
|
+
const config = yield* CloudflareDurableRuntimeConfig;
|
|
196
|
+
const failpoint = yield* ThreadMaintenanceFailpoint;
|
|
197
|
+
const activeMutations = yield* Ref.make(0);
|
|
198
|
+
const generationGate = yield* Semaphore.make(1);
|
|
199
|
+
const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
|
|
200
|
+
const runTransaction = yield* makeStorageOperation;
|
|
201
|
+
const beginMutation = Effect.fn("ThreadMaintenance.beginMutation")(function* (invalidatesRecovery) {
|
|
202
|
+
yield* failpoint.hit("maintenance:dirty:before");
|
|
203
|
+
const now = yield* Clock.currentTimeMillis;
|
|
204
|
+
yield* runTransaction("advance maintenance generation", () => ctx.storage.transaction(async (transaction) => {
|
|
205
|
+
const { state, initialized } = await readMaintenanceState(transaction);
|
|
206
|
+
const next = ThreadMaintenanceState.make({
|
|
207
|
+
...state,
|
|
208
|
+
dirty: state.dirty + (invalidatesRecovery ? 1n : 0n)
|
|
209
|
+
});
|
|
210
|
+
if (invalidatesRecovery || !initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
|
|
211
|
+
await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
|
|
212
|
+
}));
|
|
213
|
+
yield* failpoint.hit("maintenance:dirty:after");
|
|
214
|
+
yield* Ref.update(activeMutations, (active) => active + 1);
|
|
215
|
+
});
|
|
216
|
+
const endMutation = generationGate.withPermit(Ref.update(activeMutations, (active) => Math.max(0, active - 1)));
|
|
217
|
+
const withMutation = (body, options) => Effect.acquireUseRelease(generationGate.withPermit(beginMutation(options?.invalidatesRecovery ?? true)), () => failpoint.hit("maintenance:mutation:armed").pipe(Effect.andThen(body), Effect.tap(() => failpoint.hit("maintenance:mutation:finished"))), () => endMutation);
|
|
218
|
+
return ThreadMutationGate.of({
|
|
219
|
+
withMutation,
|
|
220
|
+
withSnapshot: (body) => generationGate.withPermit(Effect.flatMap(Ref.get(activeMutations), body))
|
|
221
|
+
});
|
|
222
|
+
}));
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* Incremental, quiescent maintenance over a durable dirty/processed generation (issue #93).
|
|
226
|
+
*
|
|
227
|
+
* `pass` = generation snapshot/pre-arm → recovery → one head Attempt → generation acknowledgement:
|
|
228
|
+
*
|
|
229
|
+
* 1. One storage transaction reads dirty/processed and re-arms before work. A caught-up forced
|
|
230
|
+
* alarm takes an O(1) path without recovery, ledger scans, or canonical-history reads.
|
|
231
|
+
* 2. Recovery strictly precedes a new claim. One head Attempt advances the lane and requests
|
|
232
|
+
* a safe yield after ten minutes. The whole event has a fourteen-minute cooperative timeout.
|
|
233
|
+
* 3. The final transaction acknowledges only the generation observed at pass start. A racing
|
|
234
|
+
* mutation therefore remains `dirty > processed` and retains its atomically-established alarm.
|
|
235
|
+
* 4. Stable external waits acknowledge and clear. Autonomous retry, indeterminate, and lease
|
|
236
|
+
* recovery states leave their generation dirty and retain bounded backoff rearming.
|
|
237
|
+
*/
|
|
238
|
+
var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effect-agent/platform-cloudflare/ThreadMaintenance") {
|
|
239
|
+
static layer = Layer.effect(ThreadMaintenance)(Effect.gen(function* () {
|
|
240
|
+
const runtime = yield* DurableAgentRuntime;
|
|
241
|
+
const ledger = yield* SubmissionLedger;
|
|
242
|
+
const alarm = yield* DurableAlarmService;
|
|
243
|
+
const config = yield* CloudflareDurableRuntimeConfig;
|
|
244
|
+
const { ctx } = yield* DurableObjectContext;
|
|
245
|
+
const failpoint = yield* ThreadMaintenanceFailpoint;
|
|
246
|
+
/**
|
|
247
|
+
* Consecutive no-progress passes — an in-memory CACHE, not state: a fresh incarnation
|
|
248
|
+
* restarts at zero and merely re-arms sooner than a long-lived one would have.
|
|
249
|
+
*/
|
|
250
|
+
const stalls = yield* Ref.make(0);
|
|
251
|
+
const mutations = yield* ThreadMutationGate;
|
|
252
|
+
const publication = yield* ThreadPublication;
|
|
253
|
+
const projection = yield* ThreadProjectionMaintenance;
|
|
254
|
+
const messages = yield* ThreadMessageDelivery;
|
|
255
|
+
const host = yield* ThreadHostMaintenance;
|
|
256
|
+
const projectionDeadline = projection.pendingDeadline.pipe(Effect.catchCauseIf((cause) => !Cause.hasInterrupts(cause), (cause) => Effect.logError("Thread projection deadline unavailable", cause).pipe(Effect.as(Option.some(0)))));
|
|
257
|
+
const pendingDeadline = Effect.gen(function* () {
|
|
258
|
+
return earliestDeadline(earliestDeadline(yield* publication.pendingDeadline, yield* messages.pendingDeadline), earliestDeadline(yield* projectionDeadline, yield* host.pendingDeadline));
|
|
259
|
+
});
|
|
260
|
+
const maintenancePassGate = yield* Semaphore.make(1);
|
|
261
|
+
const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
|
|
262
|
+
const runTransaction = yield* makeStorageOperation;
|
|
263
|
+
const ensureAlarm = Effect.fn("ThreadMaintenance.ensureAlarm")(function* () {
|
|
264
|
+
yield* failpoint.hit("maintenance:ensure:before");
|
|
265
|
+
const now = yield* Clock.currentTimeMillis;
|
|
266
|
+
yield* runTransaction("ensure maintenance alarm", () => ctx.storage.transaction(async (transaction) => {
|
|
267
|
+
const { state, initialized } = await readMaintenanceState(transaction);
|
|
268
|
+
if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
|
|
269
|
+
if (state.dirty > state.processed) await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
|
|
270
|
+
}));
|
|
271
|
+
const deadline = yield* pendingDeadline;
|
|
272
|
+
if (Option.isSome(deadline)) yield* runTransaction("ensure publication alarm", () => ctx.storage.transaction((transaction) => ensureTransactionAlarmBy(transaction, Math.max(now + minimumAlarmDelay, deadline.value))));
|
|
273
|
+
yield* failpoint.hit("maintenance:ensure:after");
|
|
274
|
+
});
|
|
275
|
+
const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* () {
|
|
276
|
+
yield* failpoint.hit("maintenance:begin:before");
|
|
277
|
+
const now = yield* Clock.currentTimeMillis;
|
|
278
|
+
const result = yield* runTransaction("begin maintenance pass", () => ctx.storage.transaction(async (transaction) => {
|
|
279
|
+
const { state, initialized } = await readMaintenanceState(transaction);
|
|
280
|
+
if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
|
|
281
|
+
if (state.processed >= state.dirty) {
|
|
282
|
+
await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
|
|
283
|
+
return {
|
|
284
|
+
_tag: "CaughtUp",
|
|
285
|
+
nonterminal: state.nonterminal
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
|
|
289
|
+
return {
|
|
290
|
+
_tag: "Actionable",
|
|
291
|
+
generation: state.dirty,
|
|
292
|
+
nonterminal: state.nonterminal
|
|
293
|
+
};
|
|
294
|
+
}));
|
|
295
|
+
yield* failpoint.hit("maintenance:begin:after");
|
|
296
|
+
return result;
|
|
297
|
+
});
|
|
298
|
+
const rearmDelay = Effect.fn("ThreadMaintenance.rearmDelay")(function* (progressed) {
|
|
299
|
+
const priorStalls = yield* Ref.getAndUpdate(stalls, (count) => progressed ? 0 : count + 1);
|
|
300
|
+
if (progressed) return config.alarmBackoffBase;
|
|
301
|
+
const exponent = Math.min(priorStalls, 30);
|
|
302
|
+
const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** exponent);
|
|
303
|
+
const jitter = yield* Random.next;
|
|
304
|
+
const jittered = Math.ceil(backoff / 2 + backoff / 2 * jitter);
|
|
305
|
+
return Math.min(jittered, config.wakeScanInterval);
|
|
306
|
+
});
|
|
307
|
+
const pass = Effect.fn("ThreadMaintenance.pass")(function* (yieldAfter) {
|
|
308
|
+
const annotate = (report) => Effect.annotateCurrentSpan({
|
|
309
|
+
phase: report.phase,
|
|
310
|
+
recovered: report.recovered,
|
|
311
|
+
settled: report.settled,
|
|
312
|
+
nonterminal: report.nonterminal,
|
|
313
|
+
alarm: report.alarm
|
|
314
|
+
}).pipe(Effect.as(report));
|
|
315
|
+
const started = yield* mutations.withSnapshot((activeAtStart) => Effect.gen(function* () {
|
|
316
|
+
const generation = yield* beginPass();
|
|
317
|
+
if (generation._tag === "Actionable" && activeAtStart === 0) yield* publication.prepareGeneration(generation.generation);
|
|
318
|
+
return {
|
|
319
|
+
...generation,
|
|
320
|
+
activeAtStart
|
|
321
|
+
};
|
|
322
|
+
}));
|
|
323
|
+
const deliveryFinished = yield* Deferred.make();
|
|
324
|
+
const delivery = yield* Effect.forkChild(messages.drainUntil?.(deliveryFinished) ?? messages.drain);
|
|
325
|
+
const hostWork = yield* Effect.forkChild(host.drainUntil(deliveryFinished));
|
|
326
|
+
const finishDelivery = Deferred.succeed(deliveryFinished, void 0).pipe(Effect.andThen(Fiber.awaitAll([delivery, hostWork])), Effect.flatMap((outcomes) => Effect.forEach(outcomes, (outcome) => outcome, { discard: true })));
|
|
327
|
+
const projected = yield* Effect.exit(drainDue.pipe(Effect.provideService(ThreadProjectionMaintenance, projection)));
|
|
328
|
+
if (Exit.isFailure(projected) && Cause.hasInterrupts(projected.cause)) return yield* Effect.failCause(projected.cause);
|
|
329
|
+
const deadline = yield* publication.pendingDeadline;
|
|
330
|
+
if (started._tag === "Actionable" || Option.isSome(deadline) && deadline.value <= (yield* Clock.currentTimeMillis)) yield* publication.drain;
|
|
331
|
+
const pending = yield* publication.pendingDeadline;
|
|
332
|
+
if (started._tag === "CaughtUp" || Option.isSome(pending)) {
|
|
333
|
+
yield* finishDelivery;
|
|
334
|
+
if (Exit.isFailure(projected)) return yield* Effect.failCause(projected.cause);
|
|
335
|
+
yield* failpoint.hit("maintenance:finish:before");
|
|
336
|
+
const disposition = yield* mutations.withSnapshot((active) => Effect.gen(function* () {
|
|
337
|
+
const latest = yield* pendingDeadline;
|
|
338
|
+
const now = yield* Clock.currentTimeMillis;
|
|
339
|
+
return yield* runTransaction("finish publication pass", () => ctx.storage.transaction(async (transaction) => {
|
|
340
|
+
const { state } = await readMaintenanceState(transaction);
|
|
341
|
+
const nativeDeadline = active > 0 || state.dirty > state.processed ? now + config.wakeScanInterval : Infinity;
|
|
342
|
+
const next = Option.isSome(latest) ? Math.min(nativeDeadline, latest.value) : nativeDeadline;
|
|
343
|
+
if (Number.isFinite(next)) {
|
|
344
|
+
await transaction.setAlarm(Math.max(now + minimumAlarmDelay, next));
|
|
345
|
+
return "rearmed";
|
|
346
|
+
}
|
|
347
|
+
await transaction.deleteAlarm();
|
|
348
|
+
return "cleared";
|
|
349
|
+
}));
|
|
350
|
+
}));
|
|
351
|
+
yield* failpoint.hit("maintenance:finish:after");
|
|
352
|
+
return yield* annotate(MaintenancePassReport.make({
|
|
353
|
+
phase: "caught-up",
|
|
354
|
+
recovered: 0,
|
|
355
|
+
settled: 0,
|
|
356
|
+
nonterminal: started.nonterminal,
|
|
357
|
+
alarm: disposition
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
const recovered = yield* runtime.runRecovery;
|
|
361
|
+
const reports = new Map(recovered.map((report) => [report.submissionId, report]));
|
|
362
|
+
const current = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
363
|
+
const heads = /* @__PURE__ */ new Map();
|
|
364
|
+
for (const row of current) if (!heads.has(row.threadId)) heads.set(row.threadId, row);
|
|
365
|
+
const eligible = [...heads.values()].filter((head) => !stableExternalWait(head, reports)).map((head) => head.threadId).sort();
|
|
366
|
+
let selected = eligible[0];
|
|
367
|
+
if (heads.size > 1 && selected !== void 0) {
|
|
368
|
+
yield* failpoint.hit("maintenance:select:before");
|
|
369
|
+
selected = yield* runTransaction("select maintenance lane", () => ctx.storage.transaction(async (transaction) => {
|
|
370
|
+
const { state } = await readMaintenanceState(transaction);
|
|
371
|
+
const next = eligible.find((threadId) => state.lastServedThreadId === void 0 || threadId > state.lastServedThreadId) ?? eligible[0];
|
|
372
|
+
if (next !== void 0) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
|
|
373
|
+
...state,
|
|
374
|
+
lastServedThreadId: next
|
|
375
|
+
})));
|
|
376
|
+
return next;
|
|
377
|
+
}));
|
|
378
|
+
yield* failpoint.hit("maintenance:select:after");
|
|
379
|
+
}
|
|
380
|
+
const settlement = selected === void 0 ? Option.none() : yield* runtime.processThreadHead(selected, { yieldAfter });
|
|
381
|
+
yield* finishDelivery;
|
|
382
|
+
if (Exit.isFailure(projected)) return yield* Effect.failCause(projected.cause);
|
|
383
|
+
const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
384
|
+
const waitingHeads = /* @__PURE__ */ new Map();
|
|
385
|
+
const autonomous = remaining.some((snapshot) => {
|
|
386
|
+
const headWaiting = waitingHeads.get(snapshot.threadId);
|
|
387
|
+
if (headWaiting === void 0) waitingHeads.set(snapshot.threadId, stableExternalWait(snapshot, reports));
|
|
388
|
+
if (headWaiting === true && snapshot.state === "ready" && reports.get(snapshot.submissionId)?.decision._tag === "ApplyInput") return false;
|
|
389
|
+
return !stableExternalWait(snapshot, reports);
|
|
390
|
+
});
|
|
391
|
+
const progressed = Option.isSome(settlement) || recovered.some((report) => report.disposition === "repaired");
|
|
392
|
+
const delay = autonomous ? yield* rearmDelay(progressed) : 0;
|
|
393
|
+
const now = yield* Clock.currentTimeMillis;
|
|
394
|
+
yield* failpoint.hit("maintenance:finish:before");
|
|
395
|
+
const alarmDisposition = yield* mutations.withSnapshot((active) => Effect.gen(function* () {
|
|
396
|
+
const publicationDeadline = yield* pendingDeadline;
|
|
397
|
+
return yield* runTransaction("finish maintenance pass", () => ctx.storage.transaction(async (transaction) => {
|
|
398
|
+
const { state } = await readMaintenanceState(transaction);
|
|
399
|
+
const processed = autonomous || started.activeAtStart > 0 || active > 0 ? state.processed : state.processed > started.generation ? state.processed : started.generation;
|
|
400
|
+
const next = ThreadMaintenanceState.make({
|
|
401
|
+
...state,
|
|
402
|
+
processed,
|
|
403
|
+
nonterminal: remaining.length
|
|
404
|
+
});
|
|
405
|
+
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
|
|
406
|
+
if (autonomous) {
|
|
407
|
+
await transaction.setAlarm(Option.isSome(publicationDeadline) ? Math.max(now + minimumAlarmDelay, Math.min(now + delay, publicationDeadline.value)) : now + delay);
|
|
408
|
+
return "rearmed";
|
|
409
|
+
}
|
|
410
|
+
if (started.activeAtStart > 0 || active > 0 || next.dirty > next.processed) {
|
|
411
|
+
await ensureTransactionAlarmBy(transaction, Option.isSome(publicationDeadline) ? Math.max(now + minimumAlarmDelay, Math.min(now + config.wakeScanInterval, publicationDeadline.value)) : now + config.wakeScanInterval);
|
|
412
|
+
return "rearmed";
|
|
413
|
+
}
|
|
414
|
+
if (Option.isSome(publicationDeadline)) {
|
|
415
|
+
await transaction.setAlarm(Math.max(now + minimumAlarmDelay, publicationDeadline.value));
|
|
416
|
+
return "rearmed";
|
|
417
|
+
}
|
|
418
|
+
await transaction.deleteAlarm();
|
|
419
|
+
return "cleared";
|
|
420
|
+
}));
|
|
421
|
+
}));
|
|
422
|
+
yield* failpoint.hit("maintenance:finish:after");
|
|
423
|
+
if (alarmDisposition === "cleared") yield* Ref.set(stalls, 0);
|
|
424
|
+
return yield* annotate(MaintenancePassReport.make({
|
|
425
|
+
phase: "actionable",
|
|
426
|
+
recovered: recovered.length,
|
|
427
|
+
settled: Option.isSome(settlement) ? 1 : 0,
|
|
428
|
+
nonterminal: remaining.length,
|
|
429
|
+
alarm: alarmDisposition
|
|
430
|
+
}));
|
|
431
|
+
});
|
|
432
|
+
return ThreadMaintenance.of({
|
|
433
|
+
pass: Effect.gen(function* () {
|
|
434
|
+
const yieldAfter = DateTime.makeUnsafe((yield* Clock.currentTimeMillis) + 6e5);
|
|
435
|
+
return yield* alarm.withWakesDeferred(maintenancePassGate.withPermit(pass(yieldAfter)));
|
|
436
|
+
}).pipe(Effect.timeoutOrElse({
|
|
437
|
+
duration: "14 minutes",
|
|
438
|
+
orElse: () => DurableAlarmError.make({
|
|
439
|
+
operation: "maintenance pass deadline",
|
|
440
|
+
message: "The maintenance event exceeded its 14 minute deadline; durable recovery remains pending"
|
|
441
|
+
})
|
|
442
|
+
})),
|
|
443
|
+
ensureAlarm: mutations.withSnapshot(() => ensureAlarm()),
|
|
444
|
+
withMutation: (body) => mutations.withMutation(body.pipe(Effect.tap(() => publishCommitted.pipe(Effect.provideService(ThreadPublication, publication)))))
|
|
445
|
+
});
|
|
446
|
+
}));
|
|
447
|
+
};
|
|
448
|
+
//#endregion
|
|
449
|
+
export { DurableAlarmError, DurableAlarmService, MaintenancePassReport, ThreadHostMaintenance, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, publishCommitted, Alarm_exports as t };
|
|
450
|
+
|
|
451
|
+
//# sourceMappingURL=Alarm.mjs.map
|