@effect-agent/platform-cloudflare 0.1.0-beta.79 → 0.1.0-beta.80
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 +17 -5
- package/dist/Alarm.mjs +48 -12
- package/dist/Alarm.mjs.map +1 -1
- package/dist/CloudflareBindings.d.mts +14 -7
- package/dist/CloudflareBindings.mjs +10 -7
- package/dist/CloudflareBindings.mjs.map +1 -1
- package/dist/CloudflareScheduling.mjs +2 -2
- package/dist/CloudflareSubscriptions.mjs +1 -1
- package/dist/CloudflareThreadClient.d.mts +41 -41
- package/dist/CloudflareThreadClient.mjs +3 -3
- package/dist/CloudflareThreadClient.mjs.map +1 -1
- package/dist/{ThreadObject-MuU9xAyn.d.mts → ThreadObject-3uDpKfB3.d.mts} +82 -44
- package/dist/{ThreadObject-KJf_5Isr.mjs → ThreadObject-DGkXv41C.mjs} +213 -68
- package/dist/ThreadObject-DGkXv41C.mjs.map +1 -0
- package/dist/ThreadObject.d.mts +2 -2
- package/dist/ThreadObject.mjs +2 -2
- package/dist/WakeScheduler.d.mts +2 -2
- package/dist/WakeScheduler.mjs +6 -6
- package/dist/WakeScheduler.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{prepared-admission-G7N4DDqS.mjs → prepared-admission-DSi55W-7.mjs} +2 -2
- package/dist/{prepared-admission-G7N4DDqS.mjs.map → prepared-admission-DSi55W-7.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/Alarm.ts +91 -14
- package/src/CloudflareBindings.ts +15 -6
- package/src/CloudflareThreadClient.ts +3 -3
- package/src/ThreadObject.ts +284 -59
- package/src/WakeScheduler.ts +6 -6
- package/src/internal/layers.ts +150 -22
- package/src/internal/message-delivery.ts +37 -21
- package/src/internal/transport.ts +2 -2
- package/dist/ThreadObject-KJf_5Isr.mjs.map +0 -1
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
-
import { DurableObjectContext, ThreadObjectIdentity, ThreadObjectNamespace, threadNamespaceFromEnv } from "./CloudflareBindings.mjs";
|
|
2
|
+
import { DurableObjectContext, ThreadObjectIdentity, ThreadObjectNamespace, ThreadObjectPlacement, threadNamespaceFromEnv } from "./CloudflareBindings.mjs";
|
|
3
3
|
import { AdmissionLimitExceeded, CLOUDFLARE_RUNTIME_DEFAULTS, CloudflareDurableRuntimeConfig, CloudflareDurableRuntimeConfigValue, CloudflarePlatformConfigError } from "./CloudflareConfig.mjs";
|
|
4
4
|
import { DurableAlarmError, DurableAlarmService, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, publishCommitted } from "./Alarm.mjs";
|
|
5
5
|
import { AbortRecorded, ApprovalRecorded, CloudflareThreadClient, HostFailed, HostProtocolError, ObservedPage, ProgressCancelled, ProgressObserved, SettlementReached, SubmissionStatusResponse, SubmitSucceeded, UnknownResolutionRecorded, boundHostDiagnostic, decodeAbortCommand, decodeApprovalDecisionCommand, decodeAwaitProgressRequest, decodeCancelProgressRequest, decodeObservePageRequest, decodeReceipt, decodeSubmitRequest, decodeUnknownResolutionCommand, encodeHostResponse } from "./CloudflareThreadClient.mjs";
|
|
6
|
-
import { t as cloudflarePreparedInputAdmissionLayer } from "./prepared-admission-
|
|
6
|
+
import { t as cloudflarePreparedInputAdmissionLayer } from "./prepared-admission-DSi55W-7.mjs";
|
|
7
7
|
import { cloudflareWakeSchedulerLayer } from "./WakeScheduler.mjs";
|
|
8
|
+
import { SubmissionId, ThreadId } from "@effect-agent/core/Identifiers";
|
|
8
9
|
import { compileRegistrations } from "@effect-agent/thread/AgentRegistration";
|
|
9
10
|
import { DurableAgentRuntime, DurableRuntimeConfig, RecoveryReport } from "@effect-agent/thread/DurableAgentRuntime";
|
|
10
|
-
import { AdmissionPolicyError, LedgerError, OwnershipLost, SettlementConflict, SubmissionLedger, SubmissionLookupByKey } from "@effect-agent/thread/SubmissionLedger";
|
|
11
|
+
import { AdmissionPolicyError, LedgerError, OwnershipLost, SettlementConflict, SubmissionLedger, SubmissionLookupById, SubmissionLookupByKey } from "@effect-agent/thread/SubmissionLedger";
|
|
11
12
|
import { ThreadProjectionMaintenance } from "@effect-agent/thread/ThreadProjectionMaintenance";
|
|
12
13
|
import { Cause, Clock, Context, Deferred, Duration, Effect, Layer, Option, Ref, Schema, Semaphore, Stream } from "effect";
|
|
13
|
-
import { SubmissionId, ThreadId } from "@effect-agent/core/Identifiers";
|
|
14
14
|
import { PersistedJson, ProducerId } from "@effect-agent/thread/Records";
|
|
15
15
|
import { DurableObject, DurableObjectState, WorkerEnvironment } from "effect-cf";
|
|
16
16
|
import { BrowserCrypto } from "@effect/platform-browser";
|
|
@@ -19,7 +19,8 @@ import { DigestError } from "@effect-agent/thread/Digest";
|
|
|
19
19
|
import { DurableRuntimeFailpoint, DurableRuntimeFailpointError } from "@effect-agent/thread/DurableFailpoint";
|
|
20
20
|
import { OperationAuthorizationRequest, OperationAuthorizer, OperationDenied, operationAuthorizerLayer } from "@effect-agent/thread/OperationAuthorizer";
|
|
21
21
|
import { AppendConflict, FenceRejected, ThreadNotMaterialized, ThreadRead, ThreadStore, ThreadStoreError } from "@effect-agent/thread/ThreadStore";
|
|
22
|
-
import
|
|
22
|
+
import "@effect-agent/thread/PreparedInputAdmission";
|
|
23
|
+
import { LedgerLookupResult, PortFailed, PortProtocolError, PortSucceeded, decodePortRequest, encodePortResponse } from "@effect-agent/storage-cloudflare/PortProtocol";
|
|
23
24
|
import { IntegrityReport, ObligationReport, ObligationThresholds, RecoveryExplanation, RetryCommand, RetryRefused } from "@effect-agent/thread/Admin";
|
|
24
25
|
import { RunJournalError } from "@effect-agent/thread/RunJournal";
|
|
25
26
|
import { WakeScheduler } from "@effect-agent/thread/WakeScheduler";
|
|
@@ -38,10 +39,10 @@ const guardedMessageDeliveryStoreLayer = Layer.effect(MessageDeliveryStore, Effe
|
|
|
38
39
|
const store = yield* MessageDeliveryStore;
|
|
39
40
|
const mutations = yield* ThreadMutationGate;
|
|
40
41
|
const wakes = yield* WakeScheduler;
|
|
41
|
-
const {
|
|
42
|
+
const { ownsThread } = yield* ThreadObjectPlacement;
|
|
42
43
|
const deadline = yield* Ref.make(void 0);
|
|
43
44
|
const cacheGate = yield* Semaphore.make(1);
|
|
44
|
-
const local = (owner, body) => owner ===
|
|
45
|
+
const local = (owner, body) => owner === void 0 || ownsThread(owner) ? body : Effect.fail(MessageDeliveryError.make({
|
|
45
46
|
reason: "validation",
|
|
46
47
|
operation: "message owner"
|
|
47
48
|
}));
|
|
@@ -52,17 +53,20 @@ const guardedMessageDeliveryStoreLayer = Layer.effect(MessageDeliveryStore, Effe
|
|
|
52
53
|
const nextDeadline = cacheGate.withPermit(Effect.gen(function* () {
|
|
53
54
|
const cached = yield* Ref.get(deadline);
|
|
54
55
|
if (cached !== void 0) return cached;
|
|
55
|
-
const current = yield* store.nextDeadline(
|
|
56
|
+
const current = yield* store.nextDeadline();
|
|
56
57
|
yield* Ref.set(deadline, current);
|
|
57
58
|
return current;
|
|
58
59
|
}));
|
|
59
60
|
return MessageDeliveryStore.of({
|
|
60
|
-
insert: (record) => local(record.key.ownerThreadId, mutate(store.insert(record)).pipe(Effect.tap(() => wakes.notify(
|
|
61
|
+
insert: (record) => local(record.key.ownerThreadId, mutate(store.insert(record)).pipe(Effect.tap(() => wakes.notify(record.key.ownerThreadId)))),
|
|
61
62
|
get: (key) => local(key.ownerThreadId, store.get(key)),
|
|
62
63
|
list: (request) => local(request.ownerThreadId, store.list(request)),
|
|
63
64
|
change: (key, change) => local(key.ownerThreadId, mutate(store.change(key, change))),
|
|
64
|
-
due: (nowMillis, limit, owner
|
|
65
|
-
|
|
65
|
+
due: (nowMillis, limit, owner) => local(owner, store.due(nowMillis, limit, owner)).pipe(Effect.flatMap((keys) => keys.every((key) => ownsThread(key.ownerThreadId)) ? Effect.succeed(keys) : Effect.fail(MessageDeliveryError.make({
|
|
66
|
+
reason: "validation",
|
|
67
|
+
operation: "message owner"
|
|
68
|
+
})))),
|
|
69
|
+
nextDeadline: (owner) => local(owner, owner === void 0 ? nextDeadline : store.nextDeadline(owner))
|
|
66
70
|
});
|
|
67
71
|
}));
|
|
68
72
|
/** Message progress shares the native alarm slot without blocking source runtime work. */
|
|
@@ -71,27 +75,26 @@ const threadMessageDeliveryLayer = Layer.effectContext(Effect.gen(function* () {
|
|
|
71
75
|
const store = yield* MessageDeliveryStore;
|
|
72
76
|
const wakes = yield* WakeScheduler;
|
|
73
77
|
const config = yield* CloudflareDurableRuntimeConfig;
|
|
74
|
-
const { threadId } = yield* ThreadObjectIdentity;
|
|
75
78
|
const failure = (operation) => () => DurableAlarmError.make({
|
|
76
79
|
operation,
|
|
77
80
|
message: "Durable message recovery remains pending"
|
|
78
81
|
});
|
|
79
82
|
const drain = Effect.gen(function* () {
|
|
80
|
-
const deadline = yield* store.nextDeadline(
|
|
81
|
-
if (deadline !== null && deadline <= (yield* Clock.currentTimeMillis)) yield* driver.runDue(
|
|
83
|
+
const deadline = yield* store.nextDeadline();
|
|
84
|
+
if (deadline !== null && deadline <= (yield* Clock.currentTimeMillis)) yield* driver.runDue();
|
|
82
85
|
}).pipe(Effect.mapError(failure("drain message delivery")));
|
|
83
86
|
return Context.make(ThreadMessageDelivery, {
|
|
84
87
|
drain,
|
|
85
88
|
drainUntil: Effect.fn("ThreadMessageDelivery.drainUntil")(function* (finished) {
|
|
89
|
+
const notified = (yield* Stream.toPull(wakes.wakes)).pipe(Effect.catch(() => Effect.never));
|
|
86
90
|
yield* Effect.gen(function* () {
|
|
87
|
-
const notified = yield* wakes.subscribe(threadId);
|
|
88
91
|
yield* drain;
|
|
89
|
-
const deadline = yield* store.nextDeadline(
|
|
92
|
+
const deadline = yield* store.nextDeadline().pipe(Effect.mapError(failure("read message deadline")));
|
|
90
93
|
const delay = deadline === null ? config.wakeScanInterval : Math.min(config.wakeScanInterval, Math.max(1, deadline - (yield* Clock.currentTimeMillis)));
|
|
91
94
|
yield* Effect.raceFirst(Deferred.await(finished), Effect.raceFirst(notified, Effect.sleep(delay)));
|
|
92
|
-
}).pipe(Effect.
|
|
93
|
-
}),
|
|
94
|
-
pendingDeadline: store.nextDeadline(
|
|
95
|
+
}).pipe(Effect.repeat({ until: () => Deferred.isDone(finished) }));
|
|
96
|
+
}, Effect.scoped),
|
|
97
|
+
pendingDeadline: store.nextDeadline().pipe(Effect.map(Option.fromNullishOr), Effect.mapError(failure("read message deadline")))
|
|
95
98
|
});
|
|
96
99
|
}));
|
|
97
100
|
//#endregion
|
|
@@ -172,9 +175,9 @@ var ProgressWaitRegistry = class ProgressWaitRegistry extends Context.Service()(
|
|
|
172
175
|
* turns exactly this error into `AdmissionIndeterminate` (SUB-031).
|
|
173
176
|
*/
|
|
174
177
|
const threadPortTransportLayer = Layer.effect(ThreadPortTransport)(Effect.gen(function* () {
|
|
175
|
-
const {
|
|
178
|
+
const { get } = yield* ThreadObjectNamespace;
|
|
176
179
|
return ThreadPortTransport.of({ call: (threadId, request) => Effect.tryPromise({
|
|
177
|
-
try: () =>
|
|
180
|
+
try: () => get(threadId).portCall(request),
|
|
178
181
|
catch: (cause) => portTransportFailure(threadId, cause)
|
|
179
182
|
}).pipe(Effect.withSpan("CloudflarePortTransport.call", { attributes: { threadId } })) });
|
|
180
183
|
}));
|
|
@@ -226,18 +229,10 @@ const threadIdFromState = (ctx) => ctx.id.name === void 0 ? Effect.fail(Cloudfla
|
|
|
226
229
|
* The native class factory builds this Layer inside its constructor gate. Custom Effect hosts
|
|
227
230
|
* can provide it around the complete application Layer with the native context already supplied.
|
|
228
231
|
*/
|
|
229
|
-
const
|
|
232
|
+
const runtimeConfigLayer = (options, producerId) => Layer.unwrap(Effect.gen(function* () {
|
|
230
233
|
const { ctx } = yield* DurableObjectContext;
|
|
231
234
|
const config = yield* configFromOptions(options);
|
|
232
|
-
|
|
233
|
-
const producerId = yield* decodeProducerId(`${config.producerPrefix}:${threadId}`).pipe(Effect.mapError((error) => CloudflarePlatformConfigError.make({
|
|
234
|
-
message: `The minted producer identity is invalid: ${error.message}`,
|
|
235
|
-
cause: error
|
|
236
|
-
})));
|
|
237
|
-
return Layer.mergeAll(Layer.succeed(CloudflareDurableRuntimeConfig, config), Layer.succeed(ThreadObjectIdentity, {
|
|
238
|
-
threadId,
|
|
239
|
-
producerId
|
|
240
|
-
}), DurableRuntimeConfig.layer({
|
|
235
|
+
return Layer.mergeAll(Layer.succeed(CloudflareDurableRuntimeConfig, config), DurableRuntimeConfig.layer({
|
|
241
236
|
deploymentId: config.deploymentId,
|
|
242
237
|
producerId,
|
|
243
238
|
settlementPollInterval: Duration.millis(config.settlementPollInterval),
|
|
@@ -249,6 +244,30 @@ const layerConfig = (options) => Layer.unwrap(Effect.gen(function* () {
|
|
|
249
244
|
failpoint: options.storageFailpoint?.(ctx)
|
|
250
245
|
}), options.runtimeFailpoint === void 0 ? DurableRuntimeFailpoint.layer : Layer.succeed(DurableRuntimeFailpoint, { hit: options.runtimeFailpoint(ctx) }), options.maintenanceFailpoint === void 0 ? ThreadMaintenanceFailpoint.layer : Layer.succeed(ThreadMaintenanceFailpoint, { hit: options.maintenanceFailpoint(ctx) }), options.toolReconciler ?? ToolReconciler.uncertain, options.operationAuthorizer === void 0 ? Layer.empty : operationAuthorizerLayer(options.operationAuthorizer), options.toolFailureObserver === void 0 ? Layer.succeed(CurrentToolFailureObserver, void 0) : toolFailureObserverLayer(options.toolFailureObserver), RunContextPreparationPassthrough, RunToolAuthorization.allowAll);
|
|
251
246
|
}));
|
|
247
|
+
const producerIdentity = (prefix, owner) => decodeProducerId(`${prefix}:${owner}`).pipe(Effect.mapError((error) => CloudflarePlatformConfigError.make({
|
|
248
|
+
message: `The minted producer identity is invalid: ${error.message}`,
|
|
249
|
+
cause: error
|
|
250
|
+
})));
|
|
251
|
+
/** Native one-Thread configuration; retains its existing producer and logical identities. */
|
|
252
|
+
const layerConfig = (options) => Layer.unwrap(Effect.gen(function* () {
|
|
253
|
+
const { ctx } = yield* DurableObjectContext;
|
|
254
|
+
const threadId = yield* threadIdFromState(ctx);
|
|
255
|
+
const producerId = yield* producerIdentity(options.producerPrefix, threadId);
|
|
256
|
+
return Layer.mergeAll(runtimeConfigLayer(options, producerId), Layer.succeed(ThreadObjectIdentity, {
|
|
257
|
+
threadId,
|
|
258
|
+
producerId
|
|
259
|
+
}), Layer.succeed(ThreadObjectPlacement, { ownsThread: (target) => target === threadId }));
|
|
260
|
+
}));
|
|
261
|
+
/**
|
|
262
|
+
* Configuration for an application Object owning several logical Threads. The producer is
|
|
263
|
+
* the stable physical Object. No logical identity is installed globally: handleRpc binds and
|
|
264
|
+
* validates it per request using the placement guard and this actual runtime producer.
|
|
265
|
+
*/
|
|
266
|
+
const layerHostConfig = (options, ownsThread) => Layer.unwrap(Effect.gen(function* () {
|
|
267
|
+
const { ctx } = yield* DurableObjectContext;
|
|
268
|
+
const producerId = yield* producerIdentity(options.producerPrefix, ctx.id.toString());
|
|
269
|
+
return Layer.merge(runtimeConfigLayer(options, producerId), Layer.succeed(ThreadObjectPlacement, { ownsThread }));
|
|
270
|
+
}));
|
|
252
271
|
/**
|
|
253
272
|
* Register typed Agents and version declarations. Hashing and dependency capture happen in
|
|
254
273
|
* this Layer's Scope, after application Layers have been provided. Every Agent's instruction,
|
|
@@ -265,10 +284,14 @@ function layer(registrations, options = {}) {
|
|
|
265
284
|
* Supply host services through `ThreadObject.make` or `ThreadObject.layerConfig` and
|
|
266
285
|
* the Durable Object context and namespace Layers when composing a custom host.
|
|
267
286
|
*/
|
|
268
|
-
const boundLayer = (bindings, options = {}) => Layer.unwrap(Effect.
|
|
287
|
+
const boundLayer = (bindings, options = {}) => Layer.unwrap(Effect.map(DurableObjectContext, ({ ctx }) => sharedLayer(DurableAgentRuntime.layerWithBindings(bindings), options).pipe(Layer.provideMerge(SqliteClient.layer({ storage: ctx.storage })))));
|
|
288
|
+
function layerInHost(application, options = {}) {
|
|
289
|
+
return sharedLayer(application, options);
|
|
290
|
+
}
|
|
291
|
+
const sharedLayer = (application, options = {}) => Layer.unwrap(Effect.gen(function* () {
|
|
269
292
|
const { ctx } = yield* DurableObjectContext;
|
|
270
293
|
const config = yield* CloudflareDurableRuntimeConfig;
|
|
271
|
-
const {
|
|
294
|
+
const { ownsThread } = yield* ThreadObjectPlacement;
|
|
272
295
|
const storageOptions = {
|
|
273
296
|
storage: ctx.storage,
|
|
274
297
|
observationPollInterval: config.observationPollInterval,
|
|
@@ -276,7 +299,7 @@ const boundLayer = (bindings, options = {}) => Layer.unwrap(Effect.gen(function*
|
|
|
276
299
|
maxStoredValueBytes: config.maxStoredValueBytes,
|
|
277
300
|
verifyOnOpen: config.verifyOnOpen
|
|
278
301
|
};
|
|
279
|
-
const infrastructure = Layer.mergeAll(storageConfigLayer(storageOptions),
|
|
302
|
+
const infrastructure = Layer.mergeAll(storageConfigLayer(storageOptions), Layer.effect(SqlClient)(SqlClient));
|
|
280
303
|
const rawLocalPorts = Layer.mergeAll(threadStoreLayer, submissionLedgerLayer).pipe(Layer.provide(infrastructure));
|
|
281
304
|
const base = Layer.mergeAll(DurableAlarmService.layer, ProgressWaitRegistry.layer);
|
|
282
305
|
const wakes = cloudflareWakeSchedulerLayer.pipe(Layer.provide(base));
|
|
@@ -317,10 +340,14 @@ const boundLayer = (bindings, options = {}) => Layer.unwrap(Effect.gen(function*
|
|
|
317
340
|
})).pipe(Layer.provide(rawLocalPorts));
|
|
318
341
|
const portsEndpointLayer = Layer.effect(ThreadObjectPorts)(Effect.gen(function* () {
|
|
319
342
|
const local = yield* Effect.context();
|
|
320
|
-
|
|
343
|
+
const ledger = Context.get(local, SubmissionLedger);
|
|
344
|
+
return ThreadObjectPorts.of({
|
|
345
|
+
handle: (request) => executePortRequest(request).pipe(Effect.provide(local)),
|
|
346
|
+
lookupSubmission: (submissionId) => ledger.lookup(SubmissionLookupById.make({ submissionId }))
|
|
347
|
+
});
|
|
321
348
|
})).pipe(Layer.provide(localPorts));
|
|
322
|
-
const routedPorts = Layer.mergeAll(routedSubmissionLedgerLayer({
|
|
323
|
-
const runtimeStack =
|
|
349
|
+
const routedPorts = Layer.mergeAll(routedSubmissionLedgerLayer({ ownsThread }), routedThreadStoreLayer({ ownsThread })).pipe(Layer.provide(localPorts), Layer.provide(threadPortTransportLayer));
|
|
350
|
+
const runtimeStack = application.pipe(Layer.provide(cloudflarePreparedInputAdmissionLayer.pipe(Layer.provide(CloudflareThreadClient.layer))), Layer.provideMerge(messageStore), Layer.provideMerge(routedPorts), Layer.provideMerge(wakes), Layer.provideMerge(base), Layer.provideMerge(portsEndpointLayer));
|
|
324
351
|
return Layer.mergeAll(runtimeStack, ThreadMaintenance.layer.pipe(Layer.provide(runtimeStack), Layer.provide(messageRecovery)), portsEndpointLayer, messageStore, messageRecovery).pipe(Layer.provideMerge(publication), Layer.provideMerge(projection), Layer.provideMerge(ThreadMutationGate.layer), Layer.provideMerge(infrastructure));
|
|
325
352
|
}));
|
|
326
353
|
//#endregion
|
|
@@ -334,6 +361,8 @@ var ThreadObject_exports = /* @__PURE__ */ __exportAll({
|
|
|
334
361
|
ExplainedRecovery: () => ExplainedRecovery,
|
|
335
362
|
ObligationsScanned: () => ObligationsScanned,
|
|
336
363
|
RetryExecuted: () => RetryExecuted,
|
|
364
|
+
ThreadObjectPorts: () => ThreadObjectPorts,
|
|
365
|
+
ThreadRpcOperation: () => ThreadRpcOperation,
|
|
337
366
|
VerifiedIntegrity: () => VerifiedIntegrity,
|
|
338
367
|
decodeAdminExplainRequest: () => decodeAdminExplainRequest,
|
|
339
368
|
decodeAdminResponse: () => decodeAdminResponse,
|
|
@@ -341,9 +370,14 @@ var ThreadObject_exports = /* @__PURE__ */ __exportAll({
|
|
|
341
370
|
decodeObligationThresholds: () => decodeObligationThresholds,
|
|
342
371
|
decodeRetryCommand: () => decodeRetryCommand,
|
|
343
372
|
encodeAdminResponse: () => encodeAdminResponse,
|
|
373
|
+
handleRpc: () => handleRpc,
|
|
344
374
|
layer: () => layer,
|
|
345
375
|
layerConfig: () => layerConfig,
|
|
346
|
-
|
|
376
|
+
layerHostConfig: () => layerHostConfig,
|
|
377
|
+
layerInHost: () => layerInHost,
|
|
378
|
+
make: () => make,
|
|
379
|
+
portCall: () => portCall,
|
|
380
|
+
submit: () => submit
|
|
347
381
|
});
|
|
348
382
|
/** Classify only a decoded port request so new protocol members cannot bypass pre-arming. */
|
|
349
383
|
const isMutatingPortRequest = (request) => {
|
|
@@ -388,13 +422,12 @@ const utf8Bytes = (value) => new TextEncoder().encode(JSON.stringify(value)).len
|
|
|
388
422
|
* exempt: its accepted-work obligation already exists, and returning the original Receipt
|
|
389
423
|
* consumes no new quota. Refusals are typed `AdmissionLimitExceeded` and nothing is written.
|
|
390
424
|
*/
|
|
391
|
-
const gateAdmissionLimits = Effect.fn("ThreadObject.gateAdmissionLimits")(function* (request) {
|
|
392
|
-
const identity = yield* ThreadObjectIdentity;
|
|
425
|
+
const gateAdmissionLimits = Effect.fn("ThreadObject.gateAdmissionLimits")(function* (threadId, request) {
|
|
393
426
|
const config = yield* CloudflareDurableRuntimeConfig;
|
|
394
427
|
const ledger = yield* SubmissionLedger;
|
|
395
428
|
const { ctx } = yield* DurableObjectContext;
|
|
396
429
|
const existing = yield* ledger.lookup(SubmissionLookupByKey.make({
|
|
397
|
-
threadId
|
|
430
|
+
threadId,
|
|
398
431
|
principal: request.principal,
|
|
399
432
|
idempotencyKey: request.idempotencyKey
|
|
400
433
|
}));
|
|
@@ -405,7 +438,7 @@ const gateAdmissionLimits = Effect.fn("ThreadObject.gateAdmissionLimits")(functi
|
|
|
405
438
|
actual: inputBytes,
|
|
406
439
|
maximum: config.limits.maxInputBytes
|
|
407
440
|
});
|
|
408
|
-
const nonterminal = yield* Stream.
|
|
441
|
+
const nonterminal = yield* ledger.scanNonterminal.pipe(Stream.filter((submission) => submission.threadId === threadId), Stream.runCollect);
|
|
409
442
|
if (nonterminal.length >= config.limits.maxQueueDepthPerLane) return yield* AdmissionLimitExceeded.make({
|
|
410
443
|
limit: "queue-depth",
|
|
411
444
|
actual: nonterminal.length,
|
|
@@ -428,13 +461,51 @@ const passthroughSubmitAgent = (agentId) => ({ definition: {
|
|
|
428
461
|
id: agentId,
|
|
429
462
|
input: PersistedJson
|
|
430
463
|
} });
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
464
|
+
/** A physical owner may hold other Threads; an addressed request cannot act on their IDs. */
|
|
465
|
+
const lookupAddressedSubmission = Effect.fn("ThreadObject.lookupAddressedSubmission")(function* (submissionId) {
|
|
466
|
+
const { threadId } = yield* ThreadObjectIdentity;
|
|
467
|
+
const submission = yield* (yield* ThreadObjectPorts).lookupSubmission(submissionId);
|
|
468
|
+
if (Option.isSome(submission) && submission.value.threadId !== threadId) return yield* HostProtocolError.make({ message: "The Submission belongs to another Thread" });
|
|
469
|
+
return submission;
|
|
470
|
+
});
|
|
471
|
+
const requireSubmissionThread = Effect.fn("ThreadObject.requireSubmissionThread")(function* (submissionId) {
|
|
472
|
+
const submission = yield* lookupAddressedSubmission(submissionId);
|
|
473
|
+
if (Option.isNone(submission)) return yield* LedgerError.make({
|
|
474
|
+
operation: "addressed Submission lookup",
|
|
475
|
+
message: "The addressed Thread has no such Submission"
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
const requireReceiptThread = Effect.fn("ThreadObject.requireReceiptThread")(function* (threadId) {
|
|
479
|
+
if (threadId !== (yield* ThreadObjectIdentity).threadId) return yield* HostProtocolError.make({ message: "The Receipt belongs to another Thread" });
|
|
480
|
+
});
|
|
481
|
+
const requirePortThread = (request) => {
|
|
482
|
+
switch (request._tag) {
|
|
483
|
+
case "LedgerLookup": return request.request._tag === "SubmissionLookupById" ? lookupAddressedSubmission(request.request.submissionId).pipe(Effect.asVoid) : requireReceiptThread(request.request.threadId);
|
|
484
|
+
case "LedgerMarkReady":
|
|
485
|
+
case "LedgerRequestAbort": return requireSubmissionThread(request.request.submissionId);
|
|
486
|
+
case "LedgerRecordChildSettled": return requireSubmissionThread(request.request.parentSubmissionId);
|
|
487
|
+
case "LedgerAdmit":
|
|
488
|
+
case "LedgerResolveAdmission":
|
|
489
|
+
case "StoreMaterialize":
|
|
490
|
+
case "StoreAppend":
|
|
491
|
+
case "StoreReadPage":
|
|
492
|
+
case "StoreInspectTail":
|
|
493
|
+
case "StoreExport": return requireReceiptThread(request.request.threadId);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
/**
|
|
497
|
+
* Admit an already Schema-decoded request to a logical Thread in this physical owner.
|
|
498
|
+
* Custom hosts validate local placement before calling this Effect and provide their same
|
|
499
|
+
* runtime/maintenance instances. The native endpoint uses this path too: queue limits,
|
|
500
|
+
* idempotent receipts and the pre-admission generation/alarm commit have one owner.
|
|
501
|
+
*/
|
|
502
|
+
const submit = Effect.fn("ThreadObject.submit")(function* (threadId, request) {
|
|
503
|
+
if (!(yield* ThreadObjectPlacement).ownsThread(threadId)) return yield* HostProtocolError.make({ message: "The Thread belongs to another Object" });
|
|
504
|
+
const mutations = yield* ThreadMutationGate;
|
|
434
505
|
const runtime = yield* DurableAgentRuntime;
|
|
435
|
-
yield* gateAdmissionLimits(request);
|
|
436
|
-
|
|
437
|
-
threadId
|
|
506
|
+
yield* gateAdmissionLimits(threadId, request);
|
|
507
|
+
return yield* mutations.withMutation(runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
|
|
508
|
+
threadId,
|
|
438
509
|
principal: request.principal,
|
|
439
510
|
idempotencyKey: request.idempotencyKey,
|
|
440
511
|
...request.admissionGroup === void 0 ? {} : { admissionGroup: request.admissionGroup },
|
|
@@ -442,14 +513,32 @@ const submitEndpoint = (encoded) => decodeSubmitRequest(encoded).pipe(Effect.map
|
|
|
442
513
|
...request.workerAdmission === void 0 ? {} : { workerAdmission: request.workerAdmission },
|
|
443
514
|
...request.messageAdmission === void 0 ? {} : { messageAdmission: request.messageAdmission },
|
|
444
515
|
definitions: request.definitions
|
|
445
|
-
}));
|
|
516
|
+
}).pipe(Effect.tap(() => publishCommitted)));
|
|
517
|
+
});
|
|
518
|
+
const submitEndpoint = (encoded) => decodeSubmitRequest(encoded).pipe(Effect.mapError(protocolFailure("The submit request could not be decoded")), Effect.flatMap((request) => Effect.gen(function* () {
|
|
519
|
+
const identity = yield* ThreadObjectIdentity;
|
|
520
|
+
const receipt = yield* submit(identity.threadId, request);
|
|
446
521
|
return SubmitSucceeded.make({ receipt });
|
|
447
522
|
})), respond, Effect.flatMap(encodeResponse));
|
|
448
523
|
const submissionStatusEndpoint = (encoded) => decodeReceipt(encoded).pipe(Effect.mapError(protocolFailure("The receipt could not be decoded")), Effect.flatMap((receipt) => Effect.gen(function* () {
|
|
524
|
+
yield* (yield* OperationAuthorizer).authorize(OperationAuthorizationRequest.make({
|
|
525
|
+
operation: "awaitSettlement",
|
|
526
|
+
threadId: receipt.threadId,
|
|
527
|
+
submissionId: receipt.submissionId
|
|
528
|
+
}));
|
|
529
|
+
yield* requireReceiptThread(receipt.threadId);
|
|
530
|
+
yield* requireSubmissionThread(receipt.submissionId);
|
|
449
531
|
const runtime = yield* DurableAgentRuntime;
|
|
450
532
|
return SubmissionStatusResponse.make({ status: yield* runtime.submissionStatus(receipt) });
|
|
451
533
|
})), respond, Effect.flatMap(encodeResponse));
|
|
452
534
|
const awaitSettlementEndpoint = (encoded) => decodeReceipt(encoded).pipe(Effect.mapError(protocolFailure("The receipt could not be decoded")), Effect.flatMap((receipt) => Effect.gen(function* () {
|
|
535
|
+
yield* (yield* OperationAuthorizer).authorize(OperationAuthorizationRequest.make({
|
|
536
|
+
operation: "awaitSettlement",
|
|
537
|
+
threadId: receipt.threadId,
|
|
538
|
+
submissionId: receipt.submissionId
|
|
539
|
+
}));
|
|
540
|
+
yield* requireReceiptThread(receipt.threadId);
|
|
541
|
+
yield* requireSubmissionThread(receipt.submissionId);
|
|
453
542
|
const settlement = yield* (yield* DurableAgentRuntime).awaitSettlement(receipt);
|
|
454
543
|
return SettlementReached.make({ settlement });
|
|
455
544
|
})), respond, Effect.flatMap(encodeResponse));
|
|
@@ -458,13 +547,14 @@ const awaitProgressEndpoint = (encoded) => decodeAwaitProgressRequest(encoded).p
|
|
|
458
547
|
const runtime = yield* DurableAgentRuntime;
|
|
459
548
|
const registry = yield* ProgressWaitRegistry;
|
|
460
549
|
yield* Effect.scoped(Effect.gen(function* () {
|
|
461
|
-
const cancelled = yield* registry.subscribe(request.waiterId);
|
|
550
|
+
const cancelled = yield* registry.subscribe(JSON.stringify([identity.threadId, request.waiterId]));
|
|
462
551
|
yield* Effect.raceFirst(runtime.awaitProgress(identity.threadId, request.afterSequence), cancelled);
|
|
463
552
|
}));
|
|
464
553
|
return ProgressObserved.make();
|
|
465
554
|
})), respond, Effect.flatMap(encodeResponse));
|
|
466
555
|
const cancelProgressEndpoint = (encoded) => decodeCancelProgressRequest(encoded).pipe(Effect.mapError(protocolFailure("The progress cancellation could not be decoded")), Effect.flatMap((request) => Effect.gen(function* () {
|
|
467
|
-
|
|
556
|
+
const identity = yield* ThreadObjectIdentity;
|
|
557
|
+
yield* (yield* ProgressWaitRegistry).cancel(JSON.stringify([identity.threadId, request.waiterId]));
|
|
468
558
|
return ProgressCancelled.make();
|
|
469
559
|
})), respond, Effect.flatMap(encodeResponse));
|
|
470
560
|
const observePageEndpoint = (encoded) => decodeObservePageRequest(encoded).pipe(Effect.mapError(protocolFailure("The observe request could not be decoded")), Effect.flatMap((request) => Effect.gen(function* () {
|
|
@@ -482,18 +572,33 @@ const observePageEndpoint = (encoded) => decodeObservePageRequest(encoded).pipe(
|
|
|
482
572
|
return ObservedPage.make({ records: [...records] });
|
|
483
573
|
})), respond, Effect.flatMap(encodeResponse));
|
|
484
574
|
const abortEndpoint = (encoded) => decodeAbortCommand(encoded).pipe(Effect.mapError(protocolFailure("The abort command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
|
|
575
|
+
yield* (yield* OperationAuthorizer).authorize(OperationAuthorizationRequest.make({
|
|
576
|
+
operation: "abort",
|
|
577
|
+
submissionId: command.submissionId
|
|
578
|
+
}));
|
|
579
|
+
yield* requireSubmissionThread(command.submissionId);
|
|
485
580
|
const maintenance = yield* ThreadMaintenance;
|
|
486
581
|
const runtime = yield* DurableAgentRuntime;
|
|
487
582
|
const intent = yield* maintenance.withMutation(runtime.abort(command));
|
|
488
583
|
return AbortRecorded.make({ intent });
|
|
489
584
|
})), respond, Effect.flatMap(encodeResponse));
|
|
490
585
|
const resolveApprovalEndpoint = (encoded) => decodeApprovalDecisionCommand(encoded).pipe(Effect.mapError(protocolFailure("The approval command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
|
|
586
|
+
yield* (yield* OperationAuthorizer).authorize(OperationAuthorizationRequest.make({
|
|
587
|
+
operation: "resolveApproval",
|
|
588
|
+
submissionId: command.submissionId
|
|
589
|
+
}));
|
|
590
|
+
yield* requireSubmissionThread(command.submissionId);
|
|
491
591
|
const maintenance = yield* ThreadMaintenance;
|
|
492
592
|
const runtime = yield* DurableAgentRuntime;
|
|
493
593
|
const intent = yield* maintenance.withMutation(runtime.resolveApproval(command));
|
|
494
594
|
return ApprovalRecorded.make({ intent });
|
|
495
595
|
})), respond, Effect.flatMap(encodeResponse));
|
|
496
596
|
const resolveUnknownEndpoint = (encoded) => decodeUnknownResolutionCommand(encoded).pipe(Effect.mapError(protocolFailure("The resolution command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
|
|
597
|
+
yield* (yield* OperationAuthorizer).authorize(OperationAuthorizationRequest.make({
|
|
598
|
+
operation: "resolveUnknown",
|
|
599
|
+
submissionId: command.submissionId
|
|
600
|
+
}));
|
|
601
|
+
yield* requireSubmissionThread(command.submissionId);
|
|
497
602
|
const maintenance = yield* ThreadMaintenance;
|
|
498
603
|
const runtime = yield* DurableAgentRuntime;
|
|
499
604
|
const intent = yield* maintenance.withMutation(runtime.resolveUnknown(command));
|
|
@@ -579,7 +684,9 @@ const obligationsEndpoint = (encoded) => decodeObligationThresholds(encoded).pip
|
|
|
579
684
|
* alarm so the mutated lane is processed promptly. Protocol anomalies answer
|
|
580
685
|
* `PortFailed(PortProtocolError)`.
|
|
581
686
|
*/
|
|
582
|
-
const
|
|
687
|
+
const encodePortResponseTotal = (response) => encodePortResponse(response).pipe(Effect.catch((error) => Effect.succeed(encodedPortProtocolFailure(`The port response could not be encoded: ${error.message}`))));
|
|
688
|
+
const portGuardFailure = (failure) => PortFailed.make({ failure: failure._tag === "LedgerError" ? failure : PortProtocolError.make({ message: "The port request is not for the addressed Thread" }) });
|
|
689
|
+
const portCall = (encoded) => Effect.gen(function* () {
|
|
583
690
|
const ports = yield* ThreadObjectPorts;
|
|
584
691
|
const maintenance = yield* ThreadMaintenance;
|
|
585
692
|
const alarm = yield* DurableAlarmService;
|
|
@@ -591,10 +698,16 @@ const portCallEndpoint = (encoded) => Effect.gen(function* () {
|
|
|
591
698
|
message: error.message
|
|
592
699
|
})));
|
|
593
700
|
if (decoded._tag === "failure") return encodedPortProtocolFailure(`The port request could not be decoded: ${decoded.message}`);
|
|
701
|
+
if (decoded.request._tag === "LedgerLookup" && decoded.request.request._tag === "SubmissionLookupById") {
|
|
702
|
+
const response = yield* lookupAddressedSubmission(decoded.request.request.submissionId).pipe(Effect.map((submission) => PortSucceeded.make({ result: LedgerLookupResult.make(Option.isSome(submission) ? { submission: submission.value } : {}) })), Effect.catch((failure) => Effect.succeed(portGuardFailure(failure))));
|
|
703
|
+
return yield* encodePortResponseTotal(response);
|
|
704
|
+
}
|
|
705
|
+
const identityCheck = yield* requirePortThread(decoded.request).pipe(Effect.result);
|
|
706
|
+
if (identityCheck._tag === "Failure") return yield* encodePortResponseTotal(portGuardFailure(identityCheck.failure));
|
|
594
707
|
const mutating = isMutatingPortRequest(decoded.request);
|
|
595
708
|
const handled = yield* (mutating ? maintenance.withMutation(ports.handle(decoded.request)) : ports.handle(decoded.request)).pipe(Effect.exit);
|
|
596
709
|
if (handled._tag === "Failure") return encodedPortProtocolFailure("The owner Object could not arm its maintenance alarm before the mutation.");
|
|
597
|
-
const response = yield*
|
|
710
|
+
const response = yield* encodePortResponseTotal(handled.value);
|
|
598
711
|
if (mutating) yield* alarm.scheduleNow.pipe(Effect.catch((error) => Effect.logWarning("ThreadObject.portCall: immediate re-arm failed", error)));
|
|
599
712
|
return response;
|
|
600
713
|
});
|
|
@@ -602,6 +715,48 @@ const wakeEndpoint = Effect.gen(function* () {
|
|
|
602
715
|
const identity = yield* ThreadObjectIdentity;
|
|
603
716
|
yield* (yield* WakeScheduler).notify(identity.threadId);
|
|
604
717
|
});
|
|
718
|
+
/** The per-Thread wire operations supported by native and application-owned endpoints. */
|
|
719
|
+
const ThreadRpcOperation = Schema.Literals([
|
|
720
|
+
"submitEncoded",
|
|
721
|
+
"submissionStatusEncoded",
|
|
722
|
+
"awaitSettlementEncoded",
|
|
723
|
+
"awaitProgressEncoded",
|
|
724
|
+
"cancelProgressEncoded",
|
|
725
|
+
"observePage",
|
|
726
|
+
"abortEncoded",
|
|
727
|
+
"resolveApprovalEncoded",
|
|
728
|
+
"resolveUnknownEncoded",
|
|
729
|
+
"portCall",
|
|
730
|
+
"wake"
|
|
731
|
+
]);
|
|
732
|
+
const threadRpc = {
|
|
733
|
+
submitEncoded: submitEndpoint,
|
|
734
|
+
submissionStatusEncoded: submissionStatusEndpoint,
|
|
735
|
+
awaitSettlementEncoded: awaitSettlementEndpoint,
|
|
736
|
+
awaitProgressEncoded: awaitProgressEndpoint,
|
|
737
|
+
cancelProgressEncoded: cancelProgressEndpoint,
|
|
738
|
+
observePage: observePageEndpoint,
|
|
739
|
+
abortEncoded: abortEndpoint,
|
|
740
|
+
resolveApprovalEncoded: resolveApprovalEndpoint,
|
|
741
|
+
resolveUnknownEncoded: resolveUnknownEndpoint,
|
|
742
|
+
portCall,
|
|
743
|
+
wake: () => wakeEndpoint
|
|
744
|
+
};
|
|
745
|
+
/**
|
|
746
|
+
* Bind an addressed request to its logical Thread while sharing one physical runtime. This
|
|
747
|
+
* validates local placement before invoking the same native handlers. Receipts, Submission
|
|
748
|
+
* commands and port envelopes must match this identity; progress cancellation is Thread-scoped.
|
|
749
|
+
* The producer comes from the actual runtime configuration, never from caller input. These
|
|
750
|
+
* guards supplement the existing current model/Tool and operation authorization policies.
|
|
751
|
+
*/
|
|
752
|
+
const handleRpc = Effect.fn("ThreadObject.handleRpc")(function* (threadId, operation, encoded) {
|
|
753
|
+
if (!(yield* ThreadObjectPlacement).ownsThread(threadId)) return yield* HostProtocolError.make({ message: "The Thread belongs to another Object" });
|
|
754
|
+
const { producerId } = yield* DurableRuntimeConfig;
|
|
755
|
+
return yield* threadRpc[operation](encoded).pipe(Effect.provideService(ThreadObjectIdentity, {
|
|
756
|
+
threadId,
|
|
757
|
+
producerId
|
|
758
|
+
}));
|
|
759
|
+
});
|
|
605
760
|
const alarmEndpoint = Effect.gen(function* () {
|
|
606
761
|
yield* (yield* ThreadMaintenance).pass;
|
|
607
762
|
});
|
|
@@ -626,7 +781,7 @@ const effectCfPlatformLayer = (namespaceBinding, rpcTracing = false) => {
|
|
|
626
781
|
const env = yield* WorkerEnvironment;
|
|
627
782
|
const binding = yield* threadNamespaceFromEnv(env, namespaceBinding);
|
|
628
783
|
return ThreadObjectNamespace.of({
|
|
629
|
-
|
|
784
|
+
get: (threadId) => binding.get(binding.idFromName(threadId)),
|
|
630
785
|
...rpcTracing === true ? { rpcTracing: namespaceBinding } : {}
|
|
631
786
|
});
|
|
632
787
|
}));
|
|
@@ -655,21 +810,11 @@ const make = (applicationLayer, options) => {
|
|
|
655
810
|
}));
|
|
656
811
|
}));
|
|
657
812
|
const rpc = {
|
|
658
|
-
|
|
659
|
-
submissionStatusEncoded: (encoded) => submissionStatusEndpoint(encoded),
|
|
660
|
-
awaitSettlementEncoded: (encoded) => awaitSettlementEndpoint(encoded),
|
|
661
|
-
awaitProgressEncoded: (encoded) => awaitProgressEndpoint(encoded),
|
|
662
|
-
cancelProgressEncoded: (encoded) => cancelProgressEndpoint(encoded),
|
|
663
|
-
observePage: (encoded) => observePageEndpoint(encoded),
|
|
664
|
-
abortEncoded: (encoded) => abortEndpoint(encoded),
|
|
665
|
-
resolveApprovalEncoded: (encoded) => resolveApprovalEndpoint(encoded),
|
|
666
|
-
resolveUnknownEncoded: (encoded) => resolveUnknownEndpoint(encoded),
|
|
813
|
+
...threadRpc,
|
|
667
814
|
explainEncoded: (encoded) => explainEndpoint(encoded),
|
|
668
815
|
verifyEncoded: (encoded) => verifyEndpoint(encoded),
|
|
669
816
|
retryEncoded: (encoded) => retryEndpoint(encoded),
|
|
670
|
-
obligationsEncoded: (encoded) => obligationsEndpoint(encoded)
|
|
671
|
-
portCall: (encoded) => portCallEndpoint(encoded),
|
|
672
|
-
wake: () => wakeEndpoint
|
|
817
|
+
obligationsEncoded: (encoded) => obligationsEndpoint(encoded)
|
|
673
818
|
};
|
|
674
819
|
const EffectCfThreadObject = DurableObject.make(runtime, {
|
|
675
820
|
...options.rpcTracing === true ? { rpcTracing: { service: options.namespaceBinding } } : {},
|
|
@@ -686,6 +831,6 @@ const make = (applicationLayer, options) => {
|
|
|
686
831
|
return ThreadObject;
|
|
687
832
|
};
|
|
688
833
|
//#endregion
|
|
689
|
-
export {
|
|
834
|
+
export { layer as C, layerInHost as E, ThreadObjectPorts as S, layerHostConfig as T, encodeAdminResponse as _, AdminVerifyRequest as a, portCall as b, RetryExecuted as c, VerifiedIntegrity as d, decodeAdminExplainRequest as f, decodeRetryCommand as g, decodeObligationThresholds as h, AdminResponse as i, ThreadObject_exports as l, decodeAdminVerifyRequest as m, AdminFailed as n, ExplainedRecovery as o, decodeAdminResponse as p, AdminFailure as r, ObligationsScanned as s, AdminExplainRequest as t, ThreadRpcOperation as u, handleRpc as v, layerConfig as w, submit as x, make as y };
|
|
690
835
|
|
|
691
|
-
//# sourceMappingURL=ThreadObject-
|
|
836
|
+
//# sourceMappingURL=ThreadObject-DGkXv41C.mjs.map
|