@effect-agent/platform-cloudflare 0.1.0-beta.8 → 0.1.0-beta.81
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 +184 -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 +112 -0
- package/dist/CloudflareBindings.mjs +101 -0
- package/dist/CloudflareBindings.mjs.map +1 -0
- package/dist/CloudflareBrowser-D4YqaTAl.mjs +495 -0
- package/dist/CloudflareBrowser-D4YqaTAl.mjs.map +1 -0
- package/dist/CloudflareBrowser-DJq53niJ.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 +195 -0
- package/dist/CloudflareMemory.mjs.map +1 -0
- package/dist/CloudflareScheduling.d.mts +52 -0
- package/dist/CloudflareScheduling.mjs +389 -0
- package/dist/CloudflareScheduling.mjs.map +1 -0
- package/dist/CloudflareSubscriptions.d.mts +87 -0
- package/dist/CloudflareSubscriptions.mjs +524 -0
- package/dist/CloudflareSubscriptions.mjs.map +1 -0
- package/dist/CloudflareThreadClient.d.mts +1286 -0
- package/dist/CloudflareThreadClient.mjs +386 -0
- package/dist/CloudflareThreadClient.mjs.map +1 -0
- package/dist/InteractiveBrowser.d.mts +144 -0
- package/dist/InteractiveBrowser.mjs +1090 -0
- package/dist/InteractiveBrowser.mjs.map +1 -0
- package/dist/ProtectedBrowser.d.mts +65 -0
- package/dist/ProtectedBrowser.mjs +859 -0
- package/dist/ProtectedBrowser.mjs.map +1 -0
- package/dist/ThreadObject-BpwkEc5E.mjs +836 -0
- package/dist/ThreadObject-BpwkEc5E.mjs.map +1 -0
- package/dist/ThreadObject-NbFOWK1I.d.mts +816 -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-CUbVfEgP.mjs +84 -0
- package/dist/browser-session-lifecycle-CUbVfEgP.mjs.map +1 -0
- package/dist/browser-session-lifecycle-DJpqLXy_.d.mts +21 -0
- package/dist/index.d.mts +13 -1547
- package/dist/index.mjs +13 -1636
- package/dist/prepared-admission-DSi55W-7.mjs +73 -0
- package/dist/prepared-admission-DSi55W-7.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -53
- package/src/Alarm.ts +967 -0
- package/src/BrowserRestCapture.ts +477 -0
- package/src/BrowserRestCrawl.ts +540 -0
- package/src/CloudflareAiGateway.ts +170 -0
- package/src/CloudflareBindings.ts +180 -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 +406 -0
- package/src/CloudflareScheduling.ts +733 -0
- package/src/CloudflareSubscriptions.ts +1045 -0
- package/src/CloudflareThreadClient.ts +807 -0
- package/src/InteractiveBrowser.ts +2357 -0
- package/src/ProtectedBrowser.ts +2 -0
- package/src/ThreadObject.ts +1179 -0
- package/src/{wake-scheduler.ts → WakeScheduler.ts} +23 -23
- 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 +148 -0
- package/src/internal/prepared-admission.ts +116 -0
- package/src/internal/progress-wait.ts +121 -0
- package/src/internal/transport.ts +42 -0
- package/src/protected-browser/binding.ts +187 -0
- package/src/protected-browser/inspect-frame.ts +125 -0
- package/src/protected-browser/native.ts +429 -0
- package/src/protected-browser/policy.ts +719 -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
|
@@ -0,0 +1,1179 @@
|
|
|
1
|
+
import { type AgentId, type ThreadId } from "@effect-agent/core/Identifiers";
|
|
2
|
+
import { SubmissionId } from "@effect-agent/core/Identifiers";
|
|
3
|
+
import {
|
|
4
|
+
decodePortRequest,
|
|
5
|
+
encodePortResponse,
|
|
6
|
+
LedgerLookupResult,
|
|
7
|
+
PortFailed,
|
|
8
|
+
PortProtocolError,
|
|
9
|
+
PortSucceeded,
|
|
10
|
+
type PortRequest,
|
|
11
|
+
type PortResponse,
|
|
12
|
+
} from "@effect-agent/storage-cloudflare/PortProtocol";
|
|
13
|
+
import {
|
|
14
|
+
IntegrityReport,
|
|
15
|
+
ObligationReport,
|
|
16
|
+
ObligationThresholds,
|
|
17
|
+
RecoveryExplanation,
|
|
18
|
+
RetryCommand,
|
|
19
|
+
RetryRefused,
|
|
20
|
+
} from "@effect-agent/thread/Admin";
|
|
21
|
+
import { DigestError } from "@effect-agent/thread/Digest";
|
|
22
|
+
import {
|
|
23
|
+
DurableAgentRuntime,
|
|
24
|
+
DurableRuntimeConfig,
|
|
25
|
+
RecoveryReport,
|
|
26
|
+
type DurableSubmitAgent,
|
|
27
|
+
} from "@effect-agent/thread/DurableAgentRuntime";
|
|
28
|
+
import { DurableRuntimeFailpointError } from "@effect-agent/thread/DurableFailpoint";
|
|
29
|
+
import {
|
|
30
|
+
OperationAuthorizationRequest,
|
|
31
|
+
OperationAuthorizer,
|
|
32
|
+
OperationDenied,
|
|
33
|
+
} from "@effect-agent/thread/OperationAuthorizer";
|
|
34
|
+
import { PersistedJson } from "@effect-agent/thread/Records";
|
|
35
|
+
import { RunJournalError } from "@effect-agent/thread/RunJournal";
|
|
36
|
+
import {
|
|
37
|
+
AdmissionPolicyError,
|
|
38
|
+
LedgerError,
|
|
39
|
+
OwnershipLost,
|
|
40
|
+
SettlementConflict,
|
|
41
|
+
SubmissionLedger,
|
|
42
|
+
SubmissionLookupByKey,
|
|
43
|
+
} from "@effect-agent/thread/SubmissionLedger";
|
|
44
|
+
import {
|
|
45
|
+
AppendConflict,
|
|
46
|
+
ThreadNotMaterialized,
|
|
47
|
+
ThreadRead,
|
|
48
|
+
ThreadStore,
|
|
49
|
+
ThreadStoreError,
|
|
50
|
+
FenceRejected,
|
|
51
|
+
} from "@effect-agent/thread/ThreadStore";
|
|
52
|
+
import { WakeScheduler } from "@effect-agent/thread/WakeScheduler";
|
|
53
|
+
import { Effect, Layer, Option, Schema, Stream } from "effect";
|
|
54
|
+
import {
|
|
55
|
+
DurableObject as EffectCfDurableObject,
|
|
56
|
+
DurableObjectState as EffectCfDurableObjectState,
|
|
57
|
+
WorkerEnvironment,
|
|
58
|
+
} from "effect-cf";
|
|
59
|
+
|
|
60
|
+
import {
|
|
61
|
+
ThreadMaintenance,
|
|
62
|
+
DurableAlarmError,
|
|
63
|
+
DurableAlarmService,
|
|
64
|
+
ThreadMutationGate,
|
|
65
|
+
publishCommitted,
|
|
66
|
+
type MaintenancePassFailure,
|
|
67
|
+
} from "./Alarm.ts";
|
|
68
|
+
import {
|
|
69
|
+
ThreadObjectIdentity,
|
|
70
|
+
ThreadObjectPlacement,
|
|
71
|
+
DurableObjectContext,
|
|
72
|
+
ThreadObjectNamespace,
|
|
73
|
+
threadNamespaceFromEnv,
|
|
74
|
+
type CloudflareBindingError,
|
|
75
|
+
} from "./CloudflareBindings.ts";
|
|
76
|
+
import { AdmissionLimitExceeded, CloudflareDurableRuntimeConfig } from "./CloudflareConfig.ts";
|
|
77
|
+
import {
|
|
78
|
+
AbortRecorded,
|
|
79
|
+
ApprovalRecorded,
|
|
80
|
+
HostFailed,
|
|
81
|
+
HostProtocolError,
|
|
82
|
+
ObservedPage,
|
|
83
|
+
ProgressObserved,
|
|
84
|
+
ProgressCancelled,
|
|
85
|
+
SettlementReached,
|
|
86
|
+
SubmissionStatusResponse,
|
|
87
|
+
SubmitSucceeded,
|
|
88
|
+
UnknownResolutionRecorded,
|
|
89
|
+
boundHostDiagnostic,
|
|
90
|
+
decodeAbortCommand,
|
|
91
|
+
decodeAwaitProgressRequest,
|
|
92
|
+
decodeCancelProgressRequest,
|
|
93
|
+
decodeApprovalDecisionCommand,
|
|
94
|
+
decodeObservePageRequest,
|
|
95
|
+
decodeReceipt,
|
|
96
|
+
decodeSubmitRequest,
|
|
97
|
+
decodeUnknownResolutionCommand,
|
|
98
|
+
encodeHostResponse,
|
|
99
|
+
type HostFailure,
|
|
100
|
+
type HostResponse,
|
|
101
|
+
type SubmitRequest,
|
|
102
|
+
} from "./CloudflareThreadClient.ts";
|
|
103
|
+
import {
|
|
104
|
+
layerConfig,
|
|
105
|
+
ThreadObjectPorts,
|
|
106
|
+
type CloudflareDurableRuntimeInitializationError,
|
|
107
|
+
type CloudflareDurableRuntimeOptions,
|
|
108
|
+
type CloudflareDurableRuntimeServices,
|
|
109
|
+
type CloudflareBootstrapServices,
|
|
110
|
+
} from "./internal/layers.ts";
|
|
111
|
+
import { ProgressWaitRegistry } from "./internal/progress-wait.ts";
|
|
112
|
+
|
|
113
|
+
export {
|
|
114
|
+
layer,
|
|
115
|
+
layerConfig,
|
|
116
|
+
layerHostConfig,
|
|
117
|
+
layerInHost,
|
|
118
|
+
ThreadObjectPorts,
|
|
119
|
+
type ThreadPublicationOptions as PublicationOptions,
|
|
120
|
+
type CloudflareDurableRuntimeOptions as RuntimeOptions,
|
|
121
|
+
type CloudflareDurableRuntimeServices as Services,
|
|
122
|
+
type CloudflareDurableRuntimeInitializationError as InitializationError,
|
|
123
|
+
type CloudflareBootstrapServices as BootstrapServices,
|
|
124
|
+
} from "./internal/layers.ts";
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* `ThreadObject.make(application, options)` — the Thread Durable Object
|
|
128
|
+
* (plan §1.4,
|
|
129
|
+
* D-P6-1): a factory returning a class that applications export from their Worker entry.
|
|
130
|
+
* One SQLite-backed Object per Thread is the serialized owner (durability §6); the
|
|
131
|
+
* Object never runs `runResolvedWorker`'s infinite loop — each ingress event or alarm runs
|
|
132
|
+
* ONE bounded `runRecovery` + `processThreadHead` pass, and the persisted alarm
|
|
133
|
+
* (the single multiplexed slot, D-P6-2) finishes accepted work across evictions WITHOUT any
|
|
134
|
+
* incoming request.
|
|
135
|
+
* `Services` exposes the same owner `SqlClient` used by the Thread stores. Compose optional
|
|
136
|
+
* local repositories after `ThreadObject.layer`; never acquire another independently locked
|
|
137
|
+
* SQL client for the same Object. Exposing the client installs no additional storage schemas.
|
|
138
|
+
*
|
|
139
|
+
* Constructor gate (`blockConcurrencyWhile`) is LOCAL-ONLY: schema migration and the
|
|
140
|
+
* exact-version check, configuration decode, and the defensive ensure-alarm half of the
|
|
141
|
+
* alarm invariant. It deliberately does NOT run the recovery pass: parent recovery can
|
|
142
|
+
* require child-Object reads and vice versa, and two Objects blocked in constructor gates
|
|
143
|
+
* awaiting each other's RPC would deadlock (plan §1.4). Instead every pass runs
|
|
144
|
+
* `runRecovery` BEFORE any claim, so reconciliation still strictly precedes new work.
|
|
145
|
+
*/
|
|
146
|
+
|
|
147
|
+
/** Construction options for one deployed Thread Object class. */
|
|
148
|
+
export interface Options<
|
|
149
|
+
ApplicationServices = never,
|
|
150
|
+
EventServices = never,
|
|
151
|
+
EventLayerError = never,
|
|
152
|
+
> extends CloudflareDurableRuntimeOptions {
|
|
153
|
+
/** Accept transient native RPC tracing through effect-cf; disabled by default. */
|
|
154
|
+
readonly rpcTracing?: boolean;
|
|
155
|
+
/**
|
|
156
|
+
* Name of the Worker `env` binding carrying THIS class's `DurableObjectNamespace` — the
|
|
157
|
+
* Object's route back to sibling Thread Objects for the WP2 cross-Object port calls
|
|
158
|
+
* and remote wakes (DEPLOY-010: the binding enters through a Layer, never ambiently).
|
|
159
|
+
*/
|
|
160
|
+
readonly namespaceBinding: string;
|
|
161
|
+
/** Acquired and finalized per native event, with access to the complete application runtime. */
|
|
162
|
+
readonly eventLayer?: Layer.Layer<
|
|
163
|
+
EventServices,
|
|
164
|
+
EventLayerError,
|
|
165
|
+
| RuntimeServices
|
|
166
|
+
| ApplicationServices
|
|
167
|
+
| EffectCfDurableObjectState.DurableObjectState
|
|
168
|
+
| WorkerEnvironment
|
|
169
|
+
>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
type EndpointServices =
|
|
173
|
+
| CloudflareDurableRuntimeServices
|
|
174
|
+
| CloudflareBootstrapServices
|
|
175
|
+
| DurableObjectContext;
|
|
176
|
+
type RuntimeServices = EndpointServices | ThreadObjectNamespace;
|
|
177
|
+
type ThreadObjectInitializationError =
|
|
178
|
+
| CloudflareDurableRuntimeInitializationError
|
|
179
|
+
| CloudflareBindingError
|
|
180
|
+
| MaintenancePassFailure;
|
|
181
|
+
|
|
182
|
+
/** Classify only a decoded port request so new protocol members cannot bypass pre-arming. */
|
|
183
|
+
const isMutatingPortRequest = (request: PortRequest): boolean => {
|
|
184
|
+
switch (request._tag) {
|
|
185
|
+
case "LedgerAdmit":
|
|
186
|
+
case "LedgerMarkReady":
|
|
187
|
+
case "LedgerRequestAbort":
|
|
188
|
+
case "LedgerRecordChildSettled":
|
|
189
|
+
case "StoreMaterialize":
|
|
190
|
+
case "StoreAppend":
|
|
191
|
+
return true;
|
|
192
|
+
case "LedgerLookup":
|
|
193
|
+
case "LedgerResolveAdmission":
|
|
194
|
+
case "StoreReadPage":
|
|
195
|
+
case "StoreInspectTail":
|
|
196
|
+
case "StoreExport":
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
request satisfies never;
|
|
200
|
+
|
|
201
|
+
return false;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/** The literal encoded `PortFailed(PortProtocolError)` fallback (same shape as WP2's). */
|
|
205
|
+
const encodedPortProtocolFailure = (message: string): unknown => ({
|
|
206
|
+
_tag: "PortFailed",
|
|
207
|
+
failure: { _tag: "PortProtocolError", message: boundHostDiagnostic(message) },
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const protocolFailure = (context: string) => (error: { readonly message: string }) =>
|
|
211
|
+
HostProtocolError.make({
|
|
212
|
+
message: boundHostDiagnostic(`${context}: ${error.message}`),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
/** Fold one endpoint's typed failures into the uniform `HostResponse` envelope. */
|
|
216
|
+
const respond = <Result extends HostResponse, Failure extends HostFailure>(
|
|
217
|
+
effect: Effect.Effect<Result, Failure, EndpointServices>,
|
|
218
|
+
): Effect.Effect<HostResponse, never, EndpointServices> =>
|
|
219
|
+
effect.pipe(
|
|
220
|
+
Effect.map((result): HostResponse => result),
|
|
221
|
+
Effect.catch((failure) => Effect.succeed<HostResponse>(HostFailed.make({ failure }))),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
/** Encode the response envelope; an unencodable response degrades to a protocol failure. */
|
|
225
|
+
const encodeResponse = (response: HostResponse): Effect.Effect<unknown> =>
|
|
226
|
+
encodeHostResponse(response).pipe(
|
|
227
|
+
Effect.catch((error) =>
|
|
228
|
+
Effect.succeed<unknown>({
|
|
229
|
+
_tag: "HostFailed",
|
|
230
|
+
failure: {
|
|
231
|
+
_tag: "HostProtocolError",
|
|
232
|
+
message: boundHostDiagnostic(`The host response could not be encoded: ${error.message}`),
|
|
233
|
+
},
|
|
234
|
+
}),
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const utf8Bytes = (value: PersistedJson): number =>
|
|
239
|
+
new TextEncoder().encode(JSON.stringify(value)).length;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The admission-limits gate, BEFORE `runtime.submit` touches the ledger (exit gate
|
|
243
|
+
* "resource limits are checked before admission"; DEPLOY-007). A replayed idempotency key is
|
|
244
|
+
* exempt: its accepted-work obligation already exists, and returning the original Receipt
|
|
245
|
+
* consumes no new quota. Refusals are typed `AdmissionLimitExceeded` and nothing is written.
|
|
246
|
+
*/
|
|
247
|
+
const gateAdmissionLimits = Effect.fn("ThreadObject.gateAdmissionLimits")(function* (
|
|
248
|
+
threadId: ThreadId,
|
|
249
|
+
request: {
|
|
250
|
+
readonly principal: SubmissionLookupByKey["principal"];
|
|
251
|
+
readonly idempotencyKey: SubmissionLookupByKey["idempotencyKey"];
|
|
252
|
+
readonly inputPayload: PersistedJson;
|
|
253
|
+
},
|
|
254
|
+
) {
|
|
255
|
+
const config = yield* CloudflareDurableRuntimeConfig;
|
|
256
|
+
const ledger = yield* SubmissionLedger;
|
|
257
|
+
const { ctx } = yield* DurableObjectContext;
|
|
258
|
+
|
|
259
|
+
const existing = yield* ledger.lookup(
|
|
260
|
+
SubmissionLookupByKey.make({
|
|
261
|
+
threadId,
|
|
262
|
+
principal: request.principal,
|
|
263
|
+
idempotencyKey: request.idempotencyKey,
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
if (Option.isSome(existing)) return;
|
|
268
|
+
|
|
269
|
+
const inputBytes = utf8Bytes(request.inputPayload);
|
|
270
|
+
|
|
271
|
+
if (inputBytes > config.limits.maxInputBytes) {
|
|
272
|
+
return yield* AdmissionLimitExceeded.make({
|
|
273
|
+
limit: "input-bytes",
|
|
274
|
+
actual: inputBytes,
|
|
275
|
+
maximum: config.limits.maxInputBytes,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const nonterminal = yield* ledger.scanNonterminal.pipe(
|
|
280
|
+
Stream.filter((submission) => submission.threadId === threadId),
|
|
281
|
+
Stream.runCollect,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
if (nonterminal.length >= config.limits.maxQueueDepthPerLane) {
|
|
285
|
+
return yield* AdmissionLimitExceeded.make({
|
|
286
|
+
limit: "queue-depth",
|
|
287
|
+
actual: nonterminal.length,
|
|
288
|
+
maximum: config.limits.maxQueueDepthPerLane,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const databaseBytes = yield* Effect.sync(() => ctx.storage.sql.databaseSize);
|
|
293
|
+
|
|
294
|
+
if (databaseBytes > config.limits.maxDatabaseBytes) {
|
|
295
|
+
return yield* AdmissionLimitExceeded.make({
|
|
296
|
+
limit: "database-bytes",
|
|
297
|
+
actual: databaseBytes,
|
|
298
|
+
maximum: config.limits.maxDatabaseBytes,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The submit-capable projection of an Agent Binding on the OBJECT side: the input arrived
|
|
305
|
+
* already encoded through the real input schema on the Worker side (`client.ts`), so the
|
|
306
|
+
* Object admits the canonical `PersistedJson` payload as-is; the resolved Binding re-derives
|
|
307
|
+
* everything else from the stored `(agentId, agentDigests)` at claim time (SUB-023).
|
|
308
|
+
*/
|
|
309
|
+
const passthroughSubmitAgent = (agentId: AgentId): DurableSubmitAgent<typeof PersistedJson> => ({
|
|
310
|
+
definition: {
|
|
311
|
+
id: agentId,
|
|
312
|
+
input: PersistedJson,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
/** A physical owner may hold other Threads; an addressed request cannot act on their IDs. */
|
|
317
|
+
const lookupAddressedSubmission = Effect.fn("ThreadObject.lookupAddressedSubmission")(function* (
|
|
318
|
+
submissionId: SubmissionId,
|
|
319
|
+
) {
|
|
320
|
+
const { threadId } = yield* ThreadObjectIdentity;
|
|
321
|
+
const ports = yield* ThreadObjectPorts;
|
|
322
|
+
const submission = yield* ports.lookupSubmission(submissionId);
|
|
323
|
+
|
|
324
|
+
if (Option.isSome(submission) && submission.value.threadId !== threadId)
|
|
325
|
+
return yield* HostProtocolError.make({ message: "The Submission belongs to another Thread" });
|
|
326
|
+
|
|
327
|
+
return submission;
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const requireSubmissionThread = Effect.fn("ThreadObject.requireSubmissionThread")(function* (
|
|
331
|
+
submissionId: SubmissionId,
|
|
332
|
+
) {
|
|
333
|
+
const submission = yield* lookupAddressedSubmission(submissionId);
|
|
334
|
+
|
|
335
|
+
if (Option.isNone(submission))
|
|
336
|
+
return yield* LedgerError.make({
|
|
337
|
+
operation: "addressed Submission lookup",
|
|
338
|
+
message: "The addressed Thread has no such Submission",
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const requireReceiptThread = Effect.fn("ThreadObject.requireReceiptThread")(function* (
|
|
343
|
+
threadId: ThreadId,
|
|
344
|
+
) {
|
|
345
|
+
const identity = yield* ThreadObjectIdentity;
|
|
346
|
+
|
|
347
|
+
if (threadId !== identity.threadId)
|
|
348
|
+
return yield* HostProtocolError.make({ message: "The Receipt belongs to another Thread" });
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const requirePortThread = (request: PortRequest) => {
|
|
352
|
+
switch (request._tag) {
|
|
353
|
+
case "LedgerLookup":
|
|
354
|
+
return request.request._tag === "SubmissionLookupById"
|
|
355
|
+
? lookupAddressedSubmission(request.request.submissionId).pipe(Effect.asVoid)
|
|
356
|
+
: requireReceiptThread(request.request.threadId);
|
|
357
|
+
case "LedgerMarkReady":
|
|
358
|
+
case "LedgerRequestAbort":
|
|
359
|
+
return requireSubmissionThread(request.request.submissionId);
|
|
360
|
+
case "LedgerRecordChildSettled":
|
|
361
|
+
return requireSubmissionThread(request.request.parentSubmissionId);
|
|
362
|
+
case "LedgerAdmit":
|
|
363
|
+
case "LedgerResolveAdmission":
|
|
364
|
+
case "StoreMaterialize":
|
|
365
|
+
case "StoreAppend":
|
|
366
|
+
case "StoreReadPage":
|
|
367
|
+
case "StoreInspectTail":
|
|
368
|
+
case "StoreExport":
|
|
369
|
+
return requireReceiptThread(request.request.threadId);
|
|
370
|
+
}
|
|
371
|
+
request satisfies never;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Admit an already Schema-decoded request to a logical Thread in this physical owner.
|
|
376
|
+
* Custom hosts validate local placement before calling this Effect and provide their same
|
|
377
|
+
* runtime/maintenance instances. The native endpoint uses this path too: queue limits,
|
|
378
|
+
* idempotent receipts and the pre-admission generation/alarm commit have one owner.
|
|
379
|
+
*/
|
|
380
|
+
export const submit = Effect.fn("ThreadObject.submit")(function* (
|
|
381
|
+
threadId: ThreadId,
|
|
382
|
+
request: SubmitRequest,
|
|
383
|
+
) {
|
|
384
|
+
const placement = yield* ThreadObjectPlacement;
|
|
385
|
+
|
|
386
|
+
if (!placement.ownsThread(threadId))
|
|
387
|
+
return yield* HostProtocolError.make({ message: "The Thread belongs to another Object" });
|
|
388
|
+
const mutations = yield* ThreadMutationGate;
|
|
389
|
+
const runtime = yield* DurableAgentRuntime;
|
|
390
|
+
|
|
391
|
+
yield* gateAdmissionLimits(threadId, request);
|
|
392
|
+
|
|
393
|
+
return yield* mutations.withMutation(
|
|
394
|
+
runtime
|
|
395
|
+
.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
|
|
396
|
+
threadId,
|
|
397
|
+
principal: request.principal,
|
|
398
|
+
idempotencyKey: request.idempotencyKey,
|
|
399
|
+
...(request.admissionGroup === undefined ? {} : { admissionGroup: request.admissionGroup }),
|
|
400
|
+
...(request.admissionFence === undefined ? {} : { admissionFence: request.admissionFence }),
|
|
401
|
+
...(request.workerAdmission === undefined
|
|
402
|
+
? {}
|
|
403
|
+
: { workerAdmission: request.workerAdmission }),
|
|
404
|
+
...(request.messageAdmission === undefined
|
|
405
|
+
? {}
|
|
406
|
+
: { messageAdmission: request.messageAdmission }),
|
|
407
|
+
definitions: request.definitions,
|
|
408
|
+
})
|
|
409
|
+
.pipe(Effect.tap(() => publishCommitted)),
|
|
410
|
+
);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
414
|
+
decodeSubmitRequest(encoded).pipe(
|
|
415
|
+
Effect.mapError(protocolFailure("The submit request could not be decoded")),
|
|
416
|
+
Effect.flatMap((request) =>
|
|
417
|
+
Effect.gen(function* () {
|
|
418
|
+
const identity = yield* ThreadObjectIdentity;
|
|
419
|
+
const receipt = yield* submit(identity.threadId, request);
|
|
420
|
+
|
|
421
|
+
return SubmitSucceeded.make({ receipt });
|
|
422
|
+
}),
|
|
423
|
+
),
|
|
424
|
+
respond,
|
|
425
|
+
Effect.flatMap(encodeResponse),
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const submissionStatusEndpoint = (
|
|
429
|
+
encoded: unknown,
|
|
430
|
+
): Effect.Effect<unknown, never, EndpointServices> =>
|
|
431
|
+
decodeReceipt(encoded).pipe(
|
|
432
|
+
Effect.mapError(protocolFailure("The receipt could not be decoded")),
|
|
433
|
+
Effect.flatMap((receipt) =>
|
|
434
|
+
Effect.gen(function* () {
|
|
435
|
+
const authorizer = yield* OperationAuthorizer;
|
|
436
|
+
|
|
437
|
+
yield* authorizer.authorize(
|
|
438
|
+
OperationAuthorizationRequest.make({
|
|
439
|
+
operation: "awaitSettlement",
|
|
440
|
+
threadId: receipt.threadId,
|
|
441
|
+
submissionId: receipt.submissionId,
|
|
442
|
+
}),
|
|
443
|
+
);
|
|
444
|
+
yield* requireReceiptThread(receipt.threadId);
|
|
445
|
+
yield* requireSubmissionThread(receipt.submissionId);
|
|
446
|
+
const runtime = yield* DurableAgentRuntime;
|
|
447
|
+
|
|
448
|
+
return SubmissionStatusResponse.make({ status: yield* runtime.submissionStatus(receipt) });
|
|
449
|
+
}),
|
|
450
|
+
),
|
|
451
|
+
respond,
|
|
452
|
+
Effect.flatMap(encodeResponse),
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const awaitSettlementEndpoint = (
|
|
456
|
+
encoded: unknown,
|
|
457
|
+
): Effect.Effect<unknown, never, EndpointServices> =>
|
|
458
|
+
decodeReceipt(encoded).pipe(
|
|
459
|
+
Effect.mapError(protocolFailure("The receipt could not be decoded")),
|
|
460
|
+
Effect.flatMap((receipt) =>
|
|
461
|
+
Effect.gen(function* () {
|
|
462
|
+
const authorizer = yield* OperationAuthorizer;
|
|
463
|
+
|
|
464
|
+
yield* authorizer.authorize(
|
|
465
|
+
OperationAuthorizationRequest.make({
|
|
466
|
+
operation: "awaitSettlement",
|
|
467
|
+
threadId: receipt.threadId,
|
|
468
|
+
submissionId: receipt.submissionId,
|
|
469
|
+
}),
|
|
470
|
+
);
|
|
471
|
+
yield* requireReceiptThread(receipt.threadId);
|
|
472
|
+
yield* requireSubmissionThread(receipt.submissionId);
|
|
473
|
+
const runtime = yield* DurableAgentRuntime;
|
|
474
|
+
const settlement = yield* runtime.awaitSettlement(receipt);
|
|
475
|
+
|
|
476
|
+
return SettlementReached.make({ settlement });
|
|
477
|
+
}),
|
|
478
|
+
),
|
|
479
|
+
respond,
|
|
480
|
+
Effect.flatMap(encodeResponse),
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
const awaitProgressEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
484
|
+
decodeAwaitProgressRequest(encoded).pipe(
|
|
485
|
+
Effect.mapError(protocolFailure("The progress request could not be decoded")),
|
|
486
|
+
Effect.flatMap((request) =>
|
|
487
|
+
Effect.gen(function* () {
|
|
488
|
+
const identity = yield* ThreadObjectIdentity;
|
|
489
|
+
const runtime = yield* DurableAgentRuntime;
|
|
490
|
+
const registry = yield* ProgressWaitRegistry;
|
|
491
|
+
|
|
492
|
+
yield* Effect.scoped(
|
|
493
|
+
Effect.gen(function* () {
|
|
494
|
+
const cancelled = yield* registry.subscribe(
|
|
495
|
+
JSON.stringify([identity.threadId, request.waiterId]),
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
yield* Effect.raceFirst(
|
|
499
|
+
runtime.awaitProgress(identity.threadId, request.afterSequence),
|
|
500
|
+
cancelled,
|
|
501
|
+
);
|
|
502
|
+
}),
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
return ProgressObserved.make();
|
|
506
|
+
}),
|
|
507
|
+
),
|
|
508
|
+
respond,
|
|
509
|
+
Effect.flatMap(encodeResponse),
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
const cancelProgressEndpoint = (
|
|
513
|
+
encoded: unknown,
|
|
514
|
+
): Effect.Effect<unknown, never, EndpointServices> =>
|
|
515
|
+
decodeCancelProgressRequest(encoded).pipe(
|
|
516
|
+
Effect.mapError(protocolFailure("The progress cancellation could not be decoded")),
|
|
517
|
+
Effect.flatMap((request) =>
|
|
518
|
+
Effect.gen(function* () {
|
|
519
|
+
const identity = yield* ThreadObjectIdentity;
|
|
520
|
+
const registry = yield* ProgressWaitRegistry;
|
|
521
|
+
|
|
522
|
+
yield* registry.cancel(JSON.stringify([identity.threadId, request.waiterId]));
|
|
523
|
+
|
|
524
|
+
return ProgressCancelled.make();
|
|
525
|
+
}),
|
|
526
|
+
),
|
|
527
|
+
respond,
|
|
528
|
+
Effect.flatMap(encodeResponse),
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
const observePageEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
532
|
+
decodeObservePageRequest(encoded).pipe(
|
|
533
|
+
Effect.mapError(protocolFailure("The observe request could not be decoded")),
|
|
534
|
+
Effect.flatMap((request) =>
|
|
535
|
+
Effect.gen(function* () {
|
|
536
|
+
const identity = yield* ThreadObjectIdentity;
|
|
537
|
+
const store = yield* ThreadStore;
|
|
538
|
+
// The same fail-closed authorization seam the runtime's `observe` consults (P7 WP1);
|
|
539
|
+
// the default reference preserves the possession behavior.
|
|
540
|
+
const authorizer = yield* OperationAuthorizer;
|
|
541
|
+
|
|
542
|
+
yield* authorizer.authorize(
|
|
543
|
+
OperationAuthorizationRequest.make({
|
|
544
|
+
operation: "observe",
|
|
545
|
+
threadId: identity.threadId,
|
|
546
|
+
}),
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
const records = yield* Stream.runCollect(
|
|
550
|
+
store.read(
|
|
551
|
+
ThreadRead.make({
|
|
552
|
+
threadId: identity.threadId,
|
|
553
|
+
...(request.afterSequence === undefined
|
|
554
|
+
? {}
|
|
555
|
+
: { afterSequence: request.afterSequence }),
|
|
556
|
+
limit: request.limit,
|
|
557
|
+
}),
|
|
558
|
+
),
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
return ObservedPage.make({ records: [...records] });
|
|
562
|
+
}),
|
|
563
|
+
),
|
|
564
|
+
respond,
|
|
565
|
+
Effect.flatMap(encodeResponse),
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
const abortEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
569
|
+
decodeAbortCommand(encoded).pipe(
|
|
570
|
+
Effect.mapError(protocolFailure("The abort command could not be decoded")),
|
|
571
|
+
Effect.flatMap((command) =>
|
|
572
|
+
Effect.gen(function* () {
|
|
573
|
+
const authorizer = yield* OperationAuthorizer;
|
|
574
|
+
|
|
575
|
+
yield* authorizer.authorize(
|
|
576
|
+
OperationAuthorizationRequest.make({
|
|
577
|
+
operation: "abort",
|
|
578
|
+
submissionId: command.submissionId,
|
|
579
|
+
}),
|
|
580
|
+
);
|
|
581
|
+
yield* requireSubmissionThread(command.submissionId);
|
|
582
|
+
const maintenance = yield* ThreadMaintenance;
|
|
583
|
+
const runtime = yield* DurableAgentRuntime;
|
|
584
|
+
const intent = yield* maintenance.withMutation(runtime.abort(command));
|
|
585
|
+
|
|
586
|
+
return AbortRecorded.make({ intent });
|
|
587
|
+
}),
|
|
588
|
+
),
|
|
589
|
+
respond,
|
|
590
|
+
Effect.flatMap(encodeResponse),
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
const resolveApprovalEndpoint = (
|
|
594
|
+
encoded: unknown,
|
|
595
|
+
): Effect.Effect<unknown, never, EndpointServices> =>
|
|
596
|
+
decodeApprovalDecisionCommand(encoded).pipe(
|
|
597
|
+
Effect.mapError(protocolFailure("The approval command could not be decoded")),
|
|
598
|
+
Effect.flatMap((command) =>
|
|
599
|
+
Effect.gen(function* () {
|
|
600
|
+
const authorizer = yield* OperationAuthorizer;
|
|
601
|
+
|
|
602
|
+
yield* authorizer.authorize(
|
|
603
|
+
OperationAuthorizationRequest.make({
|
|
604
|
+
operation: "resolveApproval",
|
|
605
|
+
submissionId: command.submissionId,
|
|
606
|
+
}),
|
|
607
|
+
);
|
|
608
|
+
yield* requireSubmissionThread(command.submissionId);
|
|
609
|
+
const maintenance = yield* ThreadMaintenance;
|
|
610
|
+
const runtime = yield* DurableAgentRuntime;
|
|
611
|
+
const intent = yield* maintenance.withMutation(runtime.resolveApproval(command));
|
|
612
|
+
|
|
613
|
+
return ApprovalRecorded.make({ intent });
|
|
614
|
+
}),
|
|
615
|
+
),
|
|
616
|
+
respond,
|
|
617
|
+
Effect.flatMap(encodeResponse),
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
const resolveUnknownEndpoint = (
|
|
621
|
+
encoded: unknown,
|
|
622
|
+
): Effect.Effect<unknown, never, EndpointServices> =>
|
|
623
|
+
decodeUnknownResolutionCommand(encoded).pipe(
|
|
624
|
+
Effect.mapError(protocolFailure("The resolution command could not be decoded")),
|
|
625
|
+
Effect.flatMap((command) =>
|
|
626
|
+
Effect.gen(function* () {
|
|
627
|
+
const authorizer = yield* OperationAuthorizer;
|
|
628
|
+
|
|
629
|
+
yield* authorizer.authorize(
|
|
630
|
+
OperationAuthorizationRequest.make({
|
|
631
|
+
operation: "resolveUnknown",
|
|
632
|
+
submissionId: command.submissionId,
|
|
633
|
+
}),
|
|
634
|
+
);
|
|
635
|
+
yield* requireSubmissionThread(command.submissionId);
|
|
636
|
+
const maintenance = yield* ThreadMaintenance;
|
|
637
|
+
const runtime = yield* DurableAgentRuntime;
|
|
638
|
+
const intent = yield* maintenance.withMutation(runtime.resolveUnknown(command));
|
|
639
|
+
|
|
640
|
+
return UnknownResolutionRecorded.make({ intent });
|
|
641
|
+
}),
|
|
642
|
+
),
|
|
643
|
+
respond,
|
|
644
|
+
Effect.flatMap(encodeResponse),
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
// ---------------------------------------------------------------------------
|
|
648
|
+
// P7 administrative entry points (plan §3): explain/verify/retry/obligations over the SAME
|
|
649
|
+
// envelope discipline as the host protocol — closed request/response Schema unions, typed
|
|
650
|
+
// failures that re-decode to identical tags, protocol anomalies answered typed. The envelopes
|
|
651
|
+
// live here (not `client.ts`) because no Worker-side client consumption exists yet; `wake`
|
|
652
|
+
// already exists as the `wake()` entry point.
|
|
653
|
+
// ---------------------------------------------------------------------------
|
|
654
|
+
|
|
655
|
+
/** Explain one Submission (`submissionId` present) or every nonterminal lane member. */
|
|
656
|
+
export class AdminExplainRequest extends Schema.Class<AdminExplainRequest>(
|
|
657
|
+
"@effect-agent/platform-cloudflare/AdminExplainRequest",
|
|
658
|
+
)({
|
|
659
|
+
submissionId: Schema.optionalKey(SubmissionId),
|
|
660
|
+
}) {}
|
|
661
|
+
|
|
662
|
+
/** Verify carries no parameters — the addressed Object IS the lane. */
|
|
663
|
+
export class AdminVerifyRequest extends Schema.Class<AdminVerifyRequest>(
|
|
664
|
+
"@effect-agent/platform-cloudflare/AdminVerifyRequest",
|
|
665
|
+
)({}) {}
|
|
666
|
+
|
|
667
|
+
/** Every typed failure of the four admin entry points, plus the protocol's own errors. */
|
|
668
|
+
export const AdminFailure = Schema.Union([
|
|
669
|
+
AdmissionPolicyError,
|
|
670
|
+
OperationDenied,
|
|
671
|
+
RetryRefused,
|
|
672
|
+
LedgerError,
|
|
673
|
+
RunJournalError,
|
|
674
|
+
DigestError,
|
|
675
|
+
OwnershipLost,
|
|
676
|
+
SettlementConflict,
|
|
677
|
+
ThreadStoreError,
|
|
678
|
+
ThreadNotMaterialized,
|
|
679
|
+
AppendConflict,
|
|
680
|
+
FenceRejected,
|
|
681
|
+
DurableRuntimeFailpointError,
|
|
682
|
+
DurableAlarmError,
|
|
683
|
+
HostProtocolError,
|
|
684
|
+
]);
|
|
685
|
+
|
|
686
|
+
export type AdminFailure = typeof AdminFailure.Type;
|
|
687
|
+
|
|
688
|
+
export class ExplainedRecovery extends Schema.TaggedClass<ExplainedRecovery>(
|
|
689
|
+
"@effect-agent/platform-cloudflare/ExplainedRecovery",
|
|
690
|
+
)("ExplainedRecovery", {
|
|
691
|
+
explanations: Schema.Array(RecoveryExplanation).check(Schema.isMaxLength(1_024)),
|
|
692
|
+
}) {}
|
|
693
|
+
|
|
694
|
+
export class VerifiedIntegrity extends Schema.TaggedClass<VerifiedIntegrity>(
|
|
695
|
+
"@effect-agent/platform-cloudflare/VerifiedIntegrity",
|
|
696
|
+
)("VerifiedIntegrity", {
|
|
697
|
+
report: IntegrityReport,
|
|
698
|
+
}) {}
|
|
699
|
+
|
|
700
|
+
export class RetryExecuted extends Schema.TaggedClass<RetryExecuted>(
|
|
701
|
+
"@effect-agent/platform-cloudflare/RetryExecuted",
|
|
702
|
+
)("RetryExecuted", {
|
|
703
|
+
report: RecoveryReport,
|
|
704
|
+
}) {}
|
|
705
|
+
|
|
706
|
+
export class ObligationsScanned extends Schema.TaggedClass<ObligationsScanned>(
|
|
707
|
+
"@effect-agent/platform-cloudflare/ObligationsScanned",
|
|
708
|
+
)("ObligationsScanned", {
|
|
709
|
+
report: ObligationReport,
|
|
710
|
+
}) {}
|
|
711
|
+
|
|
712
|
+
/** The admin entry point failed TYPED on the Object; the failure re-decodes verbatim. */
|
|
713
|
+
export class AdminFailed extends Schema.TaggedClass<AdminFailed>(
|
|
714
|
+
"@effect-agent/platform-cloudflare/AdminFailed",
|
|
715
|
+
)("AdminFailed", {
|
|
716
|
+
failure: AdminFailure,
|
|
717
|
+
}) {}
|
|
718
|
+
|
|
719
|
+
/** The uniform answer of one admin entry point. Callers narrow by the tag their call implies. */
|
|
720
|
+
export const AdminResponse = Schema.Union([
|
|
721
|
+
ExplainedRecovery,
|
|
722
|
+
VerifiedIntegrity,
|
|
723
|
+
RetryExecuted,
|
|
724
|
+
ObligationsScanned,
|
|
725
|
+
AdminFailed,
|
|
726
|
+
]);
|
|
727
|
+
|
|
728
|
+
export type AdminResponse = typeof AdminResponse.Type;
|
|
729
|
+
|
|
730
|
+
export const decodeAdminExplainRequest = Schema.decodeUnknownEffect(AdminExplainRequest);
|
|
731
|
+
export const decodeAdminVerifyRequest = Schema.decodeUnknownEffect(AdminVerifyRequest);
|
|
732
|
+
export const decodeRetryCommand = Schema.decodeUnknownEffect(RetryCommand);
|
|
733
|
+
export const decodeObligationThresholds = Schema.decodeUnknownEffect(ObligationThresholds);
|
|
734
|
+
export const encodeAdminResponse = Schema.encodeEffect(AdminResponse);
|
|
735
|
+
export const decodeAdminResponse = Schema.decodeUnknownEffect(AdminResponse);
|
|
736
|
+
|
|
737
|
+
/** Fold one admin endpoint's typed failures into the uniform `AdminResponse` envelope. */
|
|
738
|
+
const respondAdmin = <Result extends AdminResponse, Failure extends AdminFailure>(
|
|
739
|
+
effect: Effect.Effect<Result, Failure, EndpointServices>,
|
|
740
|
+
): Effect.Effect<AdminResponse, never, EndpointServices> =>
|
|
741
|
+
effect.pipe(
|
|
742
|
+
Effect.map((result): AdminResponse => result),
|
|
743
|
+
Effect.catch((failure) => Effect.succeed<AdminResponse>(AdminFailed.make({ failure }))),
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
/** Encode the admin response envelope; an unencodable response degrades to a protocol failure. */
|
|
747
|
+
const encodeAdminResponseTotal = (response: AdminResponse): Effect.Effect<unknown> =>
|
|
748
|
+
encodeAdminResponse(response).pipe(
|
|
749
|
+
Effect.catch((error) =>
|
|
750
|
+
Effect.succeed<unknown>({
|
|
751
|
+
_tag: "AdminFailed",
|
|
752
|
+
failure: {
|
|
753
|
+
_tag: "HostProtocolError",
|
|
754
|
+
message: boundHostDiagnostic(`The admin response could not be encoded: ${error.message}`),
|
|
755
|
+
},
|
|
756
|
+
}),
|
|
757
|
+
),
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
const explainEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
761
|
+
decodeAdminExplainRequest(encoded).pipe(
|
|
762
|
+
Effect.mapError(protocolFailure("The explain request could not be decoded")),
|
|
763
|
+
Effect.flatMap((request) =>
|
|
764
|
+
Effect.gen(function* () {
|
|
765
|
+
const identity = yield* ThreadObjectIdentity;
|
|
766
|
+
const runtime = yield* DurableAgentRuntime;
|
|
767
|
+
|
|
768
|
+
const explanations =
|
|
769
|
+
request.submissionId === undefined
|
|
770
|
+
? yield* runtime.explainThread(identity.threadId)
|
|
771
|
+
: [yield* runtime.explain(request.submissionId)];
|
|
772
|
+
|
|
773
|
+
return ExplainedRecovery.make({ explanations });
|
|
774
|
+
}),
|
|
775
|
+
),
|
|
776
|
+
respondAdmin,
|
|
777
|
+
Effect.flatMap(encodeAdminResponseTotal),
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
const verifyEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
781
|
+
decodeAdminVerifyRequest(encoded).pipe(
|
|
782
|
+
Effect.mapError(protocolFailure("The verify request could not be decoded")),
|
|
783
|
+
Effect.flatMap(() =>
|
|
784
|
+
Effect.gen(function* () {
|
|
785
|
+
const identity = yield* ThreadObjectIdentity;
|
|
786
|
+
const runtime = yield* DurableAgentRuntime;
|
|
787
|
+
const report = yield* runtime.verify(identity.threadId);
|
|
788
|
+
|
|
789
|
+
return VerifiedIntegrity.make({ report });
|
|
790
|
+
}),
|
|
791
|
+
),
|
|
792
|
+
respondAdmin,
|
|
793
|
+
Effect.flatMap(encodeAdminResponseTotal),
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
const retryEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
797
|
+
decodeRetryCommand(encoded).pipe(
|
|
798
|
+
Effect.mapError(protocolFailure("The retry command could not be decoded")),
|
|
799
|
+
Effect.flatMap((command) =>
|
|
800
|
+
Effect.gen(function* () {
|
|
801
|
+
const maintenance = yield* ThreadMaintenance;
|
|
802
|
+
const runtime = yield* DurableAgentRuntime;
|
|
803
|
+
// Retry may repair durable state, so its generation + alarm commit before the mutation.
|
|
804
|
+
const report = yield* maintenance.withMutation(runtime.retry(command));
|
|
805
|
+
|
|
806
|
+
return RetryExecuted.make({ report });
|
|
807
|
+
}),
|
|
808
|
+
),
|
|
809
|
+
respondAdmin,
|
|
810
|
+
Effect.flatMap(encodeAdminResponseTotal),
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
const obligationsEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
814
|
+
decodeObligationThresholds(encoded).pipe(
|
|
815
|
+
Effect.mapError(protocolFailure("The obligation thresholds could not be decoded")),
|
|
816
|
+
Effect.flatMap((thresholds) =>
|
|
817
|
+
Effect.gen(function* () {
|
|
818
|
+
const runtime = yield* DurableAgentRuntime;
|
|
819
|
+
const report = yield* runtime.scanObligations(thresholds);
|
|
820
|
+
|
|
821
|
+
return ObligationsScanned.make({ report });
|
|
822
|
+
}),
|
|
823
|
+
),
|
|
824
|
+
respondAdmin,
|
|
825
|
+
Effect.flatMap(encodeAdminResponseTotal),
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Owner-side `portCall`: wrap a mutating envelope in the same pre-armed generation protocol as
|
|
830
|
+
* public RPC (a routed mutation committed by THIS Object must already carry the alarm that will
|
|
831
|
+
* finish it), execute on the LOCAL facets (never the routed decorators), then arm an immediate
|
|
832
|
+
* alarm so the mutated lane is processed promptly. Protocol anomalies answer
|
|
833
|
+
* `PortFailed(PortProtocolError)`.
|
|
834
|
+
*/
|
|
835
|
+
const encodePortResponseTotal = (response: PortResponse) =>
|
|
836
|
+
encodePortResponse(response).pipe(
|
|
837
|
+
Effect.catch((error) =>
|
|
838
|
+
Effect.succeed(
|
|
839
|
+
encodedPortProtocolFailure(`The port response could not be encoded: ${error.message}`),
|
|
840
|
+
),
|
|
841
|
+
),
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
const portGuardFailure = (failure: LedgerError | HostProtocolError): PortFailed =>
|
|
845
|
+
PortFailed.make({
|
|
846
|
+
failure:
|
|
847
|
+
failure._tag === "LedgerError"
|
|
848
|
+
? failure
|
|
849
|
+
: PortProtocolError.make({ message: "The port request is not for the addressed Thread" }),
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
export const portCall = (
|
|
853
|
+
encoded: unknown,
|
|
854
|
+
): Effect.Effect<
|
|
855
|
+
unknown,
|
|
856
|
+
never,
|
|
857
|
+
ThreadObjectPorts | ThreadMaintenance | DurableAlarmService | ThreadObjectIdentity
|
|
858
|
+
> =>
|
|
859
|
+
Effect.gen(function* () {
|
|
860
|
+
const ports = yield* ThreadObjectPorts;
|
|
861
|
+
const maintenance = yield* ThreadMaintenance;
|
|
862
|
+
const alarm = yield* DurableAlarmService;
|
|
863
|
+
|
|
864
|
+
const decoded = yield* decodePortRequest(encoded).pipe(
|
|
865
|
+
Effect.map((request) => ({ _tag: "success" as const, request })),
|
|
866
|
+
Effect.catch((error) => Effect.succeed({ _tag: "failure" as const, message: error.message })),
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
if (decoded._tag === "failure") {
|
|
870
|
+
return encodedPortProtocolFailure(
|
|
871
|
+
`The port request could not be decoded: ${decoded.message}`,
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
if (
|
|
875
|
+
decoded.request._tag === "LedgerLookup" &&
|
|
876
|
+
decoded.request.request._tag === "SubmissionLookupById"
|
|
877
|
+
) {
|
|
878
|
+
const response = yield* lookupAddressedSubmission(decoded.request.request.submissionId).pipe(
|
|
879
|
+
Effect.map((submission) =>
|
|
880
|
+
PortSucceeded.make({
|
|
881
|
+
result: LedgerLookupResult.make(
|
|
882
|
+
Option.isSome(submission) ? { submission: submission.value } : {},
|
|
883
|
+
),
|
|
884
|
+
}),
|
|
885
|
+
),
|
|
886
|
+
Effect.catch((failure) => Effect.succeed(portGuardFailure(failure))),
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
return yield* encodePortResponseTotal(response);
|
|
890
|
+
}
|
|
891
|
+
const identityCheck = yield* requirePortThread(decoded.request).pipe(Effect.result);
|
|
892
|
+
|
|
893
|
+
if (identityCheck._tag === "Failure")
|
|
894
|
+
return yield* encodePortResponseTotal(portGuardFailure(identityCheck.failure));
|
|
895
|
+
const mutating = isMutatingPortRequest(decoded.request);
|
|
896
|
+
|
|
897
|
+
const handled = yield* (
|
|
898
|
+
mutating
|
|
899
|
+
? maintenance.withMutation(ports.handle(decoded.request))
|
|
900
|
+
: ports.handle(decoded.request)
|
|
901
|
+
).pipe(Effect.exit);
|
|
902
|
+
|
|
903
|
+
if (handled._tag === "Failure") {
|
|
904
|
+
// Without the committed generation/alarm the invariant cannot be promised; refuse before
|
|
905
|
+
// the port mutation runs. `ports.handle` itself is total, so this is the maintenance error.
|
|
906
|
+
return encodedPortProtocolFailure(
|
|
907
|
+
"The owner Object could not arm its maintenance alarm before the mutation.",
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const response = yield* encodePortResponseTotal(handled.value);
|
|
912
|
+
|
|
913
|
+
if (mutating) {
|
|
914
|
+
// Prompt processing hint; the pre-armed alarm already guarantees convergence.
|
|
915
|
+
yield* alarm.scheduleNow.pipe(
|
|
916
|
+
Effect.catch((error) =>
|
|
917
|
+
Effect.logWarning("ThreadObject.portCall: immediate re-arm failed", error),
|
|
918
|
+
),
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return response;
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
const wakeEndpoint: Effect.Effect<void, never, EndpointServices> = Effect.gen(function* () {
|
|
926
|
+
const identity = yield* ThreadObjectIdentity;
|
|
927
|
+
const wake = yield* WakeScheduler;
|
|
928
|
+
|
|
929
|
+
// Route the remote hint through this incarnation's scheduler so scoped progress waiters and
|
|
930
|
+
// the alarm receive the same hint. Delivery remains droppable; canonical storage is authority.
|
|
931
|
+
yield* wake.notify(identity.threadId);
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
/** The per-Thread wire operations supported by native and application-owned endpoints. */
|
|
935
|
+
export const ThreadRpcOperation = Schema.Literals([
|
|
936
|
+
"submitEncoded",
|
|
937
|
+
"submissionStatusEncoded",
|
|
938
|
+
"awaitSettlementEncoded",
|
|
939
|
+
"awaitProgressEncoded",
|
|
940
|
+
"cancelProgressEncoded",
|
|
941
|
+
"observePage",
|
|
942
|
+
"abortEncoded",
|
|
943
|
+
"resolveApprovalEncoded",
|
|
944
|
+
"resolveUnknownEncoded",
|
|
945
|
+
"portCall",
|
|
946
|
+
"wake",
|
|
947
|
+
]);
|
|
948
|
+
|
|
949
|
+
export type ThreadRpcOperation = typeof ThreadRpcOperation.Type;
|
|
950
|
+
|
|
951
|
+
const threadRpc = {
|
|
952
|
+
submitEncoded: submitEndpoint,
|
|
953
|
+
submissionStatusEncoded: submissionStatusEndpoint,
|
|
954
|
+
awaitSettlementEncoded: awaitSettlementEndpoint,
|
|
955
|
+
awaitProgressEncoded: awaitProgressEndpoint,
|
|
956
|
+
cancelProgressEncoded: cancelProgressEndpoint,
|
|
957
|
+
observePage: observePageEndpoint,
|
|
958
|
+
abortEncoded: abortEndpoint,
|
|
959
|
+
resolveApprovalEncoded: resolveApprovalEndpoint,
|
|
960
|
+
resolveUnknownEncoded: resolveUnknownEndpoint,
|
|
961
|
+
portCall,
|
|
962
|
+
wake: () => wakeEndpoint,
|
|
963
|
+
} satisfies Record<
|
|
964
|
+
ThreadRpcOperation,
|
|
965
|
+
(encoded: unknown) => Effect.Effect<unknown, never, EndpointServices>
|
|
966
|
+
>;
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Bind an addressed request to its logical Thread while sharing one physical runtime. This
|
|
970
|
+
* validates local placement before invoking the same native handlers. Receipts, Submission
|
|
971
|
+
* commands and port envelopes must match this identity; progress cancellation is Thread-scoped.
|
|
972
|
+
* The producer comes from the actual runtime configuration, never from caller input. These
|
|
973
|
+
* guards supplement the existing current model/Tool and operation authorization policies.
|
|
974
|
+
*/
|
|
975
|
+
export const handleRpc = Effect.fn("ThreadObject.handleRpc")(function* (
|
|
976
|
+
threadId: ThreadId,
|
|
977
|
+
operation: ThreadRpcOperation,
|
|
978
|
+
encoded: unknown,
|
|
979
|
+
) {
|
|
980
|
+
const placement = yield* ThreadObjectPlacement;
|
|
981
|
+
|
|
982
|
+
if (!placement.ownsThread(threadId))
|
|
983
|
+
return yield* HostProtocolError.make({ message: "The Thread belongs to another Object" });
|
|
984
|
+
const { producerId } = yield* DurableRuntimeConfig;
|
|
985
|
+
|
|
986
|
+
return yield* threadRpc[operation](encoded).pipe(
|
|
987
|
+
Effect.provideService(ThreadObjectIdentity, { threadId, producerId }),
|
|
988
|
+
);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
const alarmEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices> = Effect.gen(
|
|
992
|
+
function* () {
|
|
993
|
+
const maintenance = yield* ThreadMaintenance;
|
|
994
|
+
|
|
995
|
+
// Typed pass failures propagate: the rejected promise makes workerd retry the alarm
|
|
996
|
+
// (at-least-once delivery), and the dirty generation retains a committed slot meanwhile.
|
|
997
|
+
yield* maintenance.pass;
|
|
998
|
+
},
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
const gateEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices> = Effect.gen(
|
|
1002
|
+
function* () {
|
|
1003
|
+
// Forcing ThreadMaintenance forces the whole Layer stack: migration + exact-version
|
|
1004
|
+
// check + configuration decode (DEPLOY-008 fails typed here, before any mutation), then
|
|
1005
|
+
// the defensive local ensure-alarm half of the invariant. LOCAL-ONLY by construction.
|
|
1006
|
+
const maintenance = yield* ThreadMaintenance;
|
|
1007
|
+
|
|
1008
|
+
yield* maintenance.ensureAlarm;
|
|
1009
|
+
},
|
|
1010
|
+
);
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Adapter from effect-cf's native Durable Object services to Effect Agent's existing platform
|
|
1014
|
+
* ports. effect-cf owns the cached ManagedRuntime and supplies these values once per Object
|
|
1015
|
+
* incarnation; the durable runtime continues to depend only on the narrow services below.
|
|
1016
|
+
*/
|
|
1017
|
+
const effectCfPlatformLayer = (
|
|
1018
|
+
namespaceBinding: string,
|
|
1019
|
+
rpcTracing = false,
|
|
1020
|
+
): Layer.Layer<
|
|
1021
|
+
DurableObjectContext | ThreadObjectNamespace,
|
|
1022
|
+
CloudflareBindingError,
|
|
1023
|
+
EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
|
|
1024
|
+
> => {
|
|
1025
|
+
const context = Layer.effect(DurableObjectContext)(
|
|
1026
|
+
Effect.gen(function* () {
|
|
1027
|
+
const state = yield* EffectCfDurableObjectState.DurableObjectState;
|
|
1028
|
+
const env = yield* WorkerEnvironment;
|
|
1029
|
+
|
|
1030
|
+
return DurableObjectContext.of({ ctx: state.raw, env });
|
|
1031
|
+
}),
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
const namespace = Layer.effect(ThreadObjectNamespace)(
|
|
1035
|
+
Effect.gen(function* () {
|
|
1036
|
+
const env = yield* WorkerEnvironment;
|
|
1037
|
+
const binding = yield* threadNamespaceFromEnv(env, namespaceBinding);
|
|
1038
|
+
|
|
1039
|
+
return ThreadObjectNamespace.of({
|
|
1040
|
+
get: (threadId) => binding.get(binding.idFromName(threadId)),
|
|
1041
|
+
...(rpcTracing === true ? { rpcTracing: namespaceBinding } : {}),
|
|
1042
|
+
});
|
|
1043
|
+
}),
|
|
1044
|
+
);
|
|
1045
|
+
|
|
1046
|
+
return Layer.merge(context, namespace);
|
|
1047
|
+
};
|
|
1048
|
+
|
|
1049
|
+
/** The public endpoints and effect-cf invocation hook of one Thread Object instance. */
|
|
1050
|
+
export interface Instance<EventServices = never> extends InstanceType<
|
|
1051
|
+
EffectCfDurableObject.DurableObjectClass<Record<never, never>, RuntimeServices | EventServices>
|
|
1052
|
+
> {
|
|
1053
|
+
submitEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
1054
|
+
submissionStatusEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
1055
|
+
awaitSettlementEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
1056
|
+
awaitProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
1057
|
+
cancelProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
1058
|
+
observePage(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
1059
|
+
abortEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
1060
|
+
resolveApprovalEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
1061
|
+
resolveUnknownEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
1062
|
+
explainEncoded(encoded: unknown): Promise<unknown>;
|
|
1063
|
+
verifyEncoded(encoded: unknown): Promise<unknown>;
|
|
1064
|
+
retryEncoded(encoded: unknown): Promise<unknown>;
|
|
1065
|
+
obligationsEncoded(encoded: unknown): Promise<unknown>;
|
|
1066
|
+
portCall(encoded: unknown): Promise<unknown>;
|
|
1067
|
+
wake(): Promise<void>;
|
|
1068
|
+
alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/** The constructor shape workerd instantiates for each Thread Object. */
|
|
1072
|
+
export interface Class<EventServices = never> {
|
|
1073
|
+
new (ctx: DurableObjectState, env: Cloudflare.Env): Instance<EventServices>;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Export a composed application Layer as a native Durable Object class.
|
|
1078
|
+
* Bootstrap services are provided to the whole graph before it acquires, so application Layers
|
|
1079
|
+
* can yield effect-cf's WorkerEnvironment and DurableObjectState, derived identity, and Crypto.
|
|
1080
|
+
* Effect Config reads scalar Worker vars and secrets through effect-cf's environment provider;
|
|
1081
|
+
* WorkerEnvironment exposes resource bindings without a separate config Layer.
|
|
1082
|
+
* Application dependencies remain visible until Layer.provide satisfies them. effect-cf owns the
|
|
1083
|
+
* cached ManagedRuntime, native RPC methods, event scopes, and telemetry flushing.
|
|
1084
|
+
* Initialization is local and bounded inside the constructor gate. Cloudflare eviction does not
|
|
1085
|
+
* guarantee finalizers; put resources requiring timely release in scoped operations or eventLayer.
|
|
1086
|
+
*/
|
|
1087
|
+
export const make = <
|
|
1088
|
+
ApplicationServices,
|
|
1089
|
+
ApplicationError,
|
|
1090
|
+
EventServices = never,
|
|
1091
|
+
EventLayerError = never,
|
|
1092
|
+
>(
|
|
1093
|
+
applicationLayer: Layer.Layer<
|
|
1094
|
+
CloudflareDurableRuntimeServices | ApplicationServices,
|
|
1095
|
+
ApplicationError,
|
|
1096
|
+
| CloudflareBootstrapServices
|
|
1097
|
+
| EffectCfDurableObjectState.DurableObjectState
|
|
1098
|
+
| WorkerEnvironment
|
|
1099
|
+
| DurableObjectContext
|
|
1100
|
+
| ThreadObjectNamespace
|
|
1101
|
+
>,
|
|
1102
|
+
options: Options<ApplicationServices, EventServices, EventLayerError>,
|
|
1103
|
+
): Class<ApplicationServices | EventServices> => {
|
|
1104
|
+
const application = applicationLayer.pipe(
|
|
1105
|
+
Layer.provideMerge(layerConfig(options)),
|
|
1106
|
+
Layer.provideMerge(effectCfPlatformLayer(options.namespaceBinding, options.rpcTracing)),
|
|
1107
|
+
);
|
|
1108
|
+
|
|
1109
|
+
// The storage/config Layer must acquire inside Cloudflare's constructor gate. effect-cf owns
|
|
1110
|
+
// the ManagedRuntime, while this effectContext ensures its first Layer build enters the gate
|
|
1111
|
+
// before migration, compatibility checks, or alarm inspection touch Object storage.
|
|
1112
|
+
const runtime: Layer.Layer<
|
|
1113
|
+
RuntimeServices | ApplicationServices,
|
|
1114
|
+
ThreadObjectInitializationError | ApplicationError,
|
|
1115
|
+
EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
|
|
1116
|
+
> = Layer.effectContext(
|
|
1117
|
+
Effect.gen(function* () {
|
|
1118
|
+
const state = yield* EffectCfDurableObjectState.DurableObjectState;
|
|
1119
|
+
const scope = yield* Effect.scope;
|
|
1120
|
+
|
|
1121
|
+
return yield* state.blockConcurrencyWhile(
|
|
1122
|
+
Effect.gen(function* () {
|
|
1123
|
+
const services = yield* Layer.buildWithScope(application, scope);
|
|
1124
|
+
|
|
1125
|
+
yield* gateEndpoint.pipe(Effect.provide(services));
|
|
1126
|
+
|
|
1127
|
+
return services;
|
|
1128
|
+
}),
|
|
1129
|
+
);
|
|
1130
|
+
}),
|
|
1131
|
+
);
|
|
1132
|
+
|
|
1133
|
+
const rpc = {
|
|
1134
|
+
...threadRpc,
|
|
1135
|
+
explainEncoded: (encoded: unknown) => explainEndpoint(encoded),
|
|
1136
|
+
verifyEncoded: (encoded: unknown) => verifyEndpoint(encoded),
|
|
1137
|
+
retryEncoded: (encoded: unknown) => retryEndpoint(encoded),
|
|
1138
|
+
obligationsEncoded: (encoded: unknown) => obligationsEndpoint(encoded),
|
|
1139
|
+
} satisfies EffectCfDurableObject.DurableObjectRpc<
|
|
1140
|
+
RuntimeServices | ApplicationServices | EventServices
|
|
1141
|
+
>;
|
|
1142
|
+
|
|
1143
|
+
type NativeOptions = EffectCfDurableObject.DurableObjectOptions<
|
|
1144
|
+
RuntimeServices | ApplicationServices,
|
|
1145
|
+
EventServices,
|
|
1146
|
+
EventLayerError,
|
|
1147
|
+
typeof rpc
|
|
1148
|
+
>;
|
|
1149
|
+
|
|
1150
|
+
const EffectCfThreadObject = EffectCfDurableObject.make<
|
|
1151
|
+
RuntimeServices | ApplicationServices,
|
|
1152
|
+
ThreadObjectInitializationError | ApplicationError,
|
|
1153
|
+
EventServices,
|
|
1154
|
+
EventLayerError,
|
|
1155
|
+
typeof rpc
|
|
1156
|
+
>(runtime, {
|
|
1157
|
+
...(options.rpcTracing === true ? { rpcTracing: { service: options.namespaceBinding } } : {}),
|
|
1158
|
+
...(options.eventLayer === undefined ? {} : { eventLayer: options.eventLayer }),
|
|
1159
|
+
// Force the gated runtime Layer when Cloudflare loads this Object incarnation. Recovery stays
|
|
1160
|
+
// in each bounded pass so cross-Object initialization cannot deadlock.
|
|
1161
|
+
initialize: Effect.void,
|
|
1162
|
+
rpc,
|
|
1163
|
+
alarm: () => alarmEndpoint,
|
|
1164
|
+
// This host owns the raw alarm and supplies event services through options.eventLayer.
|
|
1165
|
+
// Upstream's conditional alarm-registration check cannot reduce over generic application
|
|
1166
|
+
// services. Options and the rpc satisfies check above retain their Effect requirements.
|
|
1167
|
+
} as NativeOptions);
|
|
1168
|
+
|
|
1169
|
+
// effect-cf's class type keeps `alarm` optional even when the handler option is present. This
|
|
1170
|
+
// concrete override reflects this factory's stronger contract while delegating execution to
|
|
1171
|
+
// the effect-cf runtime unchanged.
|
|
1172
|
+
class ThreadObject extends EffectCfThreadObject {
|
|
1173
|
+
override alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void {
|
|
1174
|
+
return super.alarm?.(alarmInfo);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
return ThreadObject;
|
|
1179
|
+
};
|