@effect-agent/platform-cloudflare 0.1.0-beta.36 → 0.1.0-beta.37
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/index.d.mts +9 -103
- package/dist/index.mjs +8 -824
- package/dist/index.mjs.map +1 -1
- package/dist/interactive-browser.d.mts +27 -5
- package/dist/interactive-browser.mjs +530 -69
- package/dist/interactive-browser.mjs.map +1 -1
- package/dist/scheduling-B-OFqoS9.mjs +1189 -0
- package/dist/scheduling-B-OFqoS9.mjs.map +1 -0
- package/dist/scheduling-BJs_kHTx.d.mts +142 -0
- package/dist/scheduling.d.mts +2 -0
- package/dist/scheduling.mjs +2 -0
- package/package.json +11 -10
- package/src/index.ts +1 -0
- package/src/interactive-browser.ts +843 -75
- package/src/layers.ts +1 -1
- package/src/scheduling.ts +676 -0
package/src/layers.ts
CHANGED
|
@@ -127,7 +127,7 @@ export interface CloudflareDurableRuntimeOptions {
|
|
|
127
127
|
*/
|
|
128
128
|
readonly toolReconciler?: Layer.Layer<ToolReconciler> | undefined;
|
|
129
129
|
/**
|
|
130
|
-
* Registered worker Bindings resolved at durable claim time
|
|
130
|
+
* Registered worker Bindings resolved at durable claim time:
|
|
131
131
|
* build each with `DurableWorkerBinding.make(binding, digests)`. The callback receives the live
|
|
132
132
|
* Object context and derived identities and is evaluated once per incarnation during Layer
|
|
133
133
|
* construction. Defaults to the empty registration (every resolved claim fails closed).
|
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import { AgentId } from "@effect-agent/core";
|
|
2
|
+
import {
|
|
3
|
+
DefinitionDigests,
|
|
4
|
+
PersistedJson,
|
|
5
|
+
type DurableSubmitAgent,
|
|
6
|
+
ScheduleAuthorizationError,
|
|
7
|
+
type ScheduleAuthorizer,
|
|
8
|
+
ScheduleCapacityError,
|
|
9
|
+
ScheduleConflict,
|
|
10
|
+
ScheduleDestination,
|
|
11
|
+
ScheduleFailpointError,
|
|
12
|
+
ScheduleId,
|
|
13
|
+
type SchedulingLimits,
|
|
14
|
+
ScheduleNotFound,
|
|
15
|
+
ScheduleOwner,
|
|
16
|
+
type ScheduleScope,
|
|
17
|
+
ScheduleScope as ScheduleScopeSchema,
|
|
18
|
+
ScheduleSnapshot,
|
|
19
|
+
ScheduleSnapshotPage as ScheduleSnapshotPageSchema,
|
|
20
|
+
ScheduleStorageError,
|
|
21
|
+
ScheduleTimingRequest,
|
|
22
|
+
ScheduleValidationError,
|
|
23
|
+
ScheduledInputAdmission,
|
|
24
|
+
ScheduledInputRetryable,
|
|
25
|
+
type ScheduledEnvelope,
|
|
26
|
+
Scheduling,
|
|
27
|
+
ScheduleDriver,
|
|
28
|
+
type ScheduleManagementFailure,
|
|
29
|
+
defaultSchedulingLimits,
|
|
30
|
+
scheduleOwnerKey,
|
|
31
|
+
ScheduleWakeNoop,
|
|
32
|
+
} from "@effect-agent/session";
|
|
33
|
+
import {
|
|
34
|
+
DoScheduleAlarmControl,
|
|
35
|
+
DoScheduleTransaction,
|
|
36
|
+
scheduleStoreLayer,
|
|
37
|
+
} from "@effect-agent/storage-cloudflare";
|
|
38
|
+
import { BrowserCrypto } from "@effect/platform-browser";
|
|
39
|
+
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
40
|
+
import { Clock, Context, DateTime, Effect, Layer, Schema } from "effect";
|
|
41
|
+
import {
|
|
42
|
+
DurableObject as EffectCfDurableObject,
|
|
43
|
+
DurableObjectAlarm,
|
|
44
|
+
DurableObjectState as EffectCfDurableObjectState,
|
|
45
|
+
type WorkerEnvironment,
|
|
46
|
+
} from "effect-cf";
|
|
47
|
+
|
|
48
|
+
import type { ConversationObjectNamespace } from "./bindings.ts";
|
|
49
|
+
import { CloudflareConversationClient, type ConversationClientError } from "./client.ts";
|
|
50
|
+
|
|
51
|
+
const SCHEDULE_ALARM_TAG = "effect-agent/ScheduleOwnerWake";
|
|
52
|
+
const SCHEDULE_ALARM_ID = "driver";
|
|
53
|
+
|
|
54
|
+
const ScheduleAlarmPayload = Schema.Struct({
|
|
55
|
+
schemaVersion: Schema.Literal(1),
|
|
56
|
+
generation: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export class ScheduleAlarmProtocolError extends Schema.TaggedError<ScheduleAlarmProtocolError>()(
|
|
60
|
+
"ScheduleAlarmProtocolError",
|
|
61
|
+
{ message: Schema.String },
|
|
62
|
+
) {}
|
|
63
|
+
|
|
64
|
+
const boundedProtocolMessage = (message: string): string =>
|
|
65
|
+
message.length <= 4_096 ? message : `${message.slice(0, 4_093)}...`;
|
|
66
|
+
|
|
67
|
+
export class ScheduleOwnerProtocolError extends Schema.TaggedError<ScheduleOwnerProtocolError>()(
|
|
68
|
+
"ScheduleOwnerProtocolError",
|
|
69
|
+
{ message: Schema.String.check(Schema.isMaxLength(4_096)) },
|
|
70
|
+
) {}
|
|
71
|
+
|
|
72
|
+
const ScheduleMutationRequestFields = {
|
|
73
|
+
schemaVersion: Schema.Literal(1),
|
|
74
|
+
agentId: AgentId,
|
|
75
|
+
input: PersistedJson,
|
|
76
|
+
scope: ScheduleScopeSchema,
|
|
77
|
+
scheduleId: ScheduleId,
|
|
78
|
+
timing: ScheduleTimingRequest,
|
|
79
|
+
destination: ScheduleDestination,
|
|
80
|
+
deliveryPrincipal: ScheduleScopeSchema.fields.principal,
|
|
81
|
+
definitions: DefinitionDigests,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const ScheduleCreateRequest = Schema.TaggedStruct("Create", ScheduleMutationRequestFields);
|
|
85
|
+
|
|
86
|
+
const ScheduleUpdateRequest = Schema.TaggedStruct("Update", {
|
|
87
|
+
...ScheduleMutationRequestFields,
|
|
88
|
+
expectedRevision: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const ScheduleGetRequest = Schema.TaggedStruct("Get", {
|
|
92
|
+
schemaVersion: Schema.Literal(1),
|
|
93
|
+
scope: ScheduleScopeSchema,
|
|
94
|
+
scheduleId: ScheduleId,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const ScheduleListRequest = Schema.TaggedStruct("List", {
|
|
98
|
+
schemaVersion: Schema.Literal(1),
|
|
99
|
+
scope: ScheduleScopeSchema,
|
|
100
|
+
after: Schema.optionalKey(ScheduleId),
|
|
101
|
+
limit: Schema.optionalKey(
|
|
102
|
+
Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(100)),
|
|
103
|
+
),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const ScheduleControlRequest = Schema.TaggedStruct("Control", {
|
|
107
|
+
schemaVersion: Schema.Literal(1),
|
|
108
|
+
operation: Schema.Literals(["pause", "resume", "cancel"]),
|
|
109
|
+
scope: ScheduleScopeSchema,
|
|
110
|
+
scheduleId: ScheduleId,
|
|
111
|
+
expectedRevision: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const ScheduleOwnerRequest = Schema.Union([
|
|
115
|
+
ScheduleCreateRequest,
|
|
116
|
+
ScheduleUpdateRequest,
|
|
117
|
+
ScheduleGetRequest,
|
|
118
|
+
ScheduleListRequest,
|
|
119
|
+
ScheduleControlRequest,
|
|
120
|
+
]);
|
|
121
|
+
type ScheduleOwnerRequest = typeof ScheduleOwnerRequest.Type;
|
|
122
|
+
|
|
123
|
+
const ScheduleOwnerFailure = Schema.Union([
|
|
124
|
+
ScheduleValidationError,
|
|
125
|
+
ScheduleAuthorizationError,
|
|
126
|
+
ScheduleConflict,
|
|
127
|
+
ScheduleNotFound,
|
|
128
|
+
ScheduleCapacityError,
|
|
129
|
+
ScheduleStorageError,
|
|
130
|
+
ScheduleFailpointError,
|
|
131
|
+
ScheduleOwnerProtocolError,
|
|
132
|
+
]);
|
|
133
|
+
type ScheduleOwnerFailure = typeof ScheduleOwnerFailure.Type;
|
|
134
|
+
|
|
135
|
+
const ScheduleOwnerResponse = Schema.Union([
|
|
136
|
+
Schema.TaggedStruct("Snapshot", { value: ScheduleSnapshot }),
|
|
137
|
+
Schema.TaggedStruct("Page", { value: ScheduleSnapshotPageSchema }),
|
|
138
|
+
Schema.TaggedStruct("Failed", { failure: ScheduleOwnerFailure }),
|
|
139
|
+
]);
|
|
140
|
+
type ScheduleOwnerResponse = typeof ScheduleOwnerResponse.Type;
|
|
141
|
+
|
|
142
|
+
const decodeScheduleOwnerRequest = Schema.decodeUnknownEffect(ScheduleOwnerRequest);
|
|
143
|
+
const encodeScheduleOwnerRequest = Schema.encodeEffect(ScheduleOwnerRequest);
|
|
144
|
+
const decodeScheduleOwnerResponse = Schema.decodeUnknownEffect(ScheduleOwnerResponse);
|
|
145
|
+
const encodeScheduleOwnerResponse = Schema.encodeEffect(ScheduleOwnerResponse);
|
|
146
|
+
|
|
147
|
+
const scheduleProtocolFailure = (message: string): ScheduleOwnerResponse => ({
|
|
148
|
+
_tag: "Failed",
|
|
149
|
+
failure: ScheduleOwnerProtocolError.make({ message: boundedProtocolMessage(message) }),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
export interface ScheduleOwnerObjectRpc extends Rpc.DurableObjectBranded {
|
|
153
|
+
schedule(encoded: unknown): Promise<unknown>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export class ScheduleOwnerNamespace extends Context.Service<
|
|
157
|
+
ScheduleOwnerNamespace,
|
|
158
|
+
{ readonly namespace: DurableObjectNamespace<ScheduleOwnerObjectRpc> }
|
|
159
|
+
>()("@effect-agent/platform-cloudflare/ScheduleOwnerNamespace") {}
|
|
160
|
+
|
|
161
|
+
const passthroughAgent = (agentId: AgentId): DurableSubmitAgent<typeof PersistedJson> => ({
|
|
162
|
+
definition: { id: agentId, input: PersistedJson },
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const requestOwner = (request: ScheduleOwnerRequest): ScheduleOwner => request.scope.owner;
|
|
166
|
+
|
|
167
|
+
/** Provides the same authorized management service as NodeScheduling.layer. */
|
|
168
|
+
export class CloudflareSchedulingClient {
|
|
169
|
+
static readonly layer: Layer.Layer<Scheduling, never, ScheduleOwnerNamespace> = Layer.effect(
|
|
170
|
+
Scheduling,
|
|
171
|
+
Effect.gen(function* () {
|
|
172
|
+
const { namespace } = yield* ScheduleOwnerNamespace;
|
|
173
|
+
|
|
174
|
+
const call = Effect.fn("CloudflareSchedulingClient.call")(function* (
|
|
175
|
+
owner: ScheduleOwner,
|
|
176
|
+
request: ScheduleOwnerRequest,
|
|
177
|
+
): Effect.fn.Return<ScheduleOwnerResponse, ScheduleManagementFailure> {
|
|
178
|
+
const encoded = yield* encodeScheduleOwnerRequest(request).pipe(
|
|
179
|
+
Effect.mapError(() =>
|
|
180
|
+
ScheduleStorageError.make({ operation: "Schedule Owner protocol", reason: "corrupt" }),
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
const raw = yield* Effect.tryPromise({
|
|
184
|
+
try: () => namespace.get(namespace.idFromName(scheduleOwnerKey(owner))).schedule(encoded),
|
|
185
|
+
catch: () =>
|
|
186
|
+
ScheduleStorageError.make({
|
|
187
|
+
operation: "call Schedule Owner",
|
|
188
|
+
reason: "unavailable",
|
|
189
|
+
}),
|
|
190
|
+
});
|
|
191
|
+
const response = yield* decodeScheduleOwnerResponse(raw).pipe(
|
|
192
|
+
Effect.mapError(() =>
|
|
193
|
+
ScheduleStorageError.make({ operation: "Schedule Owner protocol", reason: "corrupt" }),
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
if (response._tag !== "Failed") return response;
|
|
197
|
+
return yield* response.failure._tag === "ScheduleOwnerProtocolError"
|
|
198
|
+
? ScheduleStorageError.make({ operation: "Schedule Owner protocol", reason: "corrupt" })
|
|
199
|
+
: response.failure;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const encodeInput = Effect.fn("CloudflareSchedulingClient.encodeInput")(function* <
|
|
203
|
+
InputSchema extends Schema.Top,
|
|
204
|
+
>(
|
|
205
|
+
agent: DurableSubmitAgent<InputSchema>,
|
|
206
|
+
input: InputSchema["Type"],
|
|
207
|
+
): Effect.fn.Return<PersistedJson, ScheduleValidationError, InputSchema["EncodingServices"]> {
|
|
208
|
+
const encoded = yield* Schema.encodeEffect(agent.definition.input)(input).pipe(
|
|
209
|
+
Effect.mapError(() =>
|
|
210
|
+
ScheduleValidationError.make({
|
|
211
|
+
message: "Unable to encode Agent input",
|
|
212
|
+
}),
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
return yield* Schema.decodeUnknownEffect(PersistedJson)(encoded).pipe(
|
|
216
|
+
Effect.mapError(() =>
|
|
217
|
+
ScheduleValidationError.make({
|
|
218
|
+
message: "Agent input does not satisfy the canonical persistence bounds",
|
|
219
|
+
}),
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const create: Scheduling["Service"]["create"] = (agent, input, options) =>
|
|
225
|
+
Effect.gen(function* () {
|
|
226
|
+
const payload = yield* encodeInput(agent, input);
|
|
227
|
+
const response = yield* call(options.scope.owner, {
|
|
228
|
+
_tag: "Create",
|
|
229
|
+
schemaVersion: 1,
|
|
230
|
+
agentId: agent.definition.id,
|
|
231
|
+
input: payload,
|
|
232
|
+
...options,
|
|
233
|
+
});
|
|
234
|
+
return response._tag === "Snapshot"
|
|
235
|
+
? response.value
|
|
236
|
+
: yield* ScheduleStorageError.make({
|
|
237
|
+
operation: "Schedule Owner protocol",
|
|
238
|
+
reason: "corrupt",
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const update: Scheduling["Service"]["update"] = (agent, input, options) =>
|
|
243
|
+
Effect.gen(function* () {
|
|
244
|
+
const payload = yield* encodeInput(agent, input);
|
|
245
|
+
const response = yield* call(options.scope.owner, {
|
|
246
|
+
_tag: "Update",
|
|
247
|
+
schemaVersion: 1,
|
|
248
|
+
agentId: agent.definition.id,
|
|
249
|
+
input: payload,
|
|
250
|
+
...options,
|
|
251
|
+
});
|
|
252
|
+
return response._tag === "Snapshot"
|
|
253
|
+
? response.value
|
|
254
|
+
: yield* ScheduleStorageError.make({
|
|
255
|
+
operation: "Schedule Owner protocol",
|
|
256
|
+
reason: "corrupt",
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const get: Scheduling["Service"]["get"] = (scope, scheduleId) =>
|
|
261
|
+
Effect.gen(function* () {
|
|
262
|
+
const response = yield* call(scope.owner, {
|
|
263
|
+
_tag: "Get",
|
|
264
|
+
schemaVersion: 1,
|
|
265
|
+
scope,
|
|
266
|
+
scheduleId,
|
|
267
|
+
});
|
|
268
|
+
return response._tag === "Snapshot"
|
|
269
|
+
? response.value
|
|
270
|
+
: yield* ScheduleStorageError.make({
|
|
271
|
+
operation: "Schedule Owner protocol",
|
|
272
|
+
reason: "corrupt",
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const list: Scheduling["Service"]["list"] = (scope, options = {}) =>
|
|
277
|
+
Effect.gen(function* () {
|
|
278
|
+
const response = yield* call(scope.owner, {
|
|
279
|
+
_tag: "List",
|
|
280
|
+
schemaVersion: 1,
|
|
281
|
+
scope,
|
|
282
|
+
...(options.after === undefined ? {} : { after: options.after }),
|
|
283
|
+
...(options.limit === undefined ? {} : { limit: options.limit }),
|
|
284
|
+
});
|
|
285
|
+
return response._tag === "Page"
|
|
286
|
+
? response.value
|
|
287
|
+
: yield* ScheduleStorageError.make({
|
|
288
|
+
operation: "Schedule Owner protocol",
|
|
289
|
+
reason: "corrupt",
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const control = (
|
|
294
|
+
operation: "pause" | "resume" | "cancel",
|
|
295
|
+
scope: ScheduleScope,
|
|
296
|
+
scheduleId: ScheduleId,
|
|
297
|
+
expectedRevision: number,
|
|
298
|
+
) =>
|
|
299
|
+
Effect.gen(function* () {
|
|
300
|
+
const response = yield* call(scope.owner, {
|
|
301
|
+
_tag: "Control",
|
|
302
|
+
schemaVersion: 1,
|
|
303
|
+
operation,
|
|
304
|
+
scope,
|
|
305
|
+
scheduleId,
|
|
306
|
+
expectedRevision,
|
|
307
|
+
});
|
|
308
|
+
return response._tag === "Snapshot"
|
|
309
|
+
? response.value
|
|
310
|
+
: yield* ScheduleStorageError.make({
|
|
311
|
+
operation: "Schedule Owner protocol",
|
|
312
|
+
reason: "corrupt",
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return Scheduling.of({
|
|
317
|
+
create,
|
|
318
|
+
update,
|
|
319
|
+
get,
|
|
320
|
+
list,
|
|
321
|
+
pause: (scope, id, revision) => control("pause", scope, id, revision),
|
|
322
|
+
resume: (scope, id, revision) => control("resume", scope, id, revision),
|
|
323
|
+
cancel: (scope, id, revision) => control("cancel", scope, id, revision),
|
|
324
|
+
});
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export class ScheduleOwnerIdentity extends Context.Service<
|
|
330
|
+
ScheduleOwnerIdentity,
|
|
331
|
+
{ readonly owner: ScheduleOwner }
|
|
332
|
+
>()("@effect-agent/platform-cloudflare/ScheduleOwnerIdentity") {}
|
|
333
|
+
|
|
334
|
+
const decodeOwnerName = Effect.fn("decodeScheduleOwnerName")(function* (
|
|
335
|
+
name: string | null | undefined,
|
|
336
|
+
): Effect.fn.Return<ScheduleOwner, ScheduleOwnerProtocolError> {
|
|
337
|
+
if (name === null || name === undefined) {
|
|
338
|
+
return yield* ScheduleOwnerProtocolError.make({
|
|
339
|
+
message: "Schedule Owner objects require an idFromName identity",
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const tuple = yield* Schema.decodeUnknownEffect(
|
|
343
|
+
Schema.fromJsonString(Schema.Tuple([Schema.String, Schema.String])),
|
|
344
|
+
)(name).pipe(
|
|
345
|
+
Effect.mapError(() =>
|
|
346
|
+
ScheduleOwnerProtocolError.make({ message: "Schedule Owner object name is malformed" }),
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
return yield* Schema.decodeUnknownEffect(ScheduleOwner)({
|
|
350
|
+
tenantId: tuple[0],
|
|
351
|
+
ownerId: tuple[1],
|
|
352
|
+
}).pipe(
|
|
353
|
+
Effect.mapError(() =>
|
|
354
|
+
ScheduleOwnerProtocolError.make({ message: "Schedule Owner object identity is invalid" }),
|
|
355
|
+
),
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const alarmStorageError = (operation: string) => (error: { readonly _tag: string }) =>
|
|
360
|
+
ScheduleStorageError.make({
|
|
361
|
+
operation,
|
|
362
|
+
reason: error._tag === "StorageOperationError" ? "unavailable" : "corrupt",
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const transactionLayer: Layer.Layer<
|
|
366
|
+
DoScheduleTransaction,
|
|
367
|
+
never,
|
|
368
|
+
DurableObjectAlarm.DurableObjectAlarm
|
|
369
|
+
> = Layer.effect(
|
|
370
|
+
DoScheduleTransaction,
|
|
371
|
+
Effect.gen(function* () {
|
|
372
|
+
const alarms = yield* DurableObjectAlarm.DurableObjectAlarm;
|
|
373
|
+
return DoScheduleTransaction.of({
|
|
374
|
+
run: (body) =>
|
|
375
|
+
Effect.gen(function* () {
|
|
376
|
+
const nowMillis = yield* Clock.currentTimeMillis;
|
|
377
|
+
return yield* alarms
|
|
378
|
+
.transaction((transaction) =>
|
|
379
|
+
body((replacement) =>
|
|
380
|
+
replacement.deadlineAtMillis === null
|
|
381
|
+
? transaction
|
|
382
|
+
.cancelAlarm({ id: SCHEDULE_ALARM_ID, tag: SCHEDULE_ALARM_TAG })
|
|
383
|
+
.pipe(Effect.mapError(alarmStorageError("cancel Schedule Owner alarm")))
|
|
384
|
+
: Effect.fromOption(
|
|
385
|
+
DateTime.make(Math.max(replacement.deadlineAtMillis, nowMillis + 1)),
|
|
386
|
+
).pipe(
|
|
387
|
+
Effect.mapError(() =>
|
|
388
|
+
ScheduleStorageError.make({
|
|
389
|
+
operation: "validate Schedule Owner alarm deadline",
|
|
390
|
+
reason: "corrupt",
|
|
391
|
+
}),
|
|
392
|
+
),
|
|
393
|
+
Effect.flatMap((runAt) =>
|
|
394
|
+
transaction
|
|
395
|
+
.scheduleAlarm({
|
|
396
|
+
id: SCHEDULE_ALARM_ID,
|
|
397
|
+
tag: SCHEDULE_ALARM_TAG,
|
|
398
|
+
runAt,
|
|
399
|
+
payload: {
|
|
400
|
+
schemaVersion: 1,
|
|
401
|
+
generation: replacement.generation,
|
|
402
|
+
},
|
|
403
|
+
})
|
|
404
|
+
.pipe(
|
|
405
|
+
Effect.mapError(alarmStorageError("schedule Schedule Owner alarm")),
|
|
406
|
+
),
|
|
407
|
+
),
|
|
408
|
+
),
|
|
409
|
+
),
|
|
410
|
+
)
|
|
411
|
+
.pipe(
|
|
412
|
+
Effect.catchTag("StorageOperationError", () =>
|
|
413
|
+
ScheduleStorageError.make({
|
|
414
|
+
operation: "commit Schedule Owner transaction",
|
|
415
|
+
reason: "unavailable",
|
|
416
|
+
}),
|
|
417
|
+
),
|
|
418
|
+
);
|
|
419
|
+
}),
|
|
420
|
+
});
|
|
421
|
+
}),
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const admissionLayer: Layer.Layer<ScheduledInputAdmission, never, CloudflareConversationClient> =
|
|
425
|
+
Layer.effect(
|
|
426
|
+
ScheduledInputAdmission,
|
|
427
|
+
Effect.gen(function* () {
|
|
428
|
+
const client = yield* CloudflareConversationClient;
|
|
429
|
+
const submit = (envelope: ScheduledEnvelope) =>
|
|
430
|
+
client
|
|
431
|
+
.submit(passthroughAgent(envelope.agentId), envelope.input, {
|
|
432
|
+
conversationId: envelope.conversationId,
|
|
433
|
+
principal: envelope.deliveryPrincipal,
|
|
434
|
+
idempotencyKey: envelope.admissionKey,
|
|
435
|
+
definitions: envelope.definitions,
|
|
436
|
+
})
|
|
437
|
+
.pipe(
|
|
438
|
+
Effect.catchTags({
|
|
439
|
+
AdmissionConflict: () =>
|
|
440
|
+
ScheduleStorageError.make({ operation: "scheduled admission", reason: "corrupt" }),
|
|
441
|
+
AdmissionLimitExceeded: () => ScheduledInputRetryable.make({ reason: "capacity" }),
|
|
442
|
+
ConversationClientError: (error: ConversationClientError) =>
|
|
443
|
+
ScheduledInputRetryable.make({
|
|
444
|
+
reason: error.overloaded === true ? "capacity" : "transport",
|
|
445
|
+
}),
|
|
446
|
+
HostProtocolError: () => ScheduledInputRetryable.make({ reason: "ambiguous" }),
|
|
447
|
+
LedgerError: () => ScheduledInputRetryable.make({ reason: "storage" }),
|
|
448
|
+
ConversationStoreError: () => ScheduledInputRetryable.make({ reason: "storage" }),
|
|
449
|
+
DurableAlarmError: () => ScheduledInputRetryable.make({ reason: "storage" }),
|
|
450
|
+
AgentInputError: () =>
|
|
451
|
+
ScheduleStorageError.make({ operation: "scheduled admission", reason: "corrupt" }),
|
|
452
|
+
DigestError: () =>
|
|
453
|
+
ScheduleStorageError.make({ operation: "scheduled admission", reason: "corrupt" }),
|
|
454
|
+
ConversationNotMaterialized: () =>
|
|
455
|
+
ScheduledInputRetryable.make({ reason: "storage" }),
|
|
456
|
+
AppendConflict: () => ScheduledInputRetryable.make({ reason: "ambiguous" }),
|
|
457
|
+
FenceRejected: () => ScheduledInputRetryable.make({ reason: "ambiguous" }),
|
|
458
|
+
DurableRuntimeFailpointError: () =>
|
|
459
|
+
ScheduledInputRetryable.make({ reason: "ambiguous" }),
|
|
460
|
+
}),
|
|
461
|
+
);
|
|
462
|
+
return ScheduledInputAdmission.of({ submit });
|
|
463
|
+
}),
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
type ScheduleRuntimeServices =
|
|
467
|
+
| Scheduling
|
|
468
|
+
| ScheduleDriver
|
|
469
|
+
| DoScheduleAlarmControl
|
|
470
|
+
| ScheduleOwnerIdentity
|
|
471
|
+
| DurableObjectAlarm.DurableObjectAlarm;
|
|
472
|
+
|
|
473
|
+
const ensureOwner = (
|
|
474
|
+
expected: ScheduleOwner,
|
|
475
|
+
request: ScheduleOwnerRequest,
|
|
476
|
+
): Effect.Effect<void, ScheduleOwnerProtocolError> => {
|
|
477
|
+
const observed = requestOwner(request);
|
|
478
|
+
return observed.tenantId === expected.tenantId && observed.ownerId === expected.ownerId
|
|
479
|
+
? Effect.void
|
|
480
|
+
: Effect.fail(
|
|
481
|
+
ScheduleOwnerProtocolError.make({
|
|
482
|
+
message: "The request owner does not match the addressed Schedule Owner object",
|
|
483
|
+
}),
|
|
484
|
+
);
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const handleScheduleRequest = Effect.fn("ScheduleOwner.handleRequest")(function* (
|
|
488
|
+
encoded: unknown,
|
|
489
|
+
): Effect.fn.Return<unknown, never, Scheduling | ScheduleOwnerIdentity> {
|
|
490
|
+
const decoded = yield* decodeScheduleOwnerRequest(encoded).pipe(Effect.result);
|
|
491
|
+
if (decoded._tag === "Failure") {
|
|
492
|
+
return yield* encodeScheduleOwnerResponse(
|
|
493
|
+
scheduleProtocolFailure("The Schedule request could not be decoded"),
|
|
494
|
+
).pipe(Effect.orDie);
|
|
495
|
+
}
|
|
496
|
+
const request = decoded.success;
|
|
497
|
+
const { owner } = yield* ScheduleOwnerIdentity;
|
|
498
|
+
const scheduling = yield* Scheduling;
|
|
499
|
+
const response = yield* Effect.gen(function* () {
|
|
500
|
+
yield* ensureOwner(owner, request);
|
|
501
|
+
switch (request._tag) {
|
|
502
|
+
case "Create": {
|
|
503
|
+
const value = yield* scheduling.create(passthroughAgent(request.agentId), request.input, {
|
|
504
|
+
scope: request.scope,
|
|
505
|
+
scheduleId: request.scheduleId,
|
|
506
|
+
timing: request.timing,
|
|
507
|
+
destination: request.destination,
|
|
508
|
+
deliveryPrincipal: request.deliveryPrincipal,
|
|
509
|
+
definitions: request.definitions,
|
|
510
|
+
});
|
|
511
|
+
return { _tag: "Snapshot" as const, value };
|
|
512
|
+
}
|
|
513
|
+
case "Update": {
|
|
514
|
+
const value = yield* scheduling.update(passthroughAgent(request.agentId), request.input, {
|
|
515
|
+
scope: request.scope,
|
|
516
|
+
scheduleId: request.scheduleId,
|
|
517
|
+
timing: request.timing,
|
|
518
|
+
destination: request.destination,
|
|
519
|
+
deliveryPrincipal: request.deliveryPrincipal,
|
|
520
|
+
definitions: request.definitions,
|
|
521
|
+
expectedRevision: request.expectedRevision,
|
|
522
|
+
});
|
|
523
|
+
return { _tag: "Snapshot" as const, value };
|
|
524
|
+
}
|
|
525
|
+
case "Get":
|
|
526
|
+
return {
|
|
527
|
+
_tag: "Snapshot" as const,
|
|
528
|
+
value: yield* scheduling.get(request.scope, request.scheduleId),
|
|
529
|
+
};
|
|
530
|
+
case "List":
|
|
531
|
+
return {
|
|
532
|
+
_tag: "Page" as const,
|
|
533
|
+
value: yield* scheduling.list(request.scope, {
|
|
534
|
+
...(request.after === undefined ? {} : { after: request.after }),
|
|
535
|
+
...(request.limit === undefined ? {} : { limit: request.limit }),
|
|
536
|
+
}),
|
|
537
|
+
};
|
|
538
|
+
case "Control": {
|
|
539
|
+
const value =
|
|
540
|
+
request.operation === "pause"
|
|
541
|
+
? yield* scheduling.pause(request.scope, request.scheduleId, request.expectedRevision)
|
|
542
|
+
: request.operation === "resume"
|
|
543
|
+
? yield* scheduling.resume(
|
|
544
|
+
request.scope,
|
|
545
|
+
request.scheduleId,
|
|
546
|
+
request.expectedRevision,
|
|
547
|
+
)
|
|
548
|
+
: yield* scheduling.cancel(
|
|
549
|
+
request.scope,
|
|
550
|
+
request.scheduleId,
|
|
551
|
+
request.expectedRevision,
|
|
552
|
+
);
|
|
553
|
+
return { _tag: "Snapshot" as const, value };
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}).pipe(
|
|
557
|
+
Effect.map((value): ScheduleOwnerResponse => value),
|
|
558
|
+
Effect.catch((failure) =>
|
|
559
|
+
Schema.is(ScheduleOwnerFailure)(failure)
|
|
560
|
+
? Effect.succeed({ _tag: "Failed" as const, failure })
|
|
561
|
+
: Effect.succeed(
|
|
562
|
+
scheduleProtocolFailure("The Schedule operation failed outside its public contract"),
|
|
563
|
+
),
|
|
564
|
+
),
|
|
565
|
+
);
|
|
566
|
+
return yield* encodeScheduleOwnerResponse(response).pipe(Effect.orDie);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const scheduleAlarmHandler = (limits: SchedulingLimits) =>
|
|
570
|
+
DurableObjectAlarm.processDue(
|
|
571
|
+
(event) =>
|
|
572
|
+
Effect.gen(function* () {
|
|
573
|
+
if (event.tag !== SCHEDULE_ALARM_TAG || event.id !== SCHEDULE_ALARM_ID) {
|
|
574
|
+
return yield* ScheduleAlarmProtocolError.make({
|
|
575
|
+
message: `Unsupported Schedule Owner alarm ${event.tag}/${event.id}`,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
yield* Schema.decodeUnknownEffect(ScheduleAlarmPayload)(event.payload).pipe(
|
|
579
|
+
Effect.mapError(() =>
|
|
580
|
+
ScheduleAlarmProtocolError.make({
|
|
581
|
+
message: "Unsupported Schedule Owner alarm payload version",
|
|
582
|
+
}),
|
|
583
|
+
),
|
|
584
|
+
);
|
|
585
|
+
const scheduling = yield* ScheduleDriver;
|
|
586
|
+
const alarmControl = yield* DoScheduleAlarmControl;
|
|
587
|
+
const { owner } = yield* ScheduleOwnerIdentity;
|
|
588
|
+
const nowMillis = yield* Clock.currentTimeMillis;
|
|
589
|
+
yield* alarmControl.prearm(nowMillis + limits.recoveryPollMillis);
|
|
590
|
+
const pass = yield* scheduling.runDue(owner);
|
|
591
|
+
if (pass.failed > 0) {
|
|
592
|
+
yield* alarmControl.prearm((yield* Clock.currentTimeMillis) + limits.recoveryPollMillis);
|
|
593
|
+
} else {
|
|
594
|
+
yield* alarmControl.reconcile;
|
|
595
|
+
}
|
|
596
|
+
}),
|
|
597
|
+
{ mode: "ordered" },
|
|
598
|
+
).pipe(Effect.asVoid);
|
|
599
|
+
|
|
600
|
+
export interface ScheduleOwnerObjectInstance extends InstanceType<
|
|
601
|
+
EffectCfDurableObject.DurableObjectClass<Record<never, never>, ScheduleRuntimeServices>
|
|
602
|
+
> {
|
|
603
|
+
schedule(encoded: unknown): Promise<unknown>;
|
|
604
|
+
alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export interface ScheduleOwnerObjectClass {
|
|
608
|
+
new (ctx: DurableObjectState, env: Cloudflare.Env): ScheduleOwnerObjectInstance;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* The host Layer supplies authorization and routing and is cached for the object incarnation.
|
|
613
|
+
* Cloudflare eviction does not guarantee its finalizers run. Do not acquire resources requiring
|
|
614
|
+
* cleanup in this Layer; acquire them inside scoped `manage` / `prepare` operations instead.
|
|
615
|
+
* Native services belong to effect-cf; the database and alarm runtime remain instance-owned.
|
|
616
|
+
*/
|
|
617
|
+
export const makeScheduleOwnerObjectClass = <E>(
|
|
618
|
+
host: Layer.Layer<
|
|
619
|
+
ScheduleAuthorizer | ConversationObjectNamespace,
|
|
620
|
+
E,
|
|
621
|
+
EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment | ScheduleOwnerIdentity
|
|
622
|
+
>,
|
|
623
|
+
limits: SchedulingLimits = defaultSchedulingLimits,
|
|
624
|
+
): ScheduleOwnerObjectClass => {
|
|
625
|
+
const ownerLayer = Layer.effect(
|
|
626
|
+
ScheduleOwnerIdentity,
|
|
627
|
+
Effect.gen(function* () {
|
|
628
|
+
const state = yield* EffectCfDurableObjectState.DurableObjectState;
|
|
629
|
+
return ScheduleOwnerIdentity.of({ owner: yield* decodeOwnerName(state.raw.id.name) });
|
|
630
|
+
}),
|
|
631
|
+
);
|
|
632
|
+
const sqlLayer = Layer.unwrap(
|
|
633
|
+
Effect.map(EffectCfDurableObjectState.DurableObjectState, (state) =>
|
|
634
|
+
SqliteClient.layer({ storage: state.raw.storage }),
|
|
635
|
+
),
|
|
636
|
+
);
|
|
637
|
+
const application = Layer.merge(Scheduling.layer(limits), ScheduleDriver.layer(limits)).pipe(
|
|
638
|
+
Layer.provideMerge(
|
|
639
|
+
scheduleStoreLayer.pipe(Layer.provide(transactionLayer), Layer.provide(sqlLayer)),
|
|
640
|
+
),
|
|
641
|
+
Layer.provide(admissionLayer.pipe(Layer.provide(CloudflareConversationClient.layer))),
|
|
642
|
+
Layer.provide(ScheduleWakeNoop),
|
|
643
|
+
Layer.provide(BrowserCrypto.layer),
|
|
644
|
+
Layer.provideMerge(DurableObjectAlarm.DurableObjectAlarm.layer),
|
|
645
|
+
Layer.provide(host),
|
|
646
|
+
Layer.provideMerge(ownerLayer),
|
|
647
|
+
);
|
|
648
|
+
const runtime: Layer.Layer<
|
|
649
|
+
ScheduleRuntimeServices,
|
|
650
|
+
E | ScheduleStorageError | ScheduleOwnerProtocolError | ScheduleValidationError,
|
|
651
|
+
EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
|
|
652
|
+
> = Layer.effectContext(
|
|
653
|
+
Effect.gen(function* () {
|
|
654
|
+
const state = yield* EffectCfDurableObjectState.DurableObjectState;
|
|
655
|
+
const scope = yield* Effect.scope;
|
|
656
|
+
return yield* state.blockConcurrencyWhile(Layer.buildWithScope(application, scope));
|
|
657
|
+
}),
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
const rpc = {
|
|
661
|
+
schedule: (encoded: unknown) => handleScheduleRequest(encoded),
|
|
662
|
+
} satisfies EffectCfDurableObject.DurableObjectRpc<ScheduleRuntimeServices>;
|
|
663
|
+
|
|
664
|
+
const Base = EffectCfDurableObject.make(runtime, {
|
|
665
|
+
initialize: Effect.void,
|
|
666
|
+
rpc,
|
|
667
|
+
alarms: scheduleAlarmHandler(limits),
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
class ScheduleOwnerObject extends Base {
|
|
671
|
+
override alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void {
|
|
672
|
+
return super.alarm?.(alarmInfo);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return ScheduleOwnerObject;
|
|
676
|
+
};
|