@effect-agent/platform-cloudflare 0.1.0-beta.9 → 0.1.0-beta.91
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/Alarm.d.mts +188 -0
- package/dist/Alarm.mjs +451 -0
- package/dist/Alarm.mjs.map +1 -0
- package/dist/BrowserRestCapture.d.mts +35 -0
- package/dist/BrowserRestCapture.mjs +238 -0
- package/dist/BrowserRestCapture.mjs.map +1 -0
- package/dist/BrowserRestCrawl.d.mts +17 -0
- package/dist/BrowserRestCrawl.mjs +244 -0
- package/dist/BrowserRestCrawl.mjs.map +1 -0
- package/dist/CloudflareAiGateway.d.mts +64 -0
- package/dist/CloudflareAiGateway.mjs +70 -0
- package/dist/CloudflareAiGateway.mjs.map +1 -0
- package/dist/CloudflareBindings.d.mts +115 -0
- package/dist/CloudflareBindings.mjs +113 -0
- package/dist/CloudflareBindings.mjs.map +1 -0
- package/dist/CloudflareBrowser-DV6kya1H.mjs +495 -0
- package/dist/CloudflareBrowser-DV6kya1H.mjs.map +1 -0
- package/dist/CloudflareBrowser-Doe1M7R5.d.mts +80 -0
- package/dist/CloudflareBrowser.d.mts +2 -0
- package/dist/CloudflareBrowser.mjs +2 -0
- package/dist/CloudflareCodeMode.d.mts +44 -0
- package/dist/CloudflareCodeMode.mjs +616 -0
- package/dist/CloudflareCodeMode.mjs.map +1 -0
- package/dist/CloudflareConfig-f3CqTel1.d.mts +101 -0
- package/dist/CloudflareConfig.d.mts +2 -0
- package/dist/CloudflareConfig.mjs +122 -0
- package/dist/CloudflareConfig.mjs.map +1 -0
- package/dist/CloudflareMemory.d.mts +142 -0
- package/dist/CloudflareMemory.mjs +202 -0
- package/dist/CloudflareMemory.mjs.map +1 -0
- package/dist/CloudflareScheduling.d.mts +52 -0
- package/dist/CloudflareScheduling.mjs +396 -0
- package/dist/CloudflareScheduling.mjs.map +1 -0
- package/dist/CloudflareSubscriptions.d.mts +87 -0
- package/dist/CloudflareSubscriptions.mjs +530 -0
- package/dist/CloudflareSubscriptions.mjs.map +1 -0
- package/dist/CloudflareThreadClient.d.mts +1496 -0
- package/dist/CloudflareThreadClient.mjs +382 -0
- package/dist/CloudflareThreadClient.mjs.map +1 -0
- package/dist/InteractiveBrowser-DUE_m12-.d.mts +161 -0
- package/dist/InteractiveBrowser-DupDtfw2.mjs +1169 -0
- package/dist/InteractiveBrowser-DupDtfw2.mjs.map +1 -0
- package/dist/InteractiveBrowser.d.mts +2 -0
- package/dist/InteractiveBrowser.mjs +2 -0
- package/dist/ProtectedBrowser.d.mts +123 -0
- package/dist/ProtectedBrowser.mjs +1135 -0
- package/dist/ProtectedBrowser.mjs.map +1 -0
- package/dist/ThreadObject-CIbmXZCY.d.mts +812 -0
- package/dist/ThreadObject-DvRG0dDz.mjs +835 -0
- package/dist/ThreadObject-DvRG0dDz.mjs.map +1 -0
- package/dist/ThreadObject.d.mts +2 -0
- package/dist/ThreadObject.mjs +3 -0
- package/dist/WakeScheduler.d.mts +23 -0
- package/dist/WakeScheduler.mjs +57 -0
- package/dist/WakeScheduler.mjs.map +1 -0
- package/dist/boundary-BguazVkh.mjs +42 -0
- package/dist/boundary-BguazVkh.mjs.map +1 -0
- package/dist/index.d.mts +13 -1548
- package/dist/index.mjs +13 -1636
- package/dist/prepared-admission-DU0_T-ev.mjs +73 -0
- package/dist/prepared-admission-DU0_T-ev.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -53
- package/src/Alarm.ts +973 -0
- package/src/BrowserRestCapture.ts +477 -0
- package/src/BrowserRestCrawl.ts +540 -0
- package/src/CloudflareAiGateway.ts +170 -0
- package/src/CloudflareBindings.ts +199 -0
- package/src/CloudflareBrowser.ts +15 -0
- package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +372 -204
- package/src/{config.ts → CloudflareConfig.ts} +9 -8
- package/src/CloudflareMemory.ts +420 -0
- package/src/CloudflareScheduling.ts +747 -0
- package/src/CloudflareSubscriptions.ts +1041 -0
- package/src/CloudflareThreadClient.ts +809 -0
- package/src/InteractiveBrowser.ts +2354 -0
- package/src/ProtectedBrowser.ts +9 -0
- package/src/ThreadObject.ts +1179 -0
- package/src/{wake-scheduler.ts → WakeScheduler.ts} +32 -26
- package/src/index.ts +12 -23
- package/src/internal/boundary.ts +55 -0
- package/src/internal/browser-quick-action.ts +857 -0
- package/src/internal/browser-session-lifecycle.ts +160 -0
- package/src/internal/layers.ts +751 -0
- package/src/internal/message-delivery.ts +152 -0
- package/src/internal/prepared-admission.ts +116 -0
- package/src/internal/progress-wait.ts +121 -0
- package/src/internal/transport.ts +44 -0
- package/src/protected-browser/binding.ts +313 -0
- package/src/protected-browser/host.ts +344 -0
- package/src/protected-browser/inspect-frame.ts +125 -0
- package/src/protected-browser/native.ts +445 -0
- package/src/protected-browser/policy.ts +821 -0
- package/dist/index.mjs.map +0 -1
- package/src/alarm.ts +0 -334
- package/src/bindings.ts +0 -145
- package/src/client.ts +0 -646
- package/src/conversation-object.ts +0 -768
- package/src/layers.ts +0 -376
- package/src/transport.ts +0 -38
|
@@ -0,0 +1,1041 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DoSubscriptionAlarmControl,
|
|
3
|
+
DoSubscriptionTransaction,
|
|
4
|
+
doSubscriptionStoreLayer,
|
|
5
|
+
} from "@effect-agent/storage-cloudflare/do-subscription-store";
|
|
6
|
+
import { BrowserCrypto } from "@effect/platform-browser";
|
|
7
|
+
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
8
|
+
import { Cause, Clock, Context, DateTime, Effect, Layer, Schema, type Scope } from "effect";
|
|
9
|
+
import { type EventSources } from "effect-agent/event-source";
|
|
10
|
+
import { PersistedJson } from "effect-agent/records";
|
|
11
|
+
import { Principal } from "effect-agent/submission-ledger";
|
|
12
|
+
import {
|
|
13
|
+
EventAcknowledgement,
|
|
14
|
+
EventSourceVersion,
|
|
15
|
+
SourcePartition,
|
|
16
|
+
type SubscriptionAuthorizer,
|
|
17
|
+
SubscriptionConfiguration,
|
|
18
|
+
SubscriptionDeliverySnapshot,
|
|
19
|
+
SubscriptionDeliveryKey,
|
|
20
|
+
SubscriptionError,
|
|
21
|
+
SubscriptionFailpointError,
|
|
22
|
+
SubscriptionKey,
|
|
23
|
+
SubscriptionScope,
|
|
24
|
+
type SubscriptionLimits,
|
|
25
|
+
SubscriptionSnapshot,
|
|
26
|
+
SubscriptionSourceError,
|
|
27
|
+
defaultSubscriptionLimits,
|
|
28
|
+
} from "effect-agent/subscription";
|
|
29
|
+
import { type SubscriptionInputBindings } from "effect-agent/subscription-input";
|
|
30
|
+
import { SubscriptionDriver, SubscriptionIntake, Subscriptions } from "effect-agent/subscriptions";
|
|
31
|
+
import {
|
|
32
|
+
DurableObject as EffectCfDurableObject,
|
|
33
|
+
DurableObjectAlarm,
|
|
34
|
+
DurableObjectState as EffectCfDurableObjectState,
|
|
35
|
+
type WorkerEnvironment,
|
|
36
|
+
} from "effect-cf";
|
|
37
|
+
|
|
38
|
+
import { type ThreadObjectNamespace } from "./CloudflareBindings.ts";
|
|
39
|
+
import { CloudflareThreadClient } from "./CloudflareThreadClient.ts";
|
|
40
|
+
import { cloudflarePreparedInputAdmissionLayer } from "./internal/prepared-admission.ts";
|
|
41
|
+
|
|
42
|
+
const SUBSCRIPTION_ALARM_TAG = "effect-agent/SubscriptionPartitionWake";
|
|
43
|
+
const SUBSCRIPTION_ALARM_ID = "driver";
|
|
44
|
+
const MAX_ALARM_WALL_MILLIS = 12 * 60_000;
|
|
45
|
+
const MAX_ALARMS_PER_INVOCATION = 16;
|
|
46
|
+
const MAX_ANCILLARY_ALARM_MILLIS = 30_000;
|
|
47
|
+
|
|
48
|
+
const SubscriptionAlarmPayload = Schema.Struct({
|
|
49
|
+
schemaVersion: Schema.Literal(1),
|
|
50
|
+
generation: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export class SubscriptionAlarmProtocolError extends Schema.TaggedError<SubscriptionAlarmProtocolError>()(
|
|
54
|
+
"SubscriptionAlarmProtocolError",
|
|
55
|
+
{ message: Schema.String },
|
|
56
|
+
) {}
|
|
57
|
+
|
|
58
|
+
/** Bounded host diagnostic; credentials and provider responses do not belong in alarm failures. */
|
|
59
|
+
export class SubscriptionAlarmExtensionError extends Schema.TaggedError<SubscriptionAlarmExtensionError>()(
|
|
60
|
+
"SubscriptionAlarmExtensionError",
|
|
61
|
+
{ code: Schema.NonEmptyString.check(Schema.isMaxLength(128)) },
|
|
62
|
+
) {}
|
|
63
|
+
|
|
64
|
+
/** Native partition services supplied at alarm invocation, after the host Layer is built. */
|
|
65
|
+
export type SubscriptionPartitionAlarmServices =
|
|
66
|
+
| Subscriptions
|
|
67
|
+
| SubscriptionIntake
|
|
68
|
+
| SubscriptionDriver;
|
|
69
|
+
|
|
70
|
+
export interface SubscriptionPartitionAlarmHandler<R = SubscriptionPartitionAlarmServices> {
|
|
71
|
+
readonly tag: string;
|
|
72
|
+
readonly handle: (
|
|
73
|
+
event: DurableObjectAlarm.DurableObjectAlarmEvent,
|
|
74
|
+
) => Effect.Effect<void, SubscriptionAlarmProtocolError | SubscriptionAlarmExtensionError, R>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Host-only handlers; the framework reserves its namespace and rejects every unknown tag. */
|
|
78
|
+
export const SubscriptionPartitionAlarmExtension = Context.Reference<{
|
|
79
|
+
readonly handlers: ReadonlyArray<SubscriptionPartitionAlarmHandler>;
|
|
80
|
+
}>("@effect-agent/platform-cloudflare/SubscriptionPartitionAlarmExtension", {
|
|
81
|
+
defaultValue: () => ({ handlers: [] }),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/** Capture host services once, deferring native partition services to invocation.
|
|
85
|
+
* Each invocation owns its codec/handler Scope and timeout.
|
|
86
|
+
* Callback failures stay typed. Defects and interruption reach the native alarm multiplexer.
|
|
87
|
+
* The host owns durable idempotency, prearming and external-effect uncertainty.
|
|
88
|
+
*/
|
|
89
|
+
export const makeSubscriptionPartitionAlarmHandler = Effect.fn(
|
|
90
|
+
"makeSubscriptionPartitionAlarmHandler",
|
|
91
|
+
)(function* <Payload extends Schema.Top, R>(options: {
|
|
92
|
+
readonly tag: string;
|
|
93
|
+
readonly payload: Payload;
|
|
94
|
+
readonly timeoutMillis: number;
|
|
95
|
+
readonly handle: (
|
|
96
|
+
event: Omit<DurableObjectAlarm.DurableObjectAlarmEvent, "payload"> & {
|
|
97
|
+
readonly payload: Payload["Type"];
|
|
98
|
+
},
|
|
99
|
+
) => Effect.Effect<void, SubscriptionAlarmExtensionError, R>;
|
|
100
|
+
}): Effect.fn.Return<
|
|
101
|
+
SubscriptionPartitionAlarmHandler<
|
|
102
|
+
Exclude<
|
|
103
|
+
Exclude<R | Payload["DecodingServices"], Scope.Scope>,
|
|
104
|
+
Exclude<
|
|
105
|
+
Exclude<R | Payload["DecodingServices"], Scope.Scope | SubscriptionPartitionAlarmServices>,
|
|
106
|
+
SubscriptionPartitionAlarmServices
|
|
107
|
+
>
|
|
108
|
+
>
|
|
109
|
+
>,
|
|
110
|
+
SubscriptionAlarmProtocolError,
|
|
111
|
+
Exclude<R | Payload["DecodingServices"], Scope.Scope | SubscriptionPartitionAlarmServices>
|
|
112
|
+
> {
|
|
113
|
+
if (
|
|
114
|
+
options.tag.length === 0 ||
|
|
115
|
+
options.tag.length > 128 ||
|
|
116
|
+
options.tag.startsWith("effect-agent/") ||
|
|
117
|
+
!Number.isSafeInteger(options.timeoutMillis) ||
|
|
118
|
+
options.timeoutMillis < 1 ||
|
|
119
|
+
options.timeoutMillis > MAX_ANCILLARY_ALARM_MILLIS
|
|
120
|
+
)
|
|
121
|
+
return yield* SubscriptionAlarmProtocolError.make({
|
|
122
|
+
message: "Invalid ancillary alarm tag or timeout",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Context capture includes unrequested services too; never retain a host override of native work.
|
|
126
|
+
const services = (yield* Effect.context<
|
|
127
|
+
Exclude<R | Payload["DecodingServices"], Scope.Scope | SubscriptionPartitionAlarmServices>
|
|
128
|
+
>()).pipe(Context.omit(Subscriptions, SubscriptionIntake, SubscriptionDriver));
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
tag: options.tag,
|
|
132
|
+
handle: (event) =>
|
|
133
|
+
Effect.gen(function* () {
|
|
134
|
+
if (event.tag !== options.tag)
|
|
135
|
+
return yield* SubscriptionAlarmProtocolError.make({
|
|
136
|
+
message: "Ancillary alarm tag mismatch",
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const payload = yield* Schema.decodeEffect(options.payload)(event.payload).pipe(
|
|
140
|
+
Effect.mapError(() =>
|
|
141
|
+
SubscriptionAlarmProtocolError.make({ message: "Invalid ancillary alarm payload" }),
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
yield* options.handle({ ...event, payload });
|
|
146
|
+
}).pipe(
|
|
147
|
+
Effect.scoped,
|
|
148
|
+
Effect.timeoutOrElse({
|
|
149
|
+
duration: options.timeoutMillis,
|
|
150
|
+
orElse: () => SubscriptionAlarmExtensionError.make({ code: "timeout" }),
|
|
151
|
+
}),
|
|
152
|
+
Effect.provideContext(services),
|
|
153
|
+
),
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
export class SubscriptionPartitionProtocolError extends Schema.TaggedError<SubscriptionPartitionProtocolError>()(
|
|
158
|
+
"SubscriptionPartitionProtocolError",
|
|
159
|
+
{ message: Schema.String.check(Schema.isMaxLength(4_096)) },
|
|
160
|
+
) {}
|
|
161
|
+
|
|
162
|
+
export class CloudflareSubscriptionConfigError extends Schema.TaggedError<CloudflareSubscriptionConfigError>()(
|
|
163
|
+
"CloudflareSubscriptionConfigError",
|
|
164
|
+
{ message: Schema.String },
|
|
165
|
+
) {}
|
|
166
|
+
|
|
167
|
+
/** Reject limits whose four bounded phases could exceed the safe Durable Object alarm budget. */
|
|
168
|
+
export const validateCloudflareSubscriptionLimits = (
|
|
169
|
+
limits: SubscriptionLimits,
|
|
170
|
+
options: { readonly ancillaryAlarms?: boolean } = {},
|
|
171
|
+
): Effect.Effect<void, CloudflareSubscriptionConfigError> => {
|
|
172
|
+
const worstCaseMillis =
|
|
173
|
+
4 * Math.ceil(limits.batchSize / limits.concurrency) * limits.operationTimeoutMillis +
|
|
174
|
+
(options.ancillaryAlarms === true ? MAX_ALARMS_PER_INVOCATION * MAX_ANCILLARY_ALARM_MILLIS : 0);
|
|
175
|
+
|
|
176
|
+
return worstCaseMillis <= MAX_ALARM_WALL_MILLIS
|
|
177
|
+
? Effect.void
|
|
178
|
+
: Effect.fail(
|
|
179
|
+
CloudflareSubscriptionConfigError.make({
|
|
180
|
+
message: "Subscription limits can exceed the bounded Durable Object alarm wall budget",
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const protocolMessage = (message: string): string =>
|
|
186
|
+
message.length <= 4_096 ? message : `${message.slice(0, 4_093)}...`;
|
|
187
|
+
|
|
188
|
+
const SubscribeRequest = Schema.TaggedStruct("Subscribe", {
|
|
189
|
+
schemaVersion: Schema.Literal(1),
|
|
190
|
+
scope: Schema.Struct({
|
|
191
|
+
...SubscriptionScope.fields,
|
|
192
|
+
}),
|
|
193
|
+
options: Schema.Struct({
|
|
194
|
+
subscriptionId: SubscriptionKey.fields.subscriptionId,
|
|
195
|
+
source: EventSourceVersion,
|
|
196
|
+
parameters: PersistedJson,
|
|
197
|
+
context: PersistedJson,
|
|
198
|
+
mode: Schema.Literals(["once", "continuous"]),
|
|
199
|
+
expiresAtMillis: Schema.NullOr(Schema.Number),
|
|
200
|
+
destination: SubscriptionConfiguration.fields.destination,
|
|
201
|
+
deliveryPrincipal: SubscriptionConfiguration.fields.deliveryPrincipal,
|
|
202
|
+
admissionGroup: SubscriptionConfiguration.fields.admissionGroup,
|
|
203
|
+
admissionFence: SubscriptionConfiguration.fields.admissionFence,
|
|
204
|
+
agentId: SubscriptionConfiguration.fields.agentId,
|
|
205
|
+
definitions: SubscriptionConfiguration.fields.definitions,
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const UpdateSubscriptionRequest = Schema.TaggedStruct("UpdateSubscription", {
|
|
210
|
+
schemaVersion: Schema.Literal(1),
|
|
211
|
+
scope: SubscribeRequest.fields.scope,
|
|
212
|
+
key: SubscriptionKey,
|
|
213
|
+
expectedRevision: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
214
|
+
options: SubscribeRequest.fields.options.mapFields(({ subscriptionId: _, ...fields }) => fields),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const SubscriptionStateRequest = Schema.Struct({
|
|
218
|
+
_tag: Schema.Literals(["PauseSubscription", "ResumeSubscription", "RecoverSubscription"]),
|
|
219
|
+
schemaVersion: Schema.Literal(1),
|
|
220
|
+
scope: SubscribeRequest.fields.scope,
|
|
221
|
+
key: SubscriptionKey,
|
|
222
|
+
expectedRevision: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const ListSubscriptionsRequest = Schema.TaggedStruct("ListSubscriptions", {
|
|
226
|
+
schemaVersion: Schema.Literal(1),
|
|
227
|
+
scope: SubscribeRequest.fields.scope,
|
|
228
|
+
after: Schema.optionalKey(Schema.Natural),
|
|
229
|
+
limit: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const CancelSubscriptionRequest = Schema.TaggedStruct("CancelSubscription", {
|
|
233
|
+
expectedRevision: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
|
|
234
|
+
schemaVersion: Schema.Literal(1),
|
|
235
|
+
scope: SubscribeRequest.fields.scope,
|
|
236
|
+
key: SubscriptionKey,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const GetSubscriptionRequest = Schema.TaggedStruct("GetSubscription", {
|
|
240
|
+
schemaVersion: Schema.Literal(1),
|
|
241
|
+
scope: SubscriptionScope,
|
|
242
|
+
key: SubscriptionKey,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const RecoverDeliveryRequest = Schema.TaggedStruct("RecoverDelivery", {
|
|
246
|
+
expectedGeneration: Schema.Natural,
|
|
247
|
+
schemaVersion: Schema.Literal(1),
|
|
248
|
+
scope: SubscribeRequest.fields.scope,
|
|
249
|
+
key: SubscriptionDeliveryKey,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const ListDeliveriesRequest = Schema.TaggedStruct("ListDeliveries", {
|
|
253
|
+
schemaVersion: Schema.Literal(1),
|
|
254
|
+
scope: SubscribeRequest.fields.scope,
|
|
255
|
+
key: SubscriptionKey,
|
|
256
|
+
after: Schema.optionalKey(Schema.String),
|
|
257
|
+
limit: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const AcceptRequest = Schema.TaggedStruct("Accept", {
|
|
261
|
+
schemaVersion: Schema.Literal(1),
|
|
262
|
+
partition: SourcePartition,
|
|
263
|
+
principal: Principal,
|
|
264
|
+
source: EventSourceVersion,
|
|
265
|
+
payload: PersistedJson,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const StatusRequest = Schema.TaggedStruct("Status", {
|
|
269
|
+
schemaVersion: Schema.Literal(1),
|
|
270
|
+
partition: SourcePartition,
|
|
271
|
+
principal: Principal,
|
|
272
|
+
source: EventSourceVersion,
|
|
273
|
+
eventId: Schema.String,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const SubscriptionPartitionRequest = Schema.Union([
|
|
277
|
+
SubscribeRequest,
|
|
278
|
+
GetSubscriptionRequest,
|
|
279
|
+
UpdateSubscriptionRequest,
|
|
280
|
+
SubscriptionStateRequest,
|
|
281
|
+
ListSubscriptionsRequest,
|
|
282
|
+
CancelSubscriptionRequest,
|
|
283
|
+
ListDeliveriesRequest,
|
|
284
|
+
RecoverDeliveryRequest,
|
|
285
|
+
AcceptRequest,
|
|
286
|
+
StatusRequest,
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
type SubscriptionPartitionRequest = typeof SubscriptionPartitionRequest.Type;
|
|
290
|
+
|
|
291
|
+
const SubscriptionPage = Schema.Struct({
|
|
292
|
+
items: Schema.Array(SubscriptionSnapshot),
|
|
293
|
+
next: Schema.NullOr(Schema.Natural),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const DeliveryPage = Schema.Struct({
|
|
297
|
+
items: Schema.Array(SubscriptionDeliverySnapshot),
|
|
298
|
+
next: Schema.NullOr(Schema.String),
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const IntakeStatus = Schema.Struct({
|
|
302
|
+
...EventAcknowledgement.fields,
|
|
303
|
+
routingComplete: Schema.Boolean,
|
|
304
|
+
routingFailure: Schema.NullOr(Schema.String),
|
|
305
|
+
nextAttemptAtMillis: Schema.Number,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const SubscriptionPartitionFailure = Schema.Union([
|
|
309
|
+
SubscriptionError,
|
|
310
|
+
SubscriptionSourceError,
|
|
311
|
+
SubscriptionFailpointError,
|
|
312
|
+
SubscriptionPartitionProtocolError,
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
type SubscriptionPartitionFailure = typeof SubscriptionPartitionFailure.Type;
|
|
316
|
+
|
|
317
|
+
const SubscriptionPartitionResponse = Schema.Union([
|
|
318
|
+
Schema.TaggedStruct("Snapshot", { value: SubscriptionSnapshot }),
|
|
319
|
+
Schema.TaggedStruct("SubscriptionPage", { value: SubscriptionPage }),
|
|
320
|
+
Schema.TaggedStruct("DeliveryPage", { value: DeliveryPage }),
|
|
321
|
+
Schema.TaggedStruct("DeliverySnapshot", { value: SubscriptionDeliverySnapshot }),
|
|
322
|
+
Schema.TaggedStruct("Acknowledgement", { value: EventAcknowledgement }),
|
|
323
|
+
Schema.TaggedStruct("Status", { value: IntakeStatus }),
|
|
324
|
+
Schema.TaggedStruct("Failed", { failure: SubscriptionPartitionFailure }),
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
type SubscriptionPartitionResponse = typeof SubscriptionPartitionResponse.Type;
|
|
328
|
+
|
|
329
|
+
const decodeRequest = Schema.decodeUnknownEffect(SubscriptionPartitionRequest);
|
|
330
|
+
const encodeRequest = Schema.encodeEffect(SubscriptionPartitionRequest);
|
|
331
|
+
const decodeResponse = Schema.decodeUnknownEffect(SubscriptionPartitionResponse);
|
|
332
|
+
const encodeResponse = Schema.encodeEffect(SubscriptionPartitionResponse);
|
|
333
|
+
|
|
334
|
+
const protocolFailure = (message: string): SubscriptionPartitionResponse => ({
|
|
335
|
+
_tag: "Failed",
|
|
336
|
+
failure: SubscriptionPartitionProtocolError.make({ message: protocolMessage(message) }),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
export const sourcePartitionName = (partition: SourcePartition): string =>
|
|
340
|
+
JSON.stringify([partition.tenantId, partition.address]);
|
|
341
|
+
|
|
342
|
+
export interface SubscriptionPartitionObjectRpc extends Rpc.DurableObjectBranded {
|
|
343
|
+
subscription(encoded: unknown): Promise<unknown>;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export class SubscriptionPartitionNamespace extends Context.Service<
|
|
347
|
+
SubscriptionPartitionNamespace,
|
|
348
|
+
{ readonly namespace: DurableObjectNamespace<SubscriptionPartitionObjectRpc> }
|
|
349
|
+
>()("@effect-agent/platform-cloudflare/SubscriptionPartitionNamespace") {}
|
|
350
|
+
|
|
351
|
+
const storageFailure = (code: string): SubscriptionError =>
|
|
352
|
+
SubscriptionError.make({ reason: "storage", code });
|
|
353
|
+
|
|
354
|
+
const corruptFailure = (code: string): SubscriptionError =>
|
|
355
|
+
SubscriptionError.make({ reason: "corrupt", code });
|
|
356
|
+
|
|
357
|
+
/** Worker-side management and trusted intake, routed to one fresh partition stub per call. */
|
|
358
|
+
export class CloudflareSubscriptionsClient {
|
|
359
|
+
static layer(
|
|
360
|
+
partition: SourcePartition,
|
|
361
|
+
): Layer.Layer<Subscriptions | SubscriptionIntake, never, SubscriptionPartitionNamespace> {
|
|
362
|
+
return Layer.effectContext(
|
|
363
|
+
Effect.gen(function* () {
|
|
364
|
+
const { namespace } = yield* SubscriptionPartitionNamespace;
|
|
365
|
+
|
|
366
|
+
const call = Effect.fn("CloudflareSubscriptionsClient.call")(function* (
|
|
367
|
+
addressedPartition: SourcePartition,
|
|
368
|
+
request: SubscriptionPartitionRequest,
|
|
369
|
+
) {
|
|
370
|
+
if (!samePartition(partition, addressedPartition)) {
|
|
371
|
+
return yield* SubscriptionError.make({ reason: "unauthorized", code: "partition" });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const encoded = yield* encodeRequest(request).pipe(
|
|
375
|
+
Effect.mapError(() => corruptFailure("subscription-partition-protocol")),
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
const raw = yield* Effect.tryPromise({
|
|
379
|
+
try: () =>
|
|
380
|
+
namespace
|
|
381
|
+
.get(namespace.idFromName(sourcePartitionName(partition)))
|
|
382
|
+
.subscription(encoded),
|
|
383
|
+
catch: () => storageFailure("call-subscription-partition"),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
return yield* decodeResponse(raw).pipe(
|
|
387
|
+
Effect.mapError(() => corruptFailure("subscription-partition-protocol")),
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const failed = (response: SubscriptionPartitionResponse): SubscriptionPartitionFailure =>
|
|
392
|
+
response._tag === "Failed"
|
|
393
|
+
? response.failure
|
|
394
|
+
: SubscriptionPartitionProtocolError.make({
|
|
395
|
+
message: "Unexpected subscription response",
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const asProtocolError = (): SubscriptionError =>
|
|
399
|
+
corruptFailure("subscription-partition-protocol");
|
|
400
|
+
|
|
401
|
+
const changeState = Effect.fn("CloudflareSubscriptions.changeState")(function* (
|
|
402
|
+
scope: typeof SubscriptionScope.Type,
|
|
403
|
+
key: typeof SubscriptionKey.Type,
|
|
404
|
+
expectedRevision: number,
|
|
405
|
+
_tag: "PauseSubscription" | "ResumeSubscription" | "RecoverSubscription",
|
|
406
|
+
) {
|
|
407
|
+
const response = yield* call(scope.partition, {
|
|
408
|
+
_tag,
|
|
409
|
+
schemaVersion: 1,
|
|
410
|
+
scope,
|
|
411
|
+
key,
|
|
412
|
+
expectedRevision,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
if (response._tag === "Snapshot") return response.value;
|
|
416
|
+
const failure = failed(response);
|
|
417
|
+
|
|
418
|
+
return yield* failure._tag === "SubscriptionError" ||
|
|
419
|
+
failure._tag === "SubscriptionFailpointError"
|
|
420
|
+
? failure
|
|
421
|
+
: asProtocolError();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const subscriptions = Subscriptions.of({
|
|
425
|
+
recoverSubscription: (scope, key, revision) =>
|
|
426
|
+
changeState(scope, key, revision, "RecoverSubscription"),
|
|
427
|
+
getSubscription: (scope, key) =>
|
|
428
|
+
Effect.gen(function* () {
|
|
429
|
+
const response = yield* call(scope.partition, {
|
|
430
|
+
_tag: "GetSubscription",
|
|
431
|
+
schemaVersion: 1,
|
|
432
|
+
scope,
|
|
433
|
+
key,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
if (response._tag === "Snapshot") return response.value;
|
|
437
|
+
const failure = failed(response);
|
|
438
|
+
|
|
439
|
+
return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
|
|
440
|
+
}),
|
|
441
|
+
recoverDelivery: (scope, key, expectedGeneration) =>
|
|
442
|
+
Effect.gen(function* () {
|
|
443
|
+
const response = yield* call(scope.partition, {
|
|
444
|
+
_tag: "RecoverDelivery",
|
|
445
|
+
expectedGeneration,
|
|
446
|
+
schemaVersion: 1,
|
|
447
|
+
scope,
|
|
448
|
+
key,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
if (response._tag === "DeliverySnapshot") return response.value;
|
|
452
|
+
const failure = failed(response);
|
|
453
|
+
|
|
454
|
+
return yield* failure._tag === "SubscriptionError" ||
|
|
455
|
+
failure._tag === "SubscriptionFailpointError"
|
|
456
|
+
? failure
|
|
457
|
+
: asProtocolError();
|
|
458
|
+
}),
|
|
459
|
+
updateSubscription: (scope, key, expectedRevision, options) =>
|
|
460
|
+
Effect.gen(function* () {
|
|
461
|
+
const response = yield* call(scope.partition, {
|
|
462
|
+
_tag: "UpdateSubscription",
|
|
463
|
+
schemaVersion: 1,
|
|
464
|
+
scope,
|
|
465
|
+
key,
|
|
466
|
+
expectedRevision,
|
|
467
|
+
options,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
if (response._tag === "Snapshot") return response.value;
|
|
471
|
+
const failure = failed(response);
|
|
472
|
+
|
|
473
|
+
return yield* failure._tag === "SubscriptionError" ||
|
|
474
|
+
failure._tag === "SubscriptionFailpointError" ||
|
|
475
|
+
failure._tag === "SubscriptionSourceError"
|
|
476
|
+
? failure
|
|
477
|
+
: asProtocolError();
|
|
478
|
+
}),
|
|
479
|
+
pauseSubscription: (scope, key, revision) =>
|
|
480
|
+
changeState(scope, key, revision, "PauseSubscription"),
|
|
481
|
+
resumeSubscription: (scope, key, revision) =>
|
|
482
|
+
changeState(scope, key, revision, "ResumeSubscription"),
|
|
483
|
+
subscribe: (scope, options) =>
|
|
484
|
+
Effect.gen(function* () {
|
|
485
|
+
const response = yield* call(scope.partition, {
|
|
486
|
+
_tag: "Subscribe",
|
|
487
|
+
schemaVersion: 1,
|
|
488
|
+
scope,
|
|
489
|
+
options,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
if (response._tag === "Snapshot") return response.value;
|
|
493
|
+
const failure = failed(response);
|
|
494
|
+
|
|
495
|
+
if (
|
|
496
|
+
failure._tag === "SubscriptionError" ||
|
|
497
|
+
failure._tag === "SubscriptionSourceError" ||
|
|
498
|
+
failure._tag === "SubscriptionFailpointError"
|
|
499
|
+
)
|
|
500
|
+
return yield* failure;
|
|
501
|
+
|
|
502
|
+
return yield* asProtocolError();
|
|
503
|
+
}),
|
|
504
|
+
listSubscriptions: (scope, after, limit) =>
|
|
505
|
+
Effect.gen(function* () {
|
|
506
|
+
const response = yield* call(scope.partition, {
|
|
507
|
+
_tag: "ListSubscriptions",
|
|
508
|
+
schemaVersion: 1,
|
|
509
|
+
scope,
|
|
510
|
+
...(after === undefined ? {} : { after }),
|
|
511
|
+
...(limit === undefined ? {} : { limit }),
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
if (response._tag === "SubscriptionPage") return response.value;
|
|
515
|
+
const failure = failed(response);
|
|
516
|
+
|
|
517
|
+
return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
|
|
518
|
+
}),
|
|
519
|
+
cancelSubscription: (scope, key, expectedRevision) =>
|
|
520
|
+
Effect.gen(function* () {
|
|
521
|
+
const response = yield* call(scope.partition, {
|
|
522
|
+
_tag: "CancelSubscription",
|
|
523
|
+
...(expectedRevision === undefined ? {} : { expectedRevision }),
|
|
524
|
+
schemaVersion: 1,
|
|
525
|
+
scope,
|
|
526
|
+
key,
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
if (response._tag === "Snapshot") return response.value;
|
|
530
|
+
const failure = failed(response);
|
|
531
|
+
|
|
532
|
+
if (
|
|
533
|
+
failure._tag === "SubscriptionError" ||
|
|
534
|
+
failure._tag === "SubscriptionFailpointError"
|
|
535
|
+
)
|
|
536
|
+
return yield* failure;
|
|
537
|
+
|
|
538
|
+
return yield* asProtocolError();
|
|
539
|
+
}),
|
|
540
|
+
listDeliveries: (scope, key, after, limit) =>
|
|
541
|
+
Effect.gen(function* () {
|
|
542
|
+
const response = yield* call(scope.partition, {
|
|
543
|
+
_tag: "ListDeliveries",
|
|
544
|
+
schemaVersion: 1,
|
|
545
|
+
scope,
|
|
546
|
+
key,
|
|
547
|
+
...(after === undefined ? {} : { after }),
|
|
548
|
+
...(limit === undefined ? {} : { limit }),
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
if (response._tag === "DeliveryPage") return response.value;
|
|
552
|
+
const failure = failed(response);
|
|
553
|
+
|
|
554
|
+
return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
|
|
555
|
+
}),
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const intake = SubscriptionIntake.of({
|
|
559
|
+
accept: (principal, source, payload) =>
|
|
560
|
+
Effect.gen(function* () {
|
|
561
|
+
const request = yield* Schema.decodeUnknownEffect(AcceptRequest)({
|
|
562
|
+
_tag: "Accept",
|
|
563
|
+
schemaVersion: 1,
|
|
564
|
+
partition,
|
|
565
|
+
principal,
|
|
566
|
+
source,
|
|
567
|
+
payload,
|
|
568
|
+
}).pipe(
|
|
569
|
+
Effect.mapError(() =>
|
|
570
|
+
SubscriptionError.make({ reason: "validation", code: "event-payload" }),
|
|
571
|
+
),
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
const response = yield* call(request.partition, request);
|
|
575
|
+
|
|
576
|
+
if (response._tag === "Acknowledgement") return response.value;
|
|
577
|
+
const failure = failed(response);
|
|
578
|
+
|
|
579
|
+
if (
|
|
580
|
+
failure._tag === "SubscriptionError" ||
|
|
581
|
+
failure._tag === "SubscriptionSourceError" ||
|
|
582
|
+
failure._tag === "SubscriptionFailpointError"
|
|
583
|
+
)
|
|
584
|
+
return yield* failure;
|
|
585
|
+
|
|
586
|
+
return yield* asProtocolError();
|
|
587
|
+
}),
|
|
588
|
+
status: (principal, source, eventId) =>
|
|
589
|
+
Effect.gen(function* () {
|
|
590
|
+
const response = yield* call(partition, {
|
|
591
|
+
_tag: "Status",
|
|
592
|
+
schemaVersion: 1,
|
|
593
|
+
partition,
|
|
594
|
+
principal,
|
|
595
|
+
source,
|
|
596
|
+
eventId,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
if (response._tag === "Status") return response.value;
|
|
600
|
+
const failure = failed(response);
|
|
601
|
+
|
|
602
|
+
return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
|
|
603
|
+
}),
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
return Context.make(Subscriptions, subscriptions).pipe(
|
|
607
|
+
Context.add(SubscriptionIntake, intake),
|
|
608
|
+
);
|
|
609
|
+
}),
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export class SubscriptionPartitionIdentity extends Context.Service<
|
|
615
|
+
SubscriptionPartitionIdentity,
|
|
616
|
+
{ readonly partition: SourcePartition }
|
|
617
|
+
>()("@effect-agent/platform-cloudflare/SubscriptionPartitionIdentity") {}
|
|
618
|
+
|
|
619
|
+
const decodePartitionName = Effect.fn("decodeSubscriptionPartitionName")(function* (
|
|
620
|
+
name: string | null | undefined,
|
|
621
|
+
) {
|
|
622
|
+
if (name === null || name === undefined) {
|
|
623
|
+
return yield* SubscriptionPartitionProtocolError.make({
|
|
624
|
+
message: "Subscription Partition objects require an idFromName identity",
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const tuple = yield* Schema.decodeEffect(
|
|
629
|
+
Schema.fromJsonString(Schema.Tuple([Schema.String, Schema.String])),
|
|
630
|
+
)(name).pipe(
|
|
631
|
+
Effect.mapError(() =>
|
|
632
|
+
SubscriptionPartitionProtocolError.make({
|
|
633
|
+
message: "Subscription Partition name is malformed",
|
|
634
|
+
}),
|
|
635
|
+
),
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
return yield* Schema.decodeEffect(SourcePartition)({
|
|
639
|
+
tenantId: tuple[0],
|
|
640
|
+
address: tuple[1],
|
|
641
|
+
}).pipe(
|
|
642
|
+
Effect.mapError(() =>
|
|
643
|
+
SubscriptionPartitionProtocolError.make({
|
|
644
|
+
message: "Subscription Partition identity is invalid",
|
|
645
|
+
}),
|
|
646
|
+
),
|
|
647
|
+
);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
const samePartition = Schema.toEquivalence(SourcePartition);
|
|
651
|
+
|
|
652
|
+
const requestPartition = (request: SubscriptionPartitionRequest): SourcePartition =>
|
|
653
|
+
request._tag === "Accept" || request._tag === "Status"
|
|
654
|
+
? request.partition
|
|
655
|
+
: request.scope.partition;
|
|
656
|
+
|
|
657
|
+
const handleRequest = Effect.fn("SubscriptionPartition.handleRequest")(function* (
|
|
658
|
+
encoded: unknown,
|
|
659
|
+
) {
|
|
660
|
+
const decoded = yield* decodeRequest(encoded).pipe(Effect.result);
|
|
661
|
+
|
|
662
|
+
if (decoded._tag === "Failure") {
|
|
663
|
+
return yield* encodeResponse(
|
|
664
|
+
protocolFailure("The subscription request could not be decoded"),
|
|
665
|
+
).pipe(Effect.orDie);
|
|
666
|
+
}
|
|
667
|
+
const request = decoded.success;
|
|
668
|
+
const { partition } = yield* SubscriptionPartitionIdentity;
|
|
669
|
+
|
|
670
|
+
if (!samePartition(partition, requestPartition(request))) {
|
|
671
|
+
return yield* encodeResponse(
|
|
672
|
+
protocolFailure("The request partition does not match the addressed object"),
|
|
673
|
+
).pipe(Effect.orDie);
|
|
674
|
+
}
|
|
675
|
+
const subscriptions = yield* Subscriptions;
|
|
676
|
+
const intake = yield* SubscriptionIntake;
|
|
677
|
+
|
|
678
|
+
const response = yield* Effect.gen(function* (): Effect.fn.Return<
|
|
679
|
+
SubscriptionPartitionResponse,
|
|
680
|
+
SubscriptionPartitionFailure
|
|
681
|
+
> {
|
|
682
|
+
switch (request._tag) {
|
|
683
|
+
case "GetSubscription":
|
|
684
|
+
return {
|
|
685
|
+
_tag: "Snapshot",
|
|
686
|
+
value: yield* subscriptions.getSubscription(request.scope, request.key),
|
|
687
|
+
};
|
|
688
|
+
case "Subscribe":
|
|
689
|
+
return {
|
|
690
|
+
_tag: "Snapshot",
|
|
691
|
+
value: yield* subscriptions.subscribe(request.scope, request.options),
|
|
692
|
+
};
|
|
693
|
+
case "UpdateSubscription":
|
|
694
|
+
return {
|
|
695
|
+
_tag: "Snapshot",
|
|
696
|
+
value: yield* subscriptions.updateSubscription(
|
|
697
|
+
request.scope,
|
|
698
|
+
request.key,
|
|
699
|
+
request.expectedRevision,
|
|
700
|
+
request.options,
|
|
701
|
+
),
|
|
702
|
+
};
|
|
703
|
+
case "RecoverSubscription":
|
|
704
|
+
return {
|
|
705
|
+
_tag: "Snapshot",
|
|
706
|
+
value: yield* subscriptions.recoverSubscription(
|
|
707
|
+
request.scope,
|
|
708
|
+
request.key,
|
|
709
|
+
request.expectedRevision,
|
|
710
|
+
),
|
|
711
|
+
};
|
|
712
|
+
case "PauseSubscription":
|
|
713
|
+
return {
|
|
714
|
+
_tag: "Snapshot",
|
|
715
|
+
value: yield* subscriptions.pauseSubscription(
|
|
716
|
+
request.scope,
|
|
717
|
+
request.key,
|
|
718
|
+
request.expectedRevision,
|
|
719
|
+
),
|
|
720
|
+
};
|
|
721
|
+
case "ResumeSubscription":
|
|
722
|
+
return {
|
|
723
|
+
_tag: "Snapshot",
|
|
724
|
+
value: yield* subscriptions.resumeSubscription(
|
|
725
|
+
request.scope,
|
|
726
|
+
request.key,
|
|
727
|
+
request.expectedRevision,
|
|
728
|
+
),
|
|
729
|
+
};
|
|
730
|
+
case "ListSubscriptions":
|
|
731
|
+
return {
|
|
732
|
+
_tag: "SubscriptionPage",
|
|
733
|
+
value: yield* subscriptions.listSubscriptions(
|
|
734
|
+
request.scope,
|
|
735
|
+
request.after,
|
|
736
|
+
request.limit,
|
|
737
|
+
),
|
|
738
|
+
};
|
|
739
|
+
case "CancelSubscription":
|
|
740
|
+
return {
|
|
741
|
+
_tag: "Snapshot",
|
|
742
|
+
value: yield* subscriptions.cancelSubscription(
|
|
743
|
+
request.scope,
|
|
744
|
+
request.key,
|
|
745
|
+
request.expectedRevision,
|
|
746
|
+
),
|
|
747
|
+
};
|
|
748
|
+
case "RecoverDelivery":
|
|
749
|
+
return {
|
|
750
|
+
_tag: "DeliverySnapshot",
|
|
751
|
+
value: yield* subscriptions.recoverDelivery(
|
|
752
|
+
request.scope,
|
|
753
|
+
request.key,
|
|
754
|
+
request.expectedGeneration,
|
|
755
|
+
),
|
|
756
|
+
};
|
|
757
|
+
case "ListDeliveries":
|
|
758
|
+
return {
|
|
759
|
+
_tag: "DeliveryPage",
|
|
760
|
+
value: yield* subscriptions.listDeliveries(
|
|
761
|
+
request.scope,
|
|
762
|
+
request.key,
|
|
763
|
+
request.after,
|
|
764
|
+
request.limit,
|
|
765
|
+
),
|
|
766
|
+
};
|
|
767
|
+
case "Accept":
|
|
768
|
+
return {
|
|
769
|
+
_tag: "Acknowledgement",
|
|
770
|
+
value: yield* intake.accept(request.principal, request.source, request.payload),
|
|
771
|
+
};
|
|
772
|
+
case "Status":
|
|
773
|
+
return {
|
|
774
|
+
_tag: "Status",
|
|
775
|
+
value: yield* intake.status(request.principal, request.source, request.eventId),
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
}).pipe(
|
|
779
|
+
Effect.catch((failure) =>
|
|
780
|
+
Schema.is(SubscriptionPartitionFailure)(failure)
|
|
781
|
+
? Effect.succeed({ _tag: "Failed" as const, failure })
|
|
782
|
+
: Effect.succeed(protocolFailure("The subscription operation failed outside its contract")),
|
|
783
|
+
),
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
return yield* encodeResponse(response).pipe(Effect.orDie);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
const alarmStorageError = (code: string) => () => storageFailure(code);
|
|
790
|
+
|
|
791
|
+
const transactionLayer: Layer.Layer<
|
|
792
|
+
DoSubscriptionTransaction,
|
|
793
|
+
never,
|
|
794
|
+
DurableObjectAlarm.DurableObjectAlarm
|
|
795
|
+
> = Layer.effect(
|
|
796
|
+
DoSubscriptionTransaction,
|
|
797
|
+
Effect.gen(function* () {
|
|
798
|
+
const alarms = yield* DurableObjectAlarm.DurableObjectAlarm;
|
|
799
|
+
|
|
800
|
+
return DoSubscriptionTransaction.of({
|
|
801
|
+
run: (body) =>
|
|
802
|
+
Effect.gen(function* () {
|
|
803
|
+
const nowMillis = yield* Clock.currentTimeMillis;
|
|
804
|
+
|
|
805
|
+
return yield* alarms
|
|
806
|
+
.transaction((transaction) =>
|
|
807
|
+
body((replacement) =>
|
|
808
|
+
replacement.deadlineAtMillis === null
|
|
809
|
+
? transaction
|
|
810
|
+
.cancelAlarm({ id: SUBSCRIPTION_ALARM_ID, tag: SUBSCRIPTION_ALARM_TAG })
|
|
811
|
+
.pipe(Effect.mapError(alarmStorageError("cancel-subscription-alarm")))
|
|
812
|
+
: Effect.fromOption(
|
|
813
|
+
DateTime.make(Math.max(replacement.deadlineAtMillis, nowMillis + 1)),
|
|
814
|
+
).pipe(
|
|
815
|
+
Effect.mapError(() => corruptFailure("subscription-alarm-deadline")),
|
|
816
|
+
Effect.flatMap((runAt) =>
|
|
817
|
+
transaction
|
|
818
|
+
.scheduleAlarm({
|
|
819
|
+
id: SUBSCRIPTION_ALARM_ID,
|
|
820
|
+
tag: SUBSCRIPTION_ALARM_TAG,
|
|
821
|
+
runAt,
|
|
822
|
+
payload: { schemaVersion: 1, generation: replacement.generation },
|
|
823
|
+
})
|
|
824
|
+
.pipe(Effect.mapError(alarmStorageError("schedule-subscription-alarm"))),
|
|
825
|
+
),
|
|
826
|
+
),
|
|
827
|
+
),
|
|
828
|
+
)
|
|
829
|
+
.pipe(
|
|
830
|
+
Effect.catchTag("StorageOperationError", () =>
|
|
831
|
+
storageFailure("commit-subscription-transaction"),
|
|
832
|
+
),
|
|
833
|
+
);
|
|
834
|
+
}),
|
|
835
|
+
});
|
|
836
|
+
}),
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
const alarmHandler = (limits: SubscriptionLimits) =>
|
|
840
|
+
DurableObjectAlarm.processDue(
|
|
841
|
+
(event) =>
|
|
842
|
+
Effect.gen(function* () {
|
|
843
|
+
if (event.tag !== SUBSCRIPTION_ALARM_TAG) {
|
|
844
|
+
const { handlers } = yield* SubscriptionPartitionAlarmExtension;
|
|
845
|
+
const matches = handlers.filter((handler) => handler.tag === event.tag);
|
|
846
|
+
const handler = matches[0];
|
|
847
|
+
|
|
848
|
+
if (
|
|
849
|
+
event.tag.startsWith("effect-agent/") ||
|
|
850
|
+
matches.length !== 1 ||
|
|
851
|
+
handler === undefined
|
|
852
|
+
)
|
|
853
|
+
return yield* SubscriptionAlarmProtocolError.make({
|
|
854
|
+
message: "Unknown or ambiguous ancillary alarm tag",
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
return yield* handler.handle(event).pipe(
|
|
858
|
+
Effect.scoped,
|
|
859
|
+
Effect.timeoutOrElse({
|
|
860
|
+
duration: MAX_ANCILLARY_ALARM_MILLIS,
|
|
861
|
+
orElse: () => SubscriptionAlarmExtensionError.make({ code: "timeout" }),
|
|
862
|
+
}),
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
if (event.id !== SUBSCRIPTION_ALARM_ID) {
|
|
866
|
+
return yield* SubscriptionAlarmProtocolError.make({
|
|
867
|
+
message: `Unsupported Subscription Partition alarm ${event.tag}/${event.id}`,
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
yield* Schema.decodeUnknownEffect(SubscriptionAlarmPayload)(event.payload).pipe(
|
|
871
|
+
Effect.mapError(() =>
|
|
872
|
+
SubscriptionAlarmProtocolError.make({
|
|
873
|
+
message: "Unsupported subscription alarm payload",
|
|
874
|
+
}),
|
|
875
|
+
),
|
|
876
|
+
);
|
|
877
|
+
const driver = yield* SubscriptionDriver;
|
|
878
|
+
const alarmControl = yield* DoSubscriptionAlarmControl;
|
|
879
|
+
|
|
880
|
+
yield* alarmControl.prearm((yield* Clock.currentTimeMillis) + limits.retryMillis);
|
|
881
|
+
|
|
882
|
+
const pass = yield* driver.runDue.pipe(
|
|
883
|
+
Effect.catchCauseIf(
|
|
884
|
+
(cause) => !Cause.hasInterrupts(cause),
|
|
885
|
+
(cause) =>
|
|
886
|
+
Clock.currentTimeMillis.pipe(
|
|
887
|
+
Effect.flatMap((time) => alarmControl.prearm(time + limits.retryMillis)),
|
|
888
|
+
Effect.andThen(Effect.failCause(cause)),
|
|
889
|
+
),
|
|
890
|
+
),
|
|
891
|
+
);
|
|
892
|
+
|
|
893
|
+
if (pass.failed > 0) {
|
|
894
|
+
yield* alarmControl.prearm((yield* Clock.currentTimeMillis) + limits.retryMillis);
|
|
895
|
+
} else {
|
|
896
|
+
yield* alarmControl.reconcile;
|
|
897
|
+
}
|
|
898
|
+
}),
|
|
899
|
+
{
|
|
900
|
+
mode: "isolated",
|
|
901
|
+
limit: MAX_ALARMS_PER_INVOCATION,
|
|
902
|
+
retryFailedAfter: limits.retryMillis,
|
|
903
|
+
onFailure: () => Effect.logWarning("Subscription partition alarm retained for retry"),
|
|
904
|
+
},
|
|
905
|
+
).pipe(Effect.asVoid);
|
|
906
|
+
|
|
907
|
+
type SubscriptionRuntimeServices =
|
|
908
|
+
| Subscriptions
|
|
909
|
+
| SubscriptionIntake
|
|
910
|
+
| SubscriptionDriver
|
|
911
|
+
| DoSubscriptionAlarmControl
|
|
912
|
+
| SubscriptionPartitionIdentity
|
|
913
|
+
| DurableObjectAlarm.DurableObjectAlarm;
|
|
914
|
+
|
|
915
|
+
export interface SubscriptionPartitionObjectInstance extends InstanceType<
|
|
916
|
+
EffectCfDurableObject.DurableObjectClass<Record<never, never>, SubscriptionRuntimeServices>
|
|
917
|
+
> {
|
|
918
|
+
subscription(encoded: unknown): Promise<unknown>;
|
|
919
|
+
alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
export interface SubscriptionPartitionObjectClass {
|
|
923
|
+
new (ctx: DurableObjectState, env: Cloudflare.Env): SubscriptionPartitionObjectInstance;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Build one source-addressed SQLite Durable Object. The host Layer binds the permitted source
|
|
928
|
+
* catalog and authority. It must not retain cleanup-scoped resources across RPC events.
|
|
929
|
+
*/
|
|
930
|
+
export const makeSubscriptionPartitionObjectClass = <E>(
|
|
931
|
+
host: Layer.Layer<
|
|
932
|
+
SubscriptionAuthorizer | EventSources | SubscriptionInputBindings | ThreadObjectNamespace,
|
|
933
|
+
E,
|
|
934
|
+
| EffectCfDurableObjectState.DurableObjectState
|
|
935
|
+
| WorkerEnvironment
|
|
936
|
+
| SubscriptionPartitionIdentity
|
|
937
|
+
>,
|
|
938
|
+
limits: SubscriptionLimits = defaultSubscriptionLimits,
|
|
939
|
+
): SubscriptionPartitionObjectClass => {
|
|
940
|
+
const cloudflareLimitsLayer = Layer.effectDiscard(validateCloudflareSubscriptionLimits(limits));
|
|
941
|
+
|
|
942
|
+
const identityLayer = Layer.effect(
|
|
943
|
+
SubscriptionPartitionIdentity,
|
|
944
|
+
Effect.gen(function* () {
|
|
945
|
+
const state = yield* EffectCfDurableObjectState.DurableObjectState;
|
|
946
|
+
|
|
947
|
+
return SubscriptionPartitionIdentity.of({
|
|
948
|
+
partition: yield* decodePartitionName(state.raw.id.name),
|
|
949
|
+
});
|
|
950
|
+
}),
|
|
951
|
+
);
|
|
952
|
+
|
|
953
|
+
const sqlLayer = Layer.unwrap(
|
|
954
|
+
Effect.map(EffectCfDurableObjectState.DurableObjectState, (state) =>
|
|
955
|
+
SqliteClient.layer({ storage: state.raw.storage }),
|
|
956
|
+
),
|
|
957
|
+
);
|
|
958
|
+
|
|
959
|
+
const partitionStore = Layer.unwrap(
|
|
960
|
+
Effect.map(SubscriptionPartitionIdentity, ({ partition }) =>
|
|
961
|
+
doSubscriptionStoreLayer(partition).pipe(
|
|
962
|
+
Layer.provide(transactionLayer),
|
|
963
|
+
Layer.provide(sqlLayer),
|
|
964
|
+
),
|
|
965
|
+
),
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
const application = Layer.mergeAll(
|
|
969
|
+
Subscriptions.layer(limits),
|
|
970
|
+
SubscriptionIntake.layer(limits),
|
|
971
|
+
SubscriptionDriver.layer(limits),
|
|
972
|
+
cloudflareLimitsLayer,
|
|
973
|
+
// Capture the host reference before its scoped input Layer is hidden from the runtime.
|
|
974
|
+
Layer.effect(
|
|
975
|
+
SubscriptionPartitionAlarmExtension,
|
|
976
|
+
Effect.gen(function* () {
|
|
977
|
+
const extension = yield* SubscriptionPartitionAlarmExtension;
|
|
978
|
+
const tags = extension.handlers.map((handler) => handler.tag);
|
|
979
|
+
|
|
980
|
+
if (
|
|
981
|
+
tags.length > MAX_ALARMS_PER_INVOCATION ||
|
|
982
|
+
new Set(tags).size !== tags.length ||
|
|
983
|
+
tags.some(
|
|
984
|
+
(tag) => tag.length === 0 || tag.length > 128 || tag.startsWith("effect-agent/"),
|
|
985
|
+
)
|
|
986
|
+
)
|
|
987
|
+
return yield* SubscriptionAlarmProtocolError.make({
|
|
988
|
+
message: "Invalid ancillary alarm handler registry",
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
yield* validateCloudflareSubscriptionLimits(limits, { ancillaryAlarms: tags.length > 0 });
|
|
992
|
+
|
|
993
|
+
return extension;
|
|
994
|
+
}),
|
|
995
|
+
),
|
|
996
|
+
).pipe(
|
|
997
|
+
Layer.provideMerge(partitionStore),
|
|
998
|
+
Layer.provide(
|
|
999
|
+
cloudflarePreparedInputAdmissionLayer.pipe(Layer.provide(CloudflareThreadClient.layer)),
|
|
1000
|
+
),
|
|
1001
|
+
Layer.provide(BrowserCrypto.layer),
|
|
1002
|
+
Layer.provideMerge(DurableObjectAlarm.DurableObjectAlarm.layer),
|
|
1003
|
+
Layer.provide(host),
|
|
1004
|
+
Layer.provideMerge(identityLayer),
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
const runtime: Layer.Layer<
|
|
1008
|
+
SubscriptionRuntimeServices,
|
|
1009
|
+
| E
|
|
1010
|
+
| SubscriptionError
|
|
1011
|
+
| SubscriptionPartitionProtocolError
|
|
1012
|
+
| CloudflareSubscriptionConfigError
|
|
1013
|
+
| SubscriptionAlarmProtocolError,
|
|
1014
|
+
EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
|
|
1015
|
+
> = Layer.effectContext(
|
|
1016
|
+
Effect.gen(function* () {
|
|
1017
|
+
const state = yield* EffectCfDurableObjectState.DurableObjectState;
|
|
1018
|
+
const scope = yield* Effect.scope;
|
|
1019
|
+
|
|
1020
|
+
return yield* state.blockConcurrencyWhile(Layer.buildWithScope(application, scope));
|
|
1021
|
+
}),
|
|
1022
|
+
);
|
|
1023
|
+
|
|
1024
|
+
const rpc = {
|
|
1025
|
+
subscription: (encoded: unknown) => handleRequest(encoded),
|
|
1026
|
+
} satisfies EffectCfDurableObject.DurableObjectRpc<SubscriptionRuntimeServices>;
|
|
1027
|
+
|
|
1028
|
+
const Base = EffectCfDurableObject.make(runtime, {
|
|
1029
|
+
initialize: Effect.void,
|
|
1030
|
+
rpc,
|
|
1031
|
+
alarms: alarmHandler(limits),
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
class SubscriptionPartitionObject extends Base {
|
|
1035
|
+
override alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void {
|
|
1036
|
+
return super.alarm?.(alarmInfo);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
return SubscriptionPartitionObject;
|
|
1041
|
+
};
|