@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
package/src/ThreadObject.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { type AgentId } from "@effect-agent/core/Identifiers";
|
|
1
|
+
import { type AgentId, type ThreadId } from "@effect-agent/core/Identifiers";
|
|
2
2
|
import { SubmissionId } from "@effect-agent/core/Identifiers";
|
|
3
3
|
import {
|
|
4
4
|
decodePortRequest,
|
|
5
5
|
encodePortResponse,
|
|
6
|
+
LedgerLookupResult,
|
|
7
|
+
PortFailed,
|
|
8
|
+
PortProtocolError,
|
|
9
|
+
PortSucceeded,
|
|
6
10
|
type PortRequest,
|
|
11
|
+
type PortResponse,
|
|
7
12
|
} from "@effect-agent/storage-cloudflare/PortProtocol";
|
|
8
13
|
import {
|
|
9
14
|
IntegrityReport,
|
|
@@ -16,6 +21,7 @@ import {
|
|
|
16
21
|
import { DigestError } from "@effect-agent/thread/Digest";
|
|
17
22
|
import {
|
|
18
23
|
DurableAgentRuntime,
|
|
24
|
+
DurableRuntimeConfig,
|
|
19
25
|
RecoveryReport,
|
|
20
26
|
type DurableSubmitAgent,
|
|
21
27
|
} from "@effect-agent/thread/DurableAgentRuntime";
|
|
@@ -55,10 +61,13 @@ import {
|
|
|
55
61
|
ThreadMaintenance,
|
|
56
62
|
DurableAlarmError,
|
|
57
63
|
DurableAlarmService,
|
|
64
|
+
ThreadMutationGate,
|
|
65
|
+
publishCommitted,
|
|
58
66
|
type MaintenancePassFailure,
|
|
59
67
|
} from "./Alarm.ts";
|
|
60
68
|
import {
|
|
61
69
|
ThreadObjectIdentity,
|
|
70
|
+
ThreadObjectPlacement,
|
|
62
71
|
DurableObjectContext,
|
|
63
72
|
ThreadObjectNamespace,
|
|
64
73
|
threadNamespaceFromEnv,
|
|
@@ -89,6 +98,7 @@ import {
|
|
|
89
98
|
encodeHostResponse,
|
|
90
99
|
type HostFailure,
|
|
91
100
|
type HostResponse,
|
|
101
|
+
type SubmitRequest,
|
|
92
102
|
} from "./CloudflareThreadClient.ts";
|
|
93
103
|
import {
|
|
94
104
|
layerConfig,
|
|
@@ -103,6 +113,9 @@ import { ProgressWaitRegistry } from "./internal/progress-wait.ts";
|
|
|
103
113
|
export {
|
|
104
114
|
layer,
|
|
105
115
|
layerConfig,
|
|
116
|
+
layerHostConfig,
|
|
117
|
+
layerInHost,
|
|
118
|
+
ThreadObjectPorts,
|
|
106
119
|
type ThreadPublicationOptions as PublicationOptions,
|
|
107
120
|
type CloudflareDurableRuntimeOptions as RuntimeOptions,
|
|
108
121
|
type CloudflareDurableRuntimeServices as Services,
|
|
@@ -231,19 +244,21 @@ const utf8Bytes = (value: PersistedJson): number =>
|
|
|
231
244
|
* exempt: its accepted-work obligation already exists, and returning the original Receipt
|
|
232
245
|
* consumes no new quota. Refusals are typed `AdmissionLimitExceeded` and nothing is written.
|
|
233
246
|
*/
|
|
234
|
-
const gateAdmissionLimits = Effect.fn("ThreadObject.gateAdmissionLimits")(function* (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
) {
|
|
240
255
|
const config = yield* CloudflareDurableRuntimeConfig;
|
|
241
256
|
const ledger = yield* SubmissionLedger;
|
|
242
257
|
const { ctx } = yield* DurableObjectContext;
|
|
243
258
|
|
|
244
259
|
const existing = yield* ledger.lookup(
|
|
245
260
|
SubmissionLookupByKey.make({
|
|
246
|
-
threadId
|
|
261
|
+
threadId,
|
|
247
262
|
principal: request.principal,
|
|
248
263
|
idempotencyKey: request.idempotencyKey,
|
|
249
264
|
}),
|
|
@@ -261,8 +276,10 @@ const gateAdmissionLimits = Effect.fn("ThreadObject.gateAdmissionLimits")(functi
|
|
|
261
276
|
});
|
|
262
277
|
}
|
|
263
278
|
|
|
264
|
-
|
|
265
|
-
|
|
279
|
+
const nonterminal = yield* ledger.scanNonterminal.pipe(
|
|
280
|
+
Stream.filter((submission) => submission.threadId === threadId),
|
|
281
|
+
Stream.runCollect,
|
|
282
|
+
);
|
|
266
283
|
|
|
267
284
|
if (nonterminal.length >= config.limits.maxQueueDepthPerLane) {
|
|
268
285
|
return yield* AdmissionLimitExceeded.make({
|
|
@@ -296,39 +313,110 @@ const passthroughSubmitAgent = (agentId: AgentId): DurableSubmitAgent<typeof Per
|
|
|
296
313
|
},
|
|
297
314
|
});
|
|
298
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
|
+
|
|
299
413
|
const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
300
414
|
decodeSubmitRequest(encoded).pipe(
|
|
301
415
|
Effect.mapError(protocolFailure("The submit request could not be decoded")),
|
|
302
416
|
Effect.flatMap((request) =>
|
|
303
417
|
Effect.gen(function* () {
|
|
304
418
|
const identity = yield* ThreadObjectIdentity;
|
|
305
|
-
const
|
|
306
|
-
const runtime = yield* DurableAgentRuntime;
|
|
307
|
-
|
|
308
|
-
yield* gateAdmissionLimits(request);
|
|
309
|
-
|
|
310
|
-
// Alarm invariant: the generation + alarm commit BEFORE the admission, and maintenance
|
|
311
|
-
// cannot acknowledge that generation until this mutation leaves its public RPC seam.
|
|
312
|
-
const receipt = yield* maintenance.withMutation(
|
|
313
|
-
runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
|
|
314
|
-
threadId: identity.threadId,
|
|
315
|
-
principal: request.principal,
|
|
316
|
-
idempotencyKey: request.idempotencyKey,
|
|
317
|
-
...(request.admissionGroup === undefined
|
|
318
|
-
? {}
|
|
319
|
-
: { admissionGroup: request.admissionGroup }),
|
|
320
|
-
...(request.admissionFence === undefined
|
|
321
|
-
? {}
|
|
322
|
-
: { admissionFence: request.admissionFence }),
|
|
323
|
-
...(request.workerAdmission === undefined
|
|
324
|
-
? {}
|
|
325
|
-
: { workerAdmission: request.workerAdmission }),
|
|
326
|
-
...(request.messageAdmission === undefined
|
|
327
|
-
? {}
|
|
328
|
-
: { messageAdmission: request.messageAdmission }),
|
|
329
|
-
definitions: request.definitions,
|
|
330
|
-
}),
|
|
331
|
-
);
|
|
419
|
+
const receipt = yield* submit(identity.threadId, request);
|
|
332
420
|
|
|
333
421
|
return SubmitSucceeded.make({ receipt });
|
|
334
422
|
}),
|
|
@@ -344,6 +432,17 @@ const submissionStatusEndpoint = (
|
|
|
344
432
|
Effect.mapError(protocolFailure("The receipt could not be decoded")),
|
|
345
433
|
Effect.flatMap((receipt) =>
|
|
346
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);
|
|
347
446
|
const runtime = yield* DurableAgentRuntime;
|
|
348
447
|
|
|
349
448
|
return SubmissionStatusResponse.make({ status: yield* runtime.submissionStatus(receipt) });
|
|
@@ -360,6 +459,17 @@ const awaitSettlementEndpoint = (
|
|
|
360
459
|
Effect.mapError(protocolFailure("The receipt could not be decoded")),
|
|
361
460
|
Effect.flatMap((receipt) =>
|
|
362
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);
|
|
363
473
|
const runtime = yield* DurableAgentRuntime;
|
|
364
474
|
const settlement = yield* runtime.awaitSettlement(receipt);
|
|
365
475
|
|
|
@@ -381,7 +491,9 @@ const awaitProgressEndpoint = (encoded: unknown): Effect.Effect<unknown, never,
|
|
|
381
491
|
|
|
382
492
|
yield* Effect.scoped(
|
|
383
493
|
Effect.gen(function* () {
|
|
384
|
-
const cancelled = yield* registry.subscribe(
|
|
494
|
+
const cancelled = yield* registry.subscribe(
|
|
495
|
+
JSON.stringify([identity.threadId, request.waiterId]),
|
|
496
|
+
);
|
|
385
497
|
|
|
386
498
|
yield* Effect.raceFirst(
|
|
387
499
|
runtime.awaitProgress(identity.threadId, request.afterSequence),
|
|
@@ -404,9 +516,10 @@ const cancelProgressEndpoint = (
|
|
|
404
516
|
Effect.mapError(protocolFailure("The progress cancellation could not be decoded")),
|
|
405
517
|
Effect.flatMap((request) =>
|
|
406
518
|
Effect.gen(function* () {
|
|
519
|
+
const identity = yield* ThreadObjectIdentity;
|
|
407
520
|
const registry = yield* ProgressWaitRegistry;
|
|
408
521
|
|
|
409
|
-
yield* registry.cancel(request.waiterId);
|
|
522
|
+
yield* registry.cancel(JSON.stringify([identity.threadId, request.waiterId]));
|
|
410
523
|
|
|
411
524
|
return ProgressCancelled.make();
|
|
412
525
|
}),
|
|
@@ -457,6 +570,15 @@ const abortEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoint
|
|
|
457
570
|
Effect.mapError(protocolFailure("The abort command could not be decoded")),
|
|
458
571
|
Effect.flatMap((command) =>
|
|
459
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);
|
|
460
582
|
const maintenance = yield* ThreadMaintenance;
|
|
461
583
|
const runtime = yield* DurableAgentRuntime;
|
|
462
584
|
const intent = yield* maintenance.withMutation(runtime.abort(command));
|
|
@@ -475,6 +597,15 @@ const resolveApprovalEndpoint = (
|
|
|
475
597
|
Effect.mapError(protocolFailure("The approval command could not be decoded")),
|
|
476
598
|
Effect.flatMap((command) =>
|
|
477
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);
|
|
478
609
|
const maintenance = yield* ThreadMaintenance;
|
|
479
610
|
const runtime = yield* DurableAgentRuntime;
|
|
480
611
|
const intent = yield* maintenance.withMutation(runtime.resolveApproval(command));
|
|
@@ -493,6 +624,15 @@ const resolveUnknownEndpoint = (
|
|
|
493
624
|
Effect.mapError(protocolFailure("The resolution command could not be decoded")),
|
|
494
625
|
Effect.flatMap((command) =>
|
|
495
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);
|
|
496
636
|
const maintenance = yield* ThreadMaintenance;
|
|
497
637
|
const runtime = yield* DurableAgentRuntime;
|
|
498
638
|
const intent = yield* maintenance.withMutation(runtime.resolveUnknown(command));
|
|
@@ -692,7 +832,30 @@ const obligationsEndpoint = (encoded: unknown): Effect.Effect<unknown, never, En
|
|
|
692
832
|
* alarm so the mutated lane is processed promptly. Protocol anomalies answer
|
|
693
833
|
* `PortFailed(PortProtocolError)`.
|
|
694
834
|
*/
|
|
695
|
-
const
|
|
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
|
+
> =>
|
|
696
859
|
Effect.gen(function* () {
|
|
697
860
|
const ports = yield* ThreadObjectPorts;
|
|
698
861
|
const maintenance = yield* ThreadMaintenance;
|
|
@@ -708,6 +871,27 @@ const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpo
|
|
|
708
871
|
`The port request could not be decoded: ${decoded.message}`,
|
|
709
872
|
);
|
|
710
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));
|
|
711
895
|
const mutating = isMutatingPortRequest(decoded.request);
|
|
712
896
|
|
|
713
897
|
const handled = yield* (
|
|
@@ -724,13 +908,7 @@ const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpo
|
|
|
724
908
|
);
|
|
725
909
|
}
|
|
726
910
|
|
|
727
|
-
const response = yield*
|
|
728
|
-
Effect.catch((error) =>
|
|
729
|
-
Effect.succeed(
|
|
730
|
-
encodedPortProtocolFailure(`The port response could not be encoded: ${error.message}`),
|
|
731
|
-
),
|
|
732
|
-
),
|
|
733
|
-
);
|
|
911
|
+
const response = yield* encodePortResponseTotal(handled.value);
|
|
734
912
|
|
|
735
913
|
if (mutating) {
|
|
736
914
|
// Prompt processing hint; the pre-armed alarm already guarantees convergence.
|
|
@@ -753,6 +931,63 @@ const wakeEndpoint: Effect.Effect<void, never, EndpointServices> = Effect.gen(fu
|
|
|
753
931
|
yield* wake.notify(identity.threadId);
|
|
754
932
|
});
|
|
755
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
|
+
|
|
756
991
|
const alarmEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices> = Effect.gen(
|
|
757
992
|
function* () {
|
|
758
993
|
const maintenance = yield* ThreadMaintenance;
|
|
@@ -802,7 +1037,7 @@ const effectCfPlatformLayer = (
|
|
|
802
1037
|
const binding = yield* threadNamespaceFromEnv(env, namespaceBinding);
|
|
803
1038
|
|
|
804
1039
|
return ThreadObjectNamespace.of({
|
|
805
|
-
|
|
1040
|
+
get: (threadId) => binding.get(binding.idFromName(threadId)),
|
|
806
1041
|
...(rpcTracing === true ? { rpcTracing: namespaceBinding } : {}),
|
|
807
1042
|
});
|
|
808
1043
|
}),
|
|
@@ -896,21 +1131,11 @@ export const make = <
|
|
|
896
1131
|
);
|
|
897
1132
|
|
|
898
1133
|
const rpc = {
|
|
899
|
-
|
|
900
|
-
submissionStatusEncoded: (encoded: unknown) => submissionStatusEndpoint(encoded),
|
|
901
|
-
awaitSettlementEncoded: (encoded: unknown) => awaitSettlementEndpoint(encoded),
|
|
902
|
-
awaitProgressEncoded: (encoded: unknown) => awaitProgressEndpoint(encoded),
|
|
903
|
-
cancelProgressEncoded: (encoded: unknown) => cancelProgressEndpoint(encoded),
|
|
904
|
-
observePage: (encoded: unknown) => observePageEndpoint(encoded),
|
|
905
|
-
abortEncoded: (encoded: unknown) => abortEndpoint(encoded),
|
|
906
|
-
resolveApprovalEncoded: (encoded: unknown) => resolveApprovalEndpoint(encoded),
|
|
907
|
-
resolveUnknownEncoded: (encoded: unknown) => resolveUnknownEndpoint(encoded),
|
|
1134
|
+
...threadRpc,
|
|
908
1135
|
explainEncoded: (encoded: unknown) => explainEndpoint(encoded),
|
|
909
1136
|
verifyEncoded: (encoded: unknown) => verifyEndpoint(encoded),
|
|
910
1137
|
retryEncoded: (encoded: unknown) => retryEndpoint(encoded),
|
|
911
1138
|
obligationsEncoded: (encoded: unknown) => obligationsEndpoint(encoded),
|
|
912
|
-
portCall: (encoded: unknown) => portCallEndpoint(encoded),
|
|
913
|
-
wake: () => wakeEndpoint,
|
|
914
1139
|
} satisfies EffectCfDurableObject.DurableObjectRpc<
|
|
915
1140
|
RuntimeServices | ApplicationServices | EventServices
|
|
916
1141
|
>;
|
package/src/WakeScheduler.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { makeWakeSubscriptionHub, WakeScheduler } from "@effect-agent/thread/Wak
|
|
|
3
3
|
import { Effect, Layer, PubSub, Schema, Stream } from "effect";
|
|
4
4
|
|
|
5
5
|
import { DurableAlarmService } from "./Alarm.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { ThreadObjectPlacement, ThreadObjectNamespace } from "./CloudflareBindings.ts";
|
|
7
7
|
import { safeCauseMessage } from "./internal/boundary.ts";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -36,12 +36,12 @@ class RemoteWakeDropped extends Schema.TaggedError<RemoteWakeDropped>()("RemoteW
|
|
|
36
36
|
export const cloudflareWakeSchedulerLayer: Layer.Layer<
|
|
37
37
|
WakeScheduler,
|
|
38
38
|
never,
|
|
39
|
-
DurableAlarmService |
|
|
39
|
+
DurableAlarmService | ThreadObjectPlacement | ThreadObjectNamespace
|
|
40
40
|
> = Layer.effect(WakeScheduler)(
|
|
41
41
|
Effect.gen(function* () {
|
|
42
42
|
const alarm = yield* DurableAlarmService;
|
|
43
|
-
const
|
|
44
|
-
const {
|
|
43
|
+
const placement = yield* ThreadObjectPlacement;
|
|
44
|
+
const { get } = yield* ThreadObjectNamespace;
|
|
45
45
|
const hints = yield* PubSub.sliding<ThreadId>(WAKE_BUFFER_CAPACITY);
|
|
46
46
|
const progress = yield* makeWakeSubscriptionHub;
|
|
47
47
|
|
|
@@ -61,7 +61,7 @@ export const cloudflareWakeSchedulerLayer: Layer.Layer<
|
|
|
61
61
|
|
|
62
62
|
const notifyRemote = (threadId: ThreadId) =>
|
|
63
63
|
Effect.tryPromise({
|
|
64
|
-
try: () =>
|
|
64
|
+
try: () => get(threadId).wake(),
|
|
65
65
|
catch: (cause) =>
|
|
66
66
|
RemoteWakeDropped.make({
|
|
67
67
|
threadId,
|
|
@@ -77,7 +77,7 @@ export const cloudflareWakeSchedulerLayer: Layer.Layer<
|
|
|
77
77
|
|
|
78
78
|
return WakeScheduler.of({
|
|
79
79
|
notify: (threadId) =>
|
|
80
|
-
|
|
80
|
+
placement.ownsThread(threadId) ? notifyLocal(threadId) : notifyRemote(threadId),
|
|
81
81
|
subscribe: progress.subscribe,
|
|
82
82
|
wakes: Stream.fromPubSub(hints),
|
|
83
83
|
});
|