@effect/platform-cloudflare 0.0.0 → 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 +506 -0
  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 +77 -1
  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,443 @@
1
+ /**
2
+ * The workflow Durable Object runtime. One object holds one workflow
3
+ * execution: its payload, run result, activity results keyed
4
+ * `${name}/${attempt}`, durable deferred exits, and the clock due table behind
5
+ * the single alarm. Handlers are looked up in the module-level workflow
6
+ * registry and built once per wake; a crash or hibernation wipes RAM and the
7
+ * next contact replays the execution from SQLite.
8
+ *
9
+ * @internal
10
+ */
11
+ import type { SqlStorage } from "@cloudflare/workers-types"
12
+ import * as Cause from "effect/Cause"
13
+ import * as Context from "effect/Context"
14
+ import * as Duration from "effect/Duration"
15
+ import * as Effect from "effect/Effect"
16
+ import * as Exit from "effect/Exit"
17
+ import * as Fiber from "effect/Fiber"
18
+ import * as Schedule from "effect/Schedule"
19
+ import * as Scope from "effect/Scope"
20
+ import * as DurableClock from "effect/workflow/DurableClock"
21
+ import * as Workflow from "effect/workflow/Workflow"
22
+ import * as WorkflowEngine from "effect/workflow/WorkflowEngine"
23
+ import { decodeName, encodeName } from "./clusterName.ts"
24
+ import { armAlarm, type EntityAlarm } from "./entityStorage.ts"
25
+ import {
26
+ CurrentExecutionHandle,
27
+ deferredState,
28
+ getWorkflowRegistration,
29
+ type WorkflowRunOptions,
30
+ type WorkflowStub
31
+ } from "./workflowRegistry.ts"
32
+ import * as WorkflowStorage from "./workflowStorage.ts"
33
+ import { decodePayload, encodeDefectResult, encodeExit, encodeResult } from "./workflowWire.ts"
34
+
35
+ /** @internal */
36
+ export const InterruptSignalName = "Workflow/InterruptSignal"
37
+
38
+ const resumeGuardMillis = 60_000
39
+
40
+ let voidExitCache: Promise<string> | undefined
41
+ const voidExitText = (): Promise<
42
+ string
43
+ > => (voidExitCache ??= Effect.runPromise(encodeExit(Exit.void, Context.empty())))
44
+
45
+ const mergeWake = (
46
+ current: WorkflowStorage.WakeMode | undefined,
47
+ mode: WorkflowStorage.WakeMode
48
+ ): WorkflowStorage.WakeMode => current === "explicit" ? current : mode
49
+
50
+ /** @internal */
51
+ export interface WorkflowRuntimeOptions {
52
+ readonly name: string
53
+ readonly sql: SqlStorage
54
+ readonly alarm: EntityAlarm
55
+ readonly now: () => number
56
+ readonly waitUntil: (promise: Promise<unknown>) => void
57
+ readonly getStub: (name: string) => WorkflowStub
58
+ }
59
+
60
+ /** @internal */
61
+ export interface WorkflowRuntime extends WorkflowStub {
62
+ readonly executionId: string
63
+ readonly loadActivity: (key: string) => string | undefined
64
+ readonly saveActivity: (key: string, exit: string) => void
65
+ readonly loadDeferred: (name: string) => string | undefined
66
+ readonly completeOwnDeferred: (name: string, exit: string) => Effect.Effect<void>
67
+ readonly runAlarm: () => Promise<void>
68
+ }
69
+
70
+ interface Inflight {
71
+ readonly instance: WorkflowEngine.WorkflowInstance["Service"]
72
+ readonly fiber: Fiber.Fiber<{
73
+ readonly result: Workflow.Result<unknown, unknown>
74
+ readonly text: string
75
+ }, unknown>
76
+ readonly promise: Promise<string>
77
+ }
78
+
79
+ /** @internal */
80
+ export const makeWorkflowRuntime = (options: WorkflowRuntimeOptions): WorkflowRuntime => {
81
+ const name = decodeName(options.name)
82
+ if (name === undefined) {
83
+ throw new Error("ClusterWorkflow requires a canonical workflow Durable Object name")
84
+ }
85
+ const workflowName = name.type
86
+ const executionId = name.id
87
+ const sql = options.sql
88
+
89
+ WorkflowStorage.ensureWorkflowStorage(sql)
90
+
91
+ let inflight: Inflight | undefined
92
+ let resumeRequested: WorkflowStorage.WakeMode | undefined
93
+ // Parent of every attempt's scope. As in the memory engine, scopes of
94
+ // attempts that suspended stay open under it, so their finalizers and
95
+ // resources last until the execution completes or an attempt defects.
96
+ let executionScope = Scope.makeUnsafe()
97
+
98
+ // The final attempt's scope has already closed with the real exit. Closing
99
+ // the parent closes earlier attempts' scopes in reverse order with
100
+ // `Exit.void`, so their superseded compensations are skipped. A fresh
101
+ // parent is swapped in first: a later replay must not fork from a closed
102
+ // scope, which would run its finalizers as soon as they are registered.
103
+ const closeExecutionScope = Effect.suspend(() => {
104
+ const closing = executionScope
105
+ executionScope = Scope.makeUnsafe()
106
+ return Scope.close(closing, Exit.void)
107
+ })
108
+
109
+ const detach = (promise: Promise<unknown>): void => {
110
+ options.waitUntil(promise.then(() => undefined, () => undefined))
111
+ }
112
+
113
+ const parseResult = (result: string) => JSON.parse(result) as { readonly _tag?: unknown; readonly failed?: unknown }
114
+
115
+ const isComplete = (result: string | undefined): boolean =>
116
+ result !== undefined && parseResult(result)._tag === "Complete"
117
+
118
+ // As in the memory engine, only an explicit wake replays a failure suspension.
119
+ const isUnwakeable = (result: string | undefined, mode: WorkflowStorage.WakeMode): boolean => {
120
+ if (result === undefined) return false
121
+ const parsed = parseResult(result)
122
+ return parsed._tag === "Complete" || (mode === "automatic" && parsed.failed === true)
123
+ }
124
+
125
+ /**
126
+ * Records a wake durably. A running attempt queues it and decides when it
127
+ * settles, since its result may still become a failure suspension. Returns
128
+ * whether a replay is now pending.
129
+ */
130
+ const requestWake = (mode: WorkflowStorage.WakeMode): boolean => {
131
+ const row = WorkflowStorage.loadExecution(sql)
132
+ if (row === undefined || isComplete(row.result)) return false
133
+ if (inflight === undefined && isUnwakeable(row.result, mode)) return false
134
+ const pending = mergeWake(row.resumePending, mode)
135
+ if (pending !== row.resumePending) WorkflowStorage.setResumePending(sql, pending)
136
+ if (inflight !== undefined) resumeRequested = mergeWake(resumeRequested, mode)
137
+ return true
138
+ }
139
+
140
+ const armResumeGuard = Effect.suspend(() => armAlarm(options.alarm, options.now() + resumeGuardMillis))
141
+
142
+ const resumeParent = (parent: { readonly workflowName: string; readonly executionId: string }) =>
143
+ Effect.promise(() => options.getStub(encodeName(parent.workflowName, parent.executionId)).resume("automatic")).pipe(
144
+ Effect.sandbox,
145
+ Effect.retry({ times: 5, schedule: Schedule.exponential(200) })
146
+ )
147
+
148
+ const logResumeParentFailure = (cause: Cause.Cause<unknown>) =>
149
+ Effect.logError("Workflow parent resume failed", cause)
150
+
151
+ const deliverParentWake = Effect.suspend(() => {
152
+ const row = WorkflowStorage.loadExecution(sql)
153
+ if (row === undefined || row.resumePending === undefined || !isComplete(row.result) || row.parent === undefined) {
154
+ return Effect.void
155
+ }
156
+ // Re-arm before sending so a crash or exhausted retries leave another attempt.
157
+ return armResumeGuard.pipe(
158
+ Effect.andThen(
159
+ resumeParent(row.parent).pipe(
160
+ Effect.tap(() => Effect.sync(() => WorkflowStorage.setResumePending(sql, undefined))),
161
+ Effect.catchCause(logResumeParentFailure)
162
+ )
163
+ )
164
+ )
165
+ })
166
+
167
+ // An attempt the machinery cannot finish (a defect with `CaptureDefects`
168
+ // off, a payload that no longer decodes, a result that fails to encode, a
169
+ // dying finalizer, a missing registration) completes the execution with the
170
+ // defect, as `ClusterWorkflowEngine` stores an exhausted defect retry. The
171
+ // object never rejects `run` for such a reason, so a caller can tell the
172
+ // child's own defect from a transport failure, and a parent replaying a
173
+ // discarded child finds the result instead of restarting it.
174
+ const failAttempt = (cause: Cause.Cause<unknown>): Promise<string> =>
175
+ Effect.runPromise(Effect.gen(function*() {
176
+ yield* Effect.logError("Workflow execution failed", cause)
177
+ const text = yield* encodeDefectResult(Cause.squash(cause))
178
+ const parent = WorkflowStorage.loadExecution(sql)?.parent
179
+ if (parent !== undefined) yield* armResumeGuard
180
+ WorkflowStorage.saveCompletion(sql, text, parent !== undefined)
181
+ inflight = undefined
182
+ resumeRequested = undefined
183
+ if (parent !== undefined) options.waitUntil(Effect.runPromise(deliverParentWake))
184
+ return text
185
+ }))
186
+
187
+ const startAttempt = (row: WorkflowStorage.ExecutionRow): Promise<string> => {
188
+ const registration = getWorkflowRegistration(workflowName)
189
+ if (registration === undefined) {
190
+ return failAttempt(Cause.die(new Error(`No workflow registered for name: ${workflowName}`)))
191
+ }
192
+ const workflow = registration.workflow
193
+ const instance = WorkflowEngine.WorkflowInstance.initial(workflow, executionId, {
194
+ scope: Scope.forkUnsafe(executionScope)
195
+ })
196
+ const execute = decodePayload(workflow, row.payload, registration.context).pipe(
197
+ Effect.flatMap((payload) => registration.execute(payload, executionId) as Effect.Effect<unknown, unknown>),
198
+ Effect.onExit((exit) => {
199
+ const suspendOnFailure = Context.get(workflow.annotations, Workflow.SuspendOnFailure)
200
+ if (!instance.suspended && !(suspendOnFailure && exit._tag === "Failure")) {
201
+ return Effect.void
202
+ }
203
+ if (WorkflowStorage.loadDeferred(sql, InterruptSignalName) === undefined) {
204
+ return Effect.void
205
+ }
206
+ instance.suspended = false
207
+ instance.interrupted = true
208
+ return Effect.withFiber<void>((fiber) => Effect.interruptible(Fiber.interrupt(fiber)))
209
+ }),
210
+ Workflow.intoResult,
211
+ (effect) => deferredState.trackRun(instance, effect),
212
+ Effect.flatMap((result) =>
213
+ Effect.map(encodeResult(workflow, result, registration.context), (text) => ({ result, text }))
214
+ ),
215
+ Effect.provideService(DurableClock.InMemoryThreshold, Duration.zero),
216
+ Effect.provideService(CurrentExecutionHandle, runtime)
217
+ )
218
+ const persist = ({ result, text }: {
219
+ readonly result: Workflow.Result<unknown, unknown>
220
+ readonly text: string
221
+ }): Promise<string> =>
222
+ Effect.runPromise(
223
+ Effect.gen(function*() {
224
+ const parent = WorkflowStorage.loadExecution(sql)?.parent
225
+ if (result._tag === "Complete" && parent !== undefined) {
226
+ // Arm before the atomic result/pending save so a persisted
227
+ // completion always has a delivery wake.
228
+ yield* armResumeGuard
229
+ }
230
+ // Stay in flight until the result is saved so a concurrent resume
231
+ // queues behind this attempt instead of replaying alongside it.
232
+ if (result._tag === "Complete") {
233
+ WorkflowStorage.saveCompletion(sql, text, parent !== undefined)
234
+ inflight = undefined
235
+ resumeRequested = undefined
236
+ if (parent !== undefined) options.waitUntil(Effect.runPromise(deliverParentWake))
237
+ return text
238
+ }
239
+ const replay = resumeRequested !== undefined && !isUnwakeable(text, resumeRequested)
240
+ ? resumeRequested
241
+ : undefined
242
+ WorkflowStorage.saveSuspension(sql, text, replay)
243
+ inflight = undefined
244
+ resumeRequested = undefined
245
+ if (replay !== undefined) detach(startAttempt(row))
246
+ return text
247
+ }).pipe(Effect.tapCause(() =>
248
+ Effect.sync(() => {
249
+ inflight = undefined
250
+ })
251
+ ))
252
+ )
253
+ const finish = (attempt: {
254
+ readonly result: Workflow.Result<unknown, unknown>
255
+ readonly text: string
256
+ }): Promise<string> => {
257
+ if (attempt.result._tag !== "Complete") return persist(attempt)
258
+ // Close retained scopes before saving, so a durable completion never
259
+ // has unrun finalizers. A dying finalizer fails the attempt instead.
260
+ return Effect.runPromise(Effect.exit(closeExecutionScope)).then((closed) =>
261
+ Exit.isSuccess(closed) ? persist(attempt) : failAttempt(closed.cause)
262
+ )
263
+ }
264
+ const fiber = Effect.runFork(execute)
265
+ const promise = Effect.runPromise(Fiber.await(fiber)).then((exit) => {
266
+ if (Exit.isSuccess(exit)) return finish(exit.value)
267
+ if (Cause.hasInterruptsOnly(exit.cause) && (instance.interrupted || instance.suspended)) {
268
+ // A hard interrupt (or a completion preempting the fiber after it
269
+ // already produced its result) kills the fiber before it can encode;
270
+ // synthesize the result from the instance state so it still persists.
271
+ const result: Workflow.Result<unknown, unknown> = instance.interrupted
272
+ ? new Workflow.Complete({ exit: exit as Exit.Exit<unknown, unknown> })
273
+ : new Workflow.Suspended({})
274
+ return Effect.runPromise(encodeResult(workflow, result, registration.context))
275
+ .then((text) => finish({ result, text }))
276
+ }
277
+ // A defect ends every retained attempt; a replay starts in fresh scopes.
278
+ // Stay in flight while closing so cleanup precedes any replay.
279
+ return Effect.runPromise(Effect.exit(closeExecutionScope)).then((closed) =>
280
+ failAttempt(Exit.isSuccess(closed) ? exit.cause : Cause.combine(exit.cause, closed.cause))
281
+ )
282
+ })
283
+ inflight = { instance, fiber, promise }
284
+ return promise
285
+ }
286
+
287
+ const run = (payload: string, opts: WorkflowRunOptions): Promise<string> => {
288
+ let row = WorkflowStorage.loadExecution(sql)
289
+ if (row === undefined) {
290
+ WorkflowStorage.createExecution(sql, workflowName, executionId, payload, opts.parent)
291
+ row = { workflowName, executionId, payload, parent: opts.parent, result: undefined, resumePending: undefined }
292
+ } else if (opts.parent !== undefined && row.parent === undefined) {
293
+ // An execution started standalone can gain a parent later; keep the
294
+ // first parent so its completion still wakes that parent.
295
+ WorkflowStorage.setParent(sql, opts.parent)
296
+ row = { ...row, parent: opts.parent }
297
+ }
298
+ if (inflight !== undefined) return opts.discard ? Promise.resolve("") : inflight.promise
299
+ if (row.result !== undefined) return Promise.resolve(opts.discard ? "" : row.result)
300
+ // The attempt stays pending under the guard alarm until it persists a
301
+ // result, so an attempt lost with this object is replayed even when no
302
+ // caller awaits it.
303
+ WorkflowStorage.setResumePending(sql, mergeWake(row.resumePending, "automatic"))
304
+ const attempt = startAttempt(row)
305
+ const armed = Effect.runPromise(armResumeGuard)
306
+ if (opts.discard) {
307
+ detach(attempt)
308
+ return armed.then(() => "")
309
+ }
310
+ return Promise.all([attempt, armed]).then(([text]) => text)
311
+ }
312
+
313
+ const startPending = (): void => {
314
+ const row = WorkflowStorage.loadExecution(sql)
315
+ if (row === undefined || row.resumePending === undefined || inflight !== undefined) return
316
+ if (isUnwakeable(row.result, row.resumePending)) return
317
+ detach(startAttempt(row))
318
+ }
319
+
320
+ const resume = (mode: WorkflowStorage.WakeMode = "explicit"): Promise<void> => {
321
+ if (!requestWake(mode)) return Promise.resolve()
322
+ // Persist and guard the resume before acknowledging it, so an abort after
323
+ // the RPC returns is still recovered.
324
+ return Effect.runPromise(armResumeGuard).then(startPending)
325
+ }
326
+
327
+ const completeDeferred = (
328
+ deferredName: string,
329
+ exitText: string,
330
+ mode: WorkflowStorage.WakeMode
331
+ ): Promise<void> => {
332
+ if (!WorkflowStorage.saveDeferred(sql, deferredName, exitText)) return Promise.resolve()
333
+ // Persisted with the exit in the same write batch: if this wake is lost
334
+ // before the replay finishes, the next wake (or the guard alarm) replays
335
+ // the execution. The in-flight request is set synchronously so a settling
336
+ // attempt cannot clear the pending resume without a restart.
337
+ if (!requestWake(mode)) return Promise.resolve()
338
+ return Effect.runPromise(
339
+ armResumeGuard.pipe(Effect.andThen(deferredState.deferredDone(executionId, deferredName)))
340
+ ).then(() => resume(mode))
341
+ }
342
+
343
+ const deferredDone = (deferredName: string, exitText: string): Promise<void> =>
344
+ completeDeferred(deferredName, exitText, "automatic")
345
+
346
+ // The caller belongs to the run this wake interrupts, so awaiting that run
347
+ // would wait on the caller's own cleanup. As in the memory engine, the wake
348
+ // starts on a separate fiber after the caller continues.
349
+ const completeOwnDeferred = (deferredName: string, exitText: string): Effect.Effect<void> =>
350
+ Effect.suspend(() => {
351
+ if (!WorkflowStorage.saveDeferred(sql, deferredName, exitText) || !requestWake("automatic")) {
352
+ return Effect.void
353
+ }
354
+ const wake = deferredState.deferredDone(executionId, deferredName).pipe(
355
+ Effect.andThen(
356
+ Effect.suspend(() => inflight === undefined ? Effect.promise(() => resume("automatic")) : Effect.void)
357
+ )
358
+ )
359
+ return armResumeGuard.pipe(
360
+ Effect.andThen(Effect.forkDetach(wake)),
361
+ Effect.flatMap((fiber) => Effect.sync(() => detach(Effect.runPromise(Fiber.await(fiber)))))
362
+ )
363
+ })
364
+
365
+ const interrupt = (): Promise<void> => {
366
+ const row = WorkflowStorage.loadExecution(sql)
367
+ if (row === undefined || isComplete(row.result)) return Promise.resolve()
368
+ return voidExitText().then((exitText) => completeDeferred(InterruptSignalName, exitText, "explicit"))
369
+ }
370
+
371
+ const interruptUnsafe = (): Promise<void> => {
372
+ const current = inflight
373
+ const signalled = interrupt()
374
+ if (current === undefined) return signalled
375
+ current.instance.interrupted = true
376
+ return signalled
377
+ .then(() => Effect.runPromise(Fiber.interrupt(current.fiber)))
378
+ .then(() => undefined)
379
+ }
380
+
381
+ const scheduleClock = (clockName: string, deferredName: string, wakeUp: number): Promise<void> => {
382
+ WorkflowStorage.saveClock(sql, clockName, deferredName, wakeUp)
383
+ const earliest = WorkflowStorage.earliestClockWakeUp(sql)
384
+ return earliest === undefined ? Promise.resolve() : Effect.runPromise(armAlarm(options.alarm, earliest))
385
+ }
386
+
387
+ const runAlarm = (): Promise<void> =>
388
+ voidExitText().then((voidExit) => {
389
+ const completed = WorkflowStorage.dueClocks(sql, options.now()).filter((clock) => {
390
+ WorkflowStorage.markClockFired(sql, clock.name)
391
+ return WorkflowStorage.saveDeferred(sql, clock.deferredName, voidExit)
392
+ })
393
+ if (completed.length > 0) requestWake("automatic")
394
+ return Effect.runPromise(Effect.forEach(
395
+ completed,
396
+ (clock) => deferredState.deferredDone(executionId, clock.deferredName),
397
+ { discard: true }
398
+ )).then(() => {
399
+ const row = WorkflowStorage.loadExecution(sql)
400
+ const mode = row?.resumePending
401
+ const pending = mode !== undefined && !isUnwakeable(row?.result, mode)
402
+ // A live attempt is not lost: only re-arm the guard. Requesting a wake
403
+ // would replay the attempt once it suspends, for no reason.
404
+ return (pending && inflight === undefined ? resume(mode) : Promise.resolve()).then(() => {
405
+ // While a resume is pending a guard alarm stays armed, so a replay
406
+ // lost with this isolate is retried instead of sleeping forever.
407
+ const targets = [
408
+ WorkflowStorage.earliestClockWakeUp(sql),
409
+ pending ? options.now() + resumeGuardMillis : undefined
410
+ ].filter((target) => target !== undefined)
411
+ if (targets.length === 0) return undefined
412
+ // The alarm that fired is spent, so set the next one outright:
413
+ // `armAlarm` only ever moves an alarm earlier.
414
+ return options.alarm.setAlarm(Math.min(...targets))
415
+ })
416
+ })
417
+ }).then(() => Effect.runPromise(deliverParentWake)).then(() => undefined)
418
+
419
+ const runtime: WorkflowRuntime = {
420
+ executionId,
421
+ run,
422
+ poll: () => Promise.resolve(WorkflowStorage.loadExecution(sql)?.result),
423
+ resume,
424
+ interrupt,
425
+ interruptUnsafe,
426
+ deferredDone,
427
+ scheduleClock,
428
+ runAlarm,
429
+ loadActivity: (key) => WorkflowStorage.loadActivity(sql, key),
430
+ saveActivity: (key, exit) => WorkflowStorage.saveActivity(sql, key, exit),
431
+ loadDeferred: (deferredName) => WorkflowStorage.loadDeferred(sql, deferredName),
432
+ completeOwnDeferred
433
+ }
434
+
435
+ // Self-heal on wake: a resume recorded by deferredDone but lost with the
436
+ // previous isolate replays now instead of waiting for external contact.
437
+ const pending = WorkflowStorage.loadExecution(sql)?.resumePending
438
+ if (pending !== undefined) {
439
+ void resume(pending)
440
+ }
441
+
442
+ return runtime
443
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Storage glue for the workflow Durable Object. The constructor must stay
3
+ * cheap: open SQLite, ensure the tables, and re-arm the single alarm from the
4
+ * earliest pending clock. No workflow handlers are built here.
5
+ *
6
+ * @internal
7
+ */
8
+ import type { SqlStorage } from "@cloudflare/workers-types"
9
+
10
+ const ddl = [
11
+ `CREATE TABLE IF NOT EXISTS workflow_execution (
12
+ id INTEGER PRIMARY KEY CHECK (id = 0),
13
+ workflow_name TEXT NOT NULL,
14
+ execution_id TEXT NOT NULL,
15
+ payload TEXT NOT NULL,
16
+ parent_name TEXT,
17
+ parent_execution_id TEXT,
18
+ result TEXT,
19
+ resume_pending INTEGER NOT NULL DEFAULT 0
20
+ )`,
21
+ `CREATE TABLE IF NOT EXISTS workflow_activities (
22
+ key TEXT PRIMARY KEY,
23
+ exit TEXT NOT NULL
24
+ )`,
25
+ `CREATE TABLE IF NOT EXISTS workflow_deferreds (
26
+ name TEXT PRIMARY KEY,
27
+ exit TEXT NOT NULL
28
+ )`,
29
+ `CREATE TABLE IF NOT EXISTS workflow_clocks (
30
+ name TEXT PRIMARY KEY,
31
+ deferred_name TEXT NOT NULL,
32
+ wake_up INTEGER NOT NULL,
33
+ fired INTEGER NOT NULL DEFAULT 0
34
+ )`
35
+ ]
36
+
37
+ /** @internal */
38
+ export const ensureWorkflowStorage = (sql: SqlStorage): void => {
39
+ for (const statement of ddl) {
40
+ sql.exec(statement)
41
+ }
42
+ }
43
+
44
+ /**
45
+ * How a replay was requested. An explicit wake (`resume`, `interrupt`) always
46
+ * replays; an automatic wake (deferred or clock completion, a child's
47
+ * completion) leaves a failure-suspended execution parked.
48
+ *
49
+ * @internal
50
+ */
51
+ export type WakeMode = "explicit" | "automatic"
52
+
53
+ /**
54
+ * The stored `(workflowName, executionId)` also serve to recover the object
55
+ * name on an alarm wake, where `ctx.id.name` is undefined.
56
+ *
57
+ * @internal
58
+ */
59
+ export interface ExecutionRow {
60
+ readonly workflowName: string
61
+ readonly executionId: string
62
+ readonly payload: string
63
+ readonly parent: { readonly workflowName: string; readonly executionId: string } | undefined
64
+ readonly result: string | undefined
65
+ /**
66
+ * The pending replay of an incomplete execution. Once complete, any value
67
+ * means the parent completion wake is still undelivered.
68
+ */
69
+ readonly resumePending: WakeMode | undefined
70
+ }
71
+
72
+ type StoredExecutionRow = {
73
+ readonly workflow_name: string
74
+ readonly execution_id: string
75
+ readonly payload: string
76
+ readonly parent_name: string | null
77
+ readonly parent_execution_id: string | null
78
+ readonly result: string | null
79
+ readonly resume_pending: number
80
+ }
81
+
82
+ type ExitRow = {
83
+ readonly exit: string
84
+ }
85
+
86
+ type ClockWakeUpRow = {
87
+ readonly wake_up: number | null
88
+ }
89
+
90
+ type ClockRow = {
91
+ readonly name: string
92
+ readonly deferred_name: string
93
+ }
94
+
95
+ // 1 predates wake modes, so a row written before them replays as explicit.
96
+ const encodeWakeMode = (mode: WakeMode | undefined): number => mode === undefined ? 0 : mode === "explicit" ? 1 : 2
97
+
98
+ const decodeWakeMode = (value: number): WakeMode | undefined =>
99
+ value === 1 ? "explicit" : value === 2 ? "automatic" : undefined
100
+
101
+ /** @internal */
102
+ export const loadExecution = (sql: SqlStorage): ExecutionRow | undefined => {
103
+ const row = sql.exec<StoredExecutionRow>(
104
+ `SELECT workflow_name, execution_id, payload, parent_name, parent_execution_id, result, resume_pending
105
+ FROM workflow_execution WHERE id = 0`
106
+ ).toArray()[0]
107
+ if (row === undefined) return undefined
108
+ return {
109
+ workflowName: row.workflow_name,
110
+ executionId: row.execution_id,
111
+ payload: row.payload,
112
+ parent: row.parent_name !== null && row.parent_execution_id !== null
113
+ ? { workflowName: row.parent_name, executionId: row.parent_execution_id }
114
+ : undefined,
115
+ result: row.result ?? undefined,
116
+ resumePending: decodeWakeMode(row.resume_pending)
117
+ }
118
+ }
119
+
120
+ /** @internal */
121
+ export const createExecution = (
122
+ sql: SqlStorage,
123
+ workflowName: string,
124
+ executionId: string,
125
+ payload: string,
126
+ parent: { readonly workflowName: string; readonly executionId: string } | undefined
127
+ ): void => {
128
+ sql.exec(
129
+ `INSERT OR IGNORE INTO workflow_execution
130
+ (id, workflow_name, execution_id, payload, parent_name, parent_execution_id, result)
131
+ VALUES (0, ?, ?, ?, ?, ?, NULL)`,
132
+ workflowName,
133
+ executionId,
134
+ payload,
135
+ parent?.workflowName ?? null,
136
+ parent?.executionId ?? null
137
+ )
138
+ }
139
+
140
+ /** @internal */
141
+ export const setParent = (
142
+ sql: SqlStorage,
143
+ parent: { readonly workflowName: string; readonly executionId: string }
144
+ ): void => {
145
+ sql.exec(
146
+ "UPDATE workflow_execution SET parent_name = ?, parent_execution_id = ? WHERE id = 0 AND parent_name IS NULL",
147
+ parent.workflowName,
148
+ parent.executionId
149
+ )
150
+ }
151
+
152
+ /** @internal */
153
+ export const saveSuspension = (sql: SqlStorage, result: string, pending: WakeMode | undefined): void => {
154
+ sql.exec(
155
+ "UPDATE workflow_execution SET result = ?, resume_pending = ? WHERE id = 0",
156
+ result,
157
+ encodeWakeMode(pending)
158
+ )
159
+ }
160
+
161
+ /** @internal */
162
+ export const saveCompletion = (sql: SqlStorage, result: string, wakeParent: boolean): void => {
163
+ sql.exec("UPDATE workflow_execution SET result = ?, resume_pending = ? WHERE id = 0", result, wakeParent ? 1 : 0)
164
+ }
165
+
166
+ /** @internal */
167
+ export const setResumePending = (sql: SqlStorage, pending: WakeMode | undefined): void => {
168
+ sql.exec("UPDATE workflow_execution SET resume_pending = ? WHERE id = 0", encodeWakeMode(pending))
169
+ }
170
+
171
+ /** @internal */
172
+ export const loadActivity = (sql: SqlStorage, key: string): string | undefined => {
173
+ const row = sql.exec<ExitRow>("SELECT exit FROM workflow_activities WHERE key = ?", key).toArray()[0]
174
+ return row?.exit
175
+ }
176
+
177
+ /** @internal */
178
+ export const saveActivity = (sql: SqlStorage, key: string, exit: string): void => {
179
+ sql.exec("INSERT OR IGNORE INTO workflow_activities (key, exit) VALUES (?, ?)", key, exit)
180
+ }
181
+
182
+ /** @internal */
183
+ export const loadDeferred = (sql: SqlStorage, name: string): string | undefined => {
184
+ const row = sql.exec<ExitRow>("SELECT exit FROM workflow_deferreds WHERE name = ?", name).toArray()[0]
185
+ return row?.exit
186
+ }
187
+
188
+ /**
189
+ * First write wins; safe without a conflict clause because a Durable Object's
190
+ * SQLite access is single-threaded.
191
+ *
192
+ * @internal
193
+ */
194
+ export const saveDeferred = (sql: SqlStorage, name: string, exit: string): boolean => {
195
+ if (loadDeferred(sql, name) !== undefined) return false
196
+ sql.exec("INSERT INTO workflow_deferreds (name, exit) VALUES (?, ?)", name, exit)
197
+ return true
198
+ }
199
+
200
+ /** @internal */
201
+ export const saveClock = (sql: SqlStorage, name: string, deferredName: string, wakeUp: number): void => {
202
+ sql.exec(
203
+ "INSERT OR IGNORE INTO workflow_clocks (name, deferred_name, wake_up, fired) VALUES (?, ?, ?, 0)",
204
+ name,
205
+ deferredName,
206
+ wakeUp
207
+ )
208
+ }
209
+
210
+ /** @internal */
211
+ export const earliestClockWakeUp = (sql: SqlStorage): number | undefined => {
212
+ const row = sql.exec<ClockWakeUpRow>(
213
+ "SELECT min(wake_up) AS wake_up FROM workflow_clocks WHERE fired = 0"
214
+ ).toArray()[0]
215
+ return row?.wake_up ?? undefined
216
+ }
217
+
218
+ /** @internal */
219
+ export const dueClocks = (
220
+ sql: SqlStorage,
221
+ now: number
222
+ ): Array<{ readonly name: string; readonly deferredName: string }> =>
223
+ sql.exec<ClockRow>(
224
+ "SELECT name, deferred_name FROM workflow_clocks WHERE fired = 0 AND wake_up <= ?",
225
+ now
226
+ ).toArray().map((row) => ({
227
+ name: row.name,
228
+ deferredName: row.deferred_name
229
+ }))
230
+
231
+ /** @internal */
232
+ export const markClockFired = (sql: SqlStorage, name: string): void => {
233
+ sql.exec("UPDATE workflow_clocks SET fired = 1 WHERE name = ?", name)
234
+ }