@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,430 @@
1
+ /**
2
+ * The Durable Object classes behind `CloudflareCluster.layer`.
3
+ *
4
+ * A Worker using the Cloudflare cluster re-exports these four classes from its
5
+ * entry module and binds each one in `wrangler.jsonc` as a SQLite-backed
6
+ * Durable Object class. The cluster resolves objects through the same-Worker
7
+ * namespace bindings only; none of these classes serve a public route, and any
8
+ * direct `fetch` of an object is rejected.
9
+ *
10
+ * @stability experimental
11
+ * @since 4.1.3
12
+ */
13
+ import { DurableObject } from "cloudflare:workers"
14
+ import * as Effect from "effect/Effect"
15
+ import {
16
+ type ClusterDurableQueueProgram,
17
+ type ClusterEntityProgram,
18
+ type ClusterSingletonProgram,
19
+ type ClusterWorkflowProgram,
20
+ type ClusterWorkflowRunOptions,
21
+ type ClusterWorkflowWakeMode,
22
+ type DurableQueueItem,
23
+ type EntityDeliveryOptions,
24
+ type EntityInvokeResult,
25
+ makeClusterDurableQueueProgram,
26
+ makeClusterEntityProgram,
27
+ makeClusterSingletonProgram,
28
+ makeClusterWorkflowProgram,
29
+ type SingletonWakeResult
30
+ } from "./CloudflareDurableObjectPrograms.ts"
31
+
32
+ const notExposed = (className: string) => () => {
33
+ throw new Error(
34
+ `@effect/platform-cloudflare: ${className} is not exposed over fetch, use the same-Worker namespace binding`
35
+ )
36
+ }
37
+
38
+ /**
39
+ * Initializes the Worker application before a Cloudflare cluster Durable
40
+ * Object handles requests or alarms. The initializer is shared by all four
41
+ * Durable Object classes and runs once per Worker isolate unless a build fails
42
+ * or stalls.
43
+ *
44
+ * **Details**
45
+ *
46
+ * The initializer runs inside a Durable Object constructor's
47
+ * `blockConcurrencyWhile`, which Cloudflare cancels after 30 seconds. It must
48
+ * complete within 15 seconds: a build still pending after that is treated as
49
+ * abandoned, and a waiting object runs the initializer again.
50
+ *
51
+ * @stability experimental
52
+ * @category models
53
+ * @since 4.1.3
54
+ */
55
+ export type Initializer<Env = unknown, A = unknown> = (env: Env) => PromiseLike<A>
56
+
57
+ interface InitializationAttempt {
58
+ readonly startedAt: number
59
+ exit: {
60
+ readonly _tag: "Success"
61
+ readonly value: unknown
62
+ } | {
63
+ readonly _tag: "Failure"
64
+ readonly error: unknown
65
+ } | undefined
66
+ }
67
+
68
+ // Durable Object waiters run inside `blockConcurrencyWhile`, which Cloudflare
69
+ // cancels after 30 seconds. A build still pending after 15 seconds is treated
70
+ // as abandoned. Only a Durable Object waiter that has itself waited less than
71
+ // 15 seconds starts a build, whether it takes over a stale one or retries after
72
+ // a failure. The first build and any replacement therefore both get at least
73
+ // 15 seconds.
74
+ const staleInitializationMillis = 15_000
75
+ const durableObjectTakeoverMillis = 15_000
76
+ const initializationPollMillis = 20
77
+
78
+ let initializer: Initializer | undefined
79
+ let initialization: InitializationAttempt | undefined
80
+
81
+ /**
82
+ * Registers the Worker application initializer used by the Cloudflare cluster
83
+ * Durable Object classes.
84
+ *
85
+ * **Details**
86
+ *
87
+ * Call this once from the Worker entry module with the same lazy application
88
+ * build used by its request handlers. Every entity, workflow, queue, and
89
+ * singleton object blocks requests and alarms until that build completes. A
90
+ * failure is reported to every caller already waiting on that build, and the
91
+ * next cold caller runs the initializer again. The initializer must complete
92
+ * within 15 seconds. A build still pending after that is treated as abandoned,
93
+ * and a waiter runs the initializer itself, so a build can run more than once
94
+ * in an isolate. A Durable Object waiter starts a build only while it has
95
+ * waited less than 15 seconds, so any replacement keeps at least 15 seconds of
96
+ * the 30 second `blockConcurrencyWhile` limit. The returned function joins the
97
+ * same readiness state from Worker handlers.
98
+ *
99
+ * @stability experimental
100
+ * @category initialization
101
+ * @since 4.1.3
102
+ */
103
+ export const setInitializer = <Env, A>(handler: Initializer<Env, A>): (env: Env) => Promise<A> => {
104
+ if (initializer !== undefined) {
105
+ throw new Error("@effect/platform-cloudflare: the Durable Object initializer is already set")
106
+ }
107
+ initializer = handler as Initializer
108
+ // Worker handlers are not bounded by `blockConcurrencyWhile`, so they may
109
+ // take over a stale build however long they have waited.
110
+ return (env) => waitForInitialization(env, Infinity) as Promise<A>
111
+ }
112
+
113
+ const waitForInitialization = async (env: unknown, takeoverMillis: number): Promise<unknown> => {
114
+ if (initializer === undefined) return
115
+ const waitStartedAt = Date.now()
116
+ const isFresh = (): boolean => Date.now() - waitStartedAt < takeoverMillis
117
+ const canTakeOver = (attempt: InitializationAttempt): boolean => isStale(attempt) && isFresh()
118
+ // A waiter too old to take over keeps following the current attempt.
119
+ const keepsWaiting = (attempt: InitializationAttempt): boolean =>
120
+ attempt.exit === undefined && initialization === attempt && !canTakeOver(attempt)
121
+ while (true) {
122
+ const attempt = initialization
123
+ if (attempt === undefined ? isFresh() : canTakeOver(attempt)) {
124
+ return runInitializer(initializer, env)
125
+ }
126
+ if (attempt === undefined) {
127
+ // The attempt this waiter followed was replaced by one that already
128
+ // failed. A waiter this old leaves the retry to a fresher caller and
129
+ // follows the attempt that caller starts.
130
+ await scheduler.wait(initializationPollMillis)
131
+ continue
132
+ }
133
+ // Cloudflare promises belong to the request context that created them. A
134
+ // fresh wait in this context observes the shared plain-JavaScript state
135
+ // without awaiting the first Durable Object's promise from another event.
136
+ while (keepsWaiting(attempt)) {
137
+ await scheduler.wait(initializationPollMillis)
138
+ }
139
+ if (attempt.exit === undefined) continue
140
+ if (attempt.exit._tag === "Failure") throw attempt.exit.error
141
+ return attempt.exit.value
142
+ }
143
+ }
144
+
145
+ const isStale = (attempt: InitializationAttempt): boolean =>
146
+ attempt.exit === undefined && Date.now() - attempt.startedAt >= staleInitializationMillis
147
+
148
+ const runInitializer = async (run: Initializer, env: unknown): Promise<unknown> => {
149
+ const attempt: InitializationAttempt = { startedAt: Date.now(), exit: undefined }
150
+ initialization = attempt
151
+ try {
152
+ const value = await run(env)
153
+ attempt.exit = { _tag: "Success", value }
154
+ // An abandoned build that settles late still makes the isolate ready.
155
+ if (initialization?.exit?._tag !== "Success") initialization = attempt
156
+ return value
157
+ } catch (error) {
158
+ attempt.exit = { _tag: "Failure", error }
159
+ // Waiters holding this attempt see the failure; the next cold caller retries.
160
+ if (initialization === attempt) initialization = undefined
161
+ throw error
162
+ }
163
+ }
164
+
165
+ const blockOnInitialization = (
166
+ ctx: DurableObjectState,
167
+ env: unknown,
168
+ after?: (() => Promise<unknown>) | undefined
169
+ ): void => {
170
+ void ctx.blockConcurrencyWhile(async () => {
171
+ await waitForInitialization(env, durableObjectTakeoverMillis)
172
+ if (after !== undefined) await after()
173
+ })
174
+ }
175
+
176
+ /**
177
+ * The shared entity class. One instance holds one entity address; the handlers
178
+ * for every `EntityType` are registered at Worker init.
179
+ *
180
+ * **Details**
181
+ *
182
+ * The constructor stays cheap: it opens SQLite, ensures the mailbox tables,
183
+ * and re-arms the single alarm from the earliest pending `deliver_at`, or the
184
+ * keep-alive heartbeat when the entity holds `Entity.keepAlive`. User
185
+ * handlers are never built in the constructor; requests and alarms wait for
186
+ * the registered {@link setInitializer} callback, then handlers are built once
187
+ * per wake. Each build gets a `DurableObjectSqlClient` on this object's SQLite
188
+ * storage; tables whose names start with `cluster_` are reserved for the mailbox.
189
+ *
190
+ * @stability experimental
191
+ * @category durable objects
192
+ * @since 4.1.3
193
+ */
194
+ export class ClusterEntity extends DurableObject<unknown> {
195
+ // blockConcurrencyWhile completes this assignment before any RPC or alarm.
196
+ #program!: ClusterEntityProgram
197
+
198
+ constructor(ctx: DurableObjectState, env: unknown) {
199
+ super(ctx, env)
200
+ blockOnInitialization(ctx, env, async () => {
201
+ this.#program = await Effect.runPromise(makeClusterEntityProgram(ctx))
202
+ })
203
+ }
204
+
205
+ override alarm(): Promise<void> {
206
+ return Effect.runPromise(this.#program.alarm())
207
+ }
208
+
209
+ /** @internal Keeps this object non-hibernateable while entity resources have holders. */
210
+ hold(): Promise<void> {
211
+ return Effect.runPromise(this.#program.hold())
212
+ }
213
+
214
+ /** @internal Same-Worker RPC transport used by `CloudflareCluster.layer`. */
215
+ invoke(envelopeText: string, discard: boolean, delivery?: EntityDeliveryOptions): Promise<EntityInvokeResult> {
216
+ return Effect.runPromise(this.#program.invoke(envelopeText, discard, delivery))
217
+ }
218
+
219
+ /** @internal Acknowledges a streamed chunk. */
220
+ acknowledge(requestId: string, replyId: string): Promise<ReadonlyArray<string>> {
221
+ return Effect.runPromise(this.#program.acknowledge(requestId, replyId))
222
+ }
223
+
224
+ /** @internal Interrupts a handler execution and completes its persisted ask, if any. */
225
+ interrupt(storageRequestId: string, clientRequestId = storageRequestId): Promise<void> {
226
+ return Effect.runPromise(this.#program.interrupt(storageRequestId, clientRequestId))
227
+ }
228
+
229
+ /** @internal Clears stored replies so a reset request replays from scratch. */
230
+ reset(requestId: string): Promise<void> {
231
+ return Effect.runPromise(this.#program.reset(requestId))
232
+ }
233
+
234
+ /** @internal Completes an in-memory delayed ask owned by this entity object. */
235
+ deliverReply(requestId: string, reply: string): Promise<boolean> {
236
+ return Effect.runPromise(this.#program.deliverReply(requestId, reply))
237
+ }
238
+
239
+ override fetch: () => never = notExposed("ClusterEntity")
240
+ }
241
+
242
+ /**
243
+ * The workflow execution class behind `CloudflareWorkflowEngine`. One
244
+ * instance holds one workflow execution: run state, activity results keyed
245
+ * `${name}/${attempt}`, durable deferred exits, and the clock due table.
246
+ *
247
+ * **Details**
248
+ *
249
+ * The constructor stays cheap: it opens SQLite, ensures the workflow tables,
250
+ * and re-arms the single alarm from the earliest pending clock. Workflow
251
+ * requests and alarms wait for the registered {@link setInitializer} callback
252
+ * before handlers are looked up in the module-level registry and built once
253
+ * per wake.
254
+ *
255
+ * @stability experimental
256
+ * @category durable objects
257
+ * @since 4.1.3
258
+ */
259
+ export class ClusterWorkflow extends DurableObject<unknown> {
260
+ // blockConcurrencyWhile completes this assignment before any RPC or alarm.
261
+ #program!: ClusterWorkflowProgram
262
+
263
+ constructor(ctx: DurableObjectState, env: unknown) {
264
+ super(ctx, env)
265
+ blockOnInitialization(ctx, env, async () => {
266
+ this.#program = await Effect.runPromise(makeClusterWorkflowProgram(ctx))
267
+ })
268
+ }
269
+
270
+ /** @internal Same-Worker RPC transport used by `CloudflareWorkflowEngine`. */
271
+ run(payload: string, options: ClusterWorkflowRunOptions): Promise<string> {
272
+ return Effect.runPromise(this.#program.run(payload, options))
273
+ }
274
+
275
+ /** @internal */
276
+ poll(): Promise<string | undefined> {
277
+ return Effect.runPromise(this.#program.poll())
278
+ }
279
+
280
+ /** @internal */
281
+ resume(mode?: ClusterWorkflowWakeMode): Promise<void> {
282
+ return Effect.runPromise(this.#program.resume(mode))
283
+ }
284
+
285
+ /** @internal */
286
+ interrupt(): Promise<void> {
287
+ return Effect.runPromise(this.#program.interrupt())
288
+ }
289
+
290
+ /** @internal */
291
+ interruptUnsafe(): Promise<void> {
292
+ return Effect.runPromise(this.#program.interruptUnsafe())
293
+ }
294
+
295
+ /** @internal Records a durable deferred exit and resumes the execution. */
296
+ deferredDone(name: string, exit: string): Promise<void> {
297
+ return Effect.runPromise(this.#program.deferredDone(name, exit))
298
+ }
299
+
300
+ /** @internal Persists a durable clock and arms the single alarm. */
301
+ scheduleClock(name: string, deferredName: string, wakeUp: number): Promise<void> {
302
+ return Effect.runPromise(this.#program.scheduleClock(name, deferredName, wakeUp))
303
+ }
304
+
305
+ override alarm(): Promise<void> {
306
+ return Effect.runPromise(this.#program.alarm())
307
+ }
308
+
309
+ override fetch: () => never = notExposed("ClusterWorkflow")
310
+ }
311
+
312
+ /**
313
+ * The durable queue class behind the `PersistedQueue` implementation used by
314
+ * `DurableQueue`. One instance holds one named queue.
315
+ *
316
+ * **Details**
317
+ *
318
+ * The constructor stays cheap: it opens SQLite, ensures the queue table, and
319
+ * re-arms the single alarm from the earliest pending lease expiry. Items are
320
+ * leased to takers for a bounded time; the alarm watchdog expires overdue
321
+ * leases so an item whose worker died is redelivered. Requests and alarms wait
322
+ * for the registered {@link setInitializer} callback first.
323
+ *
324
+ * @stability experimental
325
+ * @category durable objects
326
+ * @since 4.1.3
327
+ */
328
+ export class ClusterDurableQueue extends DurableObject<unknown> {
329
+ // blockConcurrencyWhile completes this assignment before any RPC or alarm.
330
+ #program!: ClusterDurableQueueProgram
331
+
332
+ constructor(ctx: DurableObjectState, env: unknown) {
333
+ super(ctx, env)
334
+ blockOnInitialization(ctx, env, async () => {
335
+ this.#program = await Effect.runPromise(makeClusterDurableQueueProgram(ctx))
336
+ })
337
+ }
338
+
339
+ /** @internal Same-Worker RPC transport used by `CloudflarePersistedQueue.layer`. */
340
+ offer(
341
+ id: string,
342
+ element: string,
343
+ delayMillis?: number,
344
+ deduplicate?: "untilExecuted" | "untilRemoved"
345
+ ): Promise<void> {
346
+ return Effect.runPromise(this.#program.offer(id, element, delayMillis, deduplicate))
347
+ }
348
+
349
+ /** @internal Waits until an item is available, then leases it to the caller. */
350
+ take(takerId: string, maxAttempts: number, leaseMillis: number): Promise<DurableQueueItem> {
351
+ return Effect.runPromise(this.#program.take(takerId, maxAttempts, leaseMillis))
352
+ }
353
+
354
+ /** @internal Cancels a waiting take, releasing an item already leased to it. */
355
+ cancelTake(takerId: string): Promise<void> {
356
+ return Effect.runPromise(this.#program.cancelTake(takerId))
357
+ }
358
+
359
+ /** @internal Completes the item while the taker still owns its lease. */
360
+ complete(id: string, takerId: string): Promise<void> {
361
+ return Effect.runPromise(this.#program.complete(id, takerId))
362
+ }
363
+
364
+ /**
365
+ * @internal Records a failed attempt and requeues the item, or marks it failed
366
+ * after its final attempt or a terminal failure. A requeued item stays hidden
367
+ * for `retryDelayMillis`.
368
+ */
369
+ fail(id: string, lastFailure: string, takerId: string, terminal?: boolean, retryDelayMillis?: number): Promise<void> {
370
+ return Effect.runPromise(this.#program.fail(id, lastFailure, takerId, terminal, retryDelayMillis))
371
+ }
372
+
373
+ /** @internal Requeues the item and refunds the attempt its lease counted. */
374
+ release(id: string, takerId: string): Promise<void> {
375
+ return Effect.runPromise(this.#program.release(id, takerId))
376
+ }
377
+
378
+ /** @internal Extends the lease of an item still being processed. */
379
+ extend(id: string, leaseMillis: number, takerId: string): Promise<void> {
380
+ return Effect.runPromise(this.#program.extend(id, leaseMillis, takerId))
381
+ }
382
+
383
+ override alarm(): Promise<void> {
384
+ return Effect.runPromise(this.#program.alarm())
385
+ }
386
+
387
+ override fetch: () => never = notExposed("ClusterDurableQueue")
388
+ }
389
+
390
+ /**
391
+ * The singleton class. One object holds one registered singleton under the
392
+ * name `Singleton/<name>` and is woken by a Worker Cron Trigger.
393
+ *
394
+ * **Details**
395
+ *
396
+ * The constructor opens SQLite, ensures the singleton state table, and
397
+ * re-arms the watchdog alarm for a wake interrupted by isolate loss. `wake()`
398
+ * runs the registered effect once and returns; it never appends
399
+ * `Effect.never`, so Cloudflare may hibernate the object afterward. A failed
400
+ * run is logged and resolves `wake()` with a `Failure`, so only a transport
401
+ * failure rejects it. Requests
402
+ * and alarms wait for the registered {@link setInitializer} callback before
403
+ * consulting the singleton registry.
404
+ *
405
+ * @stability experimental
406
+ * @category durable objects
407
+ * @since 4.1.3
408
+ */
409
+ export class ClusterSingleton extends DurableObject<unknown> {
410
+ // blockConcurrencyWhile completes this assignment before any RPC or alarm.
411
+ #program!: ClusterSingletonProgram
412
+
413
+ constructor(ctx: DurableObjectState, env: unknown) {
414
+ super(ctx, env)
415
+ blockOnInitialization(ctx, env, async () => {
416
+ this.#program = await Effect.runPromise(makeClusterSingletonProgram(ctx))
417
+ })
418
+ }
419
+
420
+ /** @internal Runs one Cron Trigger fire, coalescing a concurrent duplicate. */
421
+ wake(): Promise<SingletonWakeResult> {
422
+ return Effect.runPromise(this.#program.wake())
423
+ }
424
+
425
+ override alarm(): Promise<void> {
426
+ return Effect.runPromise(this.#program.alarm())
427
+ }
428
+
429
+ override fetch: () => never = notExposed("ClusterSingleton")
430
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Runs persisted queues on the dedicated queue Durable Object class.
3
+ *
4
+ * On this path one queue name is one Durable Object: the Worker encodes the
5
+ * queue name into a Durable Object name with the same length-prefix scheme as
6
+ * entities and resolves the object through the queue namespace binding. Items,
7
+ * their attempt counts, and their in-flight leases live on the object's SQLite
8
+ * storage behind its single alarm; a take with no available item waits inside
9
+ * the object until an offer, a retry, or an expired lease produces one.
10
+ *
11
+ * Delivery is at-least-once: a taken item is leased for a bounded time and the
12
+ * lease is refreshed while the handler runs, so an item whose worker died is
13
+ * redelivered once the alarm watchdog expires the lease. Leasing an item counts
14
+ * an attempt, so a worker that dies still consumes one, and an expired lease on
15
+ * the last attempt marks the item failed.
16
+ * Offer delays use the object's clock and persist an initial visibility
17
+ * deadline; the alarm wakes waiting takers when that deadline expires.
18
+ *
19
+ * This backs the `DurableQueue` user API; `CloudflareCluster.layer` already
20
+ * includes this layer.
21
+ *
22
+ * A failed item stays hidden on the object for the configured retry delay, so
23
+ * it does not hold a consumer while other items are ready. An element that
24
+ * fails to decode is marked failed immediately.
25
+ *
26
+ * `PersistedQueue.layerCleanup` is unsupported because the namespace binding
27
+ * cannot enumerate queue objects. Completed and failed rows remain retained for
28
+ * deduplication. A `deduplicate: "untilExecuted"` offer replaces such a row
29
+ * with a new element, which keeps only the latest failure.
30
+ *
31
+ * @stability experimental
32
+ * @since 4.1.3
33
+ */
34
+ import * as Cause from "effect/Cause"
35
+ import * as Duration from "effect/Duration"
36
+ import * as Effect from "effect/Effect"
37
+ import * as Exit from "effect/Exit"
38
+ import * as Layer from "effect/Layer"
39
+ import * as PersistedQueue from "effect/persistence/PersistedQueue"
40
+ import * as Schedule from "effect/Schedule"
41
+ import { encodeName } from "./internal/clusterName.ts"
42
+ import type { QueueItem } from "./internal/queueStorage.ts"
43
+
44
+ /**
45
+ * The queue Durable Object namespace binding the store is built from.
46
+ *
47
+ * @stability experimental
48
+ * @category layers
49
+ * @since 4.1.3
50
+ */
51
+ export interface LayerOptions {
52
+ readonly queueNamespace: DurableObjectNamespace
53
+ }
54
+
55
+ interface QueueStub {
56
+ readonly offer: (
57
+ id: string,
58
+ element: string,
59
+ delayMillis?: number,
60
+ deduplicate?: "untilExecuted" | "untilRemoved"
61
+ ) => Promise<void>
62
+ readonly take: (takerId: string, maxAttempts: number, leaseMillis: number) => Promise<QueueItem>
63
+ readonly cancelTake: (takerId: string) => Promise<void>
64
+ readonly complete: (id: string, takerId: string) => Promise<void>
65
+ readonly fail: (
66
+ id: string,
67
+ lastFailure: string,
68
+ takerId: string,
69
+ terminal?: boolean,
70
+ retryDelayMillis?: number
71
+ ) => Promise<void>
72
+ readonly release: (id: string, takerId: string) => Promise<void>
73
+ readonly extend: (id: string, leaseMillis: number, takerId: string) => Promise<void>
74
+ }
75
+
76
+ const leaseMillis = 120_000
77
+ const leaseRefreshMillis = 30_000
78
+
79
+ const attempt = (run: () => Promise<void>) =>
80
+ Effect.promise(run).pipe(
81
+ Effect.sandbox,
82
+ Effect.retry({ times: 5, schedule: Schedule.exponential(100, 1.5) })
83
+ )
84
+
85
+ const finalize = (run: () => Promise<void>): Effect.Effect<void> => Effect.orDie(attempt(run))
86
+
87
+ /**
88
+ * Creates the `PersistedQueueStore` backed by the queue Durable Object
89
+ * namespace binding.
90
+ *
91
+ * @stability experimental
92
+ * @category constructors
93
+ * @since 4.1.3
94
+ */
95
+ export const make = (options: LayerOptions): PersistedQueue.PersistedQueueStore["Service"] => {
96
+ const stubFor = (name: string): QueueStub =>
97
+ options.queueNamespace.getByName(encodeName("PersistedQueue", name)) as unknown as QueueStub
98
+
99
+ return PersistedQueue.PersistedQueueStore.of({
100
+ cleanup: () =>
101
+ Effect.fail(
102
+ new PersistedQueue.PersistedQueueError({
103
+ message:
104
+ "Cloudflare persisted queue cleanup is unsupported: the namespace binding cannot enumerate queue objects"
105
+ })
106
+ ),
107
+ offer: ({ deduplicate, delay, element, id, isCustomId, name }) =>
108
+ Effect.tryPromise({
109
+ try: () =>
110
+ stubFor(name).offer(
111
+ id,
112
+ JSON.stringify(element),
113
+ delay === undefined ? 0 : Duration.toMillis(delay),
114
+ // generated ids are never offered twice
115
+ isCustomId ? deduplicate : undefined
116
+ ),
117
+ catch: (cause) =>
118
+ new PersistedQueue.PersistedQueueError({
119
+ message: "Failed to offer element to persisted queue",
120
+ cause
121
+ })
122
+ }),
123
+ take: ({ maxAttempts, name, retryDelay }) =>
124
+ // Uninterruptible outside `restore` so the release finalizer is always
125
+ // registered once an item is leased; an interrupt while still waiting
126
+ // cancels the take by taker id, releasing an item that was already
127
+ // leased to it on the object.
128
+ Effect.uninterruptibleMask((restore) =>
129
+ Effect.gen(function*() {
130
+ const takerId = crypto.randomUUID()
131
+ // A broken take RPC means the object was evicted while this taker
132
+ // waited; retrying re-enters the queue with nothing lost.
133
+ const item = yield* restore(
134
+ Effect.promise(() => stubFor(name).take(takerId, maxAttempts, leaseMillis)).pipe(
135
+ Effect.sandbox,
136
+ Effect.tapCause((cause) => Effect.logWarning("PersistedQueue take failed, retrying", cause)),
137
+ Effect.retry(Schedule.spaced(500)),
138
+ Effect.orDie
139
+ )
140
+ ).pipe(
141
+ Effect.onInterrupt(() => Effect.ignore(attempt(() => stubFor(name).cancelTake(takerId))))
142
+ )
143
+ yield* Effect.addFinalizer(Exit.match({
144
+ onFailure: (cause) =>
145
+ Cause.hasInterruptsOnly(cause)
146
+ ? finalize(() => stubFor(name).release(item.id, takerId))
147
+ : PersistedQueue.isDeadLetterCause(cause)
148
+ ? finalize(() => stubFor(name).fail(item.id, Cause.pretty(cause), takerId, true))
149
+ : Effect.flatMap(retryDelay(item.attempts + 1), (delay) =>
150
+ // The delay follows the attempt count including this one.
151
+ finalize(() =>
152
+ stubFor(name).fail(item.id, Cause.pretty(cause), takerId, false, Duration.toMillis(delay))
153
+ )),
154
+ onSuccess: () => finalize(() => stubFor(name).complete(item.id, takerId))
155
+ }))
156
+ yield* Effect.promise(() => stubFor(name).extend(item.id, leaseMillis, takerId)).pipe(
157
+ Effect.sandbox,
158
+ Effect.ignore,
159
+ Effect.schedule(Schedule.spaced(leaseRefreshMillis)),
160
+ Effect.forkScoped,
161
+ Effect.interruptible
162
+ )
163
+ return {
164
+ id: item.id,
165
+ attempts: item.attempts + 1,
166
+ element: JSON.parse(item.element)
167
+ }
168
+ })
169
+ )
170
+ })
171
+ }
172
+
173
+ /**
174
+ * Layer that provides the `PersistedQueueFactory` backed by the queue Durable
175
+ * Object namespace binding.
176
+ *
177
+ * **Details**
178
+ *
179
+ * `CloudflareCluster.layer` already includes this layer; use it directly only
180
+ * when persisted queues are needed without the rest of the cluster.
181
+ *
182
+ * @stability experimental
183
+ * @category layers
184
+ * @since 4.1.3
185
+ */
186
+ export const layer = (options: LayerOptions): Layer.Layer<PersistedQueue.PersistedQueueFactory> =>
187
+ PersistedQueue.layer.pipe(
188
+ Layer.provide(Layer.succeed(PersistedQueue.PersistedQueueStore)(make(options)))
189
+ )