@effect-agent/platform-cloudflare 0.1.0-beta.14 → 0.1.0-beta.16
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/index.d.mts +192 -120
- package/dist/index.mjs +351 -105
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/alarm.ts +330 -113
- package/src/bindings.ts +4 -0
- package/src/client.ts +150 -2
- package/src/conversation-object.ts +87 -67
- package/src/index.ts +4 -2
- package/src/layers.ts +39 -7
- package/src/progress-wait.ts +103 -0
- package/src/wake-scheduler.ts +5 -2
package/src/client.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
IdempotencyKey,
|
|
19
19
|
JoinedToHost,
|
|
20
20
|
LedgerError,
|
|
21
|
+
OperationDenied,
|
|
21
22
|
PersistedJson,
|
|
22
23
|
Principal,
|
|
23
24
|
Receipt,
|
|
@@ -29,7 +30,7 @@ import {
|
|
|
29
30
|
type DurableSubmitAgent,
|
|
30
31
|
type DurableSubmitOptions,
|
|
31
32
|
} from "@effect-agent/session";
|
|
32
|
-
import { Context, Effect, Layer, Schema } from "effect";
|
|
33
|
+
import { Context, Crypto, Duration, Effect, Layer, Schema } from "effect";
|
|
33
34
|
|
|
34
35
|
import { DurableAlarmError } from "./alarm.ts";
|
|
35
36
|
import { ConversationObjectNamespace, type ConversationObjectRpc } from "./bindings.ts";
|
|
@@ -73,6 +74,10 @@ export class ConversationClientError extends Schema.TaggedError<ConversationClie
|
|
|
73
74
|
conversationId: Schema.String,
|
|
74
75
|
message: Schema.String,
|
|
75
76
|
cause: Schema.optionalKey(Schema.Defect()),
|
|
77
|
+
/** Cloudflare's own classification for a failure safe to retry with a fresh stub. */
|
|
78
|
+
retryable: Schema.optionalKey(Schema.Boolean),
|
|
79
|
+
/** Cloudflare overloads are surfaced immediately instead of adding retry pressure. */
|
|
80
|
+
overloaded: Schema.optionalKey(Schema.Boolean),
|
|
76
81
|
},
|
|
77
82
|
) {}
|
|
78
83
|
|
|
@@ -104,6 +109,21 @@ export class ObservePageRequest extends Schema.Class<ObservePageRequest>(
|
|
|
104
109
|
limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1_024)),
|
|
105
110
|
}) {}
|
|
106
111
|
|
|
112
|
+
/** One event-driven wait for canonical progress strictly after this sequence. */
|
|
113
|
+
export class AwaitProgressRequest extends Schema.Class<AwaitProgressRequest>(
|
|
114
|
+
"@effect-agent/platform-cloudflare/AwaitProgressRequest",
|
|
115
|
+
)({
|
|
116
|
+
afterSequence: CanonicalSequence,
|
|
117
|
+
waiterId: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256)),
|
|
118
|
+
}) {}
|
|
119
|
+
|
|
120
|
+
/** Best-effort cancellation of one in-flight progress RPC. */
|
|
121
|
+
export class CancelProgressRequest extends Schema.Class<CancelProgressRequest>(
|
|
122
|
+
"@effect-agent/platform-cloudflare/CancelProgressRequest",
|
|
123
|
+
)({
|
|
124
|
+
waiterId: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256)),
|
|
125
|
+
}) {}
|
|
126
|
+
|
|
107
127
|
// ---------------------------------------------------------------------------
|
|
108
128
|
// Responses
|
|
109
129
|
// ---------------------------------------------------------------------------
|
|
@@ -130,6 +150,7 @@ export const HostFailure = Schema.Union([
|
|
|
130
150
|
DurableRuntimeFailpointError,
|
|
131
151
|
AdmissionLimitExceeded,
|
|
132
152
|
DurableAlarmError,
|
|
153
|
+
OperationDenied,
|
|
133
154
|
HostProtocolError,
|
|
134
155
|
]);
|
|
135
156
|
export type HostFailure = typeof HostFailure.Type;
|
|
@@ -152,6 +173,15 @@ export class ObservedPage extends Schema.TaggedClass<ObservedPage>(
|
|
|
152
173
|
records: Schema.Array(CanonicalRecordEnvelope).check(Schema.isMaxLength(1_024)),
|
|
153
174
|
}) {}
|
|
154
175
|
|
|
176
|
+
/** A record was already committed or an incarnation-local hint says the caller should re-read. */
|
|
177
|
+
export class ProgressObserved extends Schema.TaggedClass<ProgressObserved>(
|
|
178
|
+
"@effect-agent/platform-cloudflare/ProgressObserved",
|
|
179
|
+
)("ProgressObserved", {}) {}
|
|
180
|
+
|
|
181
|
+
export class ProgressCancelled extends Schema.TaggedClass<ProgressCancelled>(
|
|
182
|
+
"@effect-agent/platform-cloudflare/ProgressCancelled",
|
|
183
|
+
)("ProgressCancelled", {}) {}
|
|
184
|
+
|
|
155
185
|
export class AbortRecorded extends Schema.TaggedClass<AbortRecorded>(
|
|
156
186
|
"@effect-agent/platform-cloudflare/AbortRecorded",
|
|
157
187
|
)("AbortRecorded", {
|
|
@@ -182,6 +212,8 @@ export const HostResponse = Schema.Union([
|
|
|
182
212
|
SubmitSucceeded,
|
|
183
213
|
SettlementReached,
|
|
184
214
|
ObservedPage,
|
|
215
|
+
ProgressObserved,
|
|
216
|
+
ProgressCancelled,
|
|
185
217
|
AbortRecorded,
|
|
186
218
|
ApprovalRecorded,
|
|
187
219
|
UnknownResolutionRecorded,
|
|
@@ -199,6 +231,10 @@ export const decodeReceipt = Schema.decodeUnknownEffect(Receipt);
|
|
|
199
231
|
export const encodeReceipt = Schema.encodeEffect(Receipt);
|
|
200
232
|
export const decodeObservePageRequest = Schema.decodeUnknownEffect(ObservePageRequest);
|
|
201
233
|
export const encodeObservePageRequest = Schema.encodeEffect(ObservePageRequest);
|
|
234
|
+
export const decodeAwaitProgressRequest = Schema.decodeUnknownEffect(AwaitProgressRequest);
|
|
235
|
+
export const encodeAwaitProgressRequest = Schema.encodeEffect(AwaitProgressRequest);
|
|
236
|
+
export const decodeCancelProgressRequest = Schema.decodeUnknownEffect(CancelProgressRequest);
|
|
237
|
+
export const encodeCancelProgressRequest = Schema.encodeEffect(CancelProgressRequest);
|
|
202
238
|
export const decodeAbortCommand = Schema.decodeUnknownEffect(AbortCommand);
|
|
203
239
|
export const encodeAbortCommand = Schema.encodeEffect(AbortCommand);
|
|
204
240
|
export const decodeApprovalDecisionCommand = Schema.decodeUnknownEffect(ApprovalDecisionCommand);
|
|
@@ -237,6 +273,14 @@ export type ClientAwaitFailure =
|
|
|
237
273
|
export type ClientObserveFailure =
|
|
238
274
|
| ConversationStoreError
|
|
239
275
|
| ConversationNotMaterialized
|
|
276
|
+
| OperationDenied
|
|
277
|
+
| HostProtocolError
|
|
278
|
+
| ConversationClientError;
|
|
279
|
+
|
|
280
|
+
export type ClientProgressFailure =
|
|
281
|
+
| ConversationStoreError
|
|
282
|
+
| ConversationNotMaterialized
|
|
283
|
+
| OperationDenied
|
|
240
284
|
| HostProtocolError
|
|
241
285
|
| ConversationClientError;
|
|
242
286
|
|
|
@@ -253,6 +297,7 @@ export type ClientApprovalFailure =
|
|
|
253
297
|
| LedgerError
|
|
254
298
|
| SettlementConflict
|
|
255
299
|
| ApprovalConflict
|
|
300
|
+
| OperationDenied
|
|
256
301
|
| DurableAlarmError
|
|
257
302
|
| HostProtocolError
|
|
258
303
|
| ConversationClientError;
|
|
@@ -263,6 +308,7 @@ export type ClientUnknownFailure =
|
|
|
263
308
|
| UnknownResolutionConflict
|
|
264
309
|
| JoinedToHost
|
|
265
310
|
| DurableRuntimeFailpointError
|
|
311
|
+
| OperationDenied
|
|
266
312
|
| DurableAlarmError
|
|
267
313
|
| HostProtocolError
|
|
268
314
|
| ConversationClientError;
|
|
@@ -289,8 +335,10 @@ const AWAIT_FAILURE_TAGS: ReadonlySet<string> = new Set([
|
|
|
289
335
|
const OBSERVE_FAILURE_TAGS: ReadonlySet<string> = new Set([
|
|
290
336
|
"ConversationStoreError",
|
|
291
337
|
"ConversationNotMaterialized",
|
|
338
|
+
"OperationDenied",
|
|
292
339
|
"HostProtocolError",
|
|
293
340
|
]);
|
|
341
|
+
const PROGRESS_FAILURE_TAGS = OBSERVE_FAILURE_TAGS;
|
|
294
342
|
const ABORT_FAILURE_TAGS: ReadonlySet<string> = new Set([
|
|
295
343
|
"LedgerError",
|
|
296
344
|
"SettlementConflict",
|
|
@@ -303,6 +351,7 @@ const APPROVAL_FAILURE_TAGS: ReadonlySet<string> = new Set([
|
|
|
303
351
|
"LedgerError",
|
|
304
352
|
"SettlementConflict",
|
|
305
353
|
"ApprovalConflict",
|
|
354
|
+
"OperationDenied",
|
|
306
355
|
"DurableAlarmError",
|
|
307
356
|
"HostProtocolError",
|
|
308
357
|
]);
|
|
@@ -312,6 +361,7 @@ const UNKNOWN_FAILURE_TAGS: ReadonlySet<string> = new Set([
|
|
|
312
361
|
"UnknownResolutionConflict",
|
|
313
362
|
"JoinedToHost",
|
|
314
363
|
"DurableRuntimeFailpointError",
|
|
364
|
+
"OperationDenied",
|
|
315
365
|
"DurableAlarmError",
|
|
316
366
|
"HostProtocolError",
|
|
317
367
|
]);
|
|
@@ -351,6 +401,14 @@ export class CloudflareConversationClient extends Context.Service<
|
|
|
351
401
|
) => Effect.Effect<Receipt, ClientSubmitFailure, InputSchema["EncodingServices"]>;
|
|
352
402
|
/** Wake-hinted, poll-guaranteed settlement wait executed inside the owning Object. */
|
|
353
403
|
readonly awaitSettlement: (receipt: Receipt) => Effect.Effect<Settlement, ClientAwaitFailure>;
|
|
404
|
+
/**
|
|
405
|
+
* Wait without polling until progress after `afterSequence` is already durable or hinted.
|
|
406
|
+
* The result is deliberately void: canonical records remain authoritative and must be read.
|
|
407
|
+
*/
|
|
408
|
+
readonly awaitProgress: (
|
|
409
|
+
conversationId: ConversationId,
|
|
410
|
+
afterSequence: CanonicalSequence,
|
|
411
|
+
) => Effect.Effect<void, ClientProgressFailure>;
|
|
354
412
|
/** One bounded page of canonical records. */
|
|
355
413
|
readonly readPage: (
|
|
356
414
|
conversationId: ConversationId,
|
|
@@ -389,10 +447,37 @@ export class CloudflareConversationClient extends Context.Service<
|
|
|
389
447
|
static readonly layer: Layer.Layer<
|
|
390
448
|
CloudflareConversationClient,
|
|
391
449
|
never,
|
|
392
|
-
ConversationObjectNamespace
|
|
450
|
+
ConversationObjectNamespace | Crypto.Crypto
|
|
393
451
|
> = Layer.effect(CloudflareConversationClient)(
|
|
394
452
|
Effect.gen(function* () {
|
|
395
453
|
const { namespace } = yield* ConversationObjectNamespace;
|
|
454
|
+
const crypto = yield* Crypto.Crypto;
|
|
455
|
+
|
|
456
|
+
const platformSignals = (cause: unknown) => {
|
|
457
|
+
let retryable: boolean | undefined;
|
|
458
|
+
let overloaded: boolean | undefined;
|
|
459
|
+
if (typeof cause === "object" && cause !== null) {
|
|
460
|
+
if ("retryable" in cause && typeof cause.retryable === "boolean") {
|
|
461
|
+
retryable = cause.retryable;
|
|
462
|
+
}
|
|
463
|
+
if ("overloaded" in cause && typeof cause.overloaded === "boolean") {
|
|
464
|
+
overloaded = cause.overloaded;
|
|
465
|
+
}
|
|
466
|
+
// Miniflare's faithful `ctx.abort()` signal predates the public `retryable` field.
|
|
467
|
+
// Treat only its explicit reset marker as the same idempotent-retry classification.
|
|
468
|
+
if (
|
|
469
|
+
retryable === undefined &&
|
|
470
|
+
"durableObjectReset" in cause &&
|
|
471
|
+
cause.durableObjectReset === true
|
|
472
|
+
) {
|
|
473
|
+
retryable = true;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
...(retryable === undefined ? {} : { retryable }),
|
|
478
|
+
...(overloaded === undefined ? {} : { overloaded }),
|
|
479
|
+
};
|
|
480
|
+
};
|
|
396
481
|
|
|
397
482
|
const call = (
|
|
398
483
|
conversationId: string,
|
|
@@ -410,6 +495,7 @@ export class CloudflareConversationClient extends Context.Service<
|
|
|
410
495
|
}`,
|
|
411
496
|
),
|
|
412
497
|
cause,
|
|
498
|
+
...platformSignals(cause),
|
|
413
499
|
}),
|
|
414
500
|
}).pipe(
|
|
415
501
|
Effect.flatMap((raw) =>
|
|
@@ -488,6 +574,19 @@ export class CloudflareConversationClient extends Context.Service<
|
|
|
488
574
|
return page.records;
|
|
489
575
|
});
|
|
490
576
|
|
|
577
|
+
const cancelProgress = (
|
|
578
|
+
conversationId: ConversationId,
|
|
579
|
+
waiterId: string,
|
|
580
|
+
): Effect.Effect<void> =>
|
|
581
|
+
encodeCancelProgressRequest(CancelProgressRequest.make({ waiterId })).pipe(
|
|
582
|
+
Effect.mapError(() => undefined),
|
|
583
|
+
Effect.flatMap((encoded) =>
|
|
584
|
+
call(conversationId, "cancelProgress", (stub) => stub.cancelProgressEncoded(encoded)),
|
|
585
|
+
),
|
|
586
|
+
Effect.asVoid,
|
|
587
|
+
Effect.ignore,
|
|
588
|
+
);
|
|
589
|
+
|
|
491
590
|
return CloudflareConversationClient.of({
|
|
492
591
|
submit: <InputSchema extends Schema.Top>(
|
|
493
592
|
agent: DurableSubmitAgent<InputSchema>,
|
|
@@ -558,6 +657,55 @@ export class CloudflareConversationClient extends Context.Service<
|
|
|
558
657
|
return settled.settlement;
|
|
559
658
|
}),
|
|
560
659
|
|
|
660
|
+
awaitProgress: (conversationId, afterSequence) =>
|
|
661
|
+
Effect.gen(function* () {
|
|
662
|
+
const waiterId = yield* crypto.randomUUIDv4.pipe(
|
|
663
|
+
Effect.mapError((error) =>
|
|
664
|
+
HostProtocolError.make({
|
|
665
|
+
message: boundHostDiagnostic(
|
|
666
|
+
`awaitProgress cancellation identity generation failed: ${error.message}`,
|
|
667
|
+
),
|
|
668
|
+
}),
|
|
669
|
+
),
|
|
670
|
+
);
|
|
671
|
+
const request = AwaitProgressRequest.make({ afterSequence, waiterId });
|
|
672
|
+
const encoded = yield* encodeAwaitProgressRequest(request).pipe(
|
|
673
|
+
Effect.mapError((error) =>
|
|
674
|
+
HostProtocolError.make({
|
|
675
|
+
message: boundHostDiagnostic(
|
|
676
|
+
`awaitProgress request encode failed: ${error.message}`,
|
|
677
|
+
),
|
|
678
|
+
}),
|
|
679
|
+
),
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
const attempt = (retry: number): Effect.Effect<void, ClientProgressFailure> =>
|
|
683
|
+
call(conversationId, "awaitProgress", (stub) =>
|
|
684
|
+
stub.awaitProgressEncoded(encoded),
|
|
685
|
+
).pipe(
|
|
686
|
+
Effect.flatMap(
|
|
687
|
+
expect<ProgressObserved, ClientProgressFailure & HostFailure>(
|
|
688
|
+
conversationId,
|
|
689
|
+
"awaitProgress",
|
|
690
|
+
"ProgressObserved",
|
|
691
|
+
PROGRESS_FAILURE_TAGS,
|
|
692
|
+
),
|
|
693
|
+
),
|
|
694
|
+
Effect.asVoid,
|
|
695
|
+
Effect.catchTag("ConversationClientError", (error) =>
|
|
696
|
+
error.retryable === true && error.overloaded !== true && retry < 5
|
|
697
|
+
? Effect.sleep(Duration.millis(10 * 2 ** retry)).pipe(
|
|
698
|
+
Effect.andThen(attempt(retry + 1)),
|
|
699
|
+
)
|
|
700
|
+
: Effect.fail(error),
|
|
701
|
+
),
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
yield* attempt(0).pipe(
|
|
705
|
+
Effect.onInterrupt(() => cancelProgress(conversationId, waiterId)),
|
|
706
|
+
);
|
|
707
|
+
}),
|
|
708
|
+
|
|
561
709
|
readPage,
|
|
562
710
|
|
|
563
711
|
readAll: (conversationId) =>
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
SettlementConflict,
|
|
28
28
|
SubmissionLedger,
|
|
29
29
|
SubmissionLookupByKey,
|
|
30
|
+
WakeScheduler,
|
|
30
31
|
type DurableSubmitAgent,
|
|
31
32
|
} from "@effect-agent/session";
|
|
32
33
|
import type { DurableObject as CloudflareDurableObject } from "cloudflare:workers";
|
|
@@ -56,11 +57,15 @@ import {
|
|
|
56
57
|
HostFailed,
|
|
57
58
|
HostProtocolError,
|
|
58
59
|
ObservedPage,
|
|
60
|
+
ProgressObserved,
|
|
61
|
+
ProgressCancelled,
|
|
59
62
|
SettlementReached,
|
|
60
63
|
SubmitSucceeded,
|
|
61
64
|
UnknownResolutionRecorded,
|
|
62
65
|
boundHostDiagnostic,
|
|
63
66
|
decodeAbortCommand,
|
|
67
|
+
decodeAwaitProgressRequest,
|
|
68
|
+
decodeCancelProgressRequest,
|
|
64
69
|
decodeApprovalDecisionCommand,
|
|
65
70
|
decodeObservePageRequest,
|
|
66
71
|
decodeReceipt,
|
|
@@ -78,6 +83,7 @@ import {
|
|
|
78
83
|
type CloudflareDurableRuntimeOptions,
|
|
79
84
|
type CloudflareDurableRuntimeServices,
|
|
80
85
|
} from "./layers.ts";
|
|
86
|
+
import { ProgressWaitRegistry } from "./progress-wait.ts";
|
|
81
87
|
|
|
82
88
|
/**
|
|
83
89
|
* `makeConversationObjectClass(options, observability?)` — the Conversation Durable Object
|
|
@@ -246,17 +252,15 @@ const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoin
|
|
|
246
252
|
const maintenance = yield* ConversationMaintenance;
|
|
247
253
|
const runtime = yield* DurableAgentRuntime;
|
|
248
254
|
yield* gateAdmissionLimits(request);
|
|
249
|
-
// Alarm invariant: the alarm
|
|
250
|
-
|
|
251
|
-
const receipt = yield*
|
|
252
|
-
passthroughSubmitAgent(request.agentId),
|
|
253
|
-
request.inputPayload,
|
|
254
|
-
{
|
|
255
|
+
// Alarm invariant: the generation + alarm commit BEFORE the admission, and maintenance
|
|
256
|
+
// cannot acknowledge that generation until this mutation leaves its public RPC seam.
|
|
257
|
+
const receipt = yield* maintenance.withMutation(
|
|
258
|
+
runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
|
|
255
259
|
conversationId: identity.conversationId,
|
|
256
260
|
principal: request.principal,
|
|
257
261
|
idempotencyKey: request.idempotencyKey,
|
|
258
262
|
definitions: request.definitions,
|
|
259
|
-
},
|
|
263
|
+
}),
|
|
260
264
|
);
|
|
261
265
|
return SubmitSucceeded.make({ receipt });
|
|
262
266
|
}),
|
|
@@ -281,6 +285,46 @@ const awaitSettlementEndpoint = (
|
|
|
281
285
|
Effect.flatMap(encodeResponse),
|
|
282
286
|
);
|
|
283
287
|
|
|
288
|
+
const awaitProgressEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
289
|
+
decodeAwaitProgressRequest(encoded).pipe(
|
|
290
|
+
Effect.mapError(protocolFailure("The progress request could not be decoded")),
|
|
291
|
+
Effect.flatMap((request) =>
|
|
292
|
+
Effect.gen(function* () {
|
|
293
|
+
const identity = yield* ConversationObjectIdentity;
|
|
294
|
+
const runtime = yield* DurableAgentRuntime;
|
|
295
|
+
const registry = yield* ProgressWaitRegistry;
|
|
296
|
+
yield* Effect.scoped(
|
|
297
|
+
Effect.gen(function* () {
|
|
298
|
+
const cancelled = yield* registry.subscribe(request.waiterId);
|
|
299
|
+
yield* Effect.raceFirst(
|
|
300
|
+
runtime.awaitProgress(identity.conversationId, request.afterSequence),
|
|
301
|
+
cancelled,
|
|
302
|
+
);
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
return ProgressObserved.make();
|
|
306
|
+
}),
|
|
307
|
+
),
|
|
308
|
+
respond,
|
|
309
|
+
Effect.flatMap(encodeResponse),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const cancelProgressEndpoint = (
|
|
313
|
+
encoded: unknown,
|
|
314
|
+
): Effect.Effect<unknown, never, EndpointServices> =>
|
|
315
|
+
decodeCancelProgressRequest(encoded).pipe(
|
|
316
|
+
Effect.mapError(protocolFailure("The progress cancellation could not be decoded")),
|
|
317
|
+
Effect.flatMap((request) =>
|
|
318
|
+
Effect.gen(function* () {
|
|
319
|
+
const registry = yield* ProgressWaitRegistry;
|
|
320
|
+
yield* registry.cancel(request.waiterId);
|
|
321
|
+
return ProgressCancelled.make();
|
|
322
|
+
}),
|
|
323
|
+
),
|
|
324
|
+
respond,
|
|
325
|
+
Effect.flatMap(encodeResponse),
|
|
326
|
+
);
|
|
327
|
+
|
|
284
328
|
const observePageEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
285
329
|
decodeObservePageRequest(encoded).pipe(
|
|
286
330
|
Effect.mapError(protocolFailure("The observe request could not be decoded")),
|
|
@@ -291,14 +335,12 @@ const observePageEndpoint = (encoded: unknown): Effect.Effect<unknown, never, En
|
|
|
291
335
|
// The same fail-closed authorization seam the runtime's `observe` consults (P7 WP1);
|
|
292
336
|
// the default reference preserves the possession behavior.
|
|
293
337
|
const authorizer = yield* OperationAuthorizer;
|
|
294
|
-
yield* authorizer
|
|
295
|
-
.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
)
|
|
301
|
-
.pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
|
|
338
|
+
yield* authorizer.authorize(
|
|
339
|
+
OperationAuthorizationRequest.make({
|
|
340
|
+
operation: "observe",
|
|
341
|
+
conversationId: identity.conversationId,
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
302
344
|
const records = yield* Stream.runCollect(
|
|
303
345
|
store.read(
|
|
304
346
|
ConversationRead.make({
|
|
@@ -324,8 +366,7 @@ const abortEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoint
|
|
|
324
366
|
Effect.gen(function* () {
|
|
325
367
|
const maintenance = yield* ConversationMaintenance;
|
|
326
368
|
const runtime = yield* DurableAgentRuntime;
|
|
327
|
-
yield* maintenance.
|
|
328
|
-
const intent = yield* runtime.abort(command);
|
|
369
|
+
const intent = yield* maintenance.withMutation(runtime.abort(command));
|
|
329
370
|
return AbortRecorded.make({ intent });
|
|
330
371
|
}),
|
|
331
372
|
),
|
|
@@ -333,24 +374,6 @@ const abortEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoint
|
|
|
333
374
|
Effect.flatMap(encodeResponse),
|
|
334
375
|
);
|
|
335
376
|
|
|
336
|
-
/**
|
|
337
|
-
* The pre-P7 host protocol's failure union does not carry `OperationDenied` (the Worker client
|
|
338
|
-
* predates the authorizer). This assembly always runs the default possession authorizer — no
|
|
339
|
-
* `CloudflareDurableRuntimeOptions` authorizer lever exists yet — so a denial here is
|
|
340
|
-
* unreachable today; if one ever surfaces it degrades to the protocol failure instead of an
|
|
341
|
-
* out-of-contract throw. The four P7 admin entry points below carry `OperationDenied` typed.
|
|
342
|
-
*/
|
|
343
|
-
const deniedToProtocolFailure = (
|
|
344
|
-
denied: OperationDenied,
|
|
345
|
-
): Effect.Effect<never, HostProtocolError> =>
|
|
346
|
-
Effect.fail(
|
|
347
|
-
HostProtocolError.make({
|
|
348
|
-
message: boundHostDiagnostic(
|
|
349
|
-
`The ${denied.operation} operation was denied: ${denied.reason}`,
|
|
350
|
-
),
|
|
351
|
-
}),
|
|
352
|
-
);
|
|
353
|
-
|
|
354
377
|
const resolveApprovalEndpoint = (
|
|
355
378
|
encoded: unknown,
|
|
356
379
|
): Effect.Effect<unknown, never, EndpointServices> =>
|
|
@@ -360,10 +383,7 @@ const resolveApprovalEndpoint = (
|
|
|
360
383
|
Effect.gen(function* () {
|
|
361
384
|
const maintenance = yield* ConversationMaintenance;
|
|
362
385
|
const runtime = yield* DurableAgentRuntime;
|
|
363
|
-
yield* maintenance.
|
|
364
|
-
const intent = yield* runtime
|
|
365
|
-
.resolveApproval(command)
|
|
366
|
-
.pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
|
|
386
|
+
const intent = yield* maintenance.withMutation(runtime.resolveApproval(command));
|
|
367
387
|
return ApprovalRecorded.make({ intent });
|
|
368
388
|
}),
|
|
369
389
|
),
|
|
@@ -380,10 +400,7 @@ const resolveUnknownEndpoint = (
|
|
|
380
400
|
Effect.gen(function* () {
|
|
381
401
|
const maintenance = yield* ConversationMaintenance;
|
|
382
402
|
const runtime = yield* DurableAgentRuntime;
|
|
383
|
-
yield* maintenance.
|
|
384
|
-
const intent = yield* runtime
|
|
385
|
-
.resolveUnknown(command)
|
|
386
|
-
.pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
|
|
403
|
+
const intent = yield* maintenance.withMutation(runtime.resolveUnknown(command));
|
|
387
404
|
return UnknownResolutionRecorded.make({ intent });
|
|
388
405
|
}),
|
|
389
406
|
),
|
|
@@ -541,10 +558,8 @@ const retryEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoint
|
|
|
541
558
|
Effect.gen(function* () {
|
|
542
559
|
const maintenance = yield* ConversationMaintenance;
|
|
543
560
|
const runtime = yield* DurableAgentRuntime;
|
|
544
|
-
//
|
|
545
|
-
|
|
546
|
-
yield* maintenance.preArm;
|
|
547
|
-
const report = yield* runtime.retry(command);
|
|
561
|
+
// Retry may repair durable state, so its generation + alarm commit before the mutation.
|
|
562
|
+
const report = yield* maintenance.withMutation(runtime.retry(command));
|
|
548
563
|
return RetryExecuted.make({ report });
|
|
549
564
|
}),
|
|
550
565
|
),
|
|
@@ -567,10 +582,11 @@ const obligationsEndpoint = (encoded: unknown): Effect.Effect<unknown, never, En
|
|
|
567
582
|
);
|
|
568
583
|
|
|
569
584
|
/**
|
|
570
|
-
* Owner-side `portCall`:
|
|
571
|
-
* THIS Object must already carry the alarm that will
|
|
572
|
-
* (never the routed decorators), then arm an immediate
|
|
573
|
-
* processed promptly. Protocol anomalies answer
|
|
585
|
+
* Owner-side `portCall`: wrap a mutating envelope in the same pre-armed generation protocol as
|
|
586
|
+
* public RPC (a routed mutation committed by THIS Object must already carry the alarm that will
|
|
587
|
+
* finish it), execute on the LOCAL facets (never the routed decorators), then arm an immediate
|
|
588
|
+
* alarm so the mutated lane is processed promptly. Protocol anomalies answer
|
|
589
|
+
* `PortFailed(PortProtocolError)`.
|
|
574
590
|
*/
|
|
575
591
|
const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
576
592
|
Effect.gen(function* () {
|
|
@@ -578,16 +594,17 @@ const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpo
|
|
|
578
594
|
const maintenance = yield* ConversationMaintenance;
|
|
579
595
|
const alarm = yield* DurableAlarmService;
|
|
580
596
|
const mutating = isMutatingPortRequest(encoded);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
597
|
+
const handled = yield* (
|
|
598
|
+
mutating ? maintenance.withMutation(ports.handle(encoded)) : ports.handle(encoded)
|
|
599
|
+
).pipe(Effect.exit);
|
|
600
|
+
if (handled._tag === "Failure") {
|
|
601
|
+
// Without the committed generation/alarm the invariant cannot be promised; refuse before
|
|
602
|
+
// the port mutation runs. `ports.handle` itself is total, so this is the maintenance error.
|
|
603
|
+
return encodedPortProtocolFailure(
|
|
604
|
+
"The owner Object could not arm its maintenance alarm before the mutation.",
|
|
605
|
+
);
|
|
589
606
|
}
|
|
590
|
-
const response =
|
|
607
|
+
const response = handled.value;
|
|
591
608
|
if (mutating) {
|
|
592
609
|
// Prompt processing hint; the pre-armed alarm already guarantees convergence.
|
|
593
610
|
yield* alarm.scheduleNow.pipe(
|
|
@@ -600,19 +617,18 @@ const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpo
|
|
|
600
617
|
});
|
|
601
618
|
|
|
602
619
|
const wakeEndpoint: Effect.Effect<void, never, EndpointServices> = Effect.gen(function* () {
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
//
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
);
|
|
620
|
+
const identity = yield* ConversationObjectIdentity;
|
|
621
|
+
const wake = yield* WakeScheduler;
|
|
622
|
+
// Route the remote hint through this incarnation's scheduler so scoped progress waiters and
|
|
623
|
+
// the alarm receive the same hint. Delivery remains droppable; canonical storage is authority.
|
|
624
|
+
yield* wake.notify(identity.conversationId);
|
|
609
625
|
});
|
|
610
626
|
|
|
611
627
|
const alarmEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices> = Effect.gen(
|
|
612
628
|
function* () {
|
|
613
629
|
const maintenance = yield* ConversationMaintenance;
|
|
614
630
|
// Typed pass failures propagate: the rejected promise makes workerd retry the alarm
|
|
615
|
-
// (at-least-once delivery), and the
|
|
631
|
+
// (at-least-once delivery), and the dirty generation retains a committed slot meanwhile.
|
|
616
632
|
yield* maintenance.pass;
|
|
617
633
|
},
|
|
618
634
|
);
|
|
@@ -660,6 +676,8 @@ const effectCfPlatformLayer = (
|
|
|
660
676
|
export interface ConversationObjectInstance extends CloudflareDurableObject {
|
|
661
677
|
submitEncoded(encoded: unknown): Promise<unknown>;
|
|
662
678
|
awaitSettlementEncoded(encoded: unknown): Promise<unknown>;
|
|
679
|
+
awaitProgressEncoded(encoded: unknown): Promise<unknown>;
|
|
680
|
+
cancelProgressEncoded(encoded: unknown): Promise<unknown>;
|
|
663
681
|
observePage(encoded: unknown): Promise<unknown>;
|
|
664
682
|
abortEncoded(encoded: unknown): Promise<unknown>;
|
|
665
683
|
resolveApprovalEncoded(encoded: unknown): Promise<unknown>;
|
|
@@ -728,6 +746,8 @@ export const makeConversationObjectClass = <EventLayerError = never, EventServic
|
|
|
728
746
|
const rpc = {
|
|
729
747
|
submitEncoded: (encoded: unknown) => submitEndpoint(encoded),
|
|
730
748
|
awaitSettlementEncoded: (encoded: unknown) => awaitSettlementEndpoint(encoded),
|
|
749
|
+
awaitProgressEncoded: (encoded: unknown) => awaitProgressEndpoint(encoded),
|
|
750
|
+
cancelProgressEncoded: (encoded: unknown) => cancelProgressEndpoint(encoded),
|
|
731
751
|
observePage: (encoded: unknown) => observePageEndpoint(encoded),
|
|
732
752
|
abortEncoded: (encoded: unknown) => abortEndpoint(encoded),
|
|
733
753
|
resolveApprovalEncoded: (encoded: unknown) => resolveApprovalEndpoint(encoded),
|
package/src/index.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* `makeConversationObjectClass` builds the class applications export from their Worker,
|
|
7
7
|
* `CloudflareDurableRuntime.layer` assembles the coordinator over the storage-cloudflare
|
|
8
8
|
* adapters and the WP2 cross-Object routing, `DurableAlarmService`/`ConversationMaintenance`
|
|
9
|
-
* multiplex every cadence into the Object's single alarm slot (
|
|
10
|
-
* committed alarm
|
|
9
|
+
* multiplex every cadence into the Object's single alarm slot (dirty or autonomously
|
|
10
|
+
* actionable work retains a committed alarm; stable external waits quiesce until their next
|
|
11
|
+
* durably pre-armed mutation), and
|
|
11
12
|
* `CloudflareConversationClient` is the Worker-side ingress. Platform bindings enter ONLY
|
|
12
13
|
* through the `bindings.ts` Layers (DEPLOY-010). This is the only workspace package allowed
|
|
13
14
|
* to import the `cloudflare:workers` runtime module.
|
|
@@ -16,6 +17,7 @@ export * from "./bindings.ts";
|
|
|
16
17
|
export * from "./config.ts";
|
|
17
18
|
export * from "./alarm.ts";
|
|
18
19
|
export * from "./wake-scheduler.ts";
|
|
20
|
+
export * from "./progress-wait.ts";
|
|
19
21
|
export * from "./transport.ts";
|
|
20
22
|
export * from "./layers.ts";
|
|
21
23
|
export * from "./conversation-object.ts";
|