@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,255 @@
1
+ /**
2
+ * Storage glue for the durable queue Durable Object. The constructor must stay
3
+ * cheap: open SQLite, ensure the queue table, and re-arm the single alarm from
4
+ * the earliest pending lease expiry or initial visibility deadline.
5
+ *
6
+ * Completed rows are retained (not deleted) so custom-id deduplication
7
+ * survives completion, matching the SQL-backed store. A failed item moves to
8
+ * the back of the queue, so a poisoned item cannot hot-loop the head while
9
+ * still being retried ahead of items offered after its failure.
10
+ *
11
+ * `completed` holds the row state: 0 while pending, processing, or waiting to
12
+ * retry, 1 once completed, and 2 once its final attempt failed. An
13
+ * `"untilExecuted"` offer replaces a row in state 1 or 2.
14
+ *
15
+ * Each lease records its taker in `lease_owner` and the taker's
16
+ * `max_attempts`. Settling or extending a lease only applies while that taker
17
+ * still owns the row, so a taker whose lease expired cannot settle work another
18
+ * taker now holds or a row an `"untilExecuted"` offer re-created.
19
+ *
20
+ * Leasing a row counts an attempt, so a taker that crashes still consumes one.
21
+ * An expired lease keeps its owner until another taker leases the row; if it
22
+ * used the row's last attempt the row moves to the failed state (2). A release
23
+ * refunds the attempt. A recorded failure or release clears the owner, so a
24
+ * retried `fail` or `release` cannot settle the same attempt twice.
25
+ * A recorded failure that leaves the row pending stores its retry delay in
26
+ * `lease_until` with no owner, the same delayed visibility an offer uses.
27
+ * Lookups by taker go through `lease_owner`, so a retried take or a cancel
28
+ * finds its row after the object restarts.
29
+ *
30
+ * @internal
31
+ */
32
+ import type { SqlStorage } from "@cloudflare/workers-types"
33
+ import type { DurableQueueItem } from "../CloudflareDurableObjectPrograms.ts"
34
+
35
+ const ddl = [
36
+ `CREATE TABLE IF NOT EXISTS queue_items (
37
+ id TEXT PRIMARY KEY,
38
+ element TEXT NOT NULL,
39
+ position INTEGER NOT NULL,
40
+ attempts INTEGER NOT NULL DEFAULT 0,
41
+ completed INTEGER NOT NULL DEFAULT 0,
42
+ lease_until INTEGER,
43
+ lease_owner TEXT,
44
+ max_attempts INTEGER,
45
+ last_failure TEXT
46
+ )`,
47
+ `CREATE INDEX IF NOT EXISTS queue_items_take_idx
48
+ ON queue_items (completed, position)`,
49
+ `CREATE INDEX IF NOT EXISTS queue_items_lease_idx
50
+ ON queue_items (lease_until)`,
51
+ // MAX(position) in offerItem/failItem needs a bare position index; the
52
+ // (completed, position) index cannot serve it and completed rows are
53
+ // retained forever for dedup.
54
+ `CREATE INDEX IF NOT EXISTS queue_items_position_idx
55
+ ON queue_items (position)`,
56
+ // A retried take and a cancel look the taker's row up by owner.
57
+ `CREATE INDEX IF NOT EXISTS queue_items_owner_idx
58
+ ON queue_items (lease_owner)`
59
+ ]
60
+
61
+ /** @internal */
62
+ export const ensureQueueStorage = (sql: SqlStorage): void => {
63
+ for (const statement of ddl) {
64
+ sql.exec(statement)
65
+ }
66
+ }
67
+
68
+ /** @internal */
69
+ export type QueueItem = DurableQueueItem
70
+
71
+ type QueueItemRow = {
72
+ readonly id: string
73
+ readonly element: string
74
+ readonly attempts: number
75
+ }
76
+
77
+ type LeaseExpiryRow = {
78
+ readonly lease_until: number | null
79
+ }
80
+
81
+ /** @internal */
82
+ export const offerItem = (
83
+ sql: SqlStorage,
84
+ id: string,
85
+ element: string,
86
+ visibleAt: number | null,
87
+ deduplicate?: "untilExecuted" | "untilRemoved" | undefined
88
+ ): void => {
89
+ if (deduplicate === "untilExecuted") {
90
+ // A completed or failed row releases the id. Deleting it lets the insert
91
+ // below store the offer as new work after every existing row. Both
92
+ // statements run synchronously, so no other offer can interleave.
93
+ sql.exec("DELETE FROM queue_items WHERE id = ? AND completed <> 0", id)
94
+ }
95
+ // Reuse lease_until for initial visibility; INSERT OR IGNORE preserves the
96
+ // original deadline and payload when an id is offered again.
97
+ sql.exec(
98
+ `INSERT OR IGNORE INTO queue_items (id, element, position, lease_until)
99
+ VALUES (?, ?, (SELECT IFNULL(MAX(position), 0) + 1 FROM queue_items), ?)`,
100
+ id,
101
+ element,
102
+ visibleAt
103
+ )
104
+ }
105
+
106
+ /**
107
+ * Leases the next available item and counts the attempt. The returned
108
+ * `attempts` is the number of earlier attempts, not counting this one.
109
+ *
110
+ * @internal
111
+ */
112
+ export const leaseNextItem = (
113
+ sql: SqlStorage,
114
+ now: number,
115
+ leaseUntil: number,
116
+ maxAttempts: number,
117
+ owner: string
118
+ ): QueueItem | undefined => {
119
+ const row = sql.exec<QueueItemRow>(
120
+ `SELECT id, element, attempts FROM queue_items
121
+ WHERE completed = 0 AND attempts < ? AND (lease_until IS NULL OR lease_until <= ?)
122
+ ORDER BY position ASC LIMIT 1`,
123
+ maxAttempts,
124
+ now
125
+ ).toArray()[0]
126
+ if (row === undefined) return undefined
127
+ sql.exec(
128
+ `UPDATE queue_items SET attempts = attempts + 1, lease_until = ?, lease_owner = ?, max_attempts = ?
129
+ WHERE id = ?`,
130
+ leaseUntil,
131
+ owner,
132
+ maxAttempts,
133
+ row.id
134
+ )
135
+ return row
136
+ }
137
+
138
+ /**
139
+ * The item still leased to `owner` under a live lease, so a retried take can
140
+ * return it. `attempts` counts earlier attempts, as `leaseNextItem` returns.
141
+ *
142
+ * @internal
143
+ */
144
+ export const leasedItem = (sql: SqlStorage, owner: string, now: number): QueueItem | undefined =>
145
+ sql.exec<QueueItemRow>(
146
+ `SELECT id, element, attempts - 1 AS attempts FROM queue_items
147
+ WHERE lease_owner = ? AND completed = 0 AND lease_until > ?
148
+ LIMIT 1`,
149
+ owner,
150
+ now
151
+ ).toArray()[0]
152
+
153
+ /** @internal */
154
+ export const completeItem = (sql: SqlStorage, id: string, owner: string): void => {
155
+ sql.exec("UPDATE queue_items SET completed = 1, lease_until = NULL WHERE id = ? AND lease_owner = ?", id, owner)
156
+ }
157
+
158
+ /**
159
+ * Records a failed attempt, which the lease already counted. When the attempt
160
+ * reaches the `max_attempts` stored with the lease, or the failure is
161
+ * `terminal`, the row moves to the failed state (2). A row that is already
162
+ * completed is left alone, even when the caller was its last owner.
163
+ * A row that stays pending is hidden until `visibleAt` (null for no delay).
164
+ * Clearing `lease_owner` makes a replayed call a no-op until the next lease
165
+ * assigns a new owner.
166
+ *
167
+ * @internal
168
+ */
169
+ export const failItem = (
170
+ sql: SqlStorage,
171
+ id: string,
172
+ lastFailure: string,
173
+ owner: string,
174
+ terminal: boolean,
175
+ visibleAt: number | null
176
+ ): void => {
177
+ const failed = "? OR attempts >= max_attempts"
178
+ sql.exec(
179
+ `UPDATE queue_items
180
+ SET lease_until = CASE WHEN ${failed} THEN NULL ELSE ? END, lease_owner = NULL, last_failure = ?,
181
+ completed = CASE WHEN ${failed} THEN 2 ELSE 0 END,
182
+ position = (SELECT IFNULL(MAX(position), 0) + 1 FROM queue_items)
183
+ WHERE id = ? AND lease_owner = ? AND completed = 0`,
184
+ terminal ? 1 : 0,
185
+ visibleAt,
186
+ lastFailure,
187
+ terminal ? 1 : 0,
188
+ id,
189
+ owner
190
+ )
191
+ }
192
+
193
+ /**
194
+ * Returns the item to the queue and refunds the attempt its lease counted.
195
+ *
196
+ * @internal
197
+ */
198
+ export const releaseItem = (sql: SqlStorage, id: string, owner: string): void => {
199
+ sql.exec(
200
+ `UPDATE queue_items SET attempts = attempts - 1, lease_until = NULL, lease_owner = NULL
201
+ WHERE id = ? AND lease_owner = ? AND completed = 0`,
202
+ id,
203
+ owner
204
+ )
205
+ }
206
+
207
+ /**
208
+ * Releases the item `owner` holds under a live lease, as `releaseItem` does.
209
+ * An expired lease can still carry the same owner until its alarm runs; it
210
+ * keeps its spent attempt so it can fail on its own.
211
+ *
212
+ * @internal
213
+ */
214
+ export const releaseOwnedItem = (sql: SqlStorage, owner: string, now: number): void => {
215
+ sql.exec(
216
+ `UPDATE queue_items SET attempts = attempts - 1, lease_until = NULL, lease_owner = NULL
217
+ WHERE lease_owner = ? AND completed = 0 AND lease_until > ?`,
218
+ owner,
219
+ now
220
+ )
221
+ }
222
+
223
+ /** @internal */
224
+ export const extendLease = (sql: SqlStorage, id: string, leaseUntil: number, owner: string): void => {
225
+ sql.exec(
226
+ "UPDATE queue_items SET lease_until = ? WHERE id = ? AND lease_owner = ? AND lease_until IS NOT NULL",
227
+ leaseUntil,
228
+ id,
229
+ owner
230
+ )
231
+ }
232
+
233
+ /**
234
+ * Expires due leases and initial visibility deadlines. A lease that used the
235
+ * row's last attempt moves the row to the failed state (2).
236
+ *
237
+ * @internal
238
+ */
239
+ export const expireLeases = (sql: SqlStorage, now: number): void => {
240
+ sql.exec(
241
+ `UPDATE queue_items
242
+ SET lease_until = NULL,
243
+ completed = CASE WHEN lease_owner IS NOT NULL AND attempts >= max_attempts THEN 2 ELSE 0 END
244
+ WHERE lease_until IS NOT NULL AND lease_until <= ? AND completed = 0`,
245
+ now
246
+ )
247
+ }
248
+
249
+ /** @internal */
250
+ export const earliestLeaseExpiry = (sql: SqlStorage): number | undefined => {
251
+ const row = sql.exec<LeaseExpiryRow>(
252
+ "SELECT min(lease_until) AS lease_until FROM queue_items WHERE lease_until IS NOT NULL"
253
+ ).toArray()[0]
254
+ return row?.lease_until ?? undefined
255
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A module-level registration map shared between the Worker layer and the
3
+ * Durable Object instances of the same isolate. Unregistering only removes
4
+ * the exact registration that was added, so a finalizer racing a re-register
5
+ * cannot drop the replacement.
6
+ *
7
+ * @internal
8
+ */
9
+
10
+ /** @internal */
11
+ export interface Registry<A> {
12
+ readonly get: (key: string) => A | undefined
13
+ readonly register: (key: string, value: A) => boolean
14
+ readonly unregister: (key: string, value: A) => void
15
+ }
16
+
17
+ /** @internal */
18
+ export const makeRegistry = <A>(): Registry<A> => {
19
+ const registrations = new Map<string, A>()
20
+ return {
21
+ get: (key) => registrations.get(key),
22
+ register: (key, value) => {
23
+ if (registrations.has(key)) return false
24
+ registrations.set(key, value)
25
+ return true
26
+ },
27
+ unregister: (key, value) => {
28
+ if (registrations.get(key) === value) registrations.delete(key)
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,21 @@
1
+ /** @internal */
2
+ import type * as Context from "effect/Context"
3
+ import type * as Effect from "effect/Effect"
4
+ import { makeRegistry } from "./registry.ts"
5
+
6
+ /** @internal */
7
+ export interface SingletonRegistration {
8
+ readonly run: Effect.Effect<void, unknown, never>
9
+ readonly context: Context.Context<never>
10
+ }
11
+
12
+ const registry = makeRegistry<SingletonRegistration>()
13
+
14
+ /** @internal */
15
+ export const getSingletonRegistration: (name: string) => SingletonRegistration | undefined = registry.get
16
+
17
+ /** @internal */
18
+ export const registerSingleton: (name: string, registration: SingletonRegistration) => boolean = registry.register
19
+
20
+ /** @internal */
21
+ export const unregisterSingleton: (name: string, registration: SingletonRegistration) => void = registry.unregister
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Runs one singleton effect for each Worker Cron Trigger wake. Concurrent
3
+ * duplicate wakes are ignored while the accepted wake is still running; once
4
+ * it returns, the object has no live work and Cloudflare may hibernate it.
5
+ *
6
+ * While a wake runs, a watchdog alarm stays armed in the future. If it fires
7
+ * mid-run it is re-armed, so losing the isolate never leaves the pending wake
8
+ * without an alarm to resume it.
9
+ *
10
+ * @internal
11
+ */
12
+ import type { DurableObjectStorage, SqlStorage } from "@cloudflare/workers-types"
13
+ import * as Effect from "effect/Effect"
14
+ import { armAlarm } from "./entityStorage.ts"
15
+ import {
16
+ beginSingletonWake,
17
+ completeSingletonWake,
18
+ ensureSingletonStorage,
19
+ loadSingletonState
20
+ } from "./singletonStorage.ts"
21
+
22
+ /**
23
+ * How far ahead the watchdog alarm is armed while a wake is running.
24
+ */
25
+ const watchdogMillis = 60_000
26
+
27
+ /** @internal */
28
+ export interface SingletonRuntimeOptions {
29
+ readonly sql: SqlStorage
30
+ readonly alarm: Pick<DurableObjectStorage, "deleteAlarm" | "getAlarm" | "setAlarm">
31
+ readonly now: () => number
32
+ readonly run: Effect.Effect<void, unknown>
33
+ }
34
+
35
+ /** @internal */
36
+ export interface SingletonRuntime {
37
+ readonly wake: () => Promise<void>
38
+ readonly runAlarm: () => Promise<void>
39
+ }
40
+
41
+ /** @internal */
42
+ export const makeSingletonRuntime = (options: SingletonRuntimeOptions): SingletonRuntime => {
43
+ ensureSingletonStorage(options.sql)
44
+ let inFlight: Promise<void> | undefined
45
+
46
+ const armWatchdog = (): Effect.Effect<void> =>
47
+ Effect.suspend(() => armAlarm(options.alarm, options.now() + watchdogMillis))
48
+
49
+ const runPending = (arm: boolean): Promise<void> => {
50
+ const run = options.run.pipe(
51
+ Effect.ensuring(
52
+ Effect.promise(() => options.alarm.deleteAlarm()).pipe(
53
+ Effect.ensuring(Effect.sync(() => completeSingletonWake(options.sql)))
54
+ )
55
+ )
56
+ )
57
+ const operation = Effect.runPromise(arm ? Effect.andThen(armWatchdog(), run) : run)
58
+ inFlight = operation
59
+ void operation.then(
60
+ () => {
61
+ inFlight = undefined
62
+ },
63
+ () => {
64
+ inFlight = undefined
65
+ }
66
+ )
67
+ return operation
68
+ }
69
+
70
+ return {
71
+ wake: () => {
72
+ if (inFlight !== undefined) return Promise.resolve()
73
+ const pending = loadSingletonState(options.sql).wakeAt
74
+ if (pending !== undefined) return runPending(false)
75
+ const now = options.now()
76
+ if (!beginSingletonWake(options.sql, now)) return Promise.resolve()
77
+ return runPending(true)
78
+ },
79
+
80
+ // Cloudflare consumes the alarm before invoking this handler, so an alarm
81
+ // that fires during a run must arm the next watchdog before returning.
82
+ runAlarm: () => {
83
+ if (loadSingletonState(options.sql).wakeAt === undefined) return Promise.resolve()
84
+ if (inFlight !== undefined) return Effect.runPromise(armWatchdog())
85
+ return runPending(false)
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Storage glue for the singleton Durable Object. A pending row is a watchdog:
3
+ * a normal wake clears it after the effect returns, while an isolate crash
4
+ * leaves it for the constructor to re-arm on the next alarm wake.
5
+ *
6
+ * @internal
7
+ */
8
+ import type { SqlStorage } from "@cloudflare/workers-types"
9
+
10
+ const ddl = `CREATE TABLE IF NOT EXISTS singleton_state (
11
+ id INTEGER PRIMARY KEY CHECK (id = 1),
12
+ name TEXT,
13
+ wake_at INTEGER
14
+ )`
15
+
16
+ /** @internal */
17
+ export const ensureSingletonStorage = (sql: SqlStorage): void => {
18
+ sql.exec(ddl)
19
+ }
20
+
21
+ /** @internal */
22
+ export interface SingletonState {
23
+ readonly name: string | undefined
24
+ readonly wakeAt: number | undefined
25
+ }
26
+
27
+ type SingletonStateRow = {
28
+ readonly name: string | null
29
+ readonly wake_at: number | null
30
+ }
31
+
32
+ /** @internal */
33
+ export const loadSingletonState = (sql: SqlStorage): SingletonState => {
34
+ const row = sql.exec<SingletonStateRow>("SELECT name, wake_at FROM singleton_state WHERE id = 1").toArray()[0]
35
+ return {
36
+ name: row?.name ?? undefined,
37
+ wakeAt: row?.wake_at ?? undefined
38
+ }
39
+ }
40
+
41
+ /** @internal */
42
+ export const rememberSingletonName = (sql: SqlStorage, name: string): void => {
43
+ sql.exec(
44
+ `INSERT INTO singleton_state (id, name) VALUES (1, ?)
45
+ ON CONFLICT(id) DO UPDATE SET name = excluded.name`,
46
+ name
47
+ )
48
+ }
49
+
50
+ /** @internal */
51
+ export const beginSingletonWake = (sql: SqlStorage, wakeAt: number): boolean => {
52
+ if (loadSingletonState(sql).wakeAt !== undefined) return false
53
+ sql.exec(
54
+ `INSERT INTO singleton_state (id, wake_at) VALUES (1, ?)
55
+ ON CONFLICT(id) DO UPDATE SET wake_at = excluded.wake_at`,
56
+ wakeAt
57
+ )
58
+ return true
59
+ }
60
+
61
+ /** @internal */
62
+ export const completeSingletonWake = (sql: SqlStorage): void => {
63
+ sql.exec("UPDATE singleton_state SET wake_at = NULL WHERE id = 1")
64
+ }
@@ -0,0 +1,67 @@
1
+ /** @internal */
2
+ import { PersistenceError } from "effect/cluster/ClusterError"
3
+ import * as Effect from "effect/Effect"
4
+ import * as Schedule from "effect/Schedule"
5
+
6
+ /**
7
+ * The flags Cloudflare sets on the Error a failed Durable Object call rejects
8
+ * with.
9
+ *
10
+ * @internal
11
+ */
12
+ export interface TransportErrorFlags {
13
+ readonly retryable?: boolean | undefined
14
+ readonly overloaded?: boolean | undefined
15
+ readonly remote?: boolean | undefined
16
+ }
17
+
18
+ /**
19
+ * Whether a failed Durable Object call may be retried. Cloudflare marks
20
+ * transient failures `retryable`, and an `overloaded` object must not be
21
+ * retried, as retrying only adds load.
22
+ *
23
+ * @internal
24
+ */
25
+ export const isRetryable = (error: PersistenceError): boolean => {
26
+ const cause = error.cause as TransportErrorFlags | null | undefined
27
+ return typeof cause === "object" && cause !== null && cause.retryable === true && cause.overloaded !== true
28
+ }
29
+
30
+ /**
31
+ * The bounded retry policy for idempotent calls outside a workflow: a few
32
+ * quick attempts, then the caller sees the typed error.
33
+ *
34
+ * @internal
35
+ */
36
+ export const boundedRetry: Schedule.Schedule<unknown> = Schedule.max([
37
+ Schedule.exponential(100, 2),
38
+ Schedule.recurs(4)
39
+ ])
40
+
41
+ /**
42
+ * Makes one Worker-to-Durable Object stub call.
43
+ *
44
+ * **Details**
45
+ *
46
+ * A rejection becomes a `PersistenceError` whose `cause` is the rejected
47
+ * value, so Cloudflare's `retryable`, `overloaded` and `remote` flags stay
48
+ * readable. Each attempt resolves a fresh stub with `resolve`, because
49
+ * Cloudflare advises against reusing a stub after it throws. With `retry`, the
50
+ * call is retried on that schedule while the error is retryable; only pass it
51
+ * for idempotent calls.
52
+ *
53
+ * @internal
54
+ */
55
+ export const callDurableObject = <Stub, A>(options: {
56
+ readonly resolve: () => Stub
57
+ readonly call: (stub: Stub) => Promise<A>
58
+ readonly retry?: Schedule.Schedule<unknown, PersistenceError> | undefined
59
+ }): Effect.Effect<A, PersistenceError> => {
60
+ const attempt = Effect.tryPromise({
61
+ try: () => options.call(options.resolve()),
62
+ catch: (cause) => new PersistenceError({ cause })
63
+ })
64
+ return options.retry === undefined
65
+ ? attempt
66
+ : Effect.retry(attempt, { schedule: options.retry, while: isRetryable })
67
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Module-level workflow state shared between the Worker layer and the workflow
3
+ * Durable Object instances of the same isolate. Registrations are recorded at
4
+ * Worker init; a running execution provides its own handle through
5
+ * `CurrentExecutionHandle` so engine operations inside the run hit local
6
+ * SQLite instead of a self-RPC.
7
+ *
8
+ * @internal
9
+ */
10
+ import * as Context from "effect/Context"
11
+ import type * as Effect from "effect/Effect"
12
+ import type * as Workflow from "effect/workflow/Workflow"
13
+ import * as WorkflowEngine from "effect/workflow/WorkflowEngine"
14
+ import type { ClusterWorkflowRunOptions, ClusterWorkflowWakeMode } from "../CloudflareDurableObjectPrograms.ts"
15
+ import { makeRegistry } from "./registry.ts"
16
+
17
+ /** @internal */
18
+ export interface WorkflowRegistration {
19
+ readonly workflow: Workflow.Any
20
+ readonly execute: (
21
+ payload: object,
22
+ executionId: string
23
+ ) => Effect.Effect<unknown, unknown, WorkflowEngine.WorkflowInstance | WorkflowEngine.WorkflowEngine>
24
+ readonly context: Context.Context<never>
25
+ }
26
+
27
+ const registry = makeRegistry<WorkflowRegistration>()
28
+
29
+ /** @internal */
30
+ export const getWorkflowRegistration: (name: string) => WorkflowRegistration | undefined = registry.get
31
+
32
+ /** @internal */
33
+ export const registerWorkflow: (name: string, registration: WorkflowRegistration) => boolean = registry.register
34
+
35
+ /** @internal */
36
+ export const unregisterWorkflow: (name: string, registration: WorkflowRegistration) => void = registry.unregister
37
+
38
+ /** @internal */
39
+ export type WorkflowRunOptions = ClusterWorkflowRunOptions
40
+
41
+ /**
42
+ * The transport shared by workflow Durable Object stubs and same-isolate
43
+ * execution handles. All payloads are JSON text.
44
+ *
45
+ * @internal
46
+ */
47
+ export interface WorkflowStub {
48
+ readonly run: (payload: string, options: WorkflowRunOptions) => Promise<string>
49
+ readonly poll: () => Promise<string | undefined>
50
+ readonly resume: (mode?: ClusterWorkflowWakeMode) => Promise<void>
51
+ readonly interrupt: () => Promise<void>
52
+ readonly interruptUnsafe: () => Promise<void>
53
+ readonly deferredDone: (name: string, exit: string) => Promise<void>
54
+ readonly scheduleClock: (name: string, deferredName: string, wakeUp: number) => Promise<void>
55
+ }
56
+
57
+ /** @internal */
58
+ export interface WorkflowExecutionHandle extends WorkflowStub {
59
+ readonly executionId: string
60
+ readonly loadActivity: (key: string) => string | undefined
61
+ readonly saveActivity: (key: string, exit: string) => void
62
+ readonly loadDeferred: (name: string) => string | undefined
63
+ /** Completes a deferred of this execution from inside its own run. */
64
+ readonly completeOwnDeferred: (name: string, exit: string) => Effect.Effect<void>
65
+ }
66
+
67
+ /**
68
+ * The handle of the workflow execution currently running in this fiber,
69
+ * provided by the workflow Durable Object runtime around each run attempt.
70
+ *
71
+ * @internal
72
+ */
73
+ export class CurrentExecutionHandle extends Context.Service<CurrentExecutionHandle, WorkflowExecutionHandle>()(
74
+ "@effect/platform-cloudflare/CloudflareWorkflowEngine/CurrentExecutionHandle"
75
+ ) {}
76
+
77
+ /** @internal */
78
+ export const deferredState = WorkflowEngine.makeDeferredState()