@effect-agent/platform-cloudflare 0.1.0-beta.38 → 0.1.0-beta.40
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/bindings-BrVEOkfe.d.mts +102 -0
- package/dist/browser-quick-action.d.mts +18 -1
- package/dist/browser-quick-action.mjs +14 -1
- package/dist/browser-quick-action.mjs.map +1 -1
- package/dist/browser-rest-capture.d.mts +17 -2
- package/dist/browser-rest-capture.mjs +12 -1
- package/dist/browser-rest-capture.mjs.map +1 -1
- package/dist/browser-rest-crawl.mjs +2 -2
- package/dist/browser-rest-crawl.mjs.map +1 -1
- package/dist/index.d.mts +245 -237
- package/dist/index.mjs +168 -164
- package/dist/index.mjs.map +1 -1
- package/dist/interactive-browser.d.mts +160 -2
- package/dist/interactive-browser.mjs +1166 -2
- package/dist/interactive-browser.mjs.map +1 -0
- package/dist/{scheduling-B-OFqoS9.mjs → prepared-admission-BKp_Upw2.mjs} +134 -433
- package/dist/prepared-admission-BKp_Upw2.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/scheduling.d.mts +47 -2
- package/dist/scheduling.mjs +339 -1
- package/dist/scheduling.mjs.map +1 -0
- package/dist/subscriptions.d.mts +52 -0
- package/dist/subscriptions.mjs +346 -0
- package/dist/subscriptions.mjs.map +1 -0
- package/package.json +20 -13
- package/src/alarm.ts +264 -265
- package/src/bindings.ts +34 -30
- package/src/browser-quick-action.ts +31 -0
- package/src/browser-rest-capture.ts +27 -0
- package/src/client.ts +111 -105
- package/src/code-mode-executor.ts +19 -0
- package/src/config.ts +7 -7
- package/src/index.ts +10 -11
- package/src/interactive-browser.ts +29 -1
- package/src/layers.ts +178 -257
- package/src/prepared-admission.ts +90 -0
- package/src/scheduling.ts +19 -55
- package/src/subscriptions.ts +661 -0
- package/src/{conversation-object.ts → thread-object.ts} +167 -136
- package/src/transport.ts +15 -15
- package/src/wake-scheduler.ts +17 -22
- package/dist/interactive-browser-BRo-Bfvb.d.mts +0 -144
- package/dist/interactive-browser-Cy5q-ldl.mjs +0 -1154
- package/dist/interactive-browser-Cy5q-ldl.mjs.map +0 -1
- package/dist/scheduling-B-OFqoS9.mjs.map +0 -1
- package/dist/scheduling-BJs_kHTx.d.mts +0 -142
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import type { AgentId } from "@effect-agent/core";
|
|
2
2
|
import { SubmissionId } from "@effect-agent/core";
|
|
3
|
+
import {
|
|
4
|
+
decodePortRequest,
|
|
5
|
+
encodePortResponse,
|
|
6
|
+
type PortRequest,
|
|
7
|
+
} from "@effect-agent/storage-cloudflare";
|
|
3
8
|
import {
|
|
4
9
|
AppendConflict,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
ThreadNotMaterialized,
|
|
11
|
+
ThreadRead,
|
|
12
|
+
ThreadStore,
|
|
13
|
+
ThreadStoreError,
|
|
9
14
|
DigestError,
|
|
10
15
|
DurableAgentRuntime,
|
|
11
16
|
DurableRuntimeFailpointError,
|
|
@@ -29,12 +34,7 @@ import {
|
|
|
29
34
|
SubmissionLookupByKey,
|
|
30
35
|
WakeScheduler,
|
|
31
36
|
type DurableSubmitAgent,
|
|
32
|
-
} from "@effect-agent/
|
|
33
|
-
import {
|
|
34
|
-
decodePortRequest,
|
|
35
|
-
encodePortResponse,
|
|
36
|
-
type PortRequest,
|
|
37
|
-
} from "@effect-agent/storage-cloudflare";
|
|
37
|
+
} from "@effect-agent/thread";
|
|
38
38
|
import { Effect, Layer, Option, Schema, Stream } from "effect";
|
|
39
39
|
import {
|
|
40
40
|
DurableObject as EffectCfDurableObject,
|
|
@@ -43,16 +43,16 @@ import {
|
|
|
43
43
|
} from "effect-cf";
|
|
44
44
|
|
|
45
45
|
import {
|
|
46
|
-
|
|
46
|
+
ThreadMaintenance,
|
|
47
47
|
DurableAlarmError,
|
|
48
48
|
DurableAlarmService,
|
|
49
49
|
type MaintenancePassFailure,
|
|
50
50
|
} from "./alarm.ts";
|
|
51
51
|
import {
|
|
52
|
-
|
|
52
|
+
ThreadObjectIdentity,
|
|
53
53
|
DurableObjectContext,
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
ThreadObjectNamespace,
|
|
55
|
+
threadNamespaceFromEnv,
|
|
56
56
|
type CloudflareBindingError,
|
|
57
57
|
} from "./bindings.ts";
|
|
58
58
|
import {
|
|
@@ -81,21 +81,31 @@ import {
|
|
|
81
81
|
} from "./client.ts";
|
|
82
82
|
import { AdmissionLimitExceeded, CloudflareDurableRuntimeConfig } from "./config.ts";
|
|
83
83
|
import {
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
layerConfig,
|
|
85
|
+
ThreadObjectPorts,
|
|
86
86
|
type CloudflareDurableRuntimeInitializationError,
|
|
87
87
|
type CloudflareDurableRuntimeOptions,
|
|
88
88
|
type CloudflareDurableRuntimeServices,
|
|
89
|
+
type CloudflareBootstrapServices,
|
|
89
90
|
} from "./layers.ts";
|
|
90
91
|
import { ProgressWaitRegistry } from "./progress-wait.ts";
|
|
91
92
|
|
|
93
|
+
export {
|
|
94
|
+
layer,
|
|
95
|
+
layerConfig,
|
|
96
|
+
type CloudflareDurableRuntimeOptions as RuntimeOptions,
|
|
97
|
+
type CloudflareDurableRuntimeServices as Services,
|
|
98
|
+
type CloudflareDurableRuntimeInitializationError as InitializationError,
|
|
99
|
+
type CloudflareBootstrapServices as BootstrapServices,
|
|
100
|
+
} from "./layers.ts";
|
|
101
|
+
|
|
92
102
|
/**
|
|
93
|
-
* `
|
|
103
|
+
* `ThreadObject.make(application, options)` — the Thread Durable Object
|
|
94
104
|
* (plan §1.4,
|
|
95
105
|
* D-P6-1): a factory returning a class that applications export from their Worker entry.
|
|
96
|
-
* One SQLite-backed Object per
|
|
106
|
+
* One SQLite-backed Object per Thread is the serialized owner (durability §6); the
|
|
97
107
|
* Object never runs `runResolvedWorker`'s infinite loop — each ingress event or alarm runs
|
|
98
|
-
* ONE bounded `runRecovery` + `
|
|
108
|
+
* ONE bounded `runRecovery` + `processThreadResolved` pass, and the persisted alarm
|
|
99
109
|
* (the single multiplexed slot, D-P6-2) finishes accepted work across evictions WITHOUT any
|
|
100
110
|
* incoming request.
|
|
101
111
|
*
|
|
@@ -107,21 +117,37 @@ import { ProgressWaitRegistry } from "./progress-wait.ts";
|
|
|
107
117
|
* `runRecovery` BEFORE any claim, so reconciliation still strictly precedes new work.
|
|
108
118
|
*/
|
|
109
119
|
|
|
110
|
-
/** Construction options for one deployed
|
|
111
|
-
export interface
|
|
120
|
+
/** Construction options for one deployed Thread Object class. */
|
|
121
|
+
export interface Options<
|
|
122
|
+
ApplicationServices = never,
|
|
123
|
+
EventServices = never,
|
|
124
|
+
EventLayerError = never,
|
|
125
|
+
> extends CloudflareDurableRuntimeOptions {
|
|
112
126
|
/** Accept transient native RPC tracing through effect-cf; disabled by default. */
|
|
113
127
|
readonly rpcTracing?: boolean;
|
|
114
128
|
/**
|
|
115
129
|
* Name of the Worker `env` binding carrying THIS class's `DurableObjectNamespace` — the
|
|
116
|
-
* Object's route back to sibling
|
|
130
|
+
* Object's route back to sibling Thread Objects for the WP2 cross-Object port calls
|
|
117
131
|
* and remote wakes (DEPLOY-010: the binding enters through a Layer, never ambiently).
|
|
118
132
|
*/
|
|
119
133
|
readonly namespaceBinding: string;
|
|
134
|
+
/** Acquired and finalized per native event, with access to the complete application runtime. */
|
|
135
|
+
readonly eventLayer?: Layer.Layer<
|
|
136
|
+
EventServices,
|
|
137
|
+
EventLayerError,
|
|
138
|
+
| RuntimeServices
|
|
139
|
+
| ApplicationServices
|
|
140
|
+
| EffectCfDurableObjectState.DurableObjectState
|
|
141
|
+
| WorkerEnvironment
|
|
142
|
+
>;
|
|
120
143
|
}
|
|
121
144
|
|
|
122
|
-
type EndpointServices =
|
|
123
|
-
|
|
124
|
-
|
|
145
|
+
type EndpointServices =
|
|
146
|
+
| CloudflareDurableRuntimeServices
|
|
147
|
+
| CloudflareBootstrapServices
|
|
148
|
+
| DurableObjectContext;
|
|
149
|
+
type RuntimeServices = EndpointServices | ThreadObjectNamespace;
|
|
150
|
+
type ThreadObjectInitializationError =
|
|
125
151
|
| CloudflareDurableRuntimeInitializationError
|
|
126
152
|
| CloudflareBindingError
|
|
127
153
|
| MaintenancePassFailure;
|
|
@@ -190,55 +216,53 @@ const utf8Bytes = (value: PersistedJson): number =>
|
|
|
190
216
|
* exempt: its accepted-work obligation already exists, and returning the original Receipt
|
|
191
217
|
* consumes no new quota. Refusals are typed `AdmissionLimitExceeded` and nothing is written.
|
|
192
218
|
*/
|
|
193
|
-
const gateAdmissionLimits = Effect.fn("
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
219
|
+
const gateAdmissionLimits = Effect.fn("ThreadObject.gateAdmissionLimits")(function* (request: {
|
|
220
|
+
readonly principal: SubmissionLookupByKey["principal"];
|
|
221
|
+
readonly idempotencyKey: SubmissionLookupByKey["idempotencyKey"];
|
|
222
|
+
readonly inputPayload: PersistedJson;
|
|
223
|
+
}) {
|
|
224
|
+
const identity = yield* ThreadObjectIdentity;
|
|
225
|
+
const config = yield* CloudflareDurableRuntimeConfig;
|
|
226
|
+
const ledger = yield* SubmissionLedger;
|
|
227
|
+
const { ctx } = yield* DurableObjectContext;
|
|
228
|
+
|
|
229
|
+
const existing = yield* ledger.lookup(
|
|
230
|
+
SubmissionLookupByKey.make({
|
|
231
|
+
threadId: identity.threadId,
|
|
232
|
+
principal: request.principal,
|
|
233
|
+
idempotencyKey: request.idempotencyKey,
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
if (Option.isSome(existing)) return;
|
|
237
|
+
|
|
238
|
+
const inputBytes = utf8Bytes(request.inputPayload);
|
|
239
|
+
if (inputBytes > config.limits.maxInputBytes) {
|
|
240
|
+
return yield* AdmissionLimitExceeded.make({
|
|
241
|
+
limit: "input-bytes",
|
|
242
|
+
actual: inputBytes,
|
|
243
|
+
maximum: config.limits.maxInputBytes,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
221
246
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
247
|
+
// One Thread per Object (durability §5): the local scan IS this lane's queue.
|
|
248
|
+
const nonterminal = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
249
|
+
if (nonterminal.length >= config.limits.maxQueueDepthPerLane) {
|
|
250
|
+
return yield* AdmissionLimitExceeded.make({
|
|
251
|
+
limit: "queue-depth",
|
|
252
|
+
actual: nonterminal.length,
|
|
253
|
+
maximum: config.limits.maxQueueDepthPerLane,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
231
256
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
);
|
|
257
|
+
const databaseBytes = yield* Effect.sync(() => ctx.storage.sql.databaseSize);
|
|
258
|
+
if (databaseBytes > config.limits.maxDatabaseBytes) {
|
|
259
|
+
return yield* AdmissionLimitExceeded.make({
|
|
260
|
+
limit: "database-bytes",
|
|
261
|
+
actual: databaseBytes,
|
|
262
|
+
maximum: config.limits.maxDatabaseBytes,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
});
|
|
242
266
|
|
|
243
267
|
/**
|
|
244
268
|
* The submit-capable projection of an Agent Binding on the OBJECT side: the input arrived
|
|
@@ -258,15 +282,15 @@ const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoin
|
|
|
258
282
|
Effect.mapError(protocolFailure("The submit request could not be decoded")),
|
|
259
283
|
Effect.flatMap((request) =>
|
|
260
284
|
Effect.gen(function* () {
|
|
261
|
-
const identity = yield*
|
|
262
|
-
const maintenance = yield*
|
|
285
|
+
const identity = yield* ThreadObjectIdentity;
|
|
286
|
+
const maintenance = yield* ThreadMaintenance;
|
|
263
287
|
const runtime = yield* DurableAgentRuntime;
|
|
264
288
|
yield* gateAdmissionLimits(request);
|
|
265
289
|
// Alarm invariant: the generation + alarm commit BEFORE the admission, and maintenance
|
|
266
290
|
// cannot acknowledge that generation until this mutation leaves its public RPC seam.
|
|
267
291
|
const receipt = yield* maintenance.withMutation(
|
|
268
292
|
runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
|
|
269
|
-
|
|
293
|
+
threadId: identity.threadId,
|
|
270
294
|
principal: request.principal,
|
|
271
295
|
idempotencyKey: request.idempotencyKey,
|
|
272
296
|
definitions: request.definitions,
|
|
@@ -300,14 +324,14 @@ const awaitProgressEndpoint = (encoded: unknown): Effect.Effect<unknown, never,
|
|
|
300
324
|
Effect.mapError(protocolFailure("The progress request could not be decoded")),
|
|
301
325
|
Effect.flatMap((request) =>
|
|
302
326
|
Effect.gen(function* () {
|
|
303
|
-
const identity = yield*
|
|
327
|
+
const identity = yield* ThreadObjectIdentity;
|
|
304
328
|
const runtime = yield* DurableAgentRuntime;
|
|
305
329
|
const registry = yield* ProgressWaitRegistry;
|
|
306
330
|
yield* Effect.scoped(
|
|
307
331
|
Effect.gen(function* () {
|
|
308
332
|
const cancelled = yield* registry.subscribe(request.waiterId);
|
|
309
333
|
yield* Effect.raceFirst(
|
|
310
|
-
runtime.awaitProgress(identity.
|
|
334
|
+
runtime.awaitProgress(identity.threadId, request.afterSequence),
|
|
311
335
|
cancelled,
|
|
312
336
|
);
|
|
313
337
|
}),
|
|
@@ -340,21 +364,21 @@ const observePageEndpoint = (encoded: unknown): Effect.Effect<unknown, never, En
|
|
|
340
364
|
Effect.mapError(protocolFailure("The observe request could not be decoded")),
|
|
341
365
|
Effect.flatMap((request) =>
|
|
342
366
|
Effect.gen(function* () {
|
|
343
|
-
const identity = yield*
|
|
344
|
-
const store = yield*
|
|
367
|
+
const identity = yield* ThreadObjectIdentity;
|
|
368
|
+
const store = yield* ThreadStore;
|
|
345
369
|
// The same fail-closed authorization seam the runtime's `observe` consults (P7 WP1);
|
|
346
370
|
// the default reference preserves the possession behavior.
|
|
347
371
|
const authorizer = yield* OperationAuthorizer;
|
|
348
372
|
yield* authorizer.authorize(
|
|
349
373
|
OperationAuthorizationRequest.make({
|
|
350
374
|
operation: "observe",
|
|
351
|
-
|
|
375
|
+
threadId: identity.threadId,
|
|
352
376
|
}),
|
|
353
377
|
);
|
|
354
378
|
const records = yield* Stream.runCollect(
|
|
355
379
|
store.read(
|
|
356
|
-
|
|
357
|
-
|
|
380
|
+
ThreadRead.make({
|
|
381
|
+
threadId: identity.threadId,
|
|
358
382
|
...(request.afterSequence === undefined
|
|
359
383
|
? {}
|
|
360
384
|
: { afterSequence: request.afterSequence }),
|
|
@@ -374,7 +398,7 @@ const abortEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoint
|
|
|
374
398
|
Effect.mapError(protocolFailure("The abort command could not be decoded")),
|
|
375
399
|
Effect.flatMap((command) =>
|
|
376
400
|
Effect.gen(function* () {
|
|
377
|
-
const maintenance = yield*
|
|
401
|
+
const maintenance = yield* ThreadMaintenance;
|
|
378
402
|
const runtime = yield* DurableAgentRuntime;
|
|
379
403
|
const intent = yield* maintenance.withMutation(runtime.abort(command));
|
|
380
404
|
return AbortRecorded.make({ intent });
|
|
@@ -391,7 +415,7 @@ const resolveApprovalEndpoint = (
|
|
|
391
415
|
Effect.mapError(protocolFailure("The approval command could not be decoded")),
|
|
392
416
|
Effect.flatMap((command) =>
|
|
393
417
|
Effect.gen(function* () {
|
|
394
|
-
const maintenance = yield*
|
|
418
|
+
const maintenance = yield* ThreadMaintenance;
|
|
395
419
|
const runtime = yield* DurableAgentRuntime;
|
|
396
420
|
const intent = yield* maintenance.withMutation(runtime.resolveApproval(command));
|
|
397
421
|
return ApprovalRecorded.make({ intent });
|
|
@@ -408,7 +432,7 @@ const resolveUnknownEndpoint = (
|
|
|
408
432
|
Effect.mapError(protocolFailure("The resolution command could not be decoded")),
|
|
409
433
|
Effect.flatMap((command) =>
|
|
410
434
|
Effect.gen(function* () {
|
|
411
|
-
const maintenance = yield*
|
|
435
|
+
const maintenance = yield* ThreadMaintenance;
|
|
412
436
|
const runtime = yield* DurableAgentRuntime;
|
|
413
437
|
const intent = yield* maintenance.withMutation(runtime.resolveUnknown(command));
|
|
414
438
|
return UnknownResolutionRecorded.make({ intent });
|
|
@@ -447,8 +471,8 @@ export const AdminFailure = Schema.Union([
|
|
|
447
471
|
DigestError,
|
|
448
472
|
OwnershipLost,
|
|
449
473
|
SettlementConflict,
|
|
450
|
-
|
|
451
|
-
|
|
474
|
+
ThreadStoreError,
|
|
475
|
+
ThreadNotMaterialized,
|
|
452
476
|
AppendConflict,
|
|
453
477
|
FenceRejected,
|
|
454
478
|
DurableRuntimeFailpointError,
|
|
@@ -533,11 +557,11 @@ const explainEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoi
|
|
|
533
557
|
Effect.mapError(protocolFailure("The explain request could not be decoded")),
|
|
534
558
|
Effect.flatMap((request) =>
|
|
535
559
|
Effect.gen(function* () {
|
|
536
|
-
const identity = yield*
|
|
560
|
+
const identity = yield* ThreadObjectIdentity;
|
|
537
561
|
const runtime = yield* DurableAgentRuntime;
|
|
538
562
|
const explanations =
|
|
539
563
|
request.submissionId === undefined
|
|
540
|
-
? yield* runtime.
|
|
564
|
+
? yield* runtime.explainThread(identity.threadId)
|
|
541
565
|
: [yield* runtime.explain(request.submissionId)];
|
|
542
566
|
return ExplainedRecovery.make({ explanations });
|
|
543
567
|
}),
|
|
@@ -551,9 +575,9 @@ const verifyEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoin
|
|
|
551
575
|
Effect.mapError(protocolFailure("The verify request could not be decoded")),
|
|
552
576
|
Effect.flatMap(() =>
|
|
553
577
|
Effect.gen(function* () {
|
|
554
|
-
const identity = yield*
|
|
578
|
+
const identity = yield* ThreadObjectIdentity;
|
|
555
579
|
const runtime = yield* DurableAgentRuntime;
|
|
556
|
-
const report = yield* runtime.verify(identity.
|
|
580
|
+
const report = yield* runtime.verify(identity.threadId);
|
|
557
581
|
return VerifiedIntegrity.make({ report });
|
|
558
582
|
}),
|
|
559
583
|
),
|
|
@@ -566,7 +590,7 @@ const retryEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoint
|
|
|
566
590
|
Effect.mapError(protocolFailure("The retry command could not be decoded")),
|
|
567
591
|
Effect.flatMap((command) =>
|
|
568
592
|
Effect.gen(function* () {
|
|
569
|
-
const maintenance = yield*
|
|
593
|
+
const maintenance = yield* ThreadMaintenance;
|
|
570
594
|
const runtime = yield* DurableAgentRuntime;
|
|
571
595
|
// Retry may repair durable state, so its generation + alarm commit before the mutation.
|
|
572
596
|
const report = yield* maintenance.withMutation(runtime.retry(command));
|
|
@@ -600,8 +624,8 @@ const obligationsEndpoint = (encoded: unknown): Effect.Effect<unknown, never, En
|
|
|
600
624
|
*/
|
|
601
625
|
const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
602
626
|
Effect.gen(function* () {
|
|
603
|
-
const ports = yield*
|
|
604
|
-
const maintenance = yield*
|
|
627
|
+
const ports = yield* ThreadObjectPorts;
|
|
628
|
+
const maintenance = yield* ThreadMaintenance;
|
|
605
629
|
const alarm = yield* DurableAlarmService;
|
|
606
630
|
const decoded = yield* decodePortRequest(encoded).pipe(
|
|
607
631
|
Effect.map((request) => ({ _tag: "success" as const, request })),
|
|
@@ -636,7 +660,7 @@ const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpo
|
|
|
636
660
|
// Prompt processing hint; the pre-armed alarm already guarantees convergence.
|
|
637
661
|
yield* alarm.scheduleNow.pipe(
|
|
638
662
|
Effect.catch((error) =>
|
|
639
|
-
Effect.logWarning("
|
|
663
|
+
Effect.logWarning("ThreadObject.portCall: immediate re-arm failed", error),
|
|
640
664
|
),
|
|
641
665
|
);
|
|
642
666
|
}
|
|
@@ -644,16 +668,16 @@ const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpo
|
|
|
644
668
|
});
|
|
645
669
|
|
|
646
670
|
const wakeEndpoint: Effect.Effect<void, never, EndpointServices> = Effect.gen(function* () {
|
|
647
|
-
const identity = yield*
|
|
671
|
+
const identity = yield* ThreadObjectIdentity;
|
|
648
672
|
const wake = yield* WakeScheduler;
|
|
649
673
|
// Route the remote hint through this incarnation's scheduler so scoped progress waiters and
|
|
650
674
|
// the alarm receive the same hint. Delivery remains droppable; canonical storage is authority.
|
|
651
|
-
yield* wake.notify(identity.
|
|
675
|
+
yield* wake.notify(identity.threadId);
|
|
652
676
|
});
|
|
653
677
|
|
|
654
678
|
const alarmEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices> = Effect.gen(
|
|
655
679
|
function* () {
|
|
656
|
-
const maintenance = yield*
|
|
680
|
+
const maintenance = yield* ThreadMaintenance;
|
|
657
681
|
// Typed pass failures propagate: the rejected promise makes workerd retry the alarm
|
|
658
682
|
// (at-least-once delivery), and the dirty generation retains a committed slot meanwhile.
|
|
659
683
|
yield* maintenance.pass;
|
|
@@ -662,10 +686,10 @@ const alarmEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointService
|
|
|
662
686
|
|
|
663
687
|
const gateEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices> = Effect.gen(
|
|
664
688
|
function* () {
|
|
665
|
-
// Forcing
|
|
689
|
+
// Forcing ThreadMaintenance forces the whole Layer stack: migration + exact-version
|
|
666
690
|
// check + configuration decode (DEPLOY-008 fails typed here, before any mutation), then
|
|
667
691
|
// the defensive local ensure-alarm half of the invariant. LOCAL-ONLY by construction.
|
|
668
|
-
const maintenance = yield*
|
|
692
|
+
const maintenance = yield* ThreadMaintenance;
|
|
669
693
|
yield* maintenance.ensureAlarm;
|
|
670
694
|
},
|
|
671
695
|
);
|
|
@@ -679,7 +703,7 @@ const effectCfPlatformLayer = (
|
|
|
679
703
|
namespaceBinding: string,
|
|
680
704
|
rpcTracing = false,
|
|
681
705
|
): Layer.Layer<
|
|
682
|
-
DurableObjectContext |
|
|
706
|
+
DurableObjectContext | ThreadObjectNamespace,
|
|
683
707
|
CloudflareBindingError,
|
|
684
708
|
EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
|
|
685
709
|
> => {
|
|
@@ -690,11 +714,11 @@ const effectCfPlatformLayer = (
|
|
|
690
714
|
return DurableObjectContext.of({ ctx: state.raw, env });
|
|
691
715
|
}),
|
|
692
716
|
);
|
|
693
|
-
const namespace = Layer.effect(
|
|
717
|
+
const namespace = Layer.effect(ThreadObjectNamespace)(
|
|
694
718
|
Effect.gen(function* () {
|
|
695
719
|
const env = yield* WorkerEnvironment;
|
|
696
|
-
const binding = yield*
|
|
697
|
-
return
|
|
720
|
+
const binding = yield* threadNamespaceFromEnv(env, namespaceBinding);
|
|
721
|
+
return ThreadObjectNamespace.of({
|
|
698
722
|
namespace: binding,
|
|
699
723
|
...(rpcTracing === true ? { rpcTracing: namespaceBinding } : {}),
|
|
700
724
|
});
|
|
@@ -703,8 +727,8 @@ const effectCfPlatformLayer = (
|
|
|
703
727
|
return Layer.merge(context, namespace);
|
|
704
728
|
};
|
|
705
729
|
|
|
706
|
-
/** The public endpoints and effect-cf invocation hook of one
|
|
707
|
-
export interface
|
|
730
|
+
/** The public endpoints and effect-cf invocation hook of one Thread Object instance. */
|
|
731
|
+
export interface Instance<EventServices = never> extends InstanceType<
|
|
708
732
|
EffectCfDurableObject.DurableObjectClass<Record<never, never>, RuntimeServices | EventServices>
|
|
709
733
|
> {
|
|
710
734
|
submitEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
@@ -724,34 +748,39 @@ export interface ConversationObjectInstance<EventServices = never> extends Insta
|
|
|
724
748
|
alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void;
|
|
725
749
|
}
|
|
726
750
|
|
|
727
|
-
/** The constructor shape workerd instantiates for each
|
|
728
|
-
export interface
|
|
729
|
-
new (ctx: DurableObjectState, env: Cloudflare.Env):
|
|
751
|
+
/** The constructor shape workerd instantiates for each Thread Object. */
|
|
752
|
+
export interface Class<EventServices = never> {
|
|
753
|
+
new (ctx: DurableObjectState, env: Cloudflare.Env): Instance<EventServices>;
|
|
730
754
|
}
|
|
731
755
|
|
|
732
756
|
/**
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
757
|
+
* Export a composed application Layer as a native Durable Object class.
|
|
758
|
+
* Bootstrap services are provided to the whole graph before it acquires, so application Layers
|
|
759
|
+
* can yield effect-cf's WorkerEnvironment and DurableObjectState, derived identity, and Crypto.
|
|
760
|
+
* Application dependencies remain visible until Layer.provide satisfies them. effect-cf owns the
|
|
761
|
+
* cached ManagedRuntime, native RPC methods, event scopes, and telemetry flushing.
|
|
762
|
+
* Initialization is local and bounded inside the constructor gate. Cloudflare eviction does not
|
|
763
|
+
* guarantee finalizers; put resources requiring timely release in scoped operations or eventLayer.
|
|
738
764
|
*/
|
|
739
|
-
export const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
765
|
+
export const make = <
|
|
766
|
+
ApplicationServices,
|
|
767
|
+
ApplicationError,
|
|
768
|
+
EventServices = never,
|
|
769
|
+
EventLayerError = never,
|
|
770
|
+
>(
|
|
771
|
+
applicationLayer: Layer.Layer<
|
|
772
|
+
CloudflareDurableRuntimeServices | ApplicationServices,
|
|
773
|
+
ApplicationError,
|
|
774
|
+
| CloudflareBootstrapServices
|
|
746
775
|
| EffectCfDurableObjectState.DurableObjectState
|
|
747
776
|
| WorkerEnvironment
|
|
777
|
+
| DurableObjectContext
|
|
778
|
+
| ThreadObjectNamespace
|
|
748
779
|
>,
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
|
|
754
|
-
> = CloudflareDurableRuntime.layer(options).pipe(
|
|
780
|
+
options: Options<ApplicationServices, EventServices, EventLayerError>,
|
|
781
|
+
): Class<ApplicationServices | EventServices> => {
|
|
782
|
+
const application = applicationLayer.pipe(
|
|
783
|
+
Layer.provideMerge(layerConfig(options)),
|
|
755
784
|
Layer.provideMerge(effectCfPlatformLayer(options.namespaceBinding, options.rpcTracing)),
|
|
756
785
|
);
|
|
757
786
|
|
|
@@ -759,8 +788,8 @@ export const makeConversationObjectClass = <EventLayerError = never, EventServic
|
|
|
759
788
|
// the ManagedRuntime, while this effectContext ensures its first Layer build enters the gate
|
|
760
789
|
// before migration, compatibility checks, or alarm inspection touch Object storage.
|
|
761
790
|
const runtime: Layer.Layer<
|
|
762
|
-
RuntimeServices,
|
|
763
|
-
|
|
791
|
+
RuntimeServices | ApplicationServices,
|
|
792
|
+
ThreadObjectInitializationError | ApplicationError,
|
|
764
793
|
EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
|
|
765
794
|
> = Layer.effectContext(
|
|
766
795
|
Effect.gen(function* () {
|
|
@@ -791,17 +820,19 @@ export const makeConversationObjectClass = <EventLayerError = never, EventServic
|
|
|
791
820
|
obligationsEncoded: (encoded: unknown) => obligationsEndpoint(encoded),
|
|
792
821
|
portCall: (encoded: unknown) => portCallEndpoint(encoded),
|
|
793
822
|
wake: () => wakeEndpoint,
|
|
794
|
-
} satisfies EffectCfDurableObject.DurableObjectRpc<
|
|
823
|
+
} satisfies EffectCfDurableObject.DurableObjectRpc<
|
|
824
|
+
RuntimeServices | ApplicationServices | EventServices
|
|
825
|
+
>;
|
|
795
826
|
|
|
796
|
-
const
|
|
797
|
-
RuntimeServices,
|
|
798
|
-
|
|
827
|
+
const EffectCfThreadObject = EffectCfDurableObject.make<
|
|
828
|
+
RuntimeServices | ApplicationServices,
|
|
829
|
+
ThreadObjectInitializationError | ApplicationError,
|
|
799
830
|
EventServices,
|
|
800
831
|
EventLayerError,
|
|
801
832
|
typeof rpc
|
|
802
833
|
>(runtime, {
|
|
803
834
|
...(options.rpcTracing === true ? { rpcTracing: { service: options.namespaceBinding } } : {}),
|
|
804
|
-
...(
|
|
835
|
+
...(options.eventLayer === undefined ? {} : { eventLayer: options.eventLayer }),
|
|
805
836
|
// Force the gated runtime Layer when Cloudflare loads this Object incarnation. Recovery stays
|
|
806
837
|
// in each bounded pass so cross-Object initialization cannot deadlock.
|
|
807
838
|
initialize: Effect.void,
|
|
@@ -812,11 +843,11 @@ export const makeConversationObjectClass = <EventLayerError = never, EventServic
|
|
|
812
843
|
// effect-cf's class type keeps `alarm` optional even when the handler option is present. This
|
|
813
844
|
// concrete override reflects this factory's stronger contract while delegating execution to
|
|
814
845
|
// the effect-cf runtime unchanged.
|
|
815
|
-
class
|
|
846
|
+
class ThreadObject extends EffectCfThreadObject {
|
|
816
847
|
override alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void {
|
|
817
848
|
return super.alarm?.(alarmInfo);
|
|
818
849
|
}
|
|
819
850
|
}
|
|
820
851
|
|
|
821
|
-
return
|
|
852
|
+
return ThreadObject;
|
|
822
853
|
};
|
package/src/transport.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ThreadPortTransport, portTransportFailure } from "@effect-agent/storage-cloudflare";
|
|
2
2
|
import { Effect, Layer } from "effect";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { ThreadObjectNamespace } from "./bindings.ts";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* `
|
|
8
|
-
* `portCall(envelope)` on the stub of the Object that owns the addressed
|
|
9
|
-
* (`namespace.idFromName(
|
|
7
|
+
* `ThreadPortTransport` over native Durable Object JS RPC (decision D-P6-3): one
|
|
8
|
+
* `portCall(envelope)` on the stub of the Object that owns the addressed Thread
|
|
9
|
+
* (`namespace.idFromName(threadId)` — the identity rule, plan §1.2). The envelopes are
|
|
10
10
|
* already Schema-encoded JSON, so the RPC boundary carries only structured-cloneable values;
|
|
11
11
|
* the protocol module stays transport-agnostic and fetch-with-JSON remains the documented
|
|
12
12
|
* fallback carrier.
|
|
@@ -16,21 +16,21 @@ import { ConversationObjectNamespace } from "./bindings.ts";
|
|
|
16
16
|
* when present) and NEVER as a fabricated answer: on `resolveAdmission` the routing layer
|
|
17
17
|
* turns exactly this error into `AdmissionIndeterminate` (SUB-031).
|
|
18
18
|
*/
|
|
19
|
-
export const
|
|
20
|
-
|
|
19
|
+
export const threadPortTransportLayer: Layer.Layer<
|
|
20
|
+
ThreadPortTransport,
|
|
21
21
|
never,
|
|
22
|
-
|
|
23
|
-
> = Layer.effect(
|
|
22
|
+
ThreadObjectNamespace
|
|
23
|
+
> = Layer.effect(ThreadPortTransport)(
|
|
24
24
|
Effect.gen(function* () {
|
|
25
|
-
const { namespace } = yield*
|
|
26
|
-
return
|
|
27
|
-
call: (
|
|
25
|
+
const { namespace } = yield* ThreadObjectNamespace;
|
|
26
|
+
return ThreadPortTransport.of({
|
|
27
|
+
call: (threadId, request) =>
|
|
28
28
|
Effect.tryPromise({
|
|
29
|
-
try: () => namespace.get(namespace.idFromName(
|
|
30
|
-
catch: (cause) => portTransportFailure(
|
|
29
|
+
try: () => namespace.get(namespace.idFromName(threadId)).portCall(request),
|
|
30
|
+
catch: (cause) => portTransportFailure(threadId, cause),
|
|
31
31
|
}).pipe(
|
|
32
32
|
Effect.withSpan("CloudflarePortTransport.call", {
|
|
33
|
-
attributes: {
|
|
33
|
+
attributes: { threadId },
|
|
34
34
|
}),
|
|
35
35
|
),
|
|
36
36
|
});
|