@effect-agent/platform-cloudflare 0.1.0-beta.8 → 0.1.0-beta.81

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.
Files changed (100) hide show
  1. package/dist/Alarm.d.mts +184 -0
  2. package/dist/Alarm.mjs +451 -0
  3. package/dist/Alarm.mjs.map +1 -0
  4. package/dist/BrowserRestCapture.d.mts +35 -0
  5. package/dist/BrowserRestCapture.mjs +238 -0
  6. package/dist/BrowserRestCapture.mjs.map +1 -0
  7. package/dist/BrowserRestCrawl.d.mts +17 -0
  8. package/dist/BrowserRestCrawl.mjs +244 -0
  9. package/dist/BrowserRestCrawl.mjs.map +1 -0
  10. package/dist/CloudflareAiGateway.d.mts +64 -0
  11. package/dist/CloudflareAiGateway.mjs +70 -0
  12. package/dist/CloudflareAiGateway.mjs.map +1 -0
  13. package/dist/CloudflareBindings.d.mts +112 -0
  14. package/dist/CloudflareBindings.mjs +101 -0
  15. package/dist/CloudflareBindings.mjs.map +1 -0
  16. package/dist/CloudflareBrowser-D4YqaTAl.mjs +495 -0
  17. package/dist/CloudflareBrowser-D4YqaTAl.mjs.map +1 -0
  18. package/dist/CloudflareBrowser-DJq53niJ.d.mts +80 -0
  19. package/dist/CloudflareBrowser.d.mts +2 -0
  20. package/dist/CloudflareBrowser.mjs +2 -0
  21. package/dist/CloudflareCodeMode.d.mts +44 -0
  22. package/dist/CloudflareCodeMode.mjs +616 -0
  23. package/dist/CloudflareCodeMode.mjs.map +1 -0
  24. package/dist/CloudflareConfig-f3CqTel1.d.mts +101 -0
  25. package/dist/CloudflareConfig.d.mts +2 -0
  26. package/dist/CloudflareConfig.mjs +122 -0
  27. package/dist/CloudflareConfig.mjs.map +1 -0
  28. package/dist/CloudflareMemory.d.mts +142 -0
  29. package/dist/CloudflareMemory.mjs +195 -0
  30. package/dist/CloudflareMemory.mjs.map +1 -0
  31. package/dist/CloudflareScheduling.d.mts +52 -0
  32. package/dist/CloudflareScheduling.mjs +389 -0
  33. package/dist/CloudflareScheduling.mjs.map +1 -0
  34. package/dist/CloudflareSubscriptions.d.mts +87 -0
  35. package/dist/CloudflareSubscriptions.mjs +524 -0
  36. package/dist/CloudflareSubscriptions.mjs.map +1 -0
  37. package/dist/CloudflareThreadClient.d.mts +1286 -0
  38. package/dist/CloudflareThreadClient.mjs +386 -0
  39. package/dist/CloudflareThreadClient.mjs.map +1 -0
  40. package/dist/InteractiveBrowser.d.mts +144 -0
  41. package/dist/InteractiveBrowser.mjs +1090 -0
  42. package/dist/InteractiveBrowser.mjs.map +1 -0
  43. package/dist/ProtectedBrowser.d.mts +65 -0
  44. package/dist/ProtectedBrowser.mjs +859 -0
  45. package/dist/ProtectedBrowser.mjs.map +1 -0
  46. package/dist/ThreadObject-BpwkEc5E.mjs +836 -0
  47. package/dist/ThreadObject-BpwkEc5E.mjs.map +1 -0
  48. package/dist/ThreadObject-NbFOWK1I.d.mts +816 -0
  49. package/dist/ThreadObject.d.mts +2 -0
  50. package/dist/ThreadObject.mjs +3 -0
  51. package/dist/WakeScheduler.d.mts +23 -0
  52. package/dist/WakeScheduler.mjs +60 -0
  53. package/dist/WakeScheduler.mjs.map +1 -0
  54. package/dist/boundary-BguazVkh.mjs +42 -0
  55. package/dist/boundary-BguazVkh.mjs.map +1 -0
  56. package/dist/browser-session-lifecycle-CUbVfEgP.mjs +84 -0
  57. package/dist/browser-session-lifecycle-CUbVfEgP.mjs.map +1 -0
  58. package/dist/browser-session-lifecycle-DJpqLXy_.d.mts +21 -0
  59. package/dist/index.d.mts +13 -1547
  60. package/dist/index.mjs +13 -1636
  61. package/dist/prepared-admission-DSi55W-7.mjs +73 -0
  62. package/dist/prepared-admission-DSi55W-7.mjs.map +1 -0
  63. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  64. package/package.json +1 -53
  65. package/src/Alarm.ts +967 -0
  66. package/src/BrowserRestCapture.ts +477 -0
  67. package/src/BrowserRestCrawl.ts +540 -0
  68. package/src/CloudflareAiGateway.ts +170 -0
  69. package/src/CloudflareBindings.ts +180 -0
  70. package/src/CloudflareBrowser.ts +15 -0
  71. package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +372 -204
  72. package/src/{config.ts → CloudflareConfig.ts} +9 -8
  73. package/src/CloudflareMemory.ts +406 -0
  74. package/src/CloudflareScheduling.ts +733 -0
  75. package/src/CloudflareSubscriptions.ts +1045 -0
  76. package/src/CloudflareThreadClient.ts +807 -0
  77. package/src/InteractiveBrowser.ts +2357 -0
  78. package/src/ProtectedBrowser.ts +2 -0
  79. package/src/ThreadObject.ts +1179 -0
  80. package/src/{wake-scheduler.ts → WakeScheduler.ts} +23 -23
  81. package/src/index.ts +12 -23
  82. package/src/internal/boundary.ts +55 -0
  83. package/src/internal/browser-quick-action.ts +857 -0
  84. package/src/internal/browser-session-lifecycle.ts +160 -0
  85. package/src/internal/layers.ts +751 -0
  86. package/src/internal/message-delivery.ts +148 -0
  87. package/src/internal/prepared-admission.ts +116 -0
  88. package/src/internal/progress-wait.ts +121 -0
  89. package/src/internal/transport.ts +42 -0
  90. package/src/protected-browser/binding.ts +187 -0
  91. package/src/protected-browser/inspect-frame.ts +125 -0
  92. package/src/protected-browser/native.ts +429 -0
  93. package/src/protected-browser/policy.ts +719 -0
  94. package/dist/index.mjs.map +0 -1
  95. package/src/alarm.ts +0 -334
  96. package/src/bindings.ts +0 -145
  97. package/src/client.ts +0 -646
  98. package/src/conversation-object.ts +0 -768
  99. package/src/layers.ts +0 -376
  100. package/src/transport.ts +0 -38
@@ -0,0 +1,1045 @@
1
+ import {
2
+ DoSubscriptionAlarmControl,
3
+ DoSubscriptionTransaction,
4
+ doSubscriptionStoreLayer,
5
+ } from "@effect-agent/storage-cloudflare/DoSubscriptionStore";
6
+ import { type EventSources } from "@effect-agent/thread/EventSource";
7
+ import { PersistedJson } from "@effect-agent/thread/Records";
8
+ import { Principal } from "@effect-agent/thread/SubmissionLedger";
9
+ import {
10
+ EventAcknowledgement,
11
+ EventSourceVersion,
12
+ SourcePartition,
13
+ type SubscriptionAuthorizer,
14
+ SubscriptionConfiguration,
15
+ SubscriptionDeliverySnapshot,
16
+ SubscriptionDeliveryKey,
17
+ SubscriptionError,
18
+ SubscriptionFailpointError,
19
+ SubscriptionKey,
20
+ SubscriptionScope,
21
+ type SubscriptionLimits,
22
+ SubscriptionSnapshot,
23
+ SubscriptionSourceError,
24
+ defaultSubscriptionLimits,
25
+ } from "@effect-agent/thread/Subscription";
26
+ import { type SubscriptionInputBindings } from "@effect-agent/thread/SubscriptionInput";
27
+ import {
28
+ SubscriptionDriver,
29
+ SubscriptionIntake,
30
+ Subscriptions,
31
+ } from "@effect-agent/thread/Subscriptions";
32
+ import { BrowserCrypto } from "@effect/platform-browser";
33
+ import { SqliteClient } from "@effect/sql-sqlite-do";
34
+ import { Cause, Clock, Context, DateTime, Effect, Layer, Schema, type Scope } from "effect";
35
+ import {
36
+ DurableObject as EffectCfDurableObject,
37
+ DurableObjectAlarm,
38
+ DurableObjectState as EffectCfDurableObjectState,
39
+ type WorkerEnvironment,
40
+ } from "effect-cf";
41
+
42
+ import { type ThreadObjectNamespace } from "./CloudflareBindings.ts";
43
+ import { CloudflareThreadClient } from "./CloudflareThreadClient.ts";
44
+ import { cloudflarePreparedInputAdmissionLayer } from "./internal/prepared-admission.ts";
45
+
46
+ const SUBSCRIPTION_ALARM_TAG = "effect-agent/SubscriptionPartitionWake";
47
+ const SUBSCRIPTION_ALARM_ID = "driver";
48
+ const MAX_ALARM_WALL_MILLIS = 12 * 60_000;
49
+ const MAX_ALARMS_PER_INVOCATION = 16;
50
+ const MAX_ANCILLARY_ALARM_MILLIS = 30_000;
51
+
52
+ const SubscriptionAlarmPayload = Schema.Struct({
53
+ schemaVersion: Schema.Literal(1),
54
+ generation: Schema.Int.check(Schema.isGreaterThan(0)),
55
+ });
56
+
57
+ export class SubscriptionAlarmProtocolError extends Schema.TaggedError<SubscriptionAlarmProtocolError>()(
58
+ "SubscriptionAlarmProtocolError",
59
+ { message: Schema.String },
60
+ ) {}
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
+
161
+ export class SubscriptionPartitionProtocolError extends Schema.TaggedError<SubscriptionPartitionProtocolError>()(
162
+ "SubscriptionPartitionProtocolError",
163
+ { message: Schema.String.check(Schema.isMaxLength(4_096)) },
164
+ ) {}
165
+
166
+ export class CloudflareSubscriptionConfigError extends Schema.TaggedError<CloudflareSubscriptionConfigError>()(
167
+ "CloudflareSubscriptionConfigError",
168
+ { message: Schema.String },
169
+ ) {}
170
+
171
+ /** Reject limits whose four bounded phases could exceed the safe Durable Object alarm budget. */
172
+ export const validateCloudflareSubscriptionLimits = (
173
+ limits: SubscriptionLimits,
174
+ options: { readonly ancillaryAlarms?: boolean } = {},
175
+ ): Effect.Effect<void, CloudflareSubscriptionConfigError> => {
176
+ const worstCaseMillis =
177
+ 4 * Math.ceil(limits.batchSize / limits.concurrency) * limits.operationTimeoutMillis +
178
+ (options.ancillaryAlarms === true ? MAX_ALARMS_PER_INVOCATION * MAX_ANCILLARY_ALARM_MILLIS : 0);
179
+
180
+ return worstCaseMillis <= MAX_ALARM_WALL_MILLIS
181
+ ? Effect.void
182
+ : Effect.fail(
183
+ CloudflareSubscriptionConfigError.make({
184
+ message: "Subscription limits can exceed the bounded Durable Object alarm wall budget",
185
+ }),
186
+ );
187
+ };
188
+
189
+ const protocolMessage = (message: string): string =>
190
+ message.length <= 4_096 ? message : `${message.slice(0, 4_093)}...`;
191
+
192
+ const SubscribeRequest = Schema.TaggedStruct("Subscribe", {
193
+ schemaVersion: Schema.Literal(1),
194
+ scope: Schema.Struct({
195
+ ...SubscriptionScope.fields,
196
+ }),
197
+ options: Schema.Struct({
198
+ subscriptionId: SubscriptionKey.fields.subscriptionId,
199
+ source: EventSourceVersion,
200
+ parameters: PersistedJson,
201
+ context: PersistedJson,
202
+ mode: Schema.Literals(["once", "continuous"]),
203
+ expiresAtMillis: Schema.NullOr(Schema.Number),
204
+ destination: SubscriptionConfiguration.fields.destination,
205
+ deliveryPrincipal: SubscriptionConfiguration.fields.deliveryPrincipal,
206
+ admissionGroup: SubscriptionConfiguration.fields.admissionGroup,
207
+ admissionFence: SubscriptionConfiguration.fields.admissionFence,
208
+ agentId: SubscriptionConfiguration.fields.agentId,
209
+ definitions: SubscriptionConfiguration.fields.definitions,
210
+ }),
211
+ });
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
+
229
+ const ListSubscriptionsRequest = Schema.TaggedStruct("ListSubscriptions", {
230
+ schemaVersion: Schema.Literal(1),
231
+ scope: SubscribeRequest.fields.scope,
232
+ after: Schema.optionalKey(Schema.Natural),
233
+ limit: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
234
+ });
235
+
236
+ const CancelSubscriptionRequest = Schema.TaggedStruct("CancelSubscription", {
237
+ expectedRevision: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
238
+ schemaVersion: Schema.Literal(1),
239
+ scope: SubscribeRequest.fields.scope,
240
+ key: SubscriptionKey,
241
+ });
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
+
256
+ const ListDeliveriesRequest = Schema.TaggedStruct("ListDeliveries", {
257
+ schemaVersion: Schema.Literal(1),
258
+ scope: SubscribeRequest.fields.scope,
259
+ key: SubscriptionKey,
260
+ after: Schema.optionalKey(Schema.String),
261
+ limit: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
262
+ });
263
+
264
+ const AcceptRequest = Schema.TaggedStruct("Accept", {
265
+ schemaVersion: Schema.Literal(1),
266
+ partition: SourcePartition,
267
+ principal: Principal,
268
+ source: EventSourceVersion,
269
+ payload: PersistedJson,
270
+ });
271
+
272
+ const StatusRequest = Schema.TaggedStruct("Status", {
273
+ schemaVersion: Schema.Literal(1),
274
+ partition: SourcePartition,
275
+ principal: Principal,
276
+ source: EventSourceVersion,
277
+ eventId: Schema.String,
278
+ });
279
+
280
+ const SubscriptionPartitionRequest = Schema.Union([
281
+ SubscribeRequest,
282
+ GetSubscriptionRequest,
283
+ UpdateSubscriptionRequest,
284
+ SubscriptionStateRequest,
285
+ ListSubscriptionsRequest,
286
+ CancelSubscriptionRequest,
287
+ ListDeliveriesRequest,
288
+ RecoverDeliveryRequest,
289
+ AcceptRequest,
290
+ StatusRequest,
291
+ ]);
292
+
293
+ type SubscriptionPartitionRequest = typeof SubscriptionPartitionRequest.Type;
294
+
295
+ const SubscriptionPage = Schema.Struct({
296
+ items: Schema.Array(SubscriptionSnapshot),
297
+ next: Schema.NullOr(Schema.Natural),
298
+ });
299
+
300
+ const DeliveryPage = Schema.Struct({
301
+ items: Schema.Array(SubscriptionDeliverySnapshot),
302
+ next: Schema.NullOr(Schema.String),
303
+ });
304
+
305
+ const IntakeStatus = Schema.Struct({
306
+ ...EventAcknowledgement.fields,
307
+ routingComplete: Schema.Boolean,
308
+ routingFailure: Schema.NullOr(Schema.String),
309
+ nextAttemptAtMillis: Schema.Number,
310
+ });
311
+
312
+ const SubscriptionPartitionFailure = Schema.Union([
313
+ SubscriptionError,
314
+ SubscriptionSourceError,
315
+ SubscriptionFailpointError,
316
+ SubscriptionPartitionProtocolError,
317
+ ]);
318
+
319
+ type SubscriptionPartitionFailure = typeof SubscriptionPartitionFailure.Type;
320
+
321
+ const SubscriptionPartitionResponse = Schema.Union([
322
+ Schema.TaggedStruct("Snapshot", { value: SubscriptionSnapshot }),
323
+ Schema.TaggedStruct("SubscriptionPage", { value: SubscriptionPage }),
324
+ Schema.TaggedStruct("DeliveryPage", { value: DeliveryPage }),
325
+ Schema.TaggedStruct("DeliverySnapshot", { value: SubscriptionDeliverySnapshot }),
326
+ Schema.TaggedStruct("Acknowledgement", { value: EventAcknowledgement }),
327
+ Schema.TaggedStruct("Status", { value: IntakeStatus }),
328
+ Schema.TaggedStruct("Failed", { failure: SubscriptionPartitionFailure }),
329
+ ]);
330
+
331
+ type SubscriptionPartitionResponse = typeof SubscriptionPartitionResponse.Type;
332
+
333
+ const decodeRequest = Schema.decodeUnknownEffect(SubscriptionPartitionRequest);
334
+ const encodeRequest = Schema.encodeEffect(SubscriptionPartitionRequest);
335
+ const decodeResponse = Schema.decodeUnknownEffect(SubscriptionPartitionResponse);
336
+ const encodeResponse = Schema.encodeEffect(SubscriptionPartitionResponse);
337
+
338
+ const protocolFailure = (message: string): SubscriptionPartitionResponse => ({
339
+ _tag: "Failed",
340
+ failure: SubscriptionPartitionProtocolError.make({ message: protocolMessage(message) }),
341
+ });
342
+
343
+ export const sourcePartitionName = (partition: SourcePartition): string =>
344
+ JSON.stringify([partition.tenantId, partition.address]);
345
+
346
+ export interface SubscriptionPartitionObjectRpc extends Rpc.DurableObjectBranded {
347
+ subscription(encoded: unknown): Promise<unknown>;
348
+ }
349
+
350
+ export class SubscriptionPartitionNamespace extends Context.Service<
351
+ SubscriptionPartitionNamespace,
352
+ { readonly namespace: DurableObjectNamespace<SubscriptionPartitionObjectRpc> }
353
+ >()("@effect-agent/platform-cloudflare/SubscriptionPartitionNamespace") {}
354
+
355
+ const storageFailure = (code: string): SubscriptionError =>
356
+ SubscriptionError.make({ reason: "storage", code });
357
+
358
+ const corruptFailure = (code: string): SubscriptionError =>
359
+ SubscriptionError.make({ reason: "corrupt", code });
360
+
361
+ /** Worker-side management and trusted intake, routed to one fresh partition stub per call. */
362
+ export class CloudflareSubscriptionsClient {
363
+ static layer(
364
+ partition: SourcePartition,
365
+ ): Layer.Layer<Subscriptions | SubscriptionIntake, never, SubscriptionPartitionNamespace> {
366
+ return Layer.effectContext(
367
+ Effect.gen(function* () {
368
+ const { namespace } = yield* SubscriptionPartitionNamespace;
369
+
370
+ const call = Effect.fn("CloudflareSubscriptionsClient.call")(function* (
371
+ addressedPartition: SourcePartition,
372
+ request: SubscriptionPartitionRequest,
373
+ ) {
374
+ if (!samePartition(partition, addressedPartition)) {
375
+ return yield* SubscriptionError.make({ reason: "unauthorized", code: "partition" });
376
+ }
377
+
378
+ const encoded = yield* encodeRequest(request).pipe(
379
+ Effect.mapError(() => corruptFailure("subscription-partition-protocol")),
380
+ );
381
+
382
+ const raw = yield* Effect.tryPromise({
383
+ try: () =>
384
+ namespace
385
+ .get(namespace.idFromName(sourcePartitionName(partition)))
386
+ .subscription(encoded),
387
+ catch: () => storageFailure("call-subscription-partition"),
388
+ });
389
+
390
+ return yield* decodeResponse(raw).pipe(
391
+ Effect.mapError(() => corruptFailure("subscription-partition-protocol")),
392
+ );
393
+ });
394
+
395
+ const failed = (response: SubscriptionPartitionResponse): SubscriptionPartitionFailure =>
396
+ response._tag === "Failed"
397
+ ? response.failure
398
+ : SubscriptionPartitionProtocolError.make({
399
+ message: "Unexpected subscription response",
400
+ });
401
+
402
+ const asProtocolError = (): SubscriptionError =>
403
+ corruptFailure("subscription-partition-protocol");
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
+
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"),
487
+ subscribe: (scope, options) =>
488
+ Effect.gen(function* () {
489
+ const response = yield* call(scope.partition, {
490
+ _tag: "Subscribe",
491
+ schemaVersion: 1,
492
+ scope,
493
+ options,
494
+ });
495
+
496
+ if (response._tag === "Snapshot") return response.value;
497
+ const failure = failed(response);
498
+
499
+ if (
500
+ failure._tag === "SubscriptionError" ||
501
+ failure._tag === "SubscriptionSourceError" ||
502
+ failure._tag === "SubscriptionFailpointError"
503
+ )
504
+ return yield* failure;
505
+
506
+ return yield* asProtocolError();
507
+ }),
508
+ listSubscriptions: (scope, after, limit) =>
509
+ Effect.gen(function* () {
510
+ const response = yield* call(scope.partition, {
511
+ _tag: "ListSubscriptions",
512
+ schemaVersion: 1,
513
+ scope,
514
+ ...(after === undefined ? {} : { after }),
515
+ ...(limit === undefined ? {} : { limit }),
516
+ });
517
+
518
+ if (response._tag === "SubscriptionPage") return response.value;
519
+ const failure = failed(response);
520
+
521
+ return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
522
+ }),
523
+ cancelSubscription: (scope, key, expectedRevision) =>
524
+ Effect.gen(function* () {
525
+ const response = yield* call(scope.partition, {
526
+ _tag: "CancelSubscription",
527
+ ...(expectedRevision === undefined ? {} : { expectedRevision }),
528
+ schemaVersion: 1,
529
+ scope,
530
+ key,
531
+ });
532
+
533
+ if (response._tag === "Snapshot") return response.value;
534
+ const failure = failed(response);
535
+
536
+ if (
537
+ failure._tag === "SubscriptionError" ||
538
+ failure._tag === "SubscriptionFailpointError"
539
+ )
540
+ return yield* failure;
541
+
542
+ return yield* asProtocolError();
543
+ }),
544
+ listDeliveries: (scope, key, after, limit) =>
545
+ Effect.gen(function* () {
546
+ const response = yield* call(scope.partition, {
547
+ _tag: "ListDeliveries",
548
+ schemaVersion: 1,
549
+ scope,
550
+ key,
551
+ ...(after === undefined ? {} : { after }),
552
+ ...(limit === undefined ? {} : { limit }),
553
+ });
554
+
555
+ if (response._tag === "DeliveryPage") return response.value;
556
+ const failure = failed(response);
557
+
558
+ return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
559
+ }),
560
+ });
561
+
562
+ const intake = SubscriptionIntake.of({
563
+ accept: (principal, source, payload) =>
564
+ Effect.gen(function* () {
565
+ const request = yield* Schema.decodeUnknownEffect(AcceptRequest)({
566
+ _tag: "Accept",
567
+ schemaVersion: 1,
568
+ partition,
569
+ principal,
570
+ source,
571
+ payload,
572
+ }).pipe(
573
+ Effect.mapError(() =>
574
+ SubscriptionError.make({ reason: "validation", code: "event-payload" }),
575
+ ),
576
+ );
577
+
578
+ const response = yield* call(request.partition, request);
579
+
580
+ if (response._tag === "Acknowledgement") return response.value;
581
+ const failure = failed(response);
582
+
583
+ if (
584
+ failure._tag === "SubscriptionError" ||
585
+ failure._tag === "SubscriptionSourceError" ||
586
+ failure._tag === "SubscriptionFailpointError"
587
+ )
588
+ return yield* failure;
589
+
590
+ return yield* asProtocolError();
591
+ }),
592
+ status: (principal, source, eventId) =>
593
+ Effect.gen(function* () {
594
+ const response = yield* call(partition, {
595
+ _tag: "Status",
596
+ schemaVersion: 1,
597
+ partition,
598
+ principal,
599
+ source,
600
+ eventId,
601
+ });
602
+
603
+ if (response._tag === "Status") return response.value;
604
+ const failure = failed(response);
605
+
606
+ return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
607
+ }),
608
+ });
609
+
610
+ return Context.make(Subscriptions, subscriptions).pipe(
611
+ Context.add(SubscriptionIntake, intake),
612
+ );
613
+ }),
614
+ );
615
+ }
616
+ }
617
+
618
+ export class SubscriptionPartitionIdentity extends Context.Service<
619
+ SubscriptionPartitionIdentity,
620
+ { readonly partition: SourcePartition }
621
+ >()("@effect-agent/platform-cloudflare/SubscriptionPartitionIdentity") {}
622
+
623
+ const decodePartitionName = Effect.fn("decodeSubscriptionPartitionName")(function* (
624
+ name: string | null | undefined,
625
+ ) {
626
+ if (name === null || name === undefined) {
627
+ return yield* SubscriptionPartitionProtocolError.make({
628
+ message: "Subscription Partition objects require an idFromName identity",
629
+ });
630
+ }
631
+
632
+ const tuple = yield* Schema.decodeUnknownEffect(
633
+ Schema.fromJsonString(Schema.Tuple([Schema.String, Schema.String])),
634
+ )(name).pipe(
635
+ Effect.mapError(() =>
636
+ SubscriptionPartitionProtocolError.make({
637
+ message: "Subscription Partition name is malformed",
638
+ }),
639
+ ),
640
+ );
641
+
642
+ return yield* Schema.decodeUnknownEffect(SourcePartition)({
643
+ tenantId: tuple[0],
644
+ address: tuple[1],
645
+ }).pipe(
646
+ Effect.mapError(() =>
647
+ SubscriptionPartitionProtocolError.make({
648
+ message: "Subscription Partition identity is invalid",
649
+ }),
650
+ ),
651
+ );
652
+ });
653
+
654
+ const samePartition = Schema.toEquivalence(SourcePartition);
655
+
656
+ const requestPartition = (request: SubscriptionPartitionRequest): SourcePartition =>
657
+ request._tag === "Accept" || request._tag === "Status"
658
+ ? request.partition
659
+ : request.scope.partition;
660
+
661
+ const handleRequest = Effect.fn("SubscriptionPartition.handleRequest")(function* (
662
+ encoded: unknown,
663
+ ) {
664
+ const decoded = yield* decodeRequest(encoded).pipe(Effect.result);
665
+
666
+ if (decoded._tag === "Failure") {
667
+ return yield* encodeResponse(
668
+ protocolFailure("The subscription request could not be decoded"),
669
+ ).pipe(Effect.orDie);
670
+ }
671
+ const request = decoded.success;
672
+ const { partition } = yield* SubscriptionPartitionIdentity;
673
+
674
+ if (!samePartition(partition, requestPartition(request))) {
675
+ return yield* encodeResponse(
676
+ protocolFailure("The request partition does not match the addressed object"),
677
+ ).pipe(Effect.orDie);
678
+ }
679
+ const subscriptions = yield* Subscriptions;
680
+ const intake = yield* SubscriptionIntake;
681
+
682
+ const response = yield* Effect.gen(function* (): Effect.fn.Return<
683
+ SubscriptionPartitionResponse,
684
+ SubscriptionPartitionFailure
685
+ > {
686
+ switch (request._tag) {
687
+ case "GetSubscription":
688
+ return {
689
+ _tag: "Snapshot",
690
+ value: yield* subscriptions.getSubscription(request.scope, request.key),
691
+ };
692
+ case "Subscribe":
693
+ return {
694
+ _tag: "Snapshot",
695
+ value: yield* subscriptions.subscribe(request.scope, request.options),
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
+ };
734
+ case "ListSubscriptions":
735
+ return {
736
+ _tag: "SubscriptionPage",
737
+ value: yield* subscriptions.listSubscriptions(
738
+ request.scope,
739
+ request.after,
740
+ request.limit,
741
+ ),
742
+ };
743
+ case "CancelSubscription":
744
+ return {
745
+ _tag: "Snapshot",
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
+ ),
760
+ };
761
+ case "ListDeliveries":
762
+ return {
763
+ _tag: "DeliveryPage",
764
+ value: yield* subscriptions.listDeliveries(
765
+ request.scope,
766
+ request.key,
767
+ request.after,
768
+ request.limit,
769
+ ),
770
+ };
771
+ case "Accept":
772
+ return {
773
+ _tag: "Acknowledgement",
774
+ value: yield* intake.accept(request.principal, request.source, request.payload),
775
+ };
776
+ case "Status":
777
+ return {
778
+ _tag: "Status",
779
+ value: yield* intake.status(request.principal, request.source, request.eventId),
780
+ };
781
+ }
782
+ }).pipe(
783
+ Effect.catch((failure) =>
784
+ Schema.is(SubscriptionPartitionFailure)(failure)
785
+ ? Effect.succeed({ _tag: "Failed" as const, failure })
786
+ : Effect.succeed(protocolFailure("The subscription operation failed outside its contract")),
787
+ ),
788
+ );
789
+
790
+ return yield* encodeResponse(response).pipe(Effect.orDie);
791
+ });
792
+
793
+ const alarmStorageError = (code: string) => () => storageFailure(code);
794
+
795
+ const transactionLayer: Layer.Layer<
796
+ DoSubscriptionTransaction,
797
+ never,
798
+ DurableObjectAlarm.DurableObjectAlarm
799
+ > = Layer.effect(
800
+ DoSubscriptionTransaction,
801
+ Effect.gen(function* () {
802
+ const alarms = yield* DurableObjectAlarm.DurableObjectAlarm;
803
+
804
+ return DoSubscriptionTransaction.of({
805
+ run: (body) =>
806
+ Effect.gen(function* () {
807
+ const nowMillis = yield* Clock.currentTimeMillis;
808
+
809
+ return yield* alarms
810
+ .transaction((transaction) =>
811
+ body((replacement) =>
812
+ replacement.deadlineAtMillis === null
813
+ ? transaction
814
+ .cancelAlarm({ id: SUBSCRIPTION_ALARM_ID, tag: SUBSCRIPTION_ALARM_TAG })
815
+ .pipe(Effect.mapError(alarmStorageError("cancel-subscription-alarm")))
816
+ : Effect.fromOption(
817
+ DateTime.make(Math.max(replacement.deadlineAtMillis, nowMillis + 1)),
818
+ ).pipe(
819
+ Effect.mapError(() => corruptFailure("subscription-alarm-deadline")),
820
+ Effect.flatMap((runAt) =>
821
+ transaction
822
+ .scheduleAlarm({
823
+ id: SUBSCRIPTION_ALARM_ID,
824
+ tag: SUBSCRIPTION_ALARM_TAG,
825
+ runAt,
826
+ payload: { schemaVersion: 1, generation: replacement.generation },
827
+ })
828
+ .pipe(Effect.mapError(alarmStorageError("schedule-subscription-alarm"))),
829
+ ),
830
+ ),
831
+ ),
832
+ )
833
+ .pipe(
834
+ Effect.catchTag("StorageOperationError", () =>
835
+ storageFailure("commit-subscription-transaction"),
836
+ ),
837
+ );
838
+ }),
839
+ });
840
+ }),
841
+ );
842
+
843
+ const alarmHandler = (limits: SubscriptionLimits) =>
844
+ DurableObjectAlarm.processDue(
845
+ (event) =>
846
+ Effect.gen(function* () {
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) {
870
+ return yield* SubscriptionAlarmProtocolError.make({
871
+ message: `Unsupported Subscription Partition alarm ${event.tag}/${event.id}`,
872
+ });
873
+ }
874
+ yield* Schema.decodeUnknownEffect(SubscriptionAlarmPayload)(event.payload).pipe(
875
+ Effect.mapError(() =>
876
+ SubscriptionAlarmProtocolError.make({
877
+ message: "Unsupported subscription alarm payload",
878
+ }),
879
+ ),
880
+ );
881
+ const driver = yield* SubscriptionDriver;
882
+ const alarmControl = yield* DoSubscriptionAlarmControl;
883
+
884
+ yield* alarmControl.prearm((yield* Clock.currentTimeMillis) + limits.retryMillis);
885
+
886
+ const pass = yield* driver.runDue.pipe(
887
+ Effect.catchCause((cause) =>
888
+ Cause.hasInterrupts(cause)
889
+ ? Effect.failCause(cause)
890
+ : Clock.currentTimeMillis.pipe(
891
+ Effect.flatMap((time) => alarmControl.prearm(time + limits.retryMillis)),
892
+ Effect.andThen(Effect.failCause(cause)),
893
+ ),
894
+ ),
895
+ );
896
+
897
+ if (pass.failed > 0) {
898
+ yield* alarmControl.prearm((yield* Clock.currentTimeMillis) + limits.retryMillis);
899
+ } else {
900
+ yield* alarmControl.reconcile;
901
+ }
902
+ }),
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
+ },
909
+ ).pipe(Effect.asVoid);
910
+
911
+ type SubscriptionRuntimeServices =
912
+ | Subscriptions
913
+ | SubscriptionIntake
914
+ | SubscriptionDriver
915
+ | DoSubscriptionAlarmControl
916
+ | SubscriptionPartitionIdentity
917
+ | DurableObjectAlarm.DurableObjectAlarm;
918
+
919
+ export interface SubscriptionPartitionObjectInstance extends InstanceType<
920
+ EffectCfDurableObject.DurableObjectClass<Record<never, never>, SubscriptionRuntimeServices>
921
+ > {
922
+ subscription(encoded: unknown): Promise<unknown>;
923
+ alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void;
924
+ }
925
+
926
+ export interface SubscriptionPartitionObjectClass {
927
+ new (ctx: DurableObjectState, env: Cloudflare.Env): SubscriptionPartitionObjectInstance;
928
+ }
929
+
930
+ /**
931
+ * Build one source-addressed SQLite Durable Object. The host Layer binds the permitted source
932
+ * catalog and authority. It must not retain cleanup-scoped resources across RPC events.
933
+ */
934
+ export const makeSubscriptionPartitionObjectClass = <E>(
935
+ host: Layer.Layer<
936
+ SubscriptionAuthorizer | EventSources | SubscriptionInputBindings | ThreadObjectNamespace,
937
+ E,
938
+ | EffectCfDurableObjectState.DurableObjectState
939
+ | WorkerEnvironment
940
+ | SubscriptionPartitionIdentity
941
+ >,
942
+ limits: SubscriptionLimits = defaultSubscriptionLimits,
943
+ ): SubscriptionPartitionObjectClass => {
944
+ const cloudflareLimitsLayer = Layer.effectDiscard(validateCloudflareSubscriptionLimits(limits));
945
+
946
+ const identityLayer = Layer.effect(
947
+ SubscriptionPartitionIdentity,
948
+ Effect.gen(function* () {
949
+ const state = yield* EffectCfDurableObjectState.DurableObjectState;
950
+
951
+ return SubscriptionPartitionIdentity.of({
952
+ partition: yield* decodePartitionName(state.raw.id.name),
953
+ });
954
+ }),
955
+ );
956
+
957
+ const sqlLayer = Layer.unwrap(
958
+ Effect.map(EffectCfDurableObjectState.DurableObjectState, (state) =>
959
+ SqliteClient.layer({ storage: state.raw.storage }),
960
+ ),
961
+ );
962
+
963
+ const partitionStore = Layer.unwrap(
964
+ Effect.map(SubscriptionPartitionIdentity, ({ partition }) =>
965
+ doSubscriptionStoreLayer(partition).pipe(
966
+ Layer.provide(transactionLayer),
967
+ Layer.provide(sqlLayer),
968
+ ),
969
+ ),
970
+ );
971
+
972
+ const application = Layer.mergeAll(
973
+ Subscriptions.layer(limits),
974
+ SubscriptionIntake.layer(limits),
975
+ SubscriptionDriver.layer(limits),
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
+ ),
1000
+ ).pipe(
1001
+ Layer.provideMerge(partitionStore),
1002
+ Layer.provide(
1003
+ cloudflarePreparedInputAdmissionLayer.pipe(Layer.provide(CloudflareThreadClient.layer)),
1004
+ ),
1005
+ Layer.provide(BrowserCrypto.layer),
1006
+ Layer.provideMerge(DurableObjectAlarm.DurableObjectAlarm.layer),
1007
+ Layer.provide(host),
1008
+ Layer.provideMerge(identityLayer),
1009
+ );
1010
+
1011
+ const runtime: Layer.Layer<
1012
+ SubscriptionRuntimeServices,
1013
+ | E
1014
+ | SubscriptionError
1015
+ | SubscriptionPartitionProtocolError
1016
+ | CloudflareSubscriptionConfigError
1017
+ | SubscriptionAlarmProtocolError,
1018
+ EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
1019
+ > = Layer.effectContext(
1020
+ Effect.gen(function* () {
1021
+ const state = yield* EffectCfDurableObjectState.DurableObjectState;
1022
+ const scope = yield* Effect.scope;
1023
+
1024
+ return yield* state.blockConcurrencyWhile(Layer.buildWithScope(application, scope));
1025
+ }),
1026
+ );
1027
+
1028
+ const rpc = {
1029
+ subscription: (encoded: unknown) => handleRequest(encoded),
1030
+ } satisfies EffectCfDurableObject.DurableObjectRpc<SubscriptionRuntimeServices>;
1031
+
1032
+ const Base = EffectCfDurableObject.make(runtime, {
1033
+ initialize: Effect.void,
1034
+ rpc,
1035
+ alarms: alarmHandler(limits),
1036
+ });
1037
+
1038
+ class SubscriptionPartitionObject extends Base {
1039
+ override alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void {
1040
+ return super.alarm?.(alarmInfo);
1041
+ }
1042
+ }
1043
+
1044
+ return SubscriptionPartitionObject;
1045
+ };