@effect-agent/platform-cloudflare 0.1.0-beta.50 → 0.1.0-beta.52
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 +34 -4
- package/dist/CloudflareSubscriptions.mjs +171 -11
- package/dist/CloudflareSubscriptions.mjs.map +1 -1
- package/dist/CloudflareThreadClient.d.mts +88 -13
- package/dist/CloudflareThreadClient.mjs +18 -2
- package/dist/CloudflareThreadClient.mjs.map +1 -1
- package/dist/{ThreadObject-CmeEjnBK.d.mts → ThreadObject-BSRVxtPI.d.mts} +11 -6
- package/dist/{ThreadObject-IrGuIO4a.mjs → ThreadObject-BY8axaWT.mjs} +11 -3
- package/dist/ThreadObject-BY8axaWT.mjs.map +1 -0
- 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 +340 -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,105 @@ 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
|
+
/** Native partition services supplied at alarm invocation, after the host Layer is built. */
|
|
69
|
+
export type SubscriptionPartitionAlarmServices =
|
|
70
|
+
| Subscriptions
|
|
71
|
+
| SubscriptionIntake
|
|
72
|
+
| SubscriptionDriver;
|
|
73
|
+
|
|
74
|
+
export interface SubscriptionPartitionAlarmHandler<R = SubscriptionPartitionAlarmServices> {
|
|
75
|
+
readonly tag: string;
|
|
76
|
+
readonly handle: (
|
|
77
|
+
event: DurableObjectAlarm.DurableObjectAlarmEvent,
|
|
78
|
+
) => Effect.Effect<void, SubscriptionAlarmProtocolError | SubscriptionAlarmExtensionError, R>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Host-only handlers; the framework reserves its namespace and rejects every unknown tag. */
|
|
82
|
+
export const SubscriptionPartitionAlarmExtension = Context.Reference<{
|
|
83
|
+
readonly handlers: ReadonlyArray<SubscriptionPartitionAlarmHandler>;
|
|
84
|
+
}>("@effect-agent/platform-cloudflare/SubscriptionPartitionAlarmExtension", {
|
|
85
|
+
defaultValue: () => ({ handlers: [] }),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/** Capture host services once, deferring native partition services to invocation.
|
|
89
|
+
* Each invocation owns its codec/handler Scope and timeout.
|
|
90
|
+
* Callback failures stay typed. Defects and interruption reach the native alarm multiplexer.
|
|
91
|
+
* The host owns durable idempotency, prearming and external-effect uncertainty.
|
|
92
|
+
*/
|
|
93
|
+
export const makeSubscriptionPartitionAlarmHandler = Effect.fn(
|
|
94
|
+
"makeSubscriptionPartitionAlarmHandler",
|
|
95
|
+
)(function* <Payload extends Schema.Top, R>(options: {
|
|
96
|
+
readonly tag: string;
|
|
97
|
+
readonly payload: Payload;
|
|
98
|
+
readonly timeoutMillis: number;
|
|
99
|
+
readonly handle: (
|
|
100
|
+
event: Omit<DurableObjectAlarm.DurableObjectAlarmEvent, "payload"> & {
|
|
101
|
+
readonly payload: Payload["Type"];
|
|
102
|
+
},
|
|
103
|
+
) => Effect.Effect<void, SubscriptionAlarmExtensionError, R>;
|
|
104
|
+
}): Effect.fn.Return<
|
|
105
|
+
SubscriptionPartitionAlarmHandler<
|
|
106
|
+
Exclude<
|
|
107
|
+
Exclude<R | Payload["DecodingServices"], Scope.Scope>,
|
|
108
|
+
Exclude<
|
|
109
|
+
Exclude<R | Payload["DecodingServices"], Scope.Scope | SubscriptionPartitionAlarmServices>,
|
|
110
|
+
SubscriptionPartitionAlarmServices
|
|
111
|
+
>
|
|
112
|
+
>
|
|
113
|
+
>,
|
|
114
|
+
SubscriptionAlarmProtocolError,
|
|
115
|
+
Exclude<R | Payload["DecodingServices"], Scope.Scope | SubscriptionPartitionAlarmServices>
|
|
116
|
+
> {
|
|
117
|
+
if (
|
|
118
|
+
options.tag.length === 0 ||
|
|
119
|
+
options.tag.length > 128 ||
|
|
120
|
+
options.tag.startsWith("effect-agent/") ||
|
|
121
|
+
!Number.isSafeInteger(options.timeoutMillis) ||
|
|
122
|
+
options.timeoutMillis < 1 ||
|
|
123
|
+
options.timeoutMillis > MAX_ANCILLARY_ALARM_MILLIS
|
|
124
|
+
)
|
|
125
|
+
return yield* SubscriptionAlarmProtocolError.make({
|
|
126
|
+
message: "Invalid ancillary alarm tag or timeout",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Context capture includes unrequested services too; never retain a host override of native work.
|
|
130
|
+
const services = (yield* Effect.context<
|
|
131
|
+
Exclude<R | Payload["DecodingServices"], Scope.Scope | SubscriptionPartitionAlarmServices>
|
|
132
|
+
>()).pipe(Context.omit(Subscriptions, SubscriptionIntake, SubscriptionDriver));
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
tag: options.tag,
|
|
136
|
+
handle: (event) =>
|
|
137
|
+
Effect.gen(function* () {
|
|
138
|
+
if (event.tag !== options.tag)
|
|
139
|
+
return yield* SubscriptionAlarmProtocolError.make({
|
|
140
|
+
message: "Ancillary alarm tag mismatch",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const payload = yield* Schema.decodeUnknownEffect(options.payload)(event.payload).pipe(
|
|
144
|
+
Effect.mapError(() =>
|
|
145
|
+
SubscriptionAlarmProtocolError.make({ message: "Invalid ancillary alarm payload" }),
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
yield* options.handle({ ...event, payload });
|
|
150
|
+
}).pipe(
|
|
151
|
+
Effect.scoped,
|
|
152
|
+
Effect.timeout(options.timeoutMillis),
|
|
153
|
+
Effect.catchTag("TimeoutError", () =>
|
|
154
|
+
SubscriptionAlarmExtensionError.make({ code: "timeout" }),
|
|
155
|
+
),
|
|
156
|
+
Effect.provideContext(services),
|
|
157
|
+
),
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
|
|
59
161
|
export class SubscriptionPartitionProtocolError extends Schema.TaggedError<SubscriptionPartitionProtocolError>()(
|
|
60
162
|
"SubscriptionPartitionProtocolError",
|
|
61
163
|
{ message: Schema.String.check(Schema.isMaxLength(4_096)) },
|
|
@@ -69,9 +171,11 @@ export class CloudflareSubscriptionConfigError extends Schema.TaggedError<Cloudf
|
|
|
69
171
|
/** Reject limits whose four bounded phases could exceed the safe Durable Object alarm budget. */
|
|
70
172
|
export const validateCloudflareSubscriptionLimits = (
|
|
71
173
|
limits: SubscriptionLimits,
|
|
174
|
+
options: { readonly ancillaryAlarms?: boolean } = {},
|
|
72
175
|
): Effect.Effect<void, CloudflareSubscriptionConfigError> => {
|
|
73
176
|
const worstCaseMillis =
|
|
74
|
-
4 * Math.ceil(limits.batchSize / limits.concurrency) * limits.operationTimeoutMillis
|
|
177
|
+
4 * Math.ceil(limits.batchSize / limits.concurrency) * limits.operationTimeoutMillis +
|
|
178
|
+
(options.ancillaryAlarms === true ? MAX_ALARMS_PER_INVOCATION * MAX_ANCILLARY_ALARM_MILLIS : 0);
|
|
75
179
|
|
|
76
180
|
return worstCaseMillis <= MAX_ALARM_WALL_MILLIS
|
|
77
181
|
? Effect.void
|
|
@@ -96,14 +200,32 @@ const SubscribeRequest = Schema.TaggedStruct("Subscribe", {
|
|
|
96
200
|
parameters: PersistedJson,
|
|
97
201
|
context: PersistedJson,
|
|
98
202
|
mode: Schema.Literals(["once", "continuous"]),
|
|
99
|
-
expiresAtMillis: Schema.Number,
|
|
203
|
+
expiresAtMillis: Schema.NullOr(Schema.Number),
|
|
100
204
|
destination: SubscriptionConfiguration.fields.destination,
|
|
101
205
|
deliveryPrincipal: SubscriptionConfiguration.fields.deliveryPrincipal,
|
|
206
|
+
admissionGroup: SubscriptionConfiguration.fields.admissionGroup,
|
|
207
|
+
admissionFence: SubscriptionConfiguration.fields.admissionFence,
|
|
102
208
|
agentId: SubscriptionConfiguration.fields.agentId,
|
|
103
209
|
definitions: SubscriptionConfiguration.fields.definitions,
|
|
104
210
|
}),
|
|
105
211
|
});
|
|
106
212
|
|
|
213
|
+
const UpdateSubscriptionRequest = Schema.TaggedStruct("UpdateSubscription", {
|
|
214
|
+
schemaVersion: Schema.Literal(1),
|
|
215
|
+
scope: SubscribeRequest.fields.scope,
|
|
216
|
+
key: SubscriptionKey,
|
|
217
|
+
expectedRevision: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
218
|
+
options: SubscribeRequest.fields.options.mapFields(({ subscriptionId: _, ...fields }) => fields),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const SubscriptionStateRequest = Schema.Struct({
|
|
222
|
+
_tag: Schema.Literals(["PauseSubscription", "ResumeSubscription", "RecoverSubscription"]),
|
|
223
|
+
schemaVersion: Schema.Literal(1),
|
|
224
|
+
scope: SubscribeRequest.fields.scope,
|
|
225
|
+
key: SubscriptionKey,
|
|
226
|
+
expectedRevision: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
227
|
+
});
|
|
228
|
+
|
|
107
229
|
const ListSubscriptionsRequest = Schema.TaggedStruct("ListSubscriptions", {
|
|
108
230
|
schemaVersion: Schema.Literal(1),
|
|
109
231
|
scope: SubscribeRequest.fields.scope,
|
|
@@ -112,11 +234,25 @@ const ListSubscriptionsRequest = Schema.TaggedStruct("ListSubscriptions", {
|
|
|
112
234
|
});
|
|
113
235
|
|
|
114
236
|
const CancelSubscriptionRequest = Schema.TaggedStruct("CancelSubscription", {
|
|
237
|
+
expectedRevision: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
|
|
115
238
|
schemaVersion: Schema.Literal(1),
|
|
116
239
|
scope: SubscribeRequest.fields.scope,
|
|
117
240
|
key: SubscriptionKey,
|
|
118
241
|
});
|
|
119
242
|
|
|
243
|
+
const GetSubscriptionRequest = Schema.TaggedStruct("GetSubscription", {
|
|
244
|
+
schemaVersion: Schema.Literal(1),
|
|
245
|
+
scope: SubscriptionScope,
|
|
246
|
+
key: SubscriptionKey,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const RecoverDeliveryRequest = Schema.TaggedStruct("RecoverDelivery", {
|
|
250
|
+
expectedGeneration: Schema.Natural,
|
|
251
|
+
schemaVersion: Schema.Literal(1),
|
|
252
|
+
scope: SubscribeRequest.fields.scope,
|
|
253
|
+
key: SubscriptionDeliveryKey,
|
|
254
|
+
});
|
|
255
|
+
|
|
120
256
|
const ListDeliveriesRequest = Schema.TaggedStruct("ListDeliveries", {
|
|
121
257
|
schemaVersion: Schema.Literal(1),
|
|
122
258
|
scope: SubscribeRequest.fields.scope,
|
|
@@ -143,9 +279,13 @@ const StatusRequest = Schema.TaggedStruct("Status", {
|
|
|
143
279
|
|
|
144
280
|
const SubscriptionPartitionRequest = Schema.Union([
|
|
145
281
|
SubscribeRequest,
|
|
282
|
+
GetSubscriptionRequest,
|
|
283
|
+
UpdateSubscriptionRequest,
|
|
284
|
+
SubscriptionStateRequest,
|
|
146
285
|
ListSubscriptionsRequest,
|
|
147
286
|
CancelSubscriptionRequest,
|
|
148
287
|
ListDeliveriesRequest,
|
|
288
|
+
RecoverDeliveryRequest,
|
|
149
289
|
AcceptRequest,
|
|
150
290
|
StatusRequest,
|
|
151
291
|
]);
|
|
@@ -182,6 +322,7 @@ const SubscriptionPartitionResponse = Schema.Union([
|
|
|
182
322
|
Schema.TaggedStruct("Snapshot", { value: SubscriptionSnapshot }),
|
|
183
323
|
Schema.TaggedStruct("SubscriptionPage", { value: SubscriptionPage }),
|
|
184
324
|
Schema.TaggedStruct("DeliveryPage", { value: DeliveryPage }),
|
|
325
|
+
Schema.TaggedStruct("DeliverySnapshot", { value: SubscriptionDeliverySnapshot }),
|
|
185
326
|
Schema.TaggedStruct("Acknowledgement", { value: EventAcknowledgement }),
|
|
186
327
|
Schema.TaggedStruct("Status", { value: IntakeStatus }),
|
|
187
328
|
Schema.TaggedStruct("Failed", { failure: SubscriptionPartitionFailure }),
|
|
@@ -261,7 +402,88 @@ export class CloudflareSubscriptionsClient {
|
|
|
261
402
|
const asProtocolError = (): SubscriptionError =>
|
|
262
403
|
corruptFailure("subscription-partition-protocol");
|
|
263
404
|
|
|
405
|
+
const changeState = Effect.fn("CloudflareSubscriptions.changeState")(function* (
|
|
406
|
+
scope: typeof SubscriptionScope.Type,
|
|
407
|
+
key: typeof SubscriptionKey.Type,
|
|
408
|
+
expectedRevision: number,
|
|
409
|
+
_tag: "PauseSubscription" | "ResumeSubscription" | "RecoverSubscription",
|
|
410
|
+
) {
|
|
411
|
+
const response = yield* call(scope.partition, {
|
|
412
|
+
_tag,
|
|
413
|
+
schemaVersion: 1,
|
|
414
|
+
scope,
|
|
415
|
+
key,
|
|
416
|
+
expectedRevision,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
if (response._tag === "Snapshot") return response.value;
|
|
420
|
+
const failure = failed(response);
|
|
421
|
+
|
|
422
|
+
return yield* failure._tag === "SubscriptionError" ||
|
|
423
|
+
failure._tag === "SubscriptionFailpointError"
|
|
424
|
+
? failure
|
|
425
|
+
: asProtocolError();
|
|
426
|
+
});
|
|
427
|
+
|
|
264
428
|
const subscriptions = Subscriptions.of({
|
|
429
|
+
recoverSubscription: (scope, key, revision) =>
|
|
430
|
+
changeState(scope, key, revision, "RecoverSubscription"),
|
|
431
|
+
getSubscription: (scope, key) =>
|
|
432
|
+
Effect.gen(function* () {
|
|
433
|
+
const response = yield* call(scope.partition, {
|
|
434
|
+
_tag: "GetSubscription",
|
|
435
|
+
schemaVersion: 1,
|
|
436
|
+
scope,
|
|
437
|
+
key,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
if (response._tag === "Snapshot") return response.value;
|
|
441
|
+
const failure = failed(response);
|
|
442
|
+
|
|
443
|
+
return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
|
|
444
|
+
}),
|
|
445
|
+
recoverDelivery: (scope, key, expectedGeneration) =>
|
|
446
|
+
Effect.gen(function* () {
|
|
447
|
+
const response = yield* call(scope.partition, {
|
|
448
|
+
_tag: "RecoverDelivery",
|
|
449
|
+
expectedGeneration,
|
|
450
|
+
schemaVersion: 1,
|
|
451
|
+
scope,
|
|
452
|
+
key,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
if (response._tag === "DeliverySnapshot") return response.value;
|
|
456
|
+
const failure = failed(response);
|
|
457
|
+
|
|
458
|
+
return yield* failure._tag === "SubscriptionError" ||
|
|
459
|
+
failure._tag === "SubscriptionFailpointError"
|
|
460
|
+
? failure
|
|
461
|
+
: asProtocolError();
|
|
462
|
+
}),
|
|
463
|
+
updateSubscription: (scope, key, expectedRevision, options) =>
|
|
464
|
+
Effect.gen(function* () {
|
|
465
|
+
const response = yield* call(scope.partition, {
|
|
466
|
+
_tag: "UpdateSubscription",
|
|
467
|
+
schemaVersion: 1,
|
|
468
|
+
scope,
|
|
469
|
+
key,
|
|
470
|
+
expectedRevision,
|
|
471
|
+
options,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
if (response._tag === "Snapshot") return response.value;
|
|
475
|
+
const failure = failed(response);
|
|
476
|
+
|
|
477
|
+
return yield* failure._tag === "SubscriptionError" ||
|
|
478
|
+
failure._tag === "SubscriptionFailpointError" ||
|
|
479
|
+
failure._tag === "SubscriptionSourceError"
|
|
480
|
+
? failure
|
|
481
|
+
: asProtocolError();
|
|
482
|
+
}),
|
|
483
|
+
pauseSubscription: (scope, key, revision) =>
|
|
484
|
+
changeState(scope, key, revision, "PauseSubscription"),
|
|
485
|
+
resumeSubscription: (scope, key, revision) =>
|
|
486
|
+
changeState(scope, key, revision, "ResumeSubscription"),
|
|
265
487
|
subscribe: (scope, options) =>
|
|
266
488
|
Effect.gen(function* () {
|
|
267
489
|
const response = yield* call(scope.partition, {
|
|
@@ -298,10 +520,11 @@ export class CloudflareSubscriptionsClient {
|
|
|
298
520
|
|
|
299
521
|
return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
|
|
300
522
|
}),
|
|
301
|
-
cancelSubscription: (scope, key) =>
|
|
523
|
+
cancelSubscription: (scope, key, expectedRevision) =>
|
|
302
524
|
Effect.gen(function* () {
|
|
303
525
|
const response = yield* call(scope.partition, {
|
|
304
526
|
_tag: "CancelSubscription",
|
|
527
|
+
...(expectedRevision === undefined ? {} : { expectedRevision }),
|
|
305
528
|
schemaVersion: 1,
|
|
306
529
|
scope,
|
|
307
530
|
key,
|
|
@@ -461,11 +684,53 @@ const handleRequest = Effect.fn("SubscriptionPartition.handleRequest")(function*
|
|
|
461
684
|
SubscriptionPartitionFailure
|
|
462
685
|
> {
|
|
463
686
|
switch (request._tag) {
|
|
687
|
+
case "GetSubscription":
|
|
688
|
+
return {
|
|
689
|
+
_tag: "Snapshot",
|
|
690
|
+
value: yield* subscriptions.getSubscription(request.scope, request.key),
|
|
691
|
+
};
|
|
464
692
|
case "Subscribe":
|
|
465
693
|
return {
|
|
466
694
|
_tag: "Snapshot",
|
|
467
695
|
value: yield* subscriptions.subscribe(request.scope, request.options),
|
|
468
696
|
};
|
|
697
|
+
case "UpdateSubscription":
|
|
698
|
+
return {
|
|
699
|
+
_tag: "Snapshot",
|
|
700
|
+
value: yield* subscriptions.updateSubscription(
|
|
701
|
+
request.scope,
|
|
702
|
+
request.key,
|
|
703
|
+
request.expectedRevision,
|
|
704
|
+
request.options,
|
|
705
|
+
),
|
|
706
|
+
};
|
|
707
|
+
case "RecoverSubscription":
|
|
708
|
+
return {
|
|
709
|
+
_tag: "Snapshot",
|
|
710
|
+
value: yield* subscriptions.recoverSubscription(
|
|
711
|
+
request.scope,
|
|
712
|
+
request.key,
|
|
713
|
+
request.expectedRevision,
|
|
714
|
+
),
|
|
715
|
+
};
|
|
716
|
+
case "PauseSubscription":
|
|
717
|
+
return {
|
|
718
|
+
_tag: "Snapshot",
|
|
719
|
+
value: yield* subscriptions.pauseSubscription(
|
|
720
|
+
request.scope,
|
|
721
|
+
request.key,
|
|
722
|
+
request.expectedRevision,
|
|
723
|
+
),
|
|
724
|
+
};
|
|
725
|
+
case "ResumeSubscription":
|
|
726
|
+
return {
|
|
727
|
+
_tag: "Snapshot",
|
|
728
|
+
value: yield* subscriptions.resumeSubscription(
|
|
729
|
+
request.scope,
|
|
730
|
+
request.key,
|
|
731
|
+
request.expectedRevision,
|
|
732
|
+
),
|
|
733
|
+
};
|
|
469
734
|
case "ListSubscriptions":
|
|
470
735
|
return {
|
|
471
736
|
_tag: "SubscriptionPage",
|
|
@@ -478,7 +743,20 @@ const handleRequest = Effect.fn("SubscriptionPartition.handleRequest")(function*
|
|
|
478
743
|
case "CancelSubscription":
|
|
479
744
|
return {
|
|
480
745
|
_tag: "Snapshot",
|
|
481
|
-
value: yield* subscriptions.cancelSubscription(
|
|
746
|
+
value: yield* subscriptions.cancelSubscription(
|
|
747
|
+
request.scope,
|
|
748
|
+
request.key,
|
|
749
|
+
request.expectedRevision,
|
|
750
|
+
),
|
|
751
|
+
};
|
|
752
|
+
case "RecoverDelivery":
|
|
753
|
+
return {
|
|
754
|
+
_tag: "DeliverySnapshot",
|
|
755
|
+
value: yield* subscriptions.recoverDelivery(
|
|
756
|
+
request.scope,
|
|
757
|
+
request.key,
|
|
758
|
+
request.expectedGeneration,
|
|
759
|
+
),
|
|
482
760
|
};
|
|
483
761
|
case "ListDeliveries":
|
|
484
762
|
return {
|
|
@@ -566,7 +844,29 @@ const alarmHandler = (limits: SubscriptionLimits) =>
|
|
|
566
844
|
DurableObjectAlarm.processDue(
|
|
567
845
|
(event) =>
|
|
568
846
|
Effect.gen(function* () {
|
|
569
|
-
if (event.tag !== SUBSCRIPTION_ALARM_TAG
|
|
847
|
+
if (event.tag !== SUBSCRIPTION_ALARM_TAG) {
|
|
848
|
+
const { handlers } = yield* SubscriptionPartitionAlarmExtension;
|
|
849
|
+
const matches = handlers.filter((handler) => handler.tag === event.tag);
|
|
850
|
+
const handler = matches[0];
|
|
851
|
+
|
|
852
|
+
if (
|
|
853
|
+
event.tag.startsWith("effect-agent/") ||
|
|
854
|
+
matches.length !== 1 ||
|
|
855
|
+
handler === undefined
|
|
856
|
+
)
|
|
857
|
+
return yield* SubscriptionAlarmProtocolError.make({
|
|
858
|
+
message: "Unknown or ambiguous ancillary alarm tag",
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
return yield* handler.handle(event).pipe(
|
|
862
|
+
Effect.scoped,
|
|
863
|
+
Effect.timeout(MAX_ANCILLARY_ALARM_MILLIS),
|
|
864
|
+
Effect.catchTag("TimeoutError", () =>
|
|
865
|
+
SubscriptionAlarmExtensionError.make({ code: "timeout" }),
|
|
866
|
+
),
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
if (event.id !== SUBSCRIPTION_ALARM_ID) {
|
|
570
870
|
return yield* SubscriptionAlarmProtocolError.make({
|
|
571
871
|
message: `Unsupported Subscription Partition alarm ${event.tag}/${event.id}`,
|
|
572
872
|
});
|
|
@@ -600,7 +900,12 @@ const alarmHandler = (limits: SubscriptionLimits) =>
|
|
|
600
900
|
yield* alarmControl.reconcile;
|
|
601
901
|
}
|
|
602
902
|
}),
|
|
603
|
-
{
|
|
903
|
+
{
|
|
904
|
+
mode: "isolated",
|
|
905
|
+
limit: MAX_ALARMS_PER_INVOCATION,
|
|
906
|
+
retryFailedAfter: limits.retryMillis,
|
|
907
|
+
onFailure: () => Effect.logWarning("Subscription partition alarm retained for retry"),
|
|
908
|
+
},
|
|
604
909
|
).pipe(Effect.asVoid);
|
|
605
910
|
|
|
606
911
|
type SubscriptionRuntimeServices =
|
|
@@ -669,6 +974,29 @@ export const makeSubscriptionPartitionObjectClass = <E>(
|
|
|
669
974
|
SubscriptionIntake.layer(limits),
|
|
670
975
|
SubscriptionDriver.layer(limits),
|
|
671
976
|
cloudflareLimitsLayer,
|
|
977
|
+
// Capture the host reference before its scoped input Layer is hidden from the runtime.
|
|
978
|
+
Layer.effect(
|
|
979
|
+
SubscriptionPartitionAlarmExtension,
|
|
980
|
+
Effect.gen(function* () {
|
|
981
|
+
const extension = yield* SubscriptionPartitionAlarmExtension;
|
|
982
|
+
const tags = extension.handlers.map((handler) => handler.tag);
|
|
983
|
+
|
|
984
|
+
if (
|
|
985
|
+
tags.length > MAX_ALARMS_PER_INVOCATION ||
|
|
986
|
+
new Set(tags).size !== tags.length ||
|
|
987
|
+
tags.some(
|
|
988
|
+
(tag) => tag.length === 0 || tag.length > 128 || tag.startsWith("effect-agent/"),
|
|
989
|
+
)
|
|
990
|
+
)
|
|
991
|
+
return yield* SubscriptionAlarmProtocolError.make({
|
|
992
|
+
message: "Invalid ancillary alarm handler registry",
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
yield* validateCloudflareSubscriptionLimits(limits, { ancillaryAlarms: tags.length > 0 });
|
|
996
|
+
|
|
997
|
+
return extension;
|
|
998
|
+
}),
|
|
999
|
+
),
|
|
672
1000
|
).pipe(
|
|
673
1001
|
Layer.provideMerge(partitionStore),
|
|
674
1002
|
Layer.provide(
|
|
@@ -682,7 +1010,11 @@ export const makeSubscriptionPartitionObjectClass = <E>(
|
|
|
682
1010
|
|
|
683
1011
|
const runtime: Layer.Layer<
|
|
684
1012
|
SubscriptionRuntimeServices,
|
|
685
|
-
|
|
1013
|
+
| E
|
|
1014
|
+
| SubscriptionError
|
|
1015
|
+
| SubscriptionPartitionProtocolError
|
|
1016
|
+
| CloudflareSubscriptionConfigError
|
|
1017
|
+
| SubscriptionAlarmProtocolError,
|
|
686
1018
|
EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
|
|
687
1019
|
> = Layer.effectContext(
|
|
688
1020
|
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),
|