@effect/platform-cloudflare 0.0.0-stage → 4.1.3

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 (237) hide show
  1. package/AGENTS.md +434 -0
  2. package/CLAUDE.md +434 -0
  3. package/LICENSE +21 -0
  4. package/README.md +505 -2
  5. package/ai-docs/README.md +44 -0
  6. package/ai-docs/package.json +36 -0
  7. package/ai-docs/src/01_effect/01_basics/01_effect-gen.ts +30 -0
  8. package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +52 -0
  9. package/ai-docs/src/01_effect/01_basics/10_creating-effects.ts +74 -0
  10. package/ai-docs/src/01_effect/01_basics/index.md +7 -0
  11. package/ai-docs/src/01_effect/02_schema/10_schema-basics.ts +43 -0
  12. package/ai-docs/src/01_effect/02_schema/index.md +7 -0
  13. package/ai-docs/src/01_effect/03_services/01_service.ts +45 -0
  14. package/ai-docs/src/01_effect/03_services/10_reference.ts +10 -0
  15. package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +70 -0
  16. package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +66 -0
  17. package/ai-docs/src/01_effect/03_services/index.md +5 -0
  18. package/ai-docs/src/01_effect/04_errors/01_error-handling.ts +30 -0
  19. package/ai-docs/src/01_effect/04_errors/10_catch-tags.ts +24 -0
  20. package/ai-docs/src/01_effect/04_errors/20_reason-errors.ts +64 -0
  21. package/ai-docs/src/01_effect/04_errors/index.md +1 -0
  22. package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +105 -0
  23. package/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts +31 -0
  24. package/ai-docs/src/01_effect/05_resources/30_layer-map.ts +86 -0
  25. package/ai-docs/src/01_effect/05_resources/index.md +3 -0
  26. package/ai-docs/src/01_effect/06_running/10_run-main.ts +30 -0
  27. package/ai-docs/src/01_effect/06_running/20_layer-launch.ts +27 -0
  28. package/ai-docs/src/01_effect/06_running/index.md +1 -0
  29. package/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts +56 -0
  30. package/ai-docs/src/01_effect/07_pubsub/index.md +3 -0
  31. package/ai-docs/src/03_stream/10_creating-streams.ts +103 -0
  32. package/ai-docs/src/03_stream/20_consuming-streams.ts +137 -0
  33. package/ai-docs/src/03_stream/30_encoding.ts +163 -0
  34. package/ai-docs/src/03_stream/index.md +4 -0
  35. package/ai-docs/src/04_integration/10_managed-runtime.ts +129 -0
  36. package/ai-docs/src/04_integration/index.md +5 -0
  37. package/ai-docs/src/05_batching/10_request-resolver.ts +89 -0
  38. package/ai-docs/src/05_batching/index.md +3 -0
  39. package/ai-docs/src/06_schedule/10_schedules.ts +111 -0
  40. package/ai-docs/src/06_schedule/index.md +3 -0
  41. package/ai-docs/src/07_datetime/10_creating-and-formatting.ts +30 -0
  42. package/ai-docs/src/07_datetime/20_time-zones.ts +44 -0
  43. package/ai-docs/src/07_datetime/index.md +5 -0
  44. package/ai-docs/src/08_observability/10_logging.ts +66 -0
  45. package/ai-docs/src/08_observability/20_otlp-tracing.ts +95 -0
  46. package/ai-docs/src/08_observability/index.md +7 -0
  47. package/ai-docs/src/09_testing/10_effect-tests.ts +55 -0
  48. package/ai-docs/src/09_testing/20_layer-tests.ts +138 -0
  49. package/ai-docs/src/09_testing/index.md +1 -0
  50. package/ai-docs/src/10_predicate/01_basics.ts +14 -0
  51. package/ai-docs/src/10_predicate/index.md +9 -0
  52. package/ai-docs/src/40_sql/10_basics.ts +175 -0
  53. package/ai-docs/src/40_sql/index.md +6 -0
  54. package/ai-docs/src/50_http-client/10_basics.ts +102 -0
  55. package/ai-docs/src/50_http-client/index.md +3 -0
  56. package/ai-docs/src/51_http-server/10_basics.ts +118 -0
  57. package/ai-docs/src/51_http-server/20_testing.ts +100 -0
  58. package/ai-docs/src/51_http-server/fixtures/api/Api.ts +14 -0
  59. package/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +36 -0
  60. package/ai-docs/src/51_http-server/fixtures/api/System.ts +10 -0
  61. package/ai-docs/src/51_http-server/fixtures/api/Users.ts +107 -0
  62. package/ai-docs/src/51_http-server/fixtures/domain/User.ts +21 -0
  63. package/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +22 -0
  64. package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +37 -0
  65. package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +80 -0
  66. package/ai-docs/src/51_http-server/fixtures/server/Users.ts +183 -0
  67. package/ai-docs/src/51_http-server/index.md +3 -0
  68. package/ai-docs/src/52_graphql/10_basics.ts +117 -0
  69. package/ai-docs/src/52_graphql/20_testing.ts +45 -0
  70. package/ai-docs/src/52_graphql/30_batching.ts +118 -0
  71. package/ai-docs/src/52_graphql/40_aliased-batching.ts +124 -0
  72. package/ai-docs/src/52_graphql/50_errors-and-retries.ts +74 -0
  73. package/ai-docs/src/52_graphql/index.md +6 -0
  74. package/ai-docs/src/60_child-process/10_working-with-child-processes.ts +117 -0
  75. package/ai-docs/src/60_child-process/index.md +3 -0
  76. package/ai-docs/src/70_cli/10_basics.ts +162 -0
  77. package/ai-docs/src/70_cli/index.md +5 -0
  78. package/ai-docs/src/71_ai/10_language-model.ts +156 -0
  79. package/ai-docs/src/71_ai/20_tools.ts +226 -0
  80. package/ai-docs/src/71_ai/30_chat.ts +158 -0
  81. package/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts +9 -0
  82. package/ai-docs/src/71_ai/index.md +5 -0
  83. package/ai-docs/src/80_cluster/10_entities.ts +97 -0
  84. package/ai-docs/src/80_cluster/index.md +4 -0
  85. package/ai-docs/src/index.md +10 -0
  86. package/ai-docs/tsconfig.json +28 -0
  87. package/dist/AlchemyCloudflareCluster.d.ts +134 -0
  88. package/dist/AlchemyCloudflareCluster.d.ts.map +1 -0
  89. package/dist/AlchemyCloudflareCluster.js +111 -0
  90. package/dist/AlchemyCloudflareCluster.js.map +1 -0
  91. package/dist/CloudflareCluster.d.ts +196 -0
  92. package/dist/CloudflareCluster.d.ts.map +1 -0
  93. package/dist/CloudflareCluster.js +533 -0
  94. package/dist/CloudflareCluster.js.map +1 -0
  95. package/dist/CloudflareDurableObjectPrograms.d.ts +279 -0
  96. package/dist/CloudflareDurableObjectPrograms.d.ts.map +1 -0
  97. package/dist/CloudflareDurableObjectPrograms.js +295 -0
  98. package/dist/CloudflareDurableObjectPrograms.js.map +1 -0
  99. package/dist/CloudflareDurableObjects.d.ts +149 -0
  100. package/dist/CloudflareDurableObjects.d.ts.map +1 -0
  101. package/dist/CloudflareDurableObjects.js +337 -0
  102. package/dist/CloudflareDurableObjects.js.map +1 -0
  103. package/dist/CloudflarePersistedQueue.d.ts +36 -0
  104. package/dist/CloudflarePersistedQueue.d.ts.map +1 -0
  105. package/dist/CloudflarePersistedQueue.js +122 -0
  106. package/dist/CloudflarePersistedQueue.js.map +1 -0
  107. package/dist/CloudflareWorkflowEngine.d.ts +72 -0
  108. package/dist/CloudflareWorkflowEngine.d.ts.map +1 -0
  109. package/dist/CloudflareWorkflowEngine.js +187 -0
  110. package/dist/CloudflareWorkflowEngine.js.map +1 -0
  111. package/dist/index.d.ts +29 -0
  112. package/dist/index.d.ts.map +1 -0
  113. package/dist/index.js +33 -0
  114. package/dist/index.js.map +1 -0
  115. package/dist/internal/alchemyCluster.d.ts +2 -0
  116. package/dist/internal/alchemyCluster.d.ts.map +1 -0
  117. package/dist/internal/alchemyCluster.js +75 -0
  118. package/dist/internal/alchemyCluster.js.map +1 -0
  119. package/dist/internal/boundedMap.d.ts +3 -0
  120. package/dist/internal/boundedMap.d.ts.map +1 -0
  121. package/dist/internal/boundedMap.js +15 -0
  122. package/dist/internal/boundedMap.js.map +1 -0
  123. package/dist/internal/cluster.d.ts +57 -0
  124. package/dist/internal/cluster.d.ts.map +1 -0
  125. package/dist/internal/cluster.js +2 -0
  126. package/dist/internal/cluster.js.map +1 -0
  127. package/dist/internal/clusterName.d.ts +2 -0
  128. package/dist/internal/clusterName.d.ts.map +1 -0
  129. package/dist/internal/clusterName.js +16 -0
  130. package/dist/internal/clusterName.js.map +1 -0
  131. package/dist/internal/entityKeepAlive.d.ts +2 -0
  132. package/dist/internal/entityKeepAlive.d.ts.map +1 -0
  133. package/dist/internal/entityKeepAlive.js +73 -0
  134. package/dist/internal/entityKeepAlive.js.map +1 -0
  135. package/dist/internal/entityMailbox.d.ts +2 -0
  136. package/dist/internal/entityMailbox.d.ts.map +1 -0
  137. package/dist/internal/entityMailbox.js +160 -0
  138. package/dist/internal/entityMailbox.js.map +1 -0
  139. package/dist/internal/entityRegistry.d.ts +16 -0
  140. package/dist/internal/entityRegistry.d.ts.map +1 -0
  141. package/dist/internal/entityRegistry.js +22 -0
  142. package/dist/internal/entityRegistry.js.map +1 -0
  143. package/dist/internal/entityReply.d.ts +2 -0
  144. package/dist/internal/entityReply.d.ts.map +1 -0
  145. package/dist/internal/entityReply.js +35 -0
  146. package/dist/internal/entityReply.js.map +1 -0
  147. package/dist/internal/entityRuntime.d.ts +2 -0
  148. package/dist/internal/entityRuntime.d.ts.map +1 -0
  149. package/dist/internal/entityRuntime.js +823 -0
  150. package/dist/internal/entityRuntime.js.map +1 -0
  151. package/dist/internal/entitySql.d.ts +2 -0
  152. package/dist/internal/entitySql.d.ts.map +1 -0
  153. package/dist/internal/entitySql.js +29 -0
  154. package/dist/internal/entitySql.js.map +1 -0
  155. package/dist/internal/entityStorage.d.ts +2 -0
  156. package/dist/internal/entityStorage.d.ts.map +1 -0
  157. package/dist/internal/entityStorage.js +98 -0
  158. package/dist/internal/entityStorage.js.map +1 -0
  159. package/dist/internal/entityWire.d.ts +2 -0
  160. package/dist/internal/entityWire.d.ts.map +1 -0
  161. package/dist/internal/entityWire.js +146 -0
  162. package/dist/internal/entityWire.js.map +1 -0
  163. package/dist/internal/queueRuntime.d.ts +2 -0
  164. package/dist/internal/queueRuntime.d.ts.map +1 -0
  165. package/dist/internal/queueRuntime.js +88 -0
  166. package/dist/internal/queueRuntime.js.map +1 -0
  167. package/dist/internal/queueStorage.d.ts +2 -0
  168. package/dist/internal/queueStorage.d.ts.map +1 -0
  169. package/dist/internal/queueStorage.js +129 -0
  170. package/dist/internal/queueStorage.js.map +1 -0
  171. package/dist/internal/registry.d.ts +10 -0
  172. package/dist/internal/registry.d.ts.map +1 -0
  173. package/dist/internal/registry.js +24 -0
  174. package/dist/internal/registry.js.map +1 -0
  175. package/dist/internal/singletonRegistry.d.ts +2 -0
  176. package/dist/internal/singletonRegistry.d.ts.map +1 -0
  177. package/dist/internal/singletonRegistry.js +9 -0
  178. package/dist/internal/singletonRegistry.js.map +1 -0
  179. package/dist/internal/singletonRuntime.d.ts +2 -0
  180. package/dist/internal/singletonRuntime.d.ts.map +1 -0
  181. package/dist/internal/singletonRuntime.js +42 -0
  182. package/dist/internal/singletonRuntime.js.map +1 -0
  183. package/dist/internal/singletonStorage.d.ts +2 -0
  184. package/dist/internal/singletonStorage.d.ts.map +1 -0
  185. package/dist/internal/singletonStorage.js +34 -0
  186. package/dist/internal/singletonStorage.js.map +1 -0
  187. package/dist/internal/transport.d.ts +2 -0
  188. package/dist/internal/transport.d.ts.map +1 -0
  189. package/dist/internal/transport.js +49 -0
  190. package/dist/internal/transport.js.map +1 -0
  191. package/dist/internal/workflowRegistry.d.ts +2 -0
  192. package/dist/internal/workflowRegistry.d.ts.map +1 -0
  193. package/dist/internal/workflowRegistry.js +29 -0
  194. package/dist/internal/workflowRegistry.js.map +1 -0
  195. package/dist/internal/workflowRuntime.d.ts +2 -0
  196. package/dist/internal/workflowRuntime.d.ts.map +1 -0
  197. package/dist/internal/workflowRuntime.js +322 -0
  198. package/dist/internal/workflowRuntime.js.map +1 -0
  199. package/dist/internal/workflowStorage.d.ts +2 -0
  200. package/dist/internal/workflowStorage.d.ts.map +1 -0
  201. package/dist/internal/workflowStorage.js +113 -0
  202. package/dist/internal/workflowStorage.js.map +1 -0
  203. package/dist/internal/workflowWire.d.ts +2 -0
  204. package/dist/internal/workflowWire.d.ts.map +1 -0
  205. package/dist/internal/workflowWire.js +52 -0
  206. package/dist/internal/workflowWire.js.map +1 -0
  207. package/package.json +74 -3
  208. package/src/AlchemyCloudflareCluster.ts +200 -0
  209. package/src/CloudflareCluster.ts +704 -0
  210. package/src/CloudflareDurableObjectPrograms.ts +543 -0
  211. package/src/CloudflareDurableObjects.ts +430 -0
  212. package/src/CloudflarePersistedQueue.ts +189 -0
  213. package/src/CloudflareWorkflowEngine.ts +220 -0
  214. package/src/index.ts +38 -0
  215. package/src/internal/alchemyCluster.ts +120 -0
  216. package/src/internal/boundedMap.ts +15 -0
  217. package/src/internal/cluster.ts +57 -0
  218. package/src/internal/clusterName.ts +19 -0
  219. package/src/internal/entityKeepAlive.ts +121 -0
  220. package/src/internal/entityMailbox.ts +324 -0
  221. package/src/internal/entityRegistry.ts +46 -0
  222. package/src/internal/entityReply.ts +54 -0
  223. package/src/internal/entityRuntime.ts +1318 -0
  224. package/src/internal/entitySql.ts +60 -0
  225. package/src/internal/entityStorage.ts +168 -0
  226. package/src/internal/entityWire.ts +204 -0
  227. package/src/internal/queueRuntime.ts +174 -0
  228. package/src/internal/queueStorage.ts +255 -0
  229. package/src/internal/registry.ts +31 -0
  230. package/src/internal/singletonRegistry.ts +21 -0
  231. package/src/internal/singletonRuntime.ts +88 -0
  232. package/src/internal/singletonStorage.ts +64 -0
  233. package/src/internal/transport.ts +67 -0
  234. package/src/internal/workflowRegistry.ts +78 -0
  235. package/src/internal/workflowRuntime.ts +443 -0
  236. package/src/internal/workflowStorage.ts +234 -0
  237. package/src/internal/workflowWire.ts +123 -0
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Runs durable workflows on the dedicated workflow Durable Object class.
3
+ *
4
+ * On this path one workflow execution is one Durable Object: the engine
5
+ * encodes `(workflowName, executionId)` into a Durable Object name with the
6
+ * same length-prefix scheme as entities and resolves the object through the
7
+ * workflow namespace binding. Execution state, activity results keyed
8
+ * `${name}/${attempt}`, durable deferred exits, and the clock due table all
9
+ * live on the object's SQLite storage behind its single alarm.
10
+ *
11
+ * Every `DurableClock` is durable on this engine: the in-memory short-sleep
12
+ * path is disabled, so even sub-minute sleeps persist a due row and arm the
13
+ * alarm.
14
+ *
15
+ * @stability experimental
16
+ * @since 4.1.3
17
+ */
18
+ import { Clock } from "effect/Clock"
19
+ import * as Context from "effect/Context"
20
+ import * as Duration from "effect/Duration"
21
+ import * as Effect from "effect/Effect"
22
+ import * as Layer from "effect/Layer"
23
+ import * as Option from "effect/Option"
24
+ import * as Schedule from "effect/Schedule"
25
+ import * as Workflow from "effect/workflow/Workflow"
26
+ import * as WorkflowEngine from "effect/workflow/WorkflowEngine"
27
+ import { encodeName } from "./internal/clusterName.ts"
28
+ import { callDurableObject } from "./internal/transport.ts"
29
+ import {
30
+ CurrentExecutionHandle,
31
+ registerWorkflow,
32
+ unregisterWorkflow,
33
+ type WorkflowRegistration,
34
+ type WorkflowStub
35
+ } from "./internal/workflowRegistry.ts"
36
+ import { InterruptSignalName } from "./internal/workflowRuntime.ts"
37
+ import { decodeExit, decodeResult, encodeExit, encodePayload } from "./internal/workflowWire.ts"
38
+
39
+ /**
40
+ * The workflow Durable Object namespace binding the engine is built from.
41
+ *
42
+ * @stability experimental
43
+ * @category layers
44
+ * @since 4.1.3
45
+ */
46
+ export interface LayerOptions {
47
+ readonly workflowNamespace: DurableObjectNamespace
48
+ }
49
+
50
+ const retryPolicy = Schedule.min([
51
+ Schedule.exponential(200, 1.5),
52
+ Schedule.spaced("1 minute")
53
+ ])
54
+
55
+ /**
56
+ * Creates the `WorkflowEngine` service backed by the workflow Durable Object
57
+ * namespace binding.
58
+ *
59
+ * **Details**
60
+ *
61
+ * Inside a workflow Durable Object the engine operates on the local execution
62
+ * handle, so activities and deferred reads never leave the object. Everywhere
63
+ * else it resolves the target execution's object with `getByName` and drives
64
+ * it over the same-Worker binding.
65
+ *
66
+ * @stability experimental
67
+ * @category constructors
68
+ * @since 4.1.3
69
+ */
70
+ export const make = Effect.fnUntraced(function*(options: LayerOptions) {
71
+ const clock = yield* Clock
72
+
73
+ // Inside a run the execution's own handle avoids a self-RPC; every other
74
+ // target resolves to its Durable Object stub through the namespace binding.
75
+ //
76
+ // As in `ClusterWorkflowEngine`, a transport failure is retried on an
77
+ // exponential schedule capped at one minute and only then becomes a defect:
78
+ // every call here is idempotent by key, and a permanently unreachable object
79
+ // stalls the caller instead of failing a workflow for good. The object never
80
+ // rejects for application reasons, so a child's own defect arrives as its
81
+ // stored `Complete` result and is never retried.
82
+ const callExecution = <A>(
83
+ workflowName: string,
84
+ executionId: string,
85
+ call: (stub: WorkflowStub) => Promise<A>
86
+ ): Effect.Effect<A> =>
87
+ Effect.flatMap(Effect.serviceOption(CurrentExecutionHandle), (handle) => {
88
+ if (Option.isSome(handle) && handle.value.executionId === executionId) {
89
+ const local = handle.value as WorkflowStub
90
+ return Effect.promise(() => call(local))
91
+ }
92
+ const name = encodeName(workflowName, executionId)
93
+ return callDurableObject({
94
+ resolve: () => options.workflowNamespace.getByName(name) as unknown as WorkflowStub,
95
+ call,
96
+ retry: retryPolicy
97
+ }).pipe(Effect.orDie)
98
+ })
99
+
100
+ const localHandle = Effect.fnUntraced(function*(operation: string) {
101
+ const instance = yield* WorkflowEngine.WorkflowInstance
102
+ const handle = yield* Effect.serviceOption(CurrentExecutionHandle)
103
+ if (Option.isNone(handle)) {
104
+ return yield* Effect.die(
105
+ `CloudflareWorkflowEngine: ${operation} is only available inside a workflow Durable Object execution`
106
+ )
107
+ }
108
+ return { instance, handle: handle.value } as const
109
+ })
110
+
111
+ return WorkflowEngine.makeUnsafe({
112
+ register: Effect.fnUntraced(function*(workflow, execute) {
113
+ const context = yield* Effect.context<never>()
114
+ const registration: WorkflowRegistration = { workflow, execute, context }
115
+ if (!registerWorkflow(workflow._tag, registration)) return
116
+ yield* Effect.addFinalizer(() =>
117
+ Effect.sync(() => {
118
+ unregisterWorkflow(workflow._tag, registration)
119
+ })
120
+ )
121
+ }),
122
+
123
+ execute: Effect.fnUntraced(function*(workflow, opts) {
124
+ const context = yield* Effect.context<never>()
125
+ const payload = yield* encodePayload(workflow, opts.payload, context)
126
+ // A detached child keeps no parent, so neither its completion nor a
127
+ // failed run wakes the parent.
128
+ const parent = opts.parent === undefined || opts.detached
129
+ ? undefined
130
+ : { workflowName: opts.parent.workflow._tag, executionId: opts.parent.executionId }
131
+ const text = yield* callExecution(
132
+ workflow._tag,
133
+ opts.executionId,
134
+ (stub) => stub.run(payload, { discard: opts.discard, parent })
135
+ )
136
+ if (opts.discard) return undefined
137
+ return yield* decodeResult(workflow, text, context)
138
+ }) as WorkflowEngine.Encoded["execute"],
139
+
140
+ poll: Effect.fnUntraced(function*(workflow, executionId) {
141
+ const context = yield* Effect.context<never>()
142
+ const text = yield* callExecution(workflow._tag, executionId, (stub) => stub.poll())
143
+ if (text === undefined) return Option.none()
144
+ return Option.some(yield* decodeResult(workflow, text, context))
145
+ }),
146
+
147
+ interrupt: (workflow, executionId) => callExecution(workflow._tag, executionId, (stub) => stub.interrupt()),
148
+
149
+ interruptUnsafe: (workflow, executionId) =>
150
+ callExecution(workflow._tag, executionId, (stub) => stub.interruptUnsafe()),
151
+
152
+ resume: (workflow, executionId) => callExecution(workflow._tag, executionId, (stub) => stub.resume()),
153
+
154
+ activityExecute: Effect.fnUntraced(function*(activity, attempt) {
155
+ const { handle, instance } = yield* localHandle("Activity execution")
156
+ const key = `${activity.name}/${attempt}`
157
+ const stored = handle.loadActivity(key)
158
+ if (stored !== undefined) {
159
+ return new Workflow.Complete({ exit: yield* decodeExit(stored, Context.empty()) })
160
+ }
161
+ // A pending interrupt replays only to rebuild compensations, so it must
162
+ // not start new activities. Suspending lets the run apply the interrupt.
163
+ if (!instance.interrupted && handle.loadDeferred(InterruptSignalName) !== undefined) {
164
+ instance.suspended = true
165
+ return new Workflow.Suspended({})
166
+ }
167
+ const activityInstance = WorkflowEngine.WorkflowInstance.initial(instance.workflow, instance.executionId)
168
+ activityInstance.interrupted = instance.interrupted
169
+ const result = yield* activity.executeEncoded.pipe(
170
+ Workflow.intoResult,
171
+ Effect.provideService(WorkflowEngine.WorkflowInstance, activityInstance)
172
+ )
173
+ if (result._tag === "Complete") {
174
+ handle.saveActivity(key, yield* encodeExit(result.exit, Context.empty()))
175
+ }
176
+ return result
177
+ }),
178
+
179
+ deferredResult: Effect.fnUntraced(function*(deferred) {
180
+ const { handle } = yield* localHandle("DurableDeferred reads")
181
+ const stored = handle.loadDeferred(deferred.name)
182
+ if (stored === undefined) return Option.none()
183
+ return Option.some(yield* decodeExit(stored, Context.empty()))
184
+ }),
185
+
186
+ deferredDone: Effect.fnUntraced(function*({ deferredName, executionId, exit, workflowName }) {
187
+ const text = yield* encodeExit(exit, Context.empty())
188
+ const handle = yield* Effect.serviceOption(CurrentExecutionHandle)
189
+ if (Option.isSome(handle) && handle.value.executionId === executionId) {
190
+ return yield* handle.value.completeOwnDeferred(deferredName, text)
191
+ }
192
+ yield* callExecution(workflowName, executionId, (stub) => stub.deferredDone(deferredName, text))
193
+ }),
194
+
195
+ scheduleClock: Effect.fnUntraced(function*(workflow, opts) {
196
+ const wakeUp = clock.currentTimeMillisUnsafe() + Math.ceil(Duration.toMillis(opts.clock.duration))
197
+ yield* callExecution(
198
+ workflow._tag,
199
+ opts.executionId,
200
+ (stub) => stub.scheduleClock(opts.clock.name, opts.clock.deferred.name, wakeUp)
201
+ )
202
+ })
203
+ })
204
+ })
205
+
206
+ /**
207
+ * Layer that provides the `WorkflowEngine` backed by the workflow Durable
208
+ * Object namespace binding.
209
+ *
210
+ * **Details**
211
+ *
212
+ * `CloudflareCluster.layer` already includes this layer; use it directly only
213
+ * when the workflow engine is needed without the rest of the cluster.
214
+ *
215
+ * @stability experimental
216
+ * @category layers
217
+ * @since 4.1.3
218
+ */
219
+ export const layer = (options: LayerOptions): Layer.Layer<WorkflowEngine.WorkflowEngine> =>
220
+ Layer.effect(WorkflowEngine.WorkflowEngine)(make(options))
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @since 4.1.3
3
+ */
4
+
5
+ // The index stays free of the optional `alchemy` peer dependency, so the
6
+ // Alchemy integration is exposed only through its own entrypoint:
7
+ // `@effect/platform-cloudflare/AlchemyCloudflareCluster`.
8
+ // @barrel(Cloudflare*.ts): Auto-generated exports. Do not edit manually.
9
+
10
+ /**
11
+ * @stability experimental
12
+ * @since 4.1.3
13
+ */
14
+ export * as CloudflareCluster from "./CloudflareCluster.ts"
15
+
16
+ /**
17
+ * @stability experimental
18
+ * @since 4.1.3
19
+ */
20
+ export * as CloudflareDurableObjectPrograms from "./CloudflareDurableObjectPrograms.ts"
21
+
22
+ /**
23
+ * @stability experimental
24
+ * @since 4.1.3
25
+ */
26
+ export * as CloudflareDurableObjects from "./CloudflareDurableObjects.ts"
27
+
28
+ /**
29
+ * @stability experimental
30
+ * @since 4.1.3
31
+ */
32
+ export * as CloudflarePersistedQueue from "./CloudflarePersistedQueue.ts"
33
+
34
+ /**
35
+ * @stability experimental
36
+ * @since 4.1.3
37
+ */
38
+ export * as CloudflareWorkflowEngine from "./CloudflareWorkflowEngine.ts"
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The alchemy-free half of `AlchemyCloudflareCluster.make`: building the
3
+ * cluster handle from resolved namespace bindings. Kept separate so it can be
4
+ * exercised without importing `alchemy`, whose runtime must match the
5
+ * published `effect` release rather than this workspace.
6
+ *
7
+ * @internal
8
+ */
9
+ import type * as Entity from "effect/cluster/Entity"
10
+ import * as Context from "effect/Context"
11
+ import type * as Duration from "effect/Duration"
12
+ import * as Effect from "effect/Effect"
13
+ import * as Layer from "effect/Layer"
14
+ import * as Scope from "effect/Scope"
15
+ import * as CloudflareCluster from "../CloudflareCluster.ts"
16
+ import type { SingletonWakeResult } from "../CloudflareDurableObjectPrograms.ts"
17
+ import type { Cluster } from "./cluster.ts"
18
+ import { boundedRetry, callDurableObject } from "./transport.ts"
19
+
20
+ interface SingletonStub {
21
+ // An object still on an older deploy resolves `undefined` on success.
22
+ readonly wake: () => Promise<SingletonWakeResult | undefined>
23
+ }
24
+
25
+ /** @internal */
26
+ export interface ClusterHandleOptions {
27
+ readonly entities: ReadonlyArray<Entity.Entity<any, any>>
28
+ readonly layer: Layer.Layer<never, any, any>
29
+ readonly env: Record<string, unknown>
30
+ readonly keepAliveHeartbeat?: Duration.Input | undefined
31
+ }
32
+
33
+ /** @internal */
34
+ export const makeClusterHandle = Effect.fnUntraced(function*(options: ClusterHandleOptions) {
35
+ const entityNamespace = options.env.ClusterEntity as DurableObjectNamespace
36
+ const workflowNamespace = options.env.ClusterWorkflow as DurableObjectNamespace
37
+ const queueNamespace = options.env.ClusterDurableQueue as DurableObjectNamespace
38
+ const singletonNamespace = options.env.ClusterSingleton as DurableObjectNamespace
39
+
40
+ // The isolate-lifetime scope. workerd has no isolate-teardown hook, so it
41
+ // is never closed; the built services live for the isolate, exactly like
42
+ // the Wrangler path's `setInitializer` build.
43
+ const scope = Scope.makeUnsafe()
44
+ const context = yield* Layer.buildWithScope(
45
+ options.layer.pipe(
46
+ Layer.provideMerge(CloudflareCluster.layer({
47
+ entities: options.entities,
48
+ entityNamespace,
49
+ workflowNamespace,
50
+ queueNamespace,
51
+ singletonNamespace,
52
+ keepAliveHeartbeat: options.keepAliveHeartbeat
53
+ }))
54
+ ),
55
+ scope
56
+ ).pipe(
57
+ // Drop the build's memo map so a Layer the user provides inside a
58
+ // handler builds per event instead of sharing one instance pinned to
59
+ // the first request's IoContext.
60
+ Effect.map(Context.omit(Layer.CurrentMemoMap))
61
+ )
62
+
63
+ const handle: Cluster<never> = {
64
+ provide: (effect) => Effect.provideContext(effect, context),
65
+ // Only a transport failure rejects `wake()`, so retrying never reruns a
66
+ // failed singleton. A failed run still fails the Cron Trigger.
67
+ wake: (name) => () =>
68
+ callDurableObject({
69
+ resolve: () => singletonNamespace.getByName(`Singleton/${name}`) as unknown as SingletonStub,
70
+ call: (stub) => stub.wake(),
71
+ retry: boundedRetry
72
+ }).pipe(
73
+ Effect.orDie,
74
+ Effect.flatMap((result) =>
75
+ result?._tag === "Failure"
76
+ ? Effect.die(new Error(`Singleton "${name}" failed: ${result.message}`))
77
+ : Effect.void
78
+ )
79
+ ),
80
+ entityNamespace,
81
+ workflowNamespace,
82
+ queueNamespace,
83
+ singletonNamespace,
84
+ context
85
+ }
86
+ return handle
87
+ })
88
+
89
+ /**
90
+ * The plan-time handle: the deploy-machine evaluation only discovers the
91
+ * Durable Object declarations, and nothing returned from the Worker init
92
+ * program runs outside the deployed isolate.
93
+ *
94
+ * @internal
95
+ */
96
+ export const inertClusterHandle = (): Cluster<never> => ({
97
+ provide: (effect) => effect,
98
+ wake: () => () => Effect.void,
99
+ get entityNamespace() {
100
+ return planTimeOnly("entityNamespace")
101
+ },
102
+ get workflowNamespace() {
103
+ return planTimeOnly("workflowNamespace")
104
+ },
105
+ get queueNamespace() {
106
+ return planTimeOnly("queueNamespace")
107
+ },
108
+ get singletonNamespace() {
109
+ return planTimeOnly("singletonNamespace")
110
+ },
111
+ get context() {
112
+ return planTimeOnly("context")
113
+ }
114
+ })
115
+
116
+ const planTimeOnly = (field: string): never => {
117
+ throw new Error(
118
+ `AlchemyCloudflareCluster: '${field}' is only available at runtime, not during plan evaluation`
119
+ )
120
+ }
@@ -0,0 +1,15 @@
1
+ /** @internal */
2
+
3
+ /**
4
+ * Inserts refreshing the key's recency, then evicts the oldest entry once the
5
+ * map exceeds `capacity`.
6
+ *
7
+ * @internal
8
+ */
9
+ export const setWithEviction = <K, V>(map: Map<K, V>, key: K, value: V, capacity: number): void => {
10
+ map.delete(key)
11
+ map.set(key, value)
12
+ if (map.size <= capacity) return
13
+ const oldest = map.keys().next().value
14
+ if (oldest !== undefined) map.delete(oldest)
15
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Shared cluster handle types without the Alchemy dependency graph.
3
+ *
4
+ * @since 4.1.3
5
+ */
6
+ import type * as Context from "effect/Context"
7
+ import type * as Effect from "effect/Effect"
8
+
9
+ /**
10
+ * The handle returned by `AlchemyCloudflareCluster.make`.
11
+ *
12
+ * **Gotchas**
13
+ *
14
+ * Namespace bindings and `context` are available only in the deployed runtime.
15
+ * Reading them during plan evaluation throws a diagnostic; `provide` and
16
+ * `wake` remain inert during planning.
17
+ *
18
+ * @category models
19
+ * @since 4.1.3
20
+ */
21
+ export interface Cluster<in R = never> {
22
+ /**
23
+ * Attaches the built cluster context to a user Effect, for the Worker's
24
+ * `fetch` handler and other entrypoints. Only provides the already-built
25
+ * Context; it does not open a new Scope.
26
+ */
27
+ readonly provide: <A, E, R2>(effect: Effect.Effect<A, E, R2>) => Effect.Effect<A, E, Exclude<R2, R>>
28
+ /**
29
+ * A handler that wakes the named singleton, for `Cloudflare.Workers.cron`.
30
+ * The name is the singleton name without the `Singleton/` prefix, and the
31
+ * returned handler is already context-provided. A `ClusterCron` fires only
32
+ * once its seed singleton is woken; get its name from
33
+ * `CloudflareCluster.clusterCronSingletonName`.
34
+ */
35
+ readonly wake: (name: string) => () => Effect.Effect<void>
36
+ /**
37
+ * The native namespace binding of the cluster entity class.
38
+ */
39
+ readonly entityNamespace: DurableObjectNamespace
40
+ /**
41
+ * The native namespace binding of the workflow class.
42
+ */
43
+ readonly workflowNamespace: DurableObjectNamespace
44
+ /**
45
+ * The native namespace binding of the durable queue class.
46
+ */
47
+ readonly queueNamespace: DurableObjectNamespace
48
+ /**
49
+ * The native namespace binding of the singleton class.
50
+ */
51
+ readonly singletonNamespace: DurableObjectNamespace
52
+ /**
53
+ * The Context built from the cluster layer and the user layer into the
54
+ * isolate-lifetime Scope.
55
+ */
56
+ readonly context: Context.Context<R>
57
+ }
@@ -0,0 +1,19 @@
1
+ import type { ClusterName } from "../CloudflareCluster.ts"
2
+
3
+ /** @internal */
4
+ export const encodeName = (type: string, id: string): string => `${type.length}:${type}${id}`
5
+
6
+ const lengthPrefix = /^([1-9]\d*):/
7
+
8
+ /** @internal */
9
+ export const decodeName = (name: string): ClusterName | undefined => {
10
+ const match = lengthPrefix.exec(name)
11
+ if (match === null) return undefined
12
+ const typeLength = Number(match[1])
13
+ const payload = name.slice(match[0].length)
14
+ if (typeLength > payload.length) return undefined
15
+ return {
16
+ type: payload.slice(0, typeLength),
17
+ id: payload.slice(typeLength)
18
+ }
19
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Keep-alive for one entity Durable Object, in three separate pieces of
3
+ * state:
4
+ *
5
+ * - `wanted` mirrors the persisted `keep_alive` flag that a restart restores.
6
+ * - `hold` is the fiber running the self `hold()` RPC while one is in flight.
7
+ * - The holder count belongs to the current handler build only. Persisting it
8
+ * would break on restart: an `EntityResource` re-acquired by the rebuild
9
+ * adds a holder on top of the stored count, which then never returns to 0.
10
+ *
11
+ * @internal
12
+ */
13
+ import * as Duration from "effect/Duration"
14
+ import * as Effect from "effect/Effect"
15
+ import type * as Fiber from "effect/Fiber"
16
+ import * as Latch from "effect/Latch"
17
+ import * as Schedule from "effect/Schedule"
18
+
19
+ /** @internal */
20
+ export interface EntityKeepAlive {
21
+ /** The `Entity.keepAlive` handler. */
22
+ readonly update: (enabled: boolean) => Effect.Effect<void>
23
+ /** The body of the self `hold()` RPC; resolves once the hold is released. */
24
+ readonly await: Effect.Effect<void>
25
+ /** Forgets the holders of a closed handler build. The hold stays. */
26
+ readonly resetHolders: () => void
27
+ /** Starts the self hold when keep-alive is wanted and none is in flight. */
28
+ readonly restore: () => void
29
+ }
30
+
31
+ /** @internal */
32
+ export interface EntityKeepAliveOptions {
33
+ /** Starts the self `hold()` RPC; the promise settles when the hold ends. */
34
+ readonly startHold: () => Promise<void>
35
+ /** The persisted flag, read when the Durable Object starts. */
36
+ readonly wanted: boolean
37
+ /**
38
+ * Persists each requested flag in a forked fiber. The hold is released only
39
+ * after a successful `false` write while keep-alive is still unwanted.
40
+ */
41
+ readonly persist: (wanted: boolean) => Effect.Effect<void>
42
+ /** Upper bound of the retry delay after a failed hold RPC. */
43
+ readonly retryCapMillis: () => number
44
+ }
45
+
46
+ /** @internal */
47
+ export const makeEntityKeepAlive = (options: EntityKeepAliveOptions): EntityKeepAlive => {
48
+ const latch = Latch.makeUnsafe(true)
49
+ let holders = 0
50
+ let wanted = options.wanted
51
+ let hold: Fiber.Fiber<void, unknown> | undefined
52
+
53
+ // A safeguard only: production probes never saw a rejected hold.
54
+ const runHold = Effect.tryPromise(() => options.startHold()).pipe(
55
+ Effect.tapError((error) => Effect.logWarning("Entity keep-alive hold failed, retrying", error)),
56
+ Effect.retry(
57
+ Schedule.exponential(100).pipe(
58
+ Schedule.modifyDelay(({ duration }) =>
59
+ Effect.succeed(Duration.min(duration, Duration.millis(options.retryCapMillis())))
60
+ ),
61
+ Schedule.jittered
62
+ )
63
+ )
64
+ )
65
+
66
+ const restore = () => {
67
+ if (!wanted || hold !== undefined) return
68
+ latch.closeUnsafe()
69
+ const fiber = Effect.runFork(runHold)
70
+ hold = fiber
71
+ fiber.addObserver(() => {
72
+ if (hold === fiber) hold = undefined
73
+ })
74
+ }
75
+
76
+ const release = () => {
77
+ if (hold === undefined) return
78
+ hold.interruptUnsafe()
79
+ hold = undefined
80
+ latch.openUnsafe()
81
+ }
82
+
83
+ const setWanted = (next: boolean) => {
84
+ if (wanted === next) return
85
+ wanted = next
86
+ // Mailbox writes wait for an open user transaction, so writing inline
87
+ // would deadlock `Entity.keepAlive` called inside `withTransaction`.
88
+ Effect.runFork(
89
+ Effect.suspend(() => options.persist(next)).pipe(
90
+ Effect.andThen(Effect.sync(() => {
91
+ if (!next && !wanted) release()
92
+ })),
93
+ Effect.catchCause((cause) => Effect.logError("Entity keep-alive persistence failed", cause))
94
+ )
95
+ )
96
+ }
97
+
98
+ const update = (enabled: boolean): Effect.Effect<void> =>
99
+ Effect.sync(() => {
100
+ if (enabled) {
101
+ holders++
102
+ setWanted(true)
103
+ // Also restarts a hold that ended without a release.
104
+ restore()
105
+ return
106
+ }
107
+ // With no holders in memory the flag may still be set: a fresh object
108
+ // after a restart has count 0 while its stored row says 1.
109
+ if (holders > 0) holders--
110
+ if (holders === 0) setWanted(false)
111
+ })
112
+
113
+ return {
114
+ update,
115
+ await: latch.await,
116
+ resetHolders: () => {
117
+ holders = 0
118
+ },
119
+ restore
120
+ }
121
+ }