@effect-agent/platform-cloudflare 0.1.0-beta.9 → 0.1.0-beta.90

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