@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,324 @@
1
+ /**
2
+ * SQLite mailbox primitives for a single entity Durable Object. Every
3
+ * operation returns an Effect whose body runs synchronously, so callers can
4
+ * compose them and wrap the composition in a storage transaction.
5
+ *
6
+ * @internal
7
+ */
8
+ import type { SqlStorage } from "@cloudflare/workers-types"
9
+ import * as Effect from "effect/Effect"
10
+
11
+ /** @internal */
12
+ export const mailboxCapacity = 4096
13
+
14
+ /** @internal */
15
+ export const maximumEncodedSize = 2 * 1024 * 1024
16
+
17
+ /** @internal */
18
+ export class MailboxFullError extends Error {
19
+ readonly _tag = "MailboxFull"
20
+ }
21
+
22
+ /** @internal */
23
+ export class EncodedMessageTooLargeError extends Error {
24
+ readonly _tag = "EncodedMessageTooLarge"
25
+ }
26
+
27
+ /** @internal */
28
+ export type PersistResult = {
29
+ readonly _tag: "Success"
30
+ } | {
31
+ readonly _tag: "Duplicate"
32
+ readonly originalId: string
33
+ readonly processed: boolean
34
+ }
35
+
36
+ type ExistingMessageRow = {
37
+ readonly request_id: string
38
+ readonly discard: number
39
+ readonly processed: number
40
+ readonly reply_to: string | null
41
+ }
42
+
43
+ type CountRow = {
44
+ readonly count: number
45
+ }
46
+
47
+ const textEncoder = new TextEncoder()
48
+
49
+ // A UTF-16 code unit encodes to at most 3 UTF-8 bytes, so most strings skip
50
+ // the byte-length copy.
51
+ const exceedsMaximumEncodedSize = (text: string): boolean =>
52
+ text.length * 3 > maximumEncodedSize && textEncoder.encode(text).byteLength > maximumEncodedSize
53
+
54
+ /** @internal */
55
+ export const persistRequest = (
56
+ sql: SqlStorage,
57
+ envelopeText: string,
58
+ primaryKey: string | null,
59
+ discard = false,
60
+ deliverAt: number | null = null,
61
+ replyTo: string | null = null
62
+ ): Effect.Effect<PersistResult, MailboxFullError | EncodedMessageTooLargeError> =>
63
+ Effect.suspend((): Effect.Effect<PersistResult, MailboxFullError | EncodedMessageTooLargeError> => {
64
+ if (exceedsMaximumEncodedSize(envelopeText)) {
65
+ return Effect.fail(new EncodedMessageTooLargeError("Encoded entity request exceeds 2 MB"))
66
+ }
67
+ const envelope = JSON.parse(envelopeText) as { readonly _tag?: unknown; readonly requestId?: unknown }
68
+ if (envelope._tag !== "Request" || typeof envelope.requestId !== "string") {
69
+ throw new TypeError("Expected an encoded Request envelope")
70
+ }
71
+
72
+ const existing = sql.exec<ExistingMessageRow>(
73
+ `SELECT m.request_id, m.discard, m.processed, m.reply_to
74
+ FROM cluster_messages m
75
+ WHERE m.request_id = ? OR (? IS NOT NULL AND m.message_id = ?)
76
+ LIMIT 1`,
77
+ envelope.requestId,
78
+ primaryKey,
79
+ primaryKey
80
+ ).toArray()[0]
81
+ if (existing !== undefined) {
82
+ if (replyTo !== null && existing.discard === 0 && existing.processed === 0) {
83
+ const replyTos = decodeReplyTargets(existing.reply_to)
84
+ const requestId = envelope.requestId
85
+ if (!replyTos.some((target) => target.replyTo === replyTo && target.requestId === requestId)) {
86
+ replyTos.push({ replyTo, requestId })
87
+ }
88
+ sql.exec(
89
+ "UPDATE cluster_messages SET reply_to = ? WHERE request_id = ?",
90
+ JSON.stringify(replyTos),
91
+ existing.request_id
92
+ )
93
+ }
94
+ return Effect.succeed<PersistResult>({
95
+ _tag: "Duplicate",
96
+ originalId: existing.request_id,
97
+ processed: existing.processed === 1
98
+ })
99
+ }
100
+
101
+ // A request counts against capacity until it is processed and, for streams,
102
+ // until its chunks are acknowledged. Two indexed counts instead of one
103
+ // `OR EXISTS` scan over the ever-growing dedup history.
104
+ const pending = sql.exec<CountRow>(
105
+ "SELECT COUNT(*) AS count FROM cluster_messages WHERE processed = 0"
106
+ ).toArray()[0]?.count ?? 0
107
+ const unacked = pending >= mailboxCapacity ?
108
+ 0 :
109
+ sql.exec<CountRow>(
110
+ `SELECT COUNT(DISTINCT r.request_id) AS count
111
+ FROM cluster_replies r
112
+ JOIN cluster_messages m ON m.request_id = r.request_id
113
+ WHERE r.kind = 'Chunk' AND r.acked = 0 AND m.processed = 1`
114
+ ).toArray()[0]?.count ?? 0
115
+ if (pending + unacked >= mailboxCapacity) {
116
+ return Effect.fail(new MailboxFullError("Entity mailbox has reached its 4096 request capacity"))
117
+ }
118
+
119
+ sql.exec(
120
+ `INSERT INTO cluster_messages
121
+ (request_id, message_id, envelope, discard, processed, last_reply_id, deliver_at, reply_to)
122
+ VALUES (?, ?, ?, ?, 0, NULL, ?, ?)`,
123
+ envelope.requestId,
124
+ primaryKey,
125
+ envelopeText,
126
+ discard ? 1 : 0,
127
+ deliverAt,
128
+ replyTo === null ? null : JSON.stringify([{ replyTo, requestId: envelope.requestId }])
129
+ )
130
+ return Effect.succeed<PersistResult>({ _tag: "Success" })
131
+ })
132
+
133
+ /** @internal */
134
+ export const saveReply = (sql: SqlStorage, replyText: string): Effect.Effect<void, EncodedMessageTooLargeError> =>
135
+ Effect.suspend(() => {
136
+ if (exceedsMaximumEncodedSize(replyText)) {
137
+ return Effect.fail(new EncodedMessageTooLargeError("Encoded entity reply chunk exceeds 2 MB"))
138
+ }
139
+ const reply = JSON.parse(replyText) as {
140
+ readonly _tag?: unknown
141
+ readonly requestId?: unknown
142
+ readonly id?: unknown
143
+ readonly sequence?: unknown
144
+ }
145
+ if (
146
+ (reply._tag !== "Chunk" && reply._tag !== "WithExit") ||
147
+ typeof reply.requestId !== "string" ||
148
+ typeof reply.id !== "string"
149
+ ) {
150
+ throw new TypeError("Expected an encoded Chunk or WithExit reply")
151
+ }
152
+ sql.exec(
153
+ `INSERT OR IGNORE INTO cluster_replies
154
+ (reply_id, request_id, reply, kind, sequence, acked)
155
+ VALUES (?, ?, ?, ?, ?, 0)`,
156
+ reply.id,
157
+ reply.requestId,
158
+ replyText,
159
+ reply._tag,
160
+ reply._tag === "Chunk" ? reply.sequence : null
161
+ )
162
+ sql.exec(
163
+ `UPDATE cluster_messages
164
+ SET last_reply_id = ?, processed = CASE WHEN ? = 'WithExit' THEN 1 ELSE processed END
165
+ WHERE request_id = ?`,
166
+ reply.id,
167
+ reply._tag,
168
+ reply.requestId
169
+ )
170
+ return Effect.void
171
+ })
172
+
173
+ /**
174
+ * A pinned caller of a scheduled ask: the reply is pushed to the `replyTo`
175
+ * entity under the caller's own request id, which it registered before
176
+ * sending, so the push cannot arrive ahead of the registration.
177
+ *
178
+ * @internal
179
+ */
180
+ export interface ReplyTarget {
181
+ readonly replyTo: string
182
+ readonly requestId: string
183
+ }
184
+
185
+ /** @internal */
186
+ export interface StoredMessage {
187
+ readonly requestId: string
188
+ readonly envelope: string
189
+ readonly lastSentChunk: string | undefined
190
+ readonly discard: boolean
191
+ readonly deliverAt?: number | undefined
192
+ readonly replyTos?: ReadonlyArray<ReplyTarget> | undefined
193
+ }
194
+
195
+ type StoredMessageRow = {
196
+ readonly request_id: string
197
+ readonly envelope: string
198
+ readonly discard: number
199
+ readonly deliver_at: number | null
200
+ readonly reply_to: string | null
201
+ readonly last_reply: string | null
202
+ }
203
+
204
+ const isReplyTarget = (item: unknown): item is ReplyTarget =>
205
+ typeof item === "object" && item !== null &&
206
+ typeof (item as ReplyTarget).replyTo === "string" &&
207
+ typeof (item as ReplyTarget).requestId === "string"
208
+
209
+ const decodeReplyTargets = (value: unknown): Array<ReplyTarget> => {
210
+ if (typeof value !== "string") return []
211
+ try {
212
+ const decoded = JSON.parse(value)
213
+ if (Array.isArray(decoded) && decoded.every(isReplyTarget)) return decoded
214
+ } catch {
215
+ // Malformed rows deliver nowhere instead of poisoning the replay.
216
+ }
217
+ return []
218
+ }
219
+
220
+ const rowToMessage = (row: StoredMessageRow): StoredMessage => {
221
+ const replyTos = decodeReplyTargets(row.reply_to)
222
+ const message: StoredMessage = {
223
+ requestId: row.request_id,
224
+ envelope: row.envelope,
225
+ lastSentChunk: row.last_reply ?? undefined,
226
+ discard: row.discard === 1,
227
+ ...(row.deliver_at === null ? undefined : { deliverAt: row.deliver_at }),
228
+ ...(replyTos.length === 0 ? undefined : { replyTos })
229
+ }
230
+ return message
231
+ }
232
+
233
+ /** @internal */
234
+ export const loadUnprocessed = (sql: SqlStorage, now?: number): Effect.Effect<Array<StoredMessage>> =>
235
+ Effect.sync(() =>
236
+ sql.exec<StoredMessageRow>(
237
+ `SELECT m.request_id, m.envelope, m.discard, m.deliver_at, m.reply_to, r.reply AS last_reply
238
+ FROM cluster_messages m
239
+ LEFT JOIN cluster_replies r ON r.reply_id = m.last_reply_id
240
+ WHERE m.processed = 0 AND (m.deliver_at IS NULL OR m.deliver_at <= ?)
241
+ ORDER BY m.rowid ASC`,
242
+ now ?? Date.now()
243
+ ).toArray().map(rowToMessage)
244
+ )
245
+
246
+ /** @internal */
247
+ export const loadMessage = (sql: SqlStorage, requestId: string): Effect.Effect<StoredMessage | undefined> =>
248
+ Effect.sync(() => {
249
+ const row = sql.exec<StoredMessageRow>(
250
+ `SELECT m.request_id, m.envelope, m.discard, m.deliver_at, m.reply_to, r.reply AS last_reply
251
+ FROM cluster_messages m
252
+ LEFT JOIN cluster_replies r ON r.reply_id = m.last_reply_id
253
+ WHERE m.request_id = ?
254
+ LIMIT 1`,
255
+ requestId
256
+ ).toArray()[0]
257
+ return row === undefined ? undefined : rowToMessage(row)
258
+ })
259
+
260
+ /** @internal */
261
+ export interface NextReply {
262
+ readonly reply: string
263
+ readonly kind: "Chunk" | "WithExit"
264
+ }
265
+
266
+ type NextReplyRow = {
267
+ readonly reply: string
268
+ readonly kind: NextReply["kind"]
269
+ }
270
+
271
+ /** @internal */
272
+ export const loadNextReply = (sql: SqlStorage, requestId: string): Effect.Effect<NextReply | undefined> =>
273
+ Effect.sync(() => {
274
+ const row = sql.exec<NextReplyRow>(
275
+ `SELECT reply, kind
276
+ FROM cluster_replies
277
+ WHERE request_id = ? AND kind = 'Chunk' AND acked = 0
278
+ ORDER BY sequence ASC
279
+ LIMIT 1`,
280
+ requestId
281
+ ).toArray()[0] ?? sql.exec<NextReplyRow>(
282
+ `SELECT reply, kind
283
+ FROM cluster_replies
284
+ WHERE request_id = ? AND kind = 'WithExit'
285
+ LIMIT 1`,
286
+ requestId
287
+ ).toArray()[0]
288
+ return row
289
+ })
290
+
291
+ /** @internal */
292
+ export const completeTell = (sql: SqlStorage, requestId: string): Effect.Effect<void> =>
293
+ Effect.sync(() => {
294
+ sql.exec(
295
+ `UPDATE cluster_messages
296
+ SET processed = 1, last_reply_id = NULL
297
+ WHERE request_id = ?`,
298
+ requestId
299
+ )
300
+ })
301
+
302
+ /** @internal */
303
+ export const ackChunk = (sql: SqlStorage, requestId: string, replyId: string): Effect.Effect<void> =>
304
+ Effect.sync(() => {
305
+ sql.exec(
306
+ `UPDATE cluster_replies
307
+ SET acked = 1
308
+ WHERE request_id = ? AND reply_id = ? AND kind = 'Chunk'`,
309
+ requestId,
310
+ replyId
311
+ )
312
+ })
313
+
314
+ /** @internal */
315
+ export const clearReplies = (sql: SqlStorage, requestId: string): Effect.Effect<void> =>
316
+ Effect.sync(() => {
317
+ sql.exec("DELETE FROM cluster_replies WHERE request_id = ?", requestId)
318
+ sql.exec(
319
+ `UPDATE cluster_messages
320
+ SET processed = 0, last_reply_id = NULL
321
+ WHERE request_id = ?`,
322
+ requestId
323
+ )
324
+ })
@@ -0,0 +1,46 @@
1
+ /** @internal */
2
+ import type * as Entity from "effect/cluster/Entity"
3
+ import type * as Context from "effect/Context"
4
+ import type * as Effect from "effect/Effect"
5
+ import { makeRegistry } from "./registry.ts"
6
+
7
+ export interface EntityRegistration {
8
+ readonly entity: Entity.Entity<any, any>
9
+ readonly build: Effect.Effect<Record<string, (request: any) => any>, never, never>
10
+ readonly options: {
11
+ readonly concurrency?: number | "unbounded" | undefined
12
+ readonly disableFatalDefects?: boolean | undefined
13
+ readonly defectRetryPolicy?: unknown
14
+ readonly spanAttributes?: Record<string, string> | undefined
15
+ } | undefined
16
+ readonly context: Context.Context<never>
17
+ /** Interval of the keep-alive heartbeat alarm, in milliseconds. */
18
+ readonly keepAliveHeartbeat: number
19
+ }
20
+
21
+ const registry = makeRegistry<EntityRegistration>()
22
+
23
+ /**
24
+ * Default interval of the keep-alive heartbeat alarm.
25
+ *
26
+ * @internal
27
+ */
28
+ export const defaultKeepAliveHeartbeatMillis = 30_000
29
+
30
+ /**
31
+ * The heartbeat interval of an entity type. An alarm can wake an object whose
32
+ * type a deploy removed, so a missing registration falls back to the default.
33
+ *
34
+ * @internal
35
+ */
36
+ export const keepAliveHeartbeatMillis = (entityType: string): number =>
37
+ registry.get(entityType)?.keepAliveHeartbeat ?? defaultKeepAliveHeartbeatMillis
38
+
39
+ /** @internal */
40
+ export const getEntityRegistration: (type: string) => EntityRegistration | undefined = registry.get
41
+
42
+ /** @internal */
43
+ export const registerEntity: (type: string, registration: EntityRegistration) => boolean = registry.register
44
+
45
+ /** @internal */
46
+ export const unregisterEntity: (type: string, registration: EntityRegistration) => void = registry.unregister
@@ -0,0 +1,54 @@
1
+ /** @internal */
2
+ import * as Context from "effect/Context"
3
+ import * as Deferred from "effect/Deferred"
4
+ import * as Effect from "effect/Effect"
5
+
6
+ /** @internal */
7
+ export class CurrentEntityName extends Context.Service<CurrentEntityName, string>()(
8
+ "@effect/platform-cloudflare/CurrentEntityName"
9
+ ) {}
10
+
11
+ /**
12
+ * Completes the delayed asks made by one entity Durable Object's handlers.
13
+ * The destination object pushes the stored reply back over the caller's
14
+ * `deliverReply` RPC, which finds the waiting `Deferred` here.
15
+ *
16
+ * @internal
17
+ */
18
+ export interface EntityReplyRegistry {
19
+ readonly register: (requestId: string, waiter: Deferred.Deferred<string>) => void
20
+ readonly unregister: (requestId: string, waiter: Deferred.Deferred<string>) => void
21
+ readonly deliver: (requestId: string, reply: string) => boolean
22
+ }
23
+
24
+ /** @internal */
25
+ export const makeReplyRegistry = (): EntityReplyRegistry => {
26
+ const waiters = new Map<string, Set<Deferred.Deferred<string>>>()
27
+ return {
28
+ register(requestId, waiter) {
29
+ const registered = waiters.get(requestId) ?? new Set()
30
+ registered.add(waiter)
31
+ waiters.set(requestId, registered)
32
+ },
33
+ unregister(requestId, waiter) {
34
+ const registered = waiters.get(requestId)
35
+ if (registered === undefined) return
36
+ registered.delete(waiter)
37
+ if (registered.size === 0) waiters.delete(requestId)
38
+ },
39
+ deliver(requestId, reply) {
40
+ const registered = waiters.get(requestId)
41
+ if (registered === undefined) return false
42
+ waiters.delete(requestId)
43
+ for (const waiter of registered) {
44
+ Deferred.doneUnsafe(waiter, Effect.succeed(reply))
45
+ }
46
+ return true
47
+ }
48
+ }
49
+ }
50
+
51
+ /** @internal */
52
+ export class CurrentReplyRegistry extends Context.Service<CurrentReplyRegistry, EntityReplyRegistry>()(
53
+ "@effect/platform-cloudflare/CurrentReplyRegistry"
54
+ ) {}