@effect-agent/platform-cloudflare 0.1.0-beta.50 → 0.1.0-beta.51
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/CloudflareBindings.d.mts +1 -0
- package/dist/CloudflareBindings.mjs.map +1 -1
- package/dist/CloudflareScheduling.mjs +36 -3
- package/dist/CloudflareScheduling.mjs.map +1 -1
- package/dist/CloudflareSubscriptions.d.mts +31 -4
- package/dist/CloudflareSubscriptions.mjs +170 -11
- package/dist/CloudflareSubscriptions.mjs.map +1 -1
- package/dist/CloudflareThreadClient.d.mts +97 -22
- package/dist/CloudflareThreadClient.mjs +18 -2
- package/dist/CloudflareThreadClient.mjs.map +1 -1
- package/dist/{ThreadObject-IrGuIO4a.mjs → ThreadObject-BY8axaWT.mjs} +11 -3
- package/dist/ThreadObject-BY8axaWT.mjs.map +1 -0
- package/dist/{ThreadObject-CmeEjnBK.d.mts → ThreadObject-sDr1JBCj.d.mts} +9 -4
- package/dist/ThreadObject.d.mts +1 -1
- package/dist/ThreadObject.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/prepared-admission-9rvlHI0y.mjs +71 -0
- package/dist/prepared-admission-9rvlHI0y.mjs.map +1 -0
- package/package.json +1 -1
- package/src/CloudflareBindings.ts +1 -0
- package/src/CloudflareScheduling.ts +52 -0
- package/src/CloudflareSubscriptions.ts +321 -8
- package/src/CloudflareThreadClient.ts +40 -0
- package/src/ThreadObject.ts +27 -0
- package/src/internal/prepared-admission.ts +19 -0
- package/dist/ThreadObject-IrGuIO4a.mjs.map +0 -1
- package/dist/prepared-admission-DRBhl2Sp.mjs +0 -63
- package/dist/prepared-admission-DRBhl2Sp.mjs.map +0 -1
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type SubscriptionAuthorizer,
|
|
14
14
|
SubscriptionConfiguration,
|
|
15
15
|
SubscriptionDeliverySnapshot,
|
|
16
|
+
SubscriptionDeliveryKey,
|
|
16
17
|
SubscriptionError,
|
|
17
18
|
SubscriptionFailpointError,
|
|
18
19
|
SubscriptionKey,
|
|
@@ -30,7 +31,7 @@ import {
|
|
|
30
31
|
} from "@effect-agent/thread/Subscriptions";
|
|
31
32
|
import { BrowserCrypto } from "@effect/platform-browser";
|
|
32
33
|
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
33
|
-
import { Cause, Clock, Context, DateTime, Effect, Layer, Schema } from "effect";
|
|
34
|
+
import { Cause, Clock, Context, DateTime, Effect, Layer, Schema, type Scope } from "effect";
|
|
34
35
|
import {
|
|
35
36
|
DurableObject as EffectCfDurableObject,
|
|
36
37
|
DurableObjectAlarm,
|
|
@@ -45,6 +46,8 @@ import { cloudflarePreparedInputAdmissionLayer } from "./internal/prepared-admis
|
|
|
45
46
|
const SUBSCRIPTION_ALARM_TAG = "effect-agent/SubscriptionPartitionWake";
|
|
46
47
|
const SUBSCRIPTION_ALARM_ID = "driver";
|
|
47
48
|
const MAX_ALARM_WALL_MILLIS = 12 * 60_000;
|
|
49
|
+
const MAX_ALARMS_PER_INVOCATION = 16;
|
|
50
|
+
const MAX_ANCILLARY_ALARM_MILLIS = 30_000;
|
|
48
51
|
|
|
49
52
|
const SubscriptionAlarmPayload = Schema.Struct({
|
|
50
53
|
schemaVersion: Schema.Literal(1),
|
|
@@ -56,6 +59,86 @@ export class SubscriptionAlarmProtocolError extends Schema.TaggedError<Subscript
|
|
|
56
59
|
{ message: Schema.String },
|
|
57
60
|
) {}
|
|
58
61
|
|
|
62
|
+
/** Bounded host diagnostic; credentials and provider responses do not belong in alarm failures. */
|
|
63
|
+
export class SubscriptionAlarmExtensionError extends Schema.TaggedError<SubscriptionAlarmExtensionError>()(
|
|
64
|
+
"SubscriptionAlarmExtensionError",
|
|
65
|
+
{ code: Schema.NonEmptyString.check(Schema.isMaxLength(128)) },
|
|
66
|
+
) {}
|
|
67
|
+
|
|
68
|
+
export interface SubscriptionPartitionAlarmHandler {
|
|
69
|
+
readonly tag: string;
|
|
70
|
+
readonly handle: (
|
|
71
|
+
event: DurableObjectAlarm.DurableObjectAlarmEvent,
|
|
72
|
+
) => Effect.Effect<void, SubscriptionAlarmProtocolError | SubscriptionAlarmExtensionError>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Host-only handlers; the framework reserves its namespace and rejects every unknown tag. */
|
|
76
|
+
export const SubscriptionPartitionAlarmExtension = Context.Reference<{
|
|
77
|
+
readonly handlers: ReadonlyArray<SubscriptionPartitionAlarmHandler>;
|
|
78
|
+
}>("@effect-agent/platform-cloudflare/SubscriptionPartitionAlarmExtension", {
|
|
79
|
+
defaultValue: () => ({ handlers: [] }),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/** Capture host services once; each invocation owns its codec/handler Scope and timeout.
|
|
83
|
+
* Callback failures stay typed. Defects and interruption reach the native alarm multiplexer.
|
|
84
|
+
* The host owns durable idempotency, prearming and external-effect uncertainty.
|
|
85
|
+
*/
|
|
86
|
+
export const makeSubscriptionPartitionAlarmHandler = Effect.fn(
|
|
87
|
+
"makeSubscriptionPartitionAlarmHandler",
|
|
88
|
+
)(function* <Payload extends Schema.Top, R>(options: {
|
|
89
|
+
readonly tag: string;
|
|
90
|
+
readonly payload: Payload;
|
|
91
|
+
readonly timeoutMillis: number;
|
|
92
|
+
readonly handle: (
|
|
93
|
+
event: Omit<DurableObjectAlarm.DurableObjectAlarmEvent, "payload"> & {
|
|
94
|
+
readonly payload: Payload["Type"];
|
|
95
|
+
},
|
|
96
|
+
) => Effect.Effect<void, SubscriptionAlarmExtensionError, R>;
|
|
97
|
+
}): Effect.fn.Return<
|
|
98
|
+
SubscriptionPartitionAlarmHandler,
|
|
99
|
+
SubscriptionAlarmProtocolError,
|
|
100
|
+
Exclude<R | Payload["DecodingServices"], Scope.Scope>
|
|
101
|
+
> {
|
|
102
|
+
if (
|
|
103
|
+
options.tag.length === 0 ||
|
|
104
|
+
options.tag.length > 128 ||
|
|
105
|
+
options.tag.startsWith("effect-agent/") ||
|
|
106
|
+
!Number.isSafeInteger(options.timeoutMillis) ||
|
|
107
|
+
options.timeoutMillis < 1 ||
|
|
108
|
+
options.timeoutMillis > MAX_ANCILLARY_ALARM_MILLIS
|
|
109
|
+
)
|
|
110
|
+
return yield* SubscriptionAlarmProtocolError.make({
|
|
111
|
+
message: "Invalid ancillary alarm tag or timeout",
|
|
112
|
+
});
|
|
113
|
+
const services = yield* Effect.context<Exclude<R | Payload["DecodingServices"], Scope.Scope>>();
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
tag: options.tag,
|
|
117
|
+
handle: (event) =>
|
|
118
|
+
Effect.gen(function* () {
|
|
119
|
+
if (event.tag !== options.tag)
|
|
120
|
+
return yield* SubscriptionAlarmProtocolError.make({
|
|
121
|
+
message: "Ancillary alarm tag mismatch",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const payload = yield* Schema.decodeUnknownEffect(options.payload)(event.payload).pipe(
|
|
125
|
+
Effect.mapError(() =>
|
|
126
|
+
SubscriptionAlarmProtocolError.make({ message: "Invalid ancillary alarm payload" }),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
yield* options.handle({ ...event, payload });
|
|
131
|
+
}).pipe(
|
|
132
|
+
Effect.scoped,
|
|
133
|
+
Effect.timeout(options.timeoutMillis),
|
|
134
|
+
Effect.catchTag("TimeoutError", () =>
|
|
135
|
+
SubscriptionAlarmExtensionError.make({ code: "timeout" }),
|
|
136
|
+
),
|
|
137
|
+
Effect.provideContext(services),
|
|
138
|
+
),
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
|
|
59
142
|
export class SubscriptionPartitionProtocolError extends Schema.TaggedError<SubscriptionPartitionProtocolError>()(
|
|
60
143
|
"SubscriptionPartitionProtocolError",
|
|
61
144
|
{ message: Schema.String.check(Schema.isMaxLength(4_096)) },
|
|
@@ -69,9 +152,11 @@ export class CloudflareSubscriptionConfigError extends Schema.TaggedError<Cloudf
|
|
|
69
152
|
/** Reject limits whose four bounded phases could exceed the safe Durable Object alarm budget. */
|
|
70
153
|
export const validateCloudflareSubscriptionLimits = (
|
|
71
154
|
limits: SubscriptionLimits,
|
|
155
|
+
options: { readonly ancillaryAlarms?: boolean } = {},
|
|
72
156
|
): Effect.Effect<void, CloudflareSubscriptionConfigError> => {
|
|
73
157
|
const worstCaseMillis =
|
|
74
|
-
4 * Math.ceil(limits.batchSize / limits.concurrency) * limits.operationTimeoutMillis
|
|
158
|
+
4 * Math.ceil(limits.batchSize / limits.concurrency) * limits.operationTimeoutMillis +
|
|
159
|
+
(options.ancillaryAlarms === true ? MAX_ALARMS_PER_INVOCATION * MAX_ANCILLARY_ALARM_MILLIS : 0);
|
|
75
160
|
|
|
76
161
|
return worstCaseMillis <= MAX_ALARM_WALL_MILLIS
|
|
77
162
|
? Effect.void
|
|
@@ -96,14 +181,32 @@ const SubscribeRequest = Schema.TaggedStruct("Subscribe", {
|
|
|
96
181
|
parameters: PersistedJson,
|
|
97
182
|
context: PersistedJson,
|
|
98
183
|
mode: Schema.Literals(["once", "continuous"]),
|
|
99
|
-
expiresAtMillis: Schema.Number,
|
|
184
|
+
expiresAtMillis: Schema.NullOr(Schema.Number),
|
|
100
185
|
destination: SubscriptionConfiguration.fields.destination,
|
|
101
186
|
deliveryPrincipal: SubscriptionConfiguration.fields.deliveryPrincipal,
|
|
187
|
+
admissionGroup: SubscriptionConfiguration.fields.admissionGroup,
|
|
188
|
+
admissionFence: SubscriptionConfiguration.fields.admissionFence,
|
|
102
189
|
agentId: SubscriptionConfiguration.fields.agentId,
|
|
103
190
|
definitions: SubscriptionConfiguration.fields.definitions,
|
|
104
191
|
}),
|
|
105
192
|
});
|
|
106
193
|
|
|
194
|
+
const UpdateSubscriptionRequest = Schema.TaggedStruct("UpdateSubscription", {
|
|
195
|
+
schemaVersion: Schema.Literal(1),
|
|
196
|
+
scope: SubscribeRequest.fields.scope,
|
|
197
|
+
key: SubscriptionKey,
|
|
198
|
+
expectedRevision: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
199
|
+
options: SubscribeRequest.fields.options.mapFields(({ subscriptionId: _, ...fields }) => fields),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const SubscriptionStateRequest = Schema.Struct({
|
|
203
|
+
_tag: Schema.Literals(["PauseSubscription", "ResumeSubscription", "RecoverSubscription"]),
|
|
204
|
+
schemaVersion: Schema.Literal(1),
|
|
205
|
+
scope: SubscribeRequest.fields.scope,
|
|
206
|
+
key: SubscriptionKey,
|
|
207
|
+
expectedRevision: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
208
|
+
});
|
|
209
|
+
|
|
107
210
|
const ListSubscriptionsRequest = Schema.TaggedStruct("ListSubscriptions", {
|
|
108
211
|
schemaVersion: Schema.Literal(1),
|
|
109
212
|
scope: SubscribeRequest.fields.scope,
|
|
@@ -112,11 +215,25 @@ const ListSubscriptionsRequest = Schema.TaggedStruct("ListSubscriptions", {
|
|
|
112
215
|
});
|
|
113
216
|
|
|
114
217
|
const CancelSubscriptionRequest = Schema.TaggedStruct("CancelSubscription", {
|
|
218
|
+
expectedRevision: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
|
|
115
219
|
schemaVersion: Schema.Literal(1),
|
|
116
220
|
scope: SubscribeRequest.fields.scope,
|
|
117
221
|
key: SubscriptionKey,
|
|
118
222
|
});
|
|
119
223
|
|
|
224
|
+
const GetSubscriptionRequest = Schema.TaggedStruct("GetSubscription", {
|
|
225
|
+
schemaVersion: Schema.Literal(1),
|
|
226
|
+
scope: SubscriptionScope,
|
|
227
|
+
key: SubscriptionKey,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const RecoverDeliveryRequest = Schema.TaggedStruct("RecoverDelivery", {
|
|
231
|
+
expectedGeneration: Schema.Natural,
|
|
232
|
+
schemaVersion: Schema.Literal(1),
|
|
233
|
+
scope: SubscribeRequest.fields.scope,
|
|
234
|
+
key: SubscriptionDeliveryKey,
|
|
235
|
+
});
|
|
236
|
+
|
|
120
237
|
const ListDeliveriesRequest = Schema.TaggedStruct("ListDeliveries", {
|
|
121
238
|
schemaVersion: Schema.Literal(1),
|
|
122
239
|
scope: SubscribeRequest.fields.scope,
|
|
@@ -143,9 +260,13 @@ const StatusRequest = Schema.TaggedStruct("Status", {
|
|
|
143
260
|
|
|
144
261
|
const SubscriptionPartitionRequest = Schema.Union([
|
|
145
262
|
SubscribeRequest,
|
|
263
|
+
GetSubscriptionRequest,
|
|
264
|
+
UpdateSubscriptionRequest,
|
|
265
|
+
SubscriptionStateRequest,
|
|
146
266
|
ListSubscriptionsRequest,
|
|
147
267
|
CancelSubscriptionRequest,
|
|
148
268
|
ListDeliveriesRequest,
|
|
269
|
+
RecoverDeliveryRequest,
|
|
149
270
|
AcceptRequest,
|
|
150
271
|
StatusRequest,
|
|
151
272
|
]);
|
|
@@ -182,6 +303,7 @@ const SubscriptionPartitionResponse = Schema.Union([
|
|
|
182
303
|
Schema.TaggedStruct("Snapshot", { value: SubscriptionSnapshot }),
|
|
183
304
|
Schema.TaggedStruct("SubscriptionPage", { value: SubscriptionPage }),
|
|
184
305
|
Schema.TaggedStruct("DeliveryPage", { value: DeliveryPage }),
|
|
306
|
+
Schema.TaggedStruct("DeliverySnapshot", { value: SubscriptionDeliverySnapshot }),
|
|
185
307
|
Schema.TaggedStruct("Acknowledgement", { value: EventAcknowledgement }),
|
|
186
308
|
Schema.TaggedStruct("Status", { value: IntakeStatus }),
|
|
187
309
|
Schema.TaggedStruct("Failed", { failure: SubscriptionPartitionFailure }),
|
|
@@ -261,7 +383,88 @@ export class CloudflareSubscriptionsClient {
|
|
|
261
383
|
const asProtocolError = (): SubscriptionError =>
|
|
262
384
|
corruptFailure("subscription-partition-protocol");
|
|
263
385
|
|
|
386
|
+
const changeState = Effect.fn("CloudflareSubscriptions.changeState")(function* (
|
|
387
|
+
scope: typeof SubscriptionScope.Type,
|
|
388
|
+
key: typeof SubscriptionKey.Type,
|
|
389
|
+
expectedRevision: number,
|
|
390
|
+
_tag: "PauseSubscription" | "ResumeSubscription" | "RecoverSubscription",
|
|
391
|
+
) {
|
|
392
|
+
const response = yield* call(scope.partition, {
|
|
393
|
+
_tag,
|
|
394
|
+
schemaVersion: 1,
|
|
395
|
+
scope,
|
|
396
|
+
key,
|
|
397
|
+
expectedRevision,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
if (response._tag === "Snapshot") return response.value;
|
|
401
|
+
const failure = failed(response);
|
|
402
|
+
|
|
403
|
+
return yield* failure._tag === "SubscriptionError" ||
|
|
404
|
+
failure._tag === "SubscriptionFailpointError"
|
|
405
|
+
? failure
|
|
406
|
+
: asProtocolError();
|
|
407
|
+
});
|
|
408
|
+
|
|
264
409
|
const subscriptions = Subscriptions.of({
|
|
410
|
+
recoverSubscription: (scope, key, revision) =>
|
|
411
|
+
changeState(scope, key, revision, "RecoverSubscription"),
|
|
412
|
+
getSubscription: (scope, key) =>
|
|
413
|
+
Effect.gen(function* () {
|
|
414
|
+
const response = yield* call(scope.partition, {
|
|
415
|
+
_tag: "GetSubscription",
|
|
416
|
+
schemaVersion: 1,
|
|
417
|
+
scope,
|
|
418
|
+
key,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (response._tag === "Snapshot") return response.value;
|
|
422
|
+
const failure = failed(response);
|
|
423
|
+
|
|
424
|
+
return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
|
|
425
|
+
}),
|
|
426
|
+
recoverDelivery: (scope, key, expectedGeneration) =>
|
|
427
|
+
Effect.gen(function* () {
|
|
428
|
+
const response = yield* call(scope.partition, {
|
|
429
|
+
_tag: "RecoverDelivery",
|
|
430
|
+
expectedGeneration,
|
|
431
|
+
schemaVersion: 1,
|
|
432
|
+
scope,
|
|
433
|
+
key,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
if (response._tag === "DeliverySnapshot") return response.value;
|
|
437
|
+
const failure = failed(response);
|
|
438
|
+
|
|
439
|
+
return yield* failure._tag === "SubscriptionError" ||
|
|
440
|
+
failure._tag === "SubscriptionFailpointError"
|
|
441
|
+
? failure
|
|
442
|
+
: asProtocolError();
|
|
443
|
+
}),
|
|
444
|
+
updateSubscription: (scope, key, expectedRevision, options) =>
|
|
445
|
+
Effect.gen(function* () {
|
|
446
|
+
const response = yield* call(scope.partition, {
|
|
447
|
+
_tag: "UpdateSubscription",
|
|
448
|
+
schemaVersion: 1,
|
|
449
|
+
scope,
|
|
450
|
+
key,
|
|
451
|
+
expectedRevision,
|
|
452
|
+
options,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
if (response._tag === "Snapshot") return response.value;
|
|
456
|
+
const failure = failed(response);
|
|
457
|
+
|
|
458
|
+
return yield* failure._tag === "SubscriptionError" ||
|
|
459
|
+
failure._tag === "SubscriptionFailpointError" ||
|
|
460
|
+
failure._tag === "SubscriptionSourceError"
|
|
461
|
+
? failure
|
|
462
|
+
: asProtocolError();
|
|
463
|
+
}),
|
|
464
|
+
pauseSubscription: (scope, key, revision) =>
|
|
465
|
+
changeState(scope, key, revision, "PauseSubscription"),
|
|
466
|
+
resumeSubscription: (scope, key, revision) =>
|
|
467
|
+
changeState(scope, key, revision, "ResumeSubscription"),
|
|
265
468
|
subscribe: (scope, options) =>
|
|
266
469
|
Effect.gen(function* () {
|
|
267
470
|
const response = yield* call(scope.partition, {
|
|
@@ -298,10 +501,11 @@ export class CloudflareSubscriptionsClient {
|
|
|
298
501
|
|
|
299
502
|
return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
|
|
300
503
|
}),
|
|
301
|
-
cancelSubscription: (scope, key) =>
|
|
504
|
+
cancelSubscription: (scope, key, expectedRevision) =>
|
|
302
505
|
Effect.gen(function* () {
|
|
303
506
|
const response = yield* call(scope.partition, {
|
|
304
507
|
_tag: "CancelSubscription",
|
|
508
|
+
...(expectedRevision === undefined ? {} : { expectedRevision }),
|
|
305
509
|
schemaVersion: 1,
|
|
306
510
|
scope,
|
|
307
511
|
key,
|
|
@@ -461,11 +665,53 @@ const handleRequest = Effect.fn("SubscriptionPartition.handleRequest")(function*
|
|
|
461
665
|
SubscriptionPartitionFailure
|
|
462
666
|
> {
|
|
463
667
|
switch (request._tag) {
|
|
668
|
+
case "GetSubscription":
|
|
669
|
+
return {
|
|
670
|
+
_tag: "Snapshot",
|
|
671
|
+
value: yield* subscriptions.getSubscription(request.scope, request.key),
|
|
672
|
+
};
|
|
464
673
|
case "Subscribe":
|
|
465
674
|
return {
|
|
466
675
|
_tag: "Snapshot",
|
|
467
676
|
value: yield* subscriptions.subscribe(request.scope, request.options),
|
|
468
677
|
};
|
|
678
|
+
case "UpdateSubscription":
|
|
679
|
+
return {
|
|
680
|
+
_tag: "Snapshot",
|
|
681
|
+
value: yield* subscriptions.updateSubscription(
|
|
682
|
+
request.scope,
|
|
683
|
+
request.key,
|
|
684
|
+
request.expectedRevision,
|
|
685
|
+
request.options,
|
|
686
|
+
),
|
|
687
|
+
};
|
|
688
|
+
case "RecoverSubscription":
|
|
689
|
+
return {
|
|
690
|
+
_tag: "Snapshot",
|
|
691
|
+
value: yield* subscriptions.recoverSubscription(
|
|
692
|
+
request.scope,
|
|
693
|
+
request.key,
|
|
694
|
+
request.expectedRevision,
|
|
695
|
+
),
|
|
696
|
+
};
|
|
697
|
+
case "PauseSubscription":
|
|
698
|
+
return {
|
|
699
|
+
_tag: "Snapshot",
|
|
700
|
+
value: yield* subscriptions.pauseSubscription(
|
|
701
|
+
request.scope,
|
|
702
|
+
request.key,
|
|
703
|
+
request.expectedRevision,
|
|
704
|
+
),
|
|
705
|
+
};
|
|
706
|
+
case "ResumeSubscription":
|
|
707
|
+
return {
|
|
708
|
+
_tag: "Snapshot",
|
|
709
|
+
value: yield* subscriptions.resumeSubscription(
|
|
710
|
+
request.scope,
|
|
711
|
+
request.key,
|
|
712
|
+
request.expectedRevision,
|
|
713
|
+
),
|
|
714
|
+
};
|
|
469
715
|
case "ListSubscriptions":
|
|
470
716
|
return {
|
|
471
717
|
_tag: "SubscriptionPage",
|
|
@@ -478,7 +724,20 @@ const handleRequest = Effect.fn("SubscriptionPartition.handleRequest")(function*
|
|
|
478
724
|
case "CancelSubscription":
|
|
479
725
|
return {
|
|
480
726
|
_tag: "Snapshot",
|
|
481
|
-
value: yield* subscriptions.cancelSubscription(
|
|
727
|
+
value: yield* subscriptions.cancelSubscription(
|
|
728
|
+
request.scope,
|
|
729
|
+
request.key,
|
|
730
|
+
request.expectedRevision,
|
|
731
|
+
),
|
|
732
|
+
};
|
|
733
|
+
case "RecoverDelivery":
|
|
734
|
+
return {
|
|
735
|
+
_tag: "DeliverySnapshot",
|
|
736
|
+
value: yield* subscriptions.recoverDelivery(
|
|
737
|
+
request.scope,
|
|
738
|
+
request.key,
|
|
739
|
+
request.expectedGeneration,
|
|
740
|
+
),
|
|
482
741
|
};
|
|
483
742
|
case "ListDeliveries":
|
|
484
743
|
return {
|
|
@@ -566,7 +825,29 @@ const alarmHandler = (limits: SubscriptionLimits) =>
|
|
|
566
825
|
DurableObjectAlarm.processDue(
|
|
567
826
|
(event) =>
|
|
568
827
|
Effect.gen(function* () {
|
|
569
|
-
if (event.tag !== SUBSCRIPTION_ALARM_TAG
|
|
828
|
+
if (event.tag !== SUBSCRIPTION_ALARM_TAG) {
|
|
829
|
+
const { handlers } = yield* SubscriptionPartitionAlarmExtension;
|
|
830
|
+
const matches = handlers.filter((handler) => handler.tag === event.tag);
|
|
831
|
+
const handler = matches[0];
|
|
832
|
+
|
|
833
|
+
if (
|
|
834
|
+
event.tag.startsWith("effect-agent/") ||
|
|
835
|
+
matches.length !== 1 ||
|
|
836
|
+
handler === undefined
|
|
837
|
+
)
|
|
838
|
+
return yield* SubscriptionAlarmProtocolError.make({
|
|
839
|
+
message: "Unknown or ambiguous ancillary alarm tag",
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
return yield* handler.handle(event).pipe(
|
|
843
|
+
Effect.scoped,
|
|
844
|
+
Effect.timeout(MAX_ANCILLARY_ALARM_MILLIS),
|
|
845
|
+
Effect.catchTag("TimeoutError", () =>
|
|
846
|
+
SubscriptionAlarmExtensionError.make({ code: "timeout" }),
|
|
847
|
+
),
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
if (event.id !== SUBSCRIPTION_ALARM_ID) {
|
|
570
851
|
return yield* SubscriptionAlarmProtocolError.make({
|
|
571
852
|
message: `Unsupported Subscription Partition alarm ${event.tag}/${event.id}`,
|
|
572
853
|
});
|
|
@@ -600,7 +881,12 @@ const alarmHandler = (limits: SubscriptionLimits) =>
|
|
|
600
881
|
yield* alarmControl.reconcile;
|
|
601
882
|
}
|
|
602
883
|
}),
|
|
603
|
-
{
|
|
884
|
+
{
|
|
885
|
+
mode: "isolated",
|
|
886
|
+
limit: MAX_ALARMS_PER_INVOCATION,
|
|
887
|
+
retryFailedAfter: limits.retryMillis,
|
|
888
|
+
onFailure: () => Effect.logWarning("Subscription partition alarm retained for retry"),
|
|
889
|
+
},
|
|
604
890
|
).pipe(Effect.asVoid);
|
|
605
891
|
|
|
606
892
|
type SubscriptionRuntimeServices =
|
|
@@ -669,6 +955,29 @@ export const makeSubscriptionPartitionObjectClass = <E>(
|
|
|
669
955
|
SubscriptionIntake.layer(limits),
|
|
670
956
|
SubscriptionDriver.layer(limits),
|
|
671
957
|
cloudflareLimitsLayer,
|
|
958
|
+
// Capture the host reference before its scoped input Layer is hidden from the runtime.
|
|
959
|
+
Layer.effect(
|
|
960
|
+
SubscriptionPartitionAlarmExtension,
|
|
961
|
+
Effect.gen(function* () {
|
|
962
|
+
const extension = yield* SubscriptionPartitionAlarmExtension;
|
|
963
|
+
const tags = extension.handlers.map((handler) => handler.tag);
|
|
964
|
+
|
|
965
|
+
if (
|
|
966
|
+
tags.length > MAX_ALARMS_PER_INVOCATION ||
|
|
967
|
+
new Set(tags).size !== tags.length ||
|
|
968
|
+
tags.some(
|
|
969
|
+
(tag) => tag.length === 0 || tag.length > 128 || tag.startsWith("effect-agent/"),
|
|
970
|
+
)
|
|
971
|
+
)
|
|
972
|
+
return yield* SubscriptionAlarmProtocolError.make({
|
|
973
|
+
message: "Invalid ancillary alarm handler registry",
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
yield* validateCloudflareSubscriptionLimits(limits, { ancillaryAlarms: tags.length > 0 });
|
|
977
|
+
|
|
978
|
+
return extension;
|
|
979
|
+
}),
|
|
980
|
+
),
|
|
672
981
|
).pipe(
|
|
673
982
|
Layer.provideMerge(partitionStore),
|
|
674
983
|
Layer.provide(
|
|
@@ -682,7 +991,11 @@ export const makeSubscriptionPartitionObjectClass = <E>(
|
|
|
682
991
|
|
|
683
992
|
const runtime: Layer.Layer<
|
|
684
993
|
SubscriptionRuntimeServices,
|
|
685
|
-
|
|
994
|
+
| E
|
|
995
|
+
| SubscriptionError
|
|
996
|
+
| SubscriptionPartitionProtocolError
|
|
997
|
+
| CloudflareSubscriptionConfigError
|
|
998
|
+
| SubscriptionAlarmProtocolError,
|
|
686
999
|
EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
|
|
687
1000
|
> = Layer.effectContext(
|
|
688
1001
|
Effect.gen(function* () {
|
|
@@ -15,9 +15,12 @@ import {
|
|
|
15
15
|
PersistedJson,
|
|
16
16
|
} from "@effect-agent/thread/Records";
|
|
17
17
|
import {
|
|
18
|
+
AdmissionFence,
|
|
19
|
+
AdmissionGroup,
|
|
18
20
|
AbortCommand,
|
|
19
21
|
AbortIntent,
|
|
20
22
|
AdmissionConflict,
|
|
23
|
+
AdmissionPolicyError,
|
|
21
24
|
ApprovalConflict,
|
|
22
25
|
ApprovalDecisionCommand,
|
|
23
26
|
ApprovalDecisionIntent,
|
|
@@ -31,6 +34,7 @@ import {
|
|
|
31
34
|
UnknownResolutionConflict,
|
|
32
35
|
UnknownResolutionIntent,
|
|
33
36
|
} from "@effect-agent/thread/SubmissionLedger";
|
|
37
|
+
import { SubmissionStatus } from "@effect-agent/thread/SubmissionStatus";
|
|
34
38
|
import {
|
|
35
39
|
AppendConflict,
|
|
36
40
|
ThreadNotMaterialized,
|
|
@@ -107,6 +111,8 @@ export class SubmitRequest extends Schema.Class<SubmitRequest>(
|
|
|
107
111
|
agentId: AgentId,
|
|
108
112
|
principal: Principal,
|
|
109
113
|
idempotencyKey: IdempotencyKey,
|
|
114
|
+
admissionGroup: Schema.optionalKey(AdmissionGroup),
|
|
115
|
+
admissionFence: Schema.optionalKey(AdmissionFence),
|
|
110
116
|
definitions: DefinitionDigests,
|
|
111
117
|
inputPayload: PersistedJson,
|
|
112
118
|
}) {}
|
|
@@ -148,6 +154,7 @@ export const HostFailure = Schema.Union([
|
|
|
148
154
|
AgentInputError,
|
|
149
155
|
DigestError,
|
|
150
156
|
AdmissionConflict,
|
|
157
|
+
AdmissionPolicyError,
|
|
151
158
|
SettlementConflict,
|
|
152
159
|
ApprovalConflict,
|
|
153
160
|
UnknownResolutionConflict,
|
|
@@ -172,6 +179,10 @@ export class SubmitSucceeded extends Schema.TaggedClass<SubmitSucceeded>(
|
|
|
172
179
|
receipt: Receipt,
|
|
173
180
|
}) {}
|
|
174
181
|
|
|
182
|
+
export class SubmissionStatusResponse extends Schema.TaggedClass<SubmissionStatusResponse>(
|
|
183
|
+
"@effect-agent/platform-cloudflare/SubmissionStatusResponse",
|
|
184
|
+
)("SubmissionStatusResponse", { status: SubmissionStatus }) {}
|
|
185
|
+
|
|
175
186
|
export class SettlementReached extends Schema.TaggedClass<SettlementReached>(
|
|
176
187
|
"@effect-agent/platform-cloudflare/SettlementReached",
|
|
177
188
|
)("SettlementReached", {
|
|
@@ -222,6 +233,7 @@ export class HostFailed extends Schema.TaggedClass<HostFailed>(
|
|
|
222
233
|
export const HostResponse = Schema.Union([
|
|
223
234
|
SubmitSucceeded,
|
|
224
235
|
SettlementReached,
|
|
236
|
+
SubmissionStatusResponse,
|
|
225
237
|
ObservedPage,
|
|
226
238
|
ProgressObserved,
|
|
227
239
|
ProgressCancelled,
|
|
@@ -265,6 +277,7 @@ const ClientSubmitHostFailure = Schema.Union([
|
|
|
265
277
|
AgentInputError,
|
|
266
278
|
DigestError,
|
|
267
279
|
AdmissionConflict,
|
|
280
|
+
AdmissionPolicyError,
|
|
268
281
|
LedgerError,
|
|
269
282
|
ThreadStoreError,
|
|
270
283
|
ThreadNotMaterialized,
|
|
@@ -339,6 +352,7 @@ const outOfContract = (threadId: string, operation: string, observed: string): T
|
|
|
339
352
|
const hostRpcMethods = {
|
|
340
353
|
submit: "submitEncoded",
|
|
341
354
|
awaitSettlement: "awaitSettlementEncoded",
|
|
355
|
+
submissionStatus: "submissionStatusEncoded",
|
|
342
356
|
awaitProgress: "awaitProgressEncoded",
|
|
343
357
|
cancelProgress: "cancelProgressEncoded",
|
|
344
358
|
observePage: "observePage",
|
|
@@ -358,6 +372,9 @@ export class CloudflareThreadClient extends Context.Service<
|
|
|
358
372
|
options: DurableSubmitOptions,
|
|
359
373
|
) => Effect.Effect<Receipt, ClientSubmitFailure, InputSchema["EncodingServices"]>;
|
|
360
374
|
/** Wake-hinted, poll-guaranteed settlement wait executed inside the owning Object. */
|
|
375
|
+
readonly submissionStatus: (
|
|
376
|
+
receipt: Receipt,
|
|
377
|
+
) => Effect.Effect<SubmissionStatus, ClientAwaitFailure>;
|
|
361
378
|
readonly awaitSettlement: (receipt: Receipt) => Effect.Effect<Settlement, ClientAwaitFailure>;
|
|
362
379
|
/**
|
|
363
380
|
* Wait without polling until progress after `afterSequence` is already durable or hinted.
|
|
@@ -572,6 +589,12 @@ export class CloudflareThreadClient extends Context.Service<
|
|
|
572
589
|
agentId: agent.definition.id,
|
|
573
590
|
principal: options.principal,
|
|
574
591
|
idempotencyKey: options.idempotencyKey,
|
|
592
|
+
...(options.admissionGroup === undefined
|
|
593
|
+
? {}
|
|
594
|
+
: { admissionGroup: options.admissionGroup }),
|
|
595
|
+
...(options.admissionFence === undefined
|
|
596
|
+
? {}
|
|
597
|
+
: { admissionFence: options.admissionFence }),
|
|
575
598
|
definitions: options.definitions,
|
|
576
599
|
inputPayload,
|
|
577
600
|
});
|
|
@@ -596,6 +619,23 @@ export class CloudflareThreadClient extends Context.Service<
|
|
|
596
619
|
return succeeded.receipt;
|
|
597
620
|
}),
|
|
598
621
|
|
|
622
|
+
submissionStatus: (receipt) =>
|
|
623
|
+
Effect.gen(function* () {
|
|
624
|
+
const encoded = yield* encodeReceipt(receipt).pipe(
|
|
625
|
+
Effect.mapError(() => HostProtocolError.make({ message: "Invalid receipt" })),
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
const response = yield* call(receipt.threadId, "submissionStatus", encoded);
|
|
629
|
+
|
|
630
|
+
const result = yield* expect(
|
|
631
|
+
receipt.threadId,
|
|
632
|
+
"submissionStatus",
|
|
633
|
+
SubmissionStatusResponse,
|
|
634
|
+
ClientAwaitHostFailure,
|
|
635
|
+
)(response);
|
|
636
|
+
|
|
637
|
+
return result.status;
|
|
638
|
+
}),
|
|
599
639
|
awaitSettlement: (receipt) =>
|
|
600
640
|
Effect.gen(function* () {
|
|
601
641
|
const encoded = yield* encodeReceipt(receipt).pipe(
|
package/src/ThreadObject.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { PersistedJson } from "@effect-agent/thread/Records";
|
|
29
29
|
import { RunJournalError } from "@effect-agent/thread/RunJournal";
|
|
30
30
|
import {
|
|
31
|
+
AdmissionPolicyError,
|
|
31
32
|
LedgerError,
|
|
32
33
|
OwnershipLost,
|
|
33
34
|
SettlementConflict,
|
|
@@ -73,6 +74,7 @@ import {
|
|
|
73
74
|
ProgressObserved,
|
|
74
75
|
ProgressCancelled,
|
|
75
76
|
SettlementReached,
|
|
77
|
+
SubmissionStatusResponse,
|
|
76
78
|
SubmitSucceeded,
|
|
77
79
|
UnknownResolutionRecorded,
|
|
78
80
|
boundHostDiagnostic,
|
|
@@ -308,6 +310,12 @@ const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoin
|
|
|
308
310
|
threadId: identity.threadId,
|
|
309
311
|
principal: request.principal,
|
|
310
312
|
idempotencyKey: request.idempotencyKey,
|
|
313
|
+
...(request.admissionGroup === undefined
|
|
314
|
+
? {}
|
|
315
|
+
: { admissionGroup: request.admissionGroup }),
|
|
316
|
+
...(request.admissionFence === undefined
|
|
317
|
+
? {}
|
|
318
|
+
: { admissionFence: request.admissionFence }),
|
|
311
319
|
definitions: request.definitions,
|
|
312
320
|
}),
|
|
313
321
|
);
|
|
@@ -319,6 +327,22 @@ const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoin
|
|
|
319
327
|
Effect.flatMap(encodeResponse),
|
|
320
328
|
);
|
|
321
329
|
|
|
330
|
+
const submissionStatusEndpoint = (
|
|
331
|
+
encoded: unknown,
|
|
332
|
+
): Effect.Effect<unknown, never, EndpointServices> =>
|
|
333
|
+
decodeReceipt(encoded).pipe(
|
|
334
|
+
Effect.mapError(protocolFailure("The receipt could not be decoded")),
|
|
335
|
+
Effect.flatMap((receipt) =>
|
|
336
|
+
Effect.gen(function* () {
|
|
337
|
+
const runtime = yield* DurableAgentRuntime;
|
|
338
|
+
|
|
339
|
+
return SubmissionStatusResponse.make({ status: yield* runtime.submissionStatus(receipt) });
|
|
340
|
+
}),
|
|
341
|
+
),
|
|
342
|
+
respond,
|
|
343
|
+
Effect.flatMap(encodeResponse),
|
|
344
|
+
);
|
|
345
|
+
|
|
322
346
|
const awaitSettlementEndpoint = (
|
|
323
347
|
encoded: unknown,
|
|
324
348
|
): Effect.Effect<unknown, never, EndpointServices> =>
|
|
@@ -492,6 +516,7 @@ export class AdminVerifyRequest extends Schema.Class<AdminVerifyRequest>(
|
|
|
492
516
|
|
|
493
517
|
/** Every typed failure of the four admin entry points, plus the protocol's own errors. */
|
|
494
518
|
export const AdminFailure = Schema.Union([
|
|
519
|
+
AdmissionPolicyError,
|
|
495
520
|
OperationDenied,
|
|
496
521
|
RetryRefused,
|
|
497
522
|
LedgerError,
|
|
@@ -781,6 +806,7 @@ export interface Instance<EventServices = never> extends InstanceType<
|
|
|
781
806
|
EffectCfDurableObject.DurableObjectClass<Record<never, never>, RuntimeServices | EventServices>
|
|
782
807
|
> {
|
|
783
808
|
submitEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
809
|
+
submissionStatusEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
784
810
|
awaitSettlementEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
785
811
|
awaitProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
786
812
|
cancelProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
|
|
@@ -859,6 +885,7 @@ export const make = <
|
|
|
859
885
|
|
|
860
886
|
const rpc = {
|
|
861
887
|
submitEncoded: (encoded: unknown) => submitEndpoint(encoded),
|
|
888
|
+
submissionStatusEncoded: (encoded: unknown) => submissionStatusEndpoint(encoded),
|
|
862
889
|
awaitSettlementEncoded: (encoded: unknown) => awaitSettlementEndpoint(encoded),
|
|
863
890
|
awaitProgressEncoded: (encoded: unknown) => awaitProgressEndpoint(encoded),
|
|
864
891
|
cancelProgressEncoded: (encoded: unknown) => cancelProgressEndpoint(encoded),
|