@effect-agent/platform-cloudflare 0.1.0-beta.38 → 0.1.0-beta.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bindings-C00ZfjsX.d.mts +100 -0
- package/dist/index.d.mts +196 -211
- package/dist/index.mjs +157 -163
- package/dist/index.mjs.map +1 -1
- package/dist/interactive-browser.d.mts +144 -2
- package/dist/interactive-browser.mjs +1153 -1
- package/dist/interactive-browser.mjs.map +1 -0
- package/dist/{scheduling-B-OFqoS9.mjs → prepared-admission-D5hQ-ofV.mjs} +121 -432
- package/dist/prepared-admission-D5hQ-ofV.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/scheduling.d.mts +47 -2
- package/dist/scheduling.mjs +339 -1
- package/dist/scheduling.mjs.map +1 -0
- package/dist/subscriptions.d.mts +52 -0
- package/dist/subscriptions.mjs +346 -0
- package/dist/subscriptions.mjs.map +1 -0
- package/package.json +18 -11
- package/src/alarm.ts +264 -265
- package/src/bindings.ts +30 -30
- package/src/client.ts +90 -98
- package/src/config.ts +7 -7
- package/src/index.ts +9 -11
- package/src/layers.ts +178 -257
- package/src/prepared-admission.ts +90 -0
- package/src/scheduling.ts +19 -55
- package/src/subscriptions.ts +661 -0
- package/src/{conversation-object.ts → thread-object.ts} +167 -136
- package/src/transport.ts +15 -15
- package/src/wake-scheduler.ts +17 -22
- package/dist/interactive-browser-BRo-Bfvb.d.mts +0 -144
- package/dist/interactive-browser-Cy5q-ldl.mjs +0 -1154
- package/dist/interactive-browser-Cy5q-ldl.mjs.map +0 -1
- package/dist/scheduling-B-OFqoS9.mjs.map +0 -1
- package/dist/scheduling-BJs_kHTx.d.mts +0 -142
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DoSubscriptionAlarmControl,
|
|
3
|
+
DoSubscriptionTransaction,
|
|
4
|
+
doSubscriptionStoreLayer,
|
|
5
|
+
} from "@effect-agent/storage-cloudflare";
|
|
6
|
+
import {
|
|
7
|
+
EventAcknowledgement,
|
|
8
|
+
EventSourceVersion,
|
|
9
|
+
type EventSources,
|
|
10
|
+
type SubscriptionInputBindings,
|
|
11
|
+
PersistedJson,
|
|
12
|
+
Principal,
|
|
13
|
+
SourcePartition,
|
|
14
|
+
type SubscriptionAuthorizer,
|
|
15
|
+
SubscriptionConfiguration,
|
|
16
|
+
SubscriptionDriver,
|
|
17
|
+
SubscriptionDeliverySnapshot,
|
|
18
|
+
SubscriptionError,
|
|
19
|
+
SubscriptionFailpointError,
|
|
20
|
+
SubscriptionIntake,
|
|
21
|
+
SubscriptionKey,
|
|
22
|
+
SubscriptionScope,
|
|
23
|
+
type SubscriptionLimits,
|
|
24
|
+
SubscriptionSnapshot,
|
|
25
|
+
SubscriptionSourceError,
|
|
26
|
+
Subscriptions,
|
|
27
|
+
defaultSubscriptionLimits,
|
|
28
|
+
} from "@effect-agent/thread";
|
|
29
|
+
import { BrowserCrypto } from "@effect/platform-browser";
|
|
30
|
+
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
31
|
+
import { Cause, Clock, Context, DateTime, Effect, Layer, Schema } from "effect";
|
|
32
|
+
import {
|
|
33
|
+
DurableObject as EffectCfDurableObject,
|
|
34
|
+
DurableObjectAlarm,
|
|
35
|
+
DurableObjectState as EffectCfDurableObjectState,
|
|
36
|
+
type WorkerEnvironment,
|
|
37
|
+
} from "effect-cf";
|
|
38
|
+
|
|
39
|
+
import { type ThreadObjectNamespace } from "./bindings.ts";
|
|
40
|
+
import { CloudflareThreadClient } from "./client.ts";
|
|
41
|
+
import { cloudflarePreparedInputAdmissionLayer } from "./prepared-admission.ts";
|
|
42
|
+
|
|
43
|
+
const SUBSCRIPTION_ALARM_TAG = "effect-agent/SubscriptionPartitionWake";
|
|
44
|
+
const SUBSCRIPTION_ALARM_ID = "driver";
|
|
45
|
+
const MAX_ALARM_WALL_MILLIS = 12 * 60_000;
|
|
46
|
+
|
|
47
|
+
const SubscriptionAlarmPayload = Schema.Struct({
|
|
48
|
+
schemaVersion: Schema.Literal(1),
|
|
49
|
+
generation: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export class SubscriptionAlarmProtocolError extends Schema.TaggedError<SubscriptionAlarmProtocolError>()(
|
|
53
|
+
"SubscriptionAlarmProtocolError",
|
|
54
|
+
{ message: Schema.String },
|
|
55
|
+
) {}
|
|
56
|
+
|
|
57
|
+
export class SubscriptionPartitionProtocolError extends Schema.TaggedError<SubscriptionPartitionProtocolError>()(
|
|
58
|
+
"SubscriptionPartitionProtocolError",
|
|
59
|
+
{ message: Schema.String.check(Schema.isMaxLength(4_096)) },
|
|
60
|
+
) {}
|
|
61
|
+
|
|
62
|
+
export class CloudflareSubscriptionConfigError extends Schema.TaggedError<CloudflareSubscriptionConfigError>()(
|
|
63
|
+
"CloudflareSubscriptionConfigError",
|
|
64
|
+
{ message: Schema.String },
|
|
65
|
+
) {}
|
|
66
|
+
|
|
67
|
+
/** Reject limits whose four bounded phases could exceed the safe Durable Object alarm budget. */
|
|
68
|
+
export const validateCloudflareSubscriptionLimits = (
|
|
69
|
+
limits: SubscriptionLimits,
|
|
70
|
+
): Effect.Effect<void, CloudflareSubscriptionConfigError> => {
|
|
71
|
+
const worstCaseMillis =
|
|
72
|
+
4 * Math.ceil(limits.batchSize / limits.concurrency) * limits.operationTimeoutMillis;
|
|
73
|
+
return worstCaseMillis <= MAX_ALARM_WALL_MILLIS
|
|
74
|
+
? Effect.void
|
|
75
|
+
: Effect.fail(
|
|
76
|
+
CloudflareSubscriptionConfigError.make({
|
|
77
|
+
message: "Subscription limits can exceed the bounded Durable Object alarm wall budget",
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const protocolMessage = (message: string): string =>
|
|
83
|
+
message.length <= 4_096 ? message : `${message.slice(0, 4_093)}...`;
|
|
84
|
+
|
|
85
|
+
const SubscribeRequest = Schema.TaggedStruct("Subscribe", {
|
|
86
|
+
schemaVersion: Schema.Literal(1),
|
|
87
|
+
scope: Schema.Struct({
|
|
88
|
+
...SubscriptionScope.fields,
|
|
89
|
+
}),
|
|
90
|
+
options: Schema.Struct({
|
|
91
|
+
subscriptionId: SubscriptionKey.fields.subscriptionId,
|
|
92
|
+
source: EventSourceVersion,
|
|
93
|
+
parameters: PersistedJson,
|
|
94
|
+
context: PersistedJson,
|
|
95
|
+
mode: Schema.Literals(["once", "continuous"]),
|
|
96
|
+
expiresAtMillis: Schema.Number,
|
|
97
|
+
destination: SubscriptionConfiguration.fields.destination,
|
|
98
|
+
deliveryPrincipal: SubscriptionConfiguration.fields.deliveryPrincipal,
|
|
99
|
+
agentId: SubscriptionConfiguration.fields.agentId,
|
|
100
|
+
definitions: SubscriptionConfiguration.fields.definitions,
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const ListSubscriptionsRequest = Schema.TaggedStruct("ListSubscriptions", {
|
|
105
|
+
schemaVersion: Schema.Literal(1),
|
|
106
|
+
scope: SubscribeRequest.fields.scope,
|
|
107
|
+
after: Schema.optionalKey(Schema.Natural),
|
|
108
|
+
limit: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const CancelSubscriptionRequest = Schema.TaggedStruct("CancelSubscription", {
|
|
112
|
+
schemaVersion: Schema.Literal(1),
|
|
113
|
+
scope: SubscribeRequest.fields.scope,
|
|
114
|
+
key: SubscriptionKey,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const ListDeliveriesRequest = Schema.TaggedStruct("ListDeliveries", {
|
|
118
|
+
schemaVersion: Schema.Literal(1),
|
|
119
|
+
scope: SubscribeRequest.fields.scope,
|
|
120
|
+
key: SubscriptionKey,
|
|
121
|
+
after: Schema.optionalKey(Schema.String),
|
|
122
|
+
limit: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const AcceptRequest = Schema.TaggedStruct("Accept", {
|
|
126
|
+
schemaVersion: Schema.Literal(1),
|
|
127
|
+
partition: SourcePartition,
|
|
128
|
+
principal: Principal,
|
|
129
|
+
source: EventSourceVersion,
|
|
130
|
+
payload: PersistedJson,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const StatusRequest = Schema.TaggedStruct("Status", {
|
|
134
|
+
schemaVersion: Schema.Literal(1),
|
|
135
|
+
partition: SourcePartition,
|
|
136
|
+
principal: Principal,
|
|
137
|
+
source: EventSourceVersion,
|
|
138
|
+
eventId: Schema.String,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const SubscriptionPartitionRequest = Schema.Union([
|
|
142
|
+
SubscribeRequest,
|
|
143
|
+
ListSubscriptionsRequest,
|
|
144
|
+
CancelSubscriptionRequest,
|
|
145
|
+
ListDeliveriesRequest,
|
|
146
|
+
AcceptRequest,
|
|
147
|
+
StatusRequest,
|
|
148
|
+
]);
|
|
149
|
+
type SubscriptionPartitionRequest = typeof SubscriptionPartitionRequest.Type;
|
|
150
|
+
|
|
151
|
+
const SubscriptionPage = Schema.Struct({
|
|
152
|
+
items: Schema.Array(SubscriptionSnapshot),
|
|
153
|
+
next: Schema.NullOr(Schema.Natural),
|
|
154
|
+
});
|
|
155
|
+
const DeliveryPage = Schema.Struct({
|
|
156
|
+
items: Schema.Array(SubscriptionDeliverySnapshot),
|
|
157
|
+
next: Schema.NullOr(Schema.String),
|
|
158
|
+
});
|
|
159
|
+
const IntakeStatus = Schema.Struct({
|
|
160
|
+
...EventAcknowledgement.fields,
|
|
161
|
+
routingComplete: Schema.Boolean,
|
|
162
|
+
routingFailure: Schema.NullOr(Schema.String),
|
|
163
|
+
nextAttemptAtMillis: Schema.Number,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const SubscriptionPartitionFailure = Schema.Union([
|
|
167
|
+
SubscriptionError,
|
|
168
|
+
SubscriptionSourceError,
|
|
169
|
+
SubscriptionFailpointError,
|
|
170
|
+
SubscriptionPartitionProtocolError,
|
|
171
|
+
]);
|
|
172
|
+
type SubscriptionPartitionFailure = typeof SubscriptionPartitionFailure.Type;
|
|
173
|
+
|
|
174
|
+
const SubscriptionPartitionResponse = Schema.Union([
|
|
175
|
+
Schema.TaggedStruct("Snapshot", { value: SubscriptionSnapshot }),
|
|
176
|
+
Schema.TaggedStruct("SubscriptionPage", { value: SubscriptionPage }),
|
|
177
|
+
Schema.TaggedStruct("DeliveryPage", { value: DeliveryPage }),
|
|
178
|
+
Schema.TaggedStruct("Acknowledgement", { value: EventAcknowledgement }),
|
|
179
|
+
Schema.TaggedStruct("Status", { value: IntakeStatus }),
|
|
180
|
+
Schema.TaggedStruct("Failed", { failure: SubscriptionPartitionFailure }),
|
|
181
|
+
]);
|
|
182
|
+
type SubscriptionPartitionResponse = typeof SubscriptionPartitionResponse.Type;
|
|
183
|
+
|
|
184
|
+
const decodeRequest = Schema.decodeUnknownEffect(SubscriptionPartitionRequest);
|
|
185
|
+
const encodeRequest = Schema.encodeEffect(SubscriptionPartitionRequest);
|
|
186
|
+
const decodeResponse = Schema.decodeUnknownEffect(SubscriptionPartitionResponse);
|
|
187
|
+
const encodeResponse = Schema.encodeEffect(SubscriptionPartitionResponse);
|
|
188
|
+
|
|
189
|
+
const protocolFailure = (message: string): SubscriptionPartitionResponse => ({
|
|
190
|
+
_tag: "Failed",
|
|
191
|
+
failure: SubscriptionPartitionProtocolError.make({ message: protocolMessage(message) }),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
export const sourcePartitionName = (partition: SourcePartition): string =>
|
|
195
|
+
JSON.stringify([partition.tenantId, partition.address]);
|
|
196
|
+
|
|
197
|
+
export interface SubscriptionPartitionObjectRpc extends Rpc.DurableObjectBranded {
|
|
198
|
+
subscription(encoded: unknown): Promise<unknown>;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export class SubscriptionPartitionNamespace extends Context.Service<
|
|
202
|
+
SubscriptionPartitionNamespace,
|
|
203
|
+
{ readonly namespace: DurableObjectNamespace<SubscriptionPartitionObjectRpc> }
|
|
204
|
+
>()("@effect-agent/platform-cloudflare/SubscriptionPartitionNamespace") {}
|
|
205
|
+
|
|
206
|
+
const storageFailure = (code: string): SubscriptionError =>
|
|
207
|
+
SubscriptionError.make({ reason: "storage", code });
|
|
208
|
+
const corruptFailure = (code: string): SubscriptionError =>
|
|
209
|
+
SubscriptionError.make({ reason: "corrupt", code });
|
|
210
|
+
|
|
211
|
+
/** Worker-side management and trusted intake, routed to one fresh partition stub per call. */
|
|
212
|
+
export class CloudflareSubscriptionsClient {
|
|
213
|
+
static layer(
|
|
214
|
+
partition: SourcePartition,
|
|
215
|
+
): Layer.Layer<Subscriptions | SubscriptionIntake, never, SubscriptionPartitionNamespace> {
|
|
216
|
+
return Layer.effectContext(
|
|
217
|
+
Effect.gen(function* () {
|
|
218
|
+
const { namespace } = yield* SubscriptionPartitionNamespace;
|
|
219
|
+
|
|
220
|
+
const call = Effect.fn("CloudflareSubscriptionsClient.call")(function* (
|
|
221
|
+
addressedPartition: SourcePartition,
|
|
222
|
+
request: SubscriptionPartitionRequest,
|
|
223
|
+
) {
|
|
224
|
+
if (!samePartition(partition, addressedPartition)) {
|
|
225
|
+
return yield* SubscriptionError.make({ reason: "unauthorized", code: "partition" });
|
|
226
|
+
}
|
|
227
|
+
const encoded = yield* encodeRequest(request).pipe(
|
|
228
|
+
Effect.mapError(() => corruptFailure("subscription-partition-protocol")),
|
|
229
|
+
);
|
|
230
|
+
const raw = yield* Effect.tryPromise({
|
|
231
|
+
try: () =>
|
|
232
|
+
namespace
|
|
233
|
+
.get(namespace.idFromName(sourcePartitionName(partition)))
|
|
234
|
+
.subscription(encoded),
|
|
235
|
+
catch: () => storageFailure("call-subscription-partition"),
|
|
236
|
+
});
|
|
237
|
+
return yield* decodeResponse(raw).pipe(
|
|
238
|
+
Effect.mapError(() => corruptFailure("subscription-partition-protocol")),
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const failed = (response: SubscriptionPartitionResponse): SubscriptionPartitionFailure =>
|
|
243
|
+
response._tag === "Failed"
|
|
244
|
+
? response.failure
|
|
245
|
+
: SubscriptionPartitionProtocolError.make({
|
|
246
|
+
message: "Unexpected subscription response",
|
|
247
|
+
});
|
|
248
|
+
const asProtocolError = (): SubscriptionError =>
|
|
249
|
+
corruptFailure("subscription-partition-protocol");
|
|
250
|
+
|
|
251
|
+
const subscriptions = Subscriptions.of({
|
|
252
|
+
subscribe: (scope, options) =>
|
|
253
|
+
Effect.gen(function* () {
|
|
254
|
+
const response = yield* call(scope.partition, {
|
|
255
|
+
_tag: "Subscribe",
|
|
256
|
+
schemaVersion: 1,
|
|
257
|
+
scope,
|
|
258
|
+
options,
|
|
259
|
+
});
|
|
260
|
+
if (response._tag === "Snapshot") return response.value;
|
|
261
|
+
const failure = failed(response);
|
|
262
|
+
if (
|
|
263
|
+
failure._tag === "SubscriptionError" ||
|
|
264
|
+
failure._tag === "SubscriptionSourceError" ||
|
|
265
|
+
failure._tag === "SubscriptionFailpointError"
|
|
266
|
+
)
|
|
267
|
+
return yield* failure;
|
|
268
|
+
return yield* asProtocolError();
|
|
269
|
+
}),
|
|
270
|
+
listSubscriptions: (scope, after, limit) =>
|
|
271
|
+
Effect.gen(function* () {
|
|
272
|
+
const response = yield* call(scope.partition, {
|
|
273
|
+
_tag: "ListSubscriptions",
|
|
274
|
+
schemaVersion: 1,
|
|
275
|
+
scope,
|
|
276
|
+
...(after === undefined ? {} : { after }),
|
|
277
|
+
...(limit === undefined ? {} : { limit }),
|
|
278
|
+
});
|
|
279
|
+
if (response._tag === "SubscriptionPage") return response.value;
|
|
280
|
+
const failure = failed(response);
|
|
281
|
+
return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
|
|
282
|
+
}),
|
|
283
|
+
cancelSubscription: (scope, key) =>
|
|
284
|
+
Effect.gen(function* () {
|
|
285
|
+
const response = yield* call(scope.partition, {
|
|
286
|
+
_tag: "CancelSubscription",
|
|
287
|
+
schemaVersion: 1,
|
|
288
|
+
scope,
|
|
289
|
+
key,
|
|
290
|
+
});
|
|
291
|
+
if (response._tag === "Snapshot") return response.value;
|
|
292
|
+
const failure = failed(response);
|
|
293
|
+
if (
|
|
294
|
+
failure._tag === "SubscriptionError" ||
|
|
295
|
+
failure._tag === "SubscriptionFailpointError"
|
|
296
|
+
)
|
|
297
|
+
return yield* failure;
|
|
298
|
+
return yield* asProtocolError();
|
|
299
|
+
}),
|
|
300
|
+
listDeliveries: (scope, key, after, limit) =>
|
|
301
|
+
Effect.gen(function* () {
|
|
302
|
+
const response = yield* call(scope.partition, {
|
|
303
|
+
_tag: "ListDeliveries",
|
|
304
|
+
schemaVersion: 1,
|
|
305
|
+
scope,
|
|
306
|
+
key,
|
|
307
|
+
...(after === undefined ? {} : { after }),
|
|
308
|
+
...(limit === undefined ? {} : { limit }),
|
|
309
|
+
});
|
|
310
|
+
if (response._tag === "DeliveryPage") return response.value;
|
|
311
|
+
const failure = failed(response);
|
|
312
|
+
return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
|
|
313
|
+
}),
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const intake = SubscriptionIntake.of({
|
|
317
|
+
accept: (principal, source, payload) =>
|
|
318
|
+
Effect.gen(function* () {
|
|
319
|
+
const request = yield* Schema.decodeUnknownEffect(AcceptRequest)({
|
|
320
|
+
_tag: "Accept",
|
|
321
|
+
schemaVersion: 1,
|
|
322
|
+
partition,
|
|
323
|
+
principal,
|
|
324
|
+
source,
|
|
325
|
+
payload,
|
|
326
|
+
}).pipe(
|
|
327
|
+
Effect.mapError(() =>
|
|
328
|
+
SubscriptionError.make({ reason: "validation", code: "event-payload" }),
|
|
329
|
+
),
|
|
330
|
+
);
|
|
331
|
+
const response = yield* call(request.partition, request);
|
|
332
|
+
if (response._tag === "Acknowledgement") return response.value;
|
|
333
|
+
const failure = failed(response);
|
|
334
|
+
if (
|
|
335
|
+
failure._tag === "SubscriptionError" ||
|
|
336
|
+
failure._tag === "SubscriptionSourceError" ||
|
|
337
|
+
failure._tag === "SubscriptionFailpointError"
|
|
338
|
+
)
|
|
339
|
+
return yield* failure;
|
|
340
|
+
return yield* asProtocolError();
|
|
341
|
+
}),
|
|
342
|
+
status: (principal, source, eventId) =>
|
|
343
|
+
Effect.gen(function* () {
|
|
344
|
+
const response = yield* call(partition, {
|
|
345
|
+
_tag: "Status",
|
|
346
|
+
schemaVersion: 1,
|
|
347
|
+
partition,
|
|
348
|
+
principal,
|
|
349
|
+
source,
|
|
350
|
+
eventId,
|
|
351
|
+
});
|
|
352
|
+
if (response._tag === "Status") return response.value;
|
|
353
|
+
const failure = failed(response);
|
|
354
|
+
return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
|
|
355
|
+
}),
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return Context.make(Subscriptions, subscriptions).pipe(
|
|
359
|
+
Context.add(SubscriptionIntake, intake),
|
|
360
|
+
);
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export class SubscriptionPartitionIdentity extends Context.Service<
|
|
367
|
+
SubscriptionPartitionIdentity,
|
|
368
|
+
{ readonly partition: SourcePartition }
|
|
369
|
+
>()("@effect-agent/platform-cloudflare/SubscriptionPartitionIdentity") {}
|
|
370
|
+
|
|
371
|
+
const decodePartitionName = Effect.fn("decodeSubscriptionPartitionName")(function* (
|
|
372
|
+
name: string | null | undefined,
|
|
373
|
+
) {
|
|
374
|
+
if (name === null || name === undefined) {
|
|
375
|
+
return yield* SubscriptionPartitionProtocolError.make({
|
|
376
|
+
message: "Subscription Partition objects require an idFromName identity",
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
const tuple = yield* Schema.decodeUnknownEffect(
|
|
380
|
+
Schema.fromJsonString(Schema.Tuple([Schema.String, Schema.String])),
|
|
381
|
+
)(name).pipe(
|
|
382
|
+
Effect.mapError(() =>
|
|
383
|
+
SubscriptionPartitionProtocolError.make({
|
|
384
|
+
message: "Subscription Partition name is malformed",
|
|
385
|
+
}),
|
|
386
|
+
),
|
|
387
|
+
);
|
|
388
|
+
return yield* Schema.decodeUnknownEffect(SourcePartition)({
|
|
389
|
+
tenantId: tuple[0],
|
|
390
|
+
address: tuple[1],
|
|
391
|
+
}).pipe(
|
|
392
|
+
Effect.mapError(() =>
|
|
393
|
+
SubscriptionPartitionProtocolError.make({
|
|
394
|
+
message: "Subscription Partition identity is invalid",
|
|
395
|
+
}),
|
|
396
|
+
),
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const samePartition = Schema.toEquivalence(SourcePartition);
|
|
401
|
+
const requestPartition = (request: SubscriptionPartitionRequest): SourcePartition =>
|
|
402
|
+
request._tag === "Accept" || request._tag === "Status"
|
|
403
|
+
? request.partition
|
|
404
|
+
: request.scope.partition;
|
|
405
|
+
|
|
406
|
+
const handleRequest = Effect.fn("SubscriptionPartition.handleRequest")(function* (
|
|
407
|
+
encoded: unknown,
|
|
408
|
+
) {
|
|
409
|
+
const decoded = yield* decodeRequest(encoded).pipe(Effect.result);
|
|
410
|
+
if (decoded._tag === "Failure") {
|
|
411
|
+
return yield* encodeResponse(
|
|
412
|
+
protocolFailure("The subscription request could not be decoded"),
|
|
413
|
+
).pipe(Effect.orDie);
|
|
414
|
+
}
|
|
415
|
+
const request = decoded.success;
|
|
416
|
+
const { partition } = yield* SubscriptionPartitionIdentity;
|
|
417
|
+
if (!samePartition(partition, requestPartition(request))) {
|
|
418
|
+
return yield* encodeResponse(
|
|
419
|
+
protocolFailure("The request partition does not match the addressed object"),
|
|
420
|
+
).pipe(Effect.orDie);
|
|
421
|
+
}
|
|
422
|
+
const subscriptions = yield* Subscriptions;
|
|
423
|
+
const intake = yield* SubscriptionIntake;
|
|
424
|
+
const response = yield* Effect.gen(function* (): Effect.fn.Return<
|
|
425
|
+
SubscriptionPartitionResponse,
|
|
426
|
+
SubscriptionPartitionFailure
|
|
427
|
+
> {
|
|
428
|
+
switch (request._tag) {
|
|
429
|
+
case "Subscribe":
|
|
430
|
+
return {
|
|
431
|
+
_tag: "Snapshot",
|
|
432
|
+
value: yield* subscriptions.subscribe(request.scope, request.options),
|
|
433
|
+
};
|
|
434
|
+
case "ListSubscriptions":
|
|
435
|
+
return {
|
|
436
|
+
_tag: "SubscriptionPage",
|
|
437
|
+
value: yield* subscriptions.listSubscriptions(
|
|
438
|
+
request.scope,
|
|
439
|
+
request.after,
|
|
440
|
+
request.limit,
|
|
441
|
+
),
|
|
442
|
+
};
|
|
443
|
+
case "CancelSubscription":
|
|
444
|
+
return {
|
|
445
|
+
_tag: "Snapshot",
|
|
446
|
+
value: yield* subscriptions.cancelSubscription(request.scope, request.key),
|
|
447
|
+
};
|
|
448
|
+
case "ListDeliveries":
|
|
449
|
+
return {
|
|
450
|
+
_tag: "DeliveryPage",
|
|
451
|
+
value: yield* subscriptions.listDeliveries(
|
|
452
|
+
request.scope,
|
|
453
|
+
request.key,
|
|
454
|
+
request.after,
|
|
455
|
+
request.limit,
|
|
456
|
+
),
|
|
457
|
+
};
|
|
458
|
+
case "Accept":
|
|
459
|
+
return {
|
|
460
|
+
_tag: "Acknowledgement",
|
|
461
|
+
value: yield* intake.accept(request.principal, request.source, request.payload),
|
|
462
|
+
};
|
|
463
|
+
case "Status":
|
|
464
|
+
return {
|
|
465
|
+
_tag: "Status",
|
|
466
|
+
value: yield* intake.status(request.principal, request.source, request.eventId),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}).pipe(
|
|
470
|
+
Effect.catch((failure) =>
|
|
471
|
+
Schema.is(SubscriptionPartitionFailure)(failure)
|
|
472
|
+
? Effect.succeed({ _tag: "Failed" as const, failure })
|
|
473
|
+
: Effect.succeed(protocolFailure("The subscription operation failed outside its contract")),
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
return yield* encodeResponse(response).pipe(Effect.orDie);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const alarmStorageError = (code: string) => () => storageFailure(code);
|
|
480
|
+
|
|
481
|
+
const transactionLayer: Layer.Layer<
|
|
482
|
+
DoSubscriptionTransaction,
|
|
483
|
+
never,
|
|
484
|
+
DurableObjectAlarm.DurableObjectAlarm
|
|
485
|
+
> = Layer.effect(
|
|
486
|
+
DoSubscriptionTransaction,
|
|
487
|
+
Effect.gen(function* () {
|
|
488
|
+
const alarms = yield* DurableObjectAlarm.DurableObjectAlarm;
|
|
489
|
+
return DoSubscriptionTransaction.of({
|
|
490
|
+
run: (body) =>
|
|
491
|
+
Effect.gen(function* () {
|
|
492
|
+
const nowMillis = yield* Clock.currentTimeMillis;
|
|
493
|
+
return yield* alarms
|
|
494
|
+
.transaction((transaction) =>
|
|
495
|
+
body((replacement) =>
|
|
496
|
+
replacement.deadlineAtMillis === null
|
|
497
|
+
? transaction
|
|
498
|
+
.cancelAlarm({ id: SUBSCRIPTION_ALARM_ID, tag: SUBSCRIPTION_ALARM_TAG })
|
|
499
|
+
.pipe(Effect.mapError(alarmStorageError("cancel-subscription-alarm")))
|
|
500
|
+
: Effect.fromOption(
|
|
501
|
+
DateTime.make(Math.max(replacement.deadlineAtMillis, nowMillis + 1)),
|
|
502
|
+
).pipe(
|
|
503
|
+
Effect.mapError(() => corruptFailure("subscription-alarm-deadline")),
|
|
504
|
+
Effect.flatMap((runAt) =>
|
|
505
|
+
transaction
|
|
506
|
+
.scheduleAlarm({
|
|
507
|
+
id: SUBSCRIPTION_ALARM_ID,
|
|
508
|
+
tag: SUBSCRIPTION_ALARM_TAG,
|
|
509
|
+
runAt,
|
|
510
|
+
payload: { schemaVersion: 1, generation: replacement.generation },
|
|
511
|
+
})
|
|
512
|
+
.pipe(Effect.mapError(alarmStorageError("schedule-subscription-alarm"))),
|
|
513
|
+
),
|
|
514
|
+
),
|
|
515
|
+
),
|
|
516
|
+
)
|
|
517
|
+
.pipe(
|
|
518
|
+
Effect.catchTag("StorageOperationError", () =>
|
|
519
|
+
storageFailure("commit-subscription-transaction"),
|
|
520
|
+
),
|
|
521
|
+
);
|
|
522
|
+
}),
|
|
523
|
+
});
|
|
524
|
+
}),
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const alarmHandler = (limits: SubscriptionLimits) =>
|
|
528
|
+
DurableObjectAlarm.processDue(
|
|
529
|
+
(event) =>
|
|
530
|
+
Effect.gen(function* () {
|
|
531
|
+
if (event.tag !== SUBSCRIPTION_ALARM_TAG || event.id !== SUBSCRIPTION_ALARM_ID) {
|
|
532
|
+
return yield* SubscriptionAlarmProtocolError.make({
|
|
533
|
+
message: `Unsupported Subscription Partition alarm ${event.tag}/${event.id}`,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
yield* Schema.decodeUnknownEffect(SubscriptionAlarmPayload)(event.payload).pipe(
|
|
537
|
+
Effect.mapError(() =>
|
|
538
|
+
SubscriptionAlarmProtocolError.make({
|
|
539
|
+
message: "Unsupported subscription alarm payload",
|
|
540
|
+
}),
|
|
541
|
+
),
|
|
542
|
+
);
|
|
543
|
+
const driver = yield* SubscriptionDriver;
|
|
544
|
+
const alarmControl = yield* DoSubscriptionAlarmControl;
|
|
545
|
+
yield* alarmControl.prearm((yield* Clock.currentTimeMillis) + limits.retryMillis);
|
|
546
|
+
const pass = yield* driver.runDue.pipe(
|
|
547
|
+
Effect.catchCause((cause) =>
|
|
548
|
+
Cause.hasInterrupts(cause)
|
|
549
|
+
? Effect.failCause(cause)
|
|
550
|
+
: Clock.currentTimeMillis.pipe(
|
|
551
|
+
Effect.flatMap((time) => alarmControl.prearm(time + limits.retryMillis)),
|
|
552
|
+
Effect.andThen(Effect.failCause(cause)),
|
|
553
|
+
),
|
|
554
|
+
),
|
|
555
|
+
);
|
|
556
|
+
if (pass.failed > 0) {
|
|
557
|
+
yield* alarmControl.prearm((yield* Clock.currentTimeMillis) + limits.retryMillis);
|
|
558
|
+
} else {
|
|
559
|
+
yield* alarmControl.reconcile;
|
|
560
|
+
}
|
|
561
|
+
}),
|
|
562
|
+
{ mode: "ordered" },
|
|
563
|
+
).pipe(Effect.asVoid);
|
|
564
|
+
|
|
565
|
+
type SubscriptionRuntimeServices =
|
|
566
|
+
| Subscriptions
|
|
567
|
+
| SubscriptionIntake
|
|
568
|
+
| SubscriptionDriver
|
|
569
|
+
| DoSubscriptionAlarmControl
|
|
570
|
+
| SubscriptionPartitionIdentity
|
|
571
|
+
| DurableObjectAlarm.DurableObjectAlarm;
|
|
572
|
+
|
|
573
|
+
export interface SubscriptionPartitionObjectInstance extends InstanceType<
|
|
574
|
+
EffectCfDurableObject.DurableObjectClass<Record<never, never>, SubscriptionRuntimeServices>
|
|
575
|
+
> {
|
|
576
|
+
subscription(encoded: unknown): Promise<unknown>;
|
|
577
|
+
alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export interface SubscriptionPartitionObjectClass {
|
|
581
|
+
new (ctx: DurableObjectState, env: Cloudflare.Env): SubscriptionPartitionObjectInstance;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Build one source-addressed SQLite Durable Object. The host Layer binds the permitted source
|
|
586
|
+
* catalog and authority. It must not retain cleanup-scoped resources across RPC events.
|
|
587
|
+
*/
|
|
588
|
+
export const makeSubscriptionPartitionObjectClass = <E>(
|
|
589
|
+
host: Layer.Layer<
|
|
590
|
+
SubscriptionAuthorizer | EventSources | SubscriptionInputBindings | ThreadObjectNamespace,
|
|
591
|
+
E,
|
|
592
|
+
| EffectCfDurableObjectState.DurableObjectState
|
|
593
|
+
| WorkerEnvironment
|
|
594
|
+
| SubscriptionPartitionIdentity
|
|
595
|
+
>,
|
|
596
|
+
limits: SubscriptionLimits = defaultSubscriptionLimits,
|
|
597
|
+
): SubscriptionPartitionObjectClass => {
|
|
598
|
+
const cloudflareLimitsLayer = Layer.effectDiscard(validateCloudflareSubscriptionLimits(limits));
|
|
599
|
+
const identityLayer = Layer.effect(
|
|
600
|
+
SubscriptionPartitionIdentity,
|
|
601
|
+
Effect.gen(function* () {
|
|
602
|
+
const state = yield* EffectCfDurableObjectState.DurableObjectState;
|
|
603
|
+
return SubscriptionPartitionIdentity.of({
|
|
604
|
+
partition: yield* decodePartitionName(state.raw.id.name),
|
|
605
|
+
});
|
|
606
|
+
}),
|
|
607
|
+
);
|
|
608
|
+
const sqlLayer = Layer.unwrap(
|
|
609
|
+
Effect.map(EffectCfDurableObjectState.DurableObjectState, (state) =>
|
|
610
|
+
SqliteClient.layer({ storage: state.raw.storage }),
|
|
611
|
+
),
|
|
612
|
+
);
|
|
613
|
+
const partitionStore = Layer.unwrap(
|
|
614
|
+
Effect.map(SubscriptionPartitionIdentity, ({ partition }) =>
|
|
615
|
+
doSubscriptionStoreLayer(partition).pipe(
|
|
616
|
+
Layer.provide(transactionLayer),
|
|
617
|
+
Layer.provide(sqlLayer),
|
|
618
|
+
),
|
|
619
|
+
),
|
|
620
|
+
);
|
|
621
|
+
const application = Layer.mergeAll(
|
|
622
|
+
Subscriptions.layer(limits),
|
|
623
|
+
SubscriptionIntake.layer(limits),
|
|
624
|
+
SubscriptionDriver.layer(limits),
|
|
625
|
+
cloudflareLimitsLayer,
|
|
626
|
+
).pipe(
|
|
627
|
+
Layer.provideMerge(partitionStore),
|
|
628
|
+
Layer.provide(
|
|
629
|
+
cloudflarePreparedInputAdmissionLayer.pipe(Layer.provide(CloudflareThreadClient.layer)),
|
|
630
|
+
),
|
|
631
|
+
Layer.provide(BrowserCrypto.layer),
|
|
632
|
+
Layer.provideMerge(DurableObjectAlarm.DurableObjectAlarm.layer),
|
|
633
|
+
Layer.provide(host),
|
|
634
|
+
Layer.provideMerge(identityLayer),
|
|
635
|
+
);
|
|
636
|
+
const runtime: Layer.Layer<
|
|
637
|
+
SubscriptionRuntimeServices,
|
|
638
|
+
E | SubscriptionError | SubscriptionPartitionProtocolError | CloudflareSubscriptionConfigError,
|
|
639
|
+
EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
|
|
640
|
+
> = Layer.effectContext(
|
|
641
|
+
Effect.gen(function* () {
|
|
642
|
+
const state = yield* EffectCfDurableObjectState.DurableObjectState;
|
|
643
|
+
const scope = yield* Effect.scope;
|
|
644
|
+
return yield* state.blockConcurrencyWhile(Layer.buildWithScope(application, scope));
|
|
645
|
+
}),
|
|
646
|
+
);
|
|
647
|
+
const rpc = {
|
|
648
|
+
subscription: (encoded: unknown) => handleRequest(encoded),
|
|
649
|
+
} satisfies EffectCfDurableObject.DurableObjectRpc<SubscriptionRuntimeServices>;
|
|
650
|
+
const Base = EffectCfDurableObject.make(runtime, {
|
|
651
|
+
initialize: Effect.void,
|
|
652
|
+
rpc,
|
|
653
|
+
alarms: alarmHandler(limits),
|
|
654
|
+
});
|
|
655
|
+
class SubscriptionPartitionObject extends Base {
|
|
656
|
+
override alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void {
|
|
657
|
+
return super.alarm?.(alarmInfo);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return SubscriptionPartitionObject;
|
|
661
|
+
};
|