@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,823 @@
1
+ import * as Cause from "effect/Cause";
2
+ import * as ClusterMetrics from "effect/cluster/ClusterMetrics";
3
+ import { isUninterruptibleForServer, Persisted } from "effect/cluster/ClusterSchema";
4
+ import { CurrentAddress, CurrentRunnerAddress, KeepAliveHandler, Request } from "effect/cluster/Entity";
5
+ import * as Envelope from "effect/cluster/Envelope";
6
+ import * as Reply from "effect/cluster/Reply";
7
+ import * as RunnerAddress from "effect/cluster/RunnerAddress";
8
+ import * as Context from "effect/Context";
9
+ import * as Deferred from "effect/Deferred";
10
+ import * as Effect from "effect/Effect";
11
+ import * as Exit from "effect/Exit";
12
+ import * as Fiber from "effect/Fiber";
13
+ import { identity } from "effect/Function";
14
+ import * as Metric from "effect/Metric";
15
+ import * as Option from "effect/Option";
16
+ import * as Pull from "effect/Pull";
17
+ import * as Queue from "effect/Queue";
18
+ import * as Result from "effect/Result";
19
+ import * as Rpc from "effect/rpc/Rpc";
20
+ import * as RpcMessage from "effect/rpc/RpcMessage";
21
+ import * as RpcSchema from "effect/rpc/RpcSchema";
22
+ import * as RpcServer from "effect/rpc/RpcServer";
23
+ import * as Scope from "effect/Scope";
24
+ import * as Semaphore from "effect/Semaphore";
25
+ import { encodeName } from "./clusterName.js";
26
+ import { ackChunk, clearReplies, completeTell, loadMessage, loadNextReply, loadUnprocessed, persistRequest, saveReply } from "./entityMailbox.js";
27
+ import { getEntityRegistration, keepAliveHeartbeatMillis } from "./entityRegistry.js";
28
+ import { CurrentEntityName, CurrentReplyRegistry, makeReplyRegistry } from "./entityReply.js";
29
+ import { makeEntitySql } from "./entitySql.js";
30
+ import { armAlarm, earliestDeliverAt, loadEntityState, nextAlarmAt, saveKeepAlive, withTransaction } from "./entityStorage.js";
31
+ import { decodeReplyFor, decodeRequest, encodeReplyFor, peekEnvelopeTag } from "./entityWire.js";
32
+ /** @internal */
33
+ export const makeEntityRuntime = /*#__PURE__*/Effect.fnUntraced(function* (registration, address, nextId, entityName = encodeName(address.entityType, address.entityId), keepAlive, replyRegistry, entitySql) {
34
+ let cached;
35
+ let building;
36
+ const metricContext = Context.merge(registration.context, Metric.CurrentMetricAttributes.context({
37
+ type: registration.entity.type
38
+ }));
39
+ // Bounds concurrent handler executions from the entity's `concurrency`
40
+ // build option; `"unbounded"` leaves handlers unlimited.
41
+ const concurrency = registration.options?.concurrency ?? 1;
42
+ const handlerSemaphore = concurrency === "unbounded" ? undefined : Semaphore.makeUnsafe(concurrency);
43
+ const serverRequests = new Map();
44
+ let nextServerRequestId = 0;
45
+ // Set while `server.write` runs; a request the server rejects before it
46
+ // forks a handler is answered with a `Defect` that carries no request id.
47
+ let writing;
48
+ const onFromServer = response => {
49
+ switch (response._tag) {
50
+ case "Exit":
51
+ {
52
+ const request = serverRequests.get(Number(response.requestId));
53
+ if (request !== undefined) Deferred.doneUnsafe(request.exit, Effect.succeed(response.exit));
54
+ return Effect.void;
55
+ }
56
+ case "Chunk":
57
+ {
58
+ const request = serverRequests.get(Number(response.requestId));
59
+ if (request === undefined || request.discard || request.interrupted) return Effect.void;
60
+ const progress = request.progress;
61
+ const reply = new Reply.Chunk({
62
+ requestId: request.envelope.requestId,
63
+ id: nextId(),
64
+ sequence: progress.sequence++,
65
+ values: response.values
66
+ });
67
+ return request.respond(reply).pipe(Effect.tap(() => Effect.sync(() => {
68
+ progress.lastSentChunk = Option.some(reply);
69
+ })),
70
+ // The server ends the request without an exit after a failed chunk
71
+ // write, so the failure becomes the request's exit here.
72
+ Effect.onError(cause => Effect.sync(() => {
73
+ if (!Cause.hasInterruptsOnly(cause)) {
74
+ Deferred.doneUnsafe(request.exit, Effect.succeed(Exit.failCause(cause)));
75
+ }
76
+ })));
77
+ }
78
+ case "Defect":
79
+ {
80
+ if (writing !== undefined) Deferred.doneUnsafe(writing.exit, Effect.succeed(Exit.die(response.defect)));
81
+ return Effect.void;
82
+ }
83
+ case "ClientEnd":
84
+ return Effect.void;
85
+ }
86
+ };
87
+ // Closes a retired server once nothing holds a lease on it. `closed` is set
88
+ // synchronously, so a lease taken afterwards moves to the current build.
89
+ const closeIfDrained = handle => Effect.suspend(() => {
90
+ if (!handle.retired || handle.leases > 0 || handle.closed) return Effect.void;
91
+ handle.closed = true;
92
+ return Scope.close(handle.scope, Exit.void);
93
+ });
94
+ const invalidate = Effect.fnUntraced(function* () {
95
+ if (cached === undefined) return;
96
+ const scope = cached.scope;
97
+ const server = cached.server;
98
+ cached = undefined;
99
+ server.retired = true;
100
+ yield* closeIfDrained(server);
101
+ // The closing build's holders are gone with it. `EntityResource` skips
102
+ // `keepAlive(false)` while its scope shuts down, so keeping the count
103
+ // would leak a holder into the rebuild.
104
+ keepAlive?.resetHolders();
105
+ yield* Scope.close(scope, Exit.void).pipe(Effect.ensuring(Effect.sync(() => {
106
+ ClusterMetrics.entities.modifyUnsafe(BigInt(-1), metricContext);
107
+ })));
108
+ });
109
+ const buildHandlers = Effect.fnUntraced(function* () {
110
+ const scope = yield* Scope.make();
111
+ let context = registration.context.pipe(Context.add(CurrentAddress, address), Context.add(CurrentRunnerAddress, RunnerAddress.make(`${address.entityType}/${address.entityId}`, 0)), Context.add(CurrentEntityName, entityName), Context.add(Scope.Scope, scope));
112
+ if (keepAlive !== undefined) {
113
+ context = Context.add(context, KeepAliveHandler, keepAlive.update);
114
+ }
115
+ if (replyRegistry !== undefined) {
116
+ context = Context.add(context, CurrentReplyRegistry, replyRegistry);
117
+ }
118
+ const {
119
+ handlers,
120
+ server
121
+ } = yield* Effect.gen(function* () {
122
+ if (entitySql !== undefined) {
123
+ context = Context.merge(context, yield* Scope.provide(entitySql.make, scope));
124
+ }
125
+ const handlers = yield* Effect.provideContext(registration.build, context);
126
+ // The server sees each handler's result before it takes a permit; a
127
+ // forked result is marked here so its request skips the entity permit.
128
+ const serverHandlers = {};
129
+ for (const tag of Object.keys(handlers)) {
130
+ const handler = handlers[tag];
131
+ serverHandlers[tag] = (request, {
132
+ requestId
133
+ }) => {
134
+ const result = handler(request);
135
+ if (Rpc.isWrapper(result) && result.fork) {
136
+ const pending = serverRequests.get(Number(requestId));
137
+ if (pending !== undefined) pending.fork = true;
138
+ }
139
+ return result;
140
+ };
141
+ }
142
+ const handlersContext = yield* Effect.provideContext(registration.entity.protocol.toHandlers(serverHandlers), context);
143
+ const serverScope = yield* Scope.make();
144
+ // Middleware services come from the registration context: `Entity.toLayer`
145
+ // requires them, and `registerEntity` captures that context.
146
+ const server = yield* RpcServer.makeNoSerialization(registration.entity.protocol, {
147
+ onFromServer,
148
+ concurrency: "unbounded",
149
+ disableTracing: true,
150
+ disableClientAcks: true,
151
+ disableFatalDefects: true
152
+ }).pipe(Effect.provideContext(Context.add(Context.merge(context, handlersContext), Scope.Scope, serverScope)));
153
+ const handle = {
154
+ server,
155
+ scope: serverScope,
156
+ leases: 0,
157
+ retired: false,
158
+ closed: false
159
+ };
160
+ return {
161
+ handlers,
162
+ server: handle
163
+ };
164
+ }).pipe(Effect.tapCause(cause => Scope.close(scope, Exit.failCause(cause))));
165
+ ClusterMetrics.entities.modifyUnsafe(BigInt(1), metricContext);
166
+ // Any event that builds the entity restores a hold persisted before a
167
+ // restart.
168
+ keepAlive?.restore();
169
+ return {
170
+ handlers,
171
+ context,
172
+ scope,
173
+ server
174
+ };
175
+ });
176
+ const getHandlers = () => Effect.suspend(() => {
177
+ if (cached !== undefined) return Effect.succeed(cached);
178
+ if (building !== undefined) return Deferred.await(building);
179
+ const deferred = Deferred.makeUnsafe();
180
+ building = deferred;
181
+ return Effect.onExit(buildHandlers(), exit => Effect.sync(() => {
182
+ building = undefined;
183
+ if (Exit.isSuccess(exit)) cached = exit.value;
184
+ Deferred.doneUnsafe(deferred, exit);
185
+ }));
186
+ });
187
+ const runWithDefectRetry = effect => {
188
+ const policy = registration.options?.defectRetryPolicy;
189
+ if (policy === undefined) return Effect.exit(effect);
190
+ const retryable = Effect.flatMap(Effect.exit(effect), exit => Exit.isFailure(exit) && Cause.hasDies(exit.cause) ? Effect.fail(exit.cause) : Effect.succeed(exit));
191
+ return Effect.retryOrElse(retryable, policy, cause => Effect.succeed(Exit.failCause(cause)));
192
+ };
193
+ // Takes a lease on the current build's server for one request, including
194
+ // its defect retries. Only the build itself stays interruptible. A server
195
+ // that closed after `getHandlers` returned it is no longer cached, so the
196
+ // lookup runs again.
197
+ const leaseHandlers = restore => Effect.flatMap(restore(getHandlers()), entry => Effect.suspend(() => {
198
+ if (entry.server.closed) return leaseHandlers(restore);
199
+ entry.server.leases++;
200
+ return Effect.succeed(entry);
201
+ }));
202
+ const releaseLease = entry => Effect.suspend(() => {
203
+ entry.server.leases--;
204
+ return closeIfDrained(entry.server);
205
+ });
206
+ const rebuildAfterDefect = invalidate().pipe(Effect.andThen(Effect.catchCause(getHandlers(), () => Effect.void)));
207
+ // Writes one attempt to the server and waits for its exit and for the
208
+ // request scope to close. Interrupting the caller interrupts the server
209
+ // request and still waits, so an `Rpc.uninterruptible` handler finishes
210
+ // before the caller's interruption completes.
211
+ const runOnServer = (server, request, permit) => {
212
+ const id = nextServerRequestId++;
213
+ const settled = Effect.flatMap(Deferred.await(request.exit), exit => request.started ? Effect.as(Deferred.await(request.finished), exit) : Effect.succeed(exit));
214
+ // `finished` completes on every exit of the request effect, including an
215
+ // interrupt while it waits for the permit or before the handler starts.
216
+ // `started` is set inside the `ensuring`, so `settled` only waits for
217
+ // `finished` once the finalizer is registered.
218
+ const onRequest = effect => Effect.ensuring(Effect.suspend(() => {
219
+ request.started = true;
220
+ if (request.interrupted) return Effect.interrupt;
221
+ return request.fork ? effect : permit.acquire(effect);
222
+ }), Effect.sync(() => Deferred.doneUnsafe(request.finished, Effect.void)));
223
+ const write = Effect.suspend(() => {
224
+ serverRequests.set(id, request);
225
+ writing = request;
226
+ return server.write(0, {
227
+ _tag: "Request",
228
+ id: RpcMessage.RequestId(id),
229
+ tag: request.envelope.tag,
230
+ payload: new Request({
231
+ ...request.envelope,
232
+ lastSentChunk: request.progress.lastSentChunk
233
+ }),
234
+ headers: request.envelope.headers
235
+ }, {
236
+ onRequest
237
+ }).pipe(Effect.ensuring(Effect.sync(() => {
238
+ writing = undefined;
239
+ })));
240
+ });
241
+ // The write and the interrupt hook are installed without an interruptible
242
+ // gap, so an interrupt that arrives just after the write still reaches
243
+ // the server request.
244
+ return Effect.uninterruptibleMask(restore => Effect.andThen(write, Effect.onInterrupt(restore(settled), () => Effect.suspend(() => {
245
+ request.interrupted = true;
246
+ const stop = request.started ? server.write(0, {
247
+ _tag: "Interrupt",
248
+ requestId: RpcMessage.RequestId(id),
249
+ interruptors: []
250
+ }) : Effect.void;
251
+ return Effect.andThen(stop, settled);
252
+ })))).pipe(Effect.flatten, Effect.ensuring(Effect.sync(() => serverRequests.delete(id))));
253
+ };
254
+ const run = (envelope, lastSentChunk, discard, respond, permit = makeHandlerPermit(handlerSemaphore)) => Effect.uninterruptibleMask(restore => Effect.flatMap(leaseHandlers(restore), entry => Effect.ensuring(restore(runLeased(entry, envelope, lastSentChunk, discard, respond, permit)), releaseLease(entry))));
255
+ const runLeased = Effect.fnUntraced(function* (entry, envelope, lastSentChunk, discard, respond, permit) {
256
+ const rpc = registration.entity.protocol.requests.get(envelope.tag);
257
+ const handler = entry.handlers[envelope.tag];
258
+ if (rpc === undefined || handler === undefined) {
259
+ const exit = Exit.die(`Unknown entity RPC tag: ${envelope.tag}`);
260
+ if (!discard) {
261
+ yield* respond(new Reply.WithExit({
262
+ requestId: envelope.requestId,
263
+ id: nextId(),
264
+ exit
265
+ }));
266
+ }
267
+ return;
268
+ }
269
+ const streamSchema = RpcSchema.isStreamSchema(rpc.successSchema) ? rpc.successSchema : undefined;
270
+ const progress = {
271
+ lastSentChunk,
272
+ sequence: Option.match(lastSentChunk, {
273
+ onNone: () => 0,
274
+ onSome: chunk => chunk.sequence + 1
275
+ })
276
+ };
277
+ const execute = Effect.suspend(() => runOnServer(entry.server.server, {
278
+ envelope,
279
+ discard,
280
+ respond,
281
+ progress,
282
+ exit: Deferred.makeUnsafe(),
283
+ finished: Deferred.makeUnsafe(),
284
+ started: false,
285
+ interrupted: false,
286
+ fork: false
287
+ }, permit)).pipe(Effect.withSpan("CloudflareCluster.handler", {
288
+ attributes: {
289
+ entityType: registration.entity.type,
290
+ entityId: String(address.entityId),
291
+ rpc: envelope.tag
292
+ }
293
+ }, {
294
+ captureStackTrace: false
295
+ }));
296
+ const exit = yield* Effect.provideContext(runWithDefectRetry(execute), entry.context);
297
+ if (!discard) {
298
+ yield* respond(new Reply.WithExit({
299
+ requestId: envelope.requestId,
300
+ id: nextId(),
301
+ exit: streamSchema !== undefined && Exit.isSuccess(exit) ? Exit.void : exit
302
+ }));
303
+ }
304
+ if (Exit.isFailure(exit) && Cause.hasDies(exit.cause)) {
305
+ yield* rebuildAfterDefect;
306
+ }
307
+ });
308
+ // Builds the handlers without running a request, for wakes that only need
309
+ // the entity alive. Building restores the hold; the explicit `restore`
310
+ // covers an entity that was already built when its hold ended.
311
+ const warm = Effect.andThen(getHandlers(), Effect.sync(() => keepAlive?.restore()));
312
+ return {
313
+ run,
314
+ invalidate,
315
+ warm,
316
+ handlerSemaphore
317
+ };
318
+ });
319
+ const success = (requestId, replies) => ({
320
+ _tag: "Success",
321
+ requestId,
322
+ replies
323
+ });
324
+ const unlimitedHandlerPermit = {
325
+ acquire: identity,
326
+ pause: identity
327
+ };
328
+ /**
329
+ * One permit per handler execution. `acquire` holds the permit for the whole
330
+ * run; `pause` gives it back while a chunk is parked on its client
331
+ * acknowledgement so an unacked stream cannot starve other handlers. The
332
+ * `held` flag keeps take/release balanced when interruption lands inside a
333
+ * paused section: a pause that never re-acquires leaves `held` false, and
334
+ * `acquire` then skips its release.
335
+ */
336
+ const makeHandlerPermit = semaphore => {
337
+ if (semaphore === undefined) return unlimitedHandlerPermit;
338
+ let held = false;
339
+ return {
340
+ acquire: effect => Effect.uninterruptibleMask(restore => restore(Semaphore.take(semaphore, 1)).pipe(Effect.flatMap(() => {
341
+ held = true;
342
+ return restore(effect);
343
+ }), Effect.onExit(() => Effect.suspend(() => {
344
+ if (!held) return Effect.void;
345
+ held = false;
346
+ return Effect.asVoid(Semaphore.release(semaphore, 1));
347
+ })))),
348
+ pause: effect => Effect.uninterruptibleMask(restore => Effect.suspend(() => {
349
+ if (!held) return restore(effect);
350
+ held = false;
351
+ return Semaphore.release(semaphore, 1).pipe(Effect.flatMap(() => restore(effect)), Effect.flatMap(value => Effect.map(restore(Semaphore.take(semaphore, 1)), () => {
352
+ held = true;
353
+ return value;
354
+ })));
355
+ }))
356
+ };
357
+ };
358
+ /**
359
+ * The Effect-land state machine behind one `ClusterEntity` Durable Object.
360
+ * The class methods are one-line `Effect.runPromise` adapters over the
361
+ * effects returned here; all serialization, sessions, and waiters live in
362
+ * Effect primitives.
363
+ *
364
+ * @internal
365
+ */
366
+ export const makeEntityManager = options => {
367
+ const storage = options.storage;
368
+ const sql = storage.sql;
369
+ // Serializes storage entry: envelope decode, persist-before-run, dedupe,
370
+ // duplicate resume, and alarm arming. Handler execution runs in forked
371
+ // fibers governed by the handler concurrency semaphore below.
372
+ const semaphore = Semaphore.makeUnsafe(1);
373
+ const sessions = new Map();
374
+ // Persisted discard/scheduled requests with an in-flight handler; guards
375
+ // against a second execution from replay or duplicate delivery in the same
376
+ // wake, the way `sessions` does for asks.
377
+ const pendingRuns = new Set();
378
+ // Sessions can retain queued replies after their handler has finished, and
379
+ // can also belong to non-persisted requests. Count persisted handler fibers
380
+ // separately so only transitions touch the recovery alarm.
381
+ let inFlightPersisted = 0;
382
+ const workerWaiters = new Map();
383
+ const replyRegistry = makeReplyRegistry();
384
+ const entitySql = makeEntitySql(storage);
385
+ // Every mailbox write runs in its own storage transaction and waits for any
386
+ // open user transaction, so a user rollback cannot take a reply with it.
387
+ const write = effect => entitySql.guard(withTransaction(storage, effect));
388
+ let runtime;
389
+ const getRuntime = registration => runtime !== undefined ? Effect.succeed(runtime) : Effect.map(makeEntityRuntime(registration, options.address, () => crypto.randomUUID(), options.entityName, options.keepAlive, replyRegistry, entitySql), built => runtime = built);
390
+ const armEarliestAlarm = Effect.suspend(() => {
391
+ const deliverAt = earliestDeliverAt(sql);
392
+ return deliverAt === undefined ? Effect.void : armAlarm(storage, deliverAt);
393
+ });
394
+ const heartbeatMillis = () => keepAliveHeartbeatMillis(options.address.entityType);
395
+ const isRunning = requestId => sessions.has(requestId) || pendingRuns.has(requestId);
396
+ // Hot paths only ever move the alarm earlier. A heartbeat while a persisted
397
+ // row runs leaves an alarm behind for the restart that kills its handler.
398
+ const armHeartbeat = Effect.suspend(() => armAlarm(storage, Date.now() + heartbeatMillis()));
399
+ // Recomputes the single alarm from the whole entity state. Callers hold the
400
+ // entry permit, so a concurrent keep-alive write cannot slip in between the
401
+ // read and a `deleteAlarm`.
402
+ const rearm = Effect.suspend(() => {
403
+ const next = nextAlarmAt(sql, {
404
+ now: Date.now(),
405
+ heartbeatMillis: heartbeatMillis(),
406
+ isRunning
407
+ });
408
+ return Effect.flatMap(Effect.promise(() => storage.getAlarm()), current => {
409
+ if (next !== undefined) {
410
+ return current === next ? Effect.void : Effect.promise(() => storage.setAlarm(next));
411
+ }
412
+ return current === null ? Effect.void : Effect.promise(() => storage.deleteAlarm());
413
+ });
414
+ });
415
+ // Called under the entry permit, before forking the handler. Overlapping
416
+ // requests share the first heartbeat instead of reading the alarm per run.
417
+ const startPersisted = Effect.suspend(() => ++inFlightPersisted === 1 ? armHeartbeat : Effect.void);
418
+ // Handler finalizers hold the entry permit and finish mailbox writes before
419
+ // dropping the count. The last completion may move the alarm later to a
420
+ // scheduled delivery, retain keep-alive, or remove it entirely.
421
+ const finishPersisted = Effect.suspend(() => --inFlightPersisted === 0 ? rearm : Effect.void);
422
+ const removeSession = (requestId, session) => {
423
+ if (!session.cancelling && sessions.get(requestId) === session) sessions.delete(requestId);
424
+ };
425
+ const signalSaved = session => {
426
+ const saved = session.saved;
427
+ if (saved === undefined) return;
428
+ session.saved = undefined;
429
+ Deferred.doneUnsafe(saved, Effect.void);
430
+ };
431
+ const storedNextReply = requestId => Effect.map(loadNextReply(sql, requestId), nextReply => nextReply === undefined ? [] : [nextReply.reply]);
432
+ // Reads the next stored reply of a running persisted stream, waiting for
433
+ // the handler to save one when there is none yet. The waiter is registered
434
+ // before reading storage, so a reply saved in between still wakes it.
435
+ const awaitStoredReply = (requestId, session) => Effect.suspend(() => {
436
+ const saved = session.saved ??= Deferred.makeUnsafe();
437
+ return Effect.map(loadNextReply(sql, requestId), nextReply => nextReply === undefined ? Effect.andThen(Deferred.await(saved), storedNextReply(requestId)) : Effect.succeed([nextReply.reply]));
438
+ });
439
+ const takeReply = (requestId, session) => Queue.take(session.queue).pipe(Effect.map(reply => {
440
+ if (reply.terminal) removeSession(requestId, session);
441
+ return [reply.text];
442
+ }), Pull.catchDone(() => Effect.succeed([])));
443
+ // Hands a scheduled reply to the pinned callers waiting in this wake and
444
+ // returns the reply targets stored now, which include any a duplicate added
445
+ // while the handler ran. Runs after the reply is saved: once the row is
446
+ // processed, a later duplicate reads the stored reply instead.
447
+ const settleScheduledReply = (requestId, reply, replyTos) => Effect.map(loadMessage(sql, requestId), message => {
448
+ const waiters = workerWaiters.get(requestId);
449
+ if (waiters !== undefined) {
450
+ workerWaiters.delete(requestId);
451
+ for (const waiter of waiters) {
452
+ Deferred.doneUnsafe(waiter.deferred, Effect.succeed(success(requestId, [reply])));
453
+ }
454
+ }
455
+ return message === undefined ? replyTos : message.replyTos;
456
+ });
457
+ const pushScheduledReply = (reply, replyTos) => Effect.suspend(() => {
458
+ if (replyTos === undefined) return Effect.void;
459
+ const namespace = options.getNamespace();
460
+ if (namespace === undefined) {
461
+ return Effect.logError("Scheduled entity reply delivery failed", new Error("CloudflareCluster: ClusterEntity export is unavailable for scheduled reply delivery"));
462
+ }
463
+ return Effect.forEach(replyTos, ({
464
+ replyTo,
465
+ requestId
466
+ }) => Effect.promise(() => namespace.getByName(replyTo).deliverReply(requestId, reply)).pipe(Effect.flatMap(delivered => delivered ? Effect.void : Effect.logError("Scheduled entity reply delivery failed", new Error(`Scheduled entity reply target is unavailable: ${replyTo}`))), Effect.catchCause(cause => Effect.logError("Scheduled entity reply delivery failed", cause))), {
467
+ concurrency: "unbounded",
468
+ discard: true
469
+ });
470
+ });
471
+ const deliverScheduledReply = (requestId, reply, replyTos) => Effect.flatMap(settleScheduledReply(requestId, reply, replyTos), targets => pushScheduledReply(reply, targets));
472
+ // Runs one request under the entry permit up to forking its handler fiber,
473
+ // then returns a continuation that awaits the handler output. Callers run
474
+ // the continuation after the entry permit is released so handlers governed
475
+ // by the concurrency semaphore can interleave with later storage entries.
476
+ const run = Effect.fnUntraced(function* (registration, entityRuntime, envelope, lastSentChunkText, discard, persisted, runOptions) {
477
+ const rpc = registration.entity.protocol.requests.get(envelope.tag);
478
+ let lastSentChunk = Option.none();
479
+ if (lastSentChunkText !== undefined) {
480
+ const reply = yield* decodeReplyFor(rpc, registration.context, lastSentChunkText);
481
+ if (reply._tag === "Chunk") lastSentChunk = Option.some(reply);
482
+ }
483
+ const requestId = String(envelope.requestId);
484
+ if (!discard) {
485
+ const active = sessions.get(requestId);
486
+ if (active !== undefined) {
487
+ // A stream duplicate resumes from storage, as on other runners.
488
+ if (persisted && RpcSchema.isStreamSchema(rpc.successSchema)) {
489
+ return yield* awaitStoredReply(requestId, active);
490
+ }
491
+ const nextReply = persisted ? yield* loadNextReply(sql, requestId) : undefined;
492
+ if (nextReply !== undefined) return Effect.succeed([nextReply.reply]);
493
+ const fiber = active.fiber;
494
+ if (!persisted || fiber === undefined) return Effect.succeed([]);
495
+ // A unary duplicate waits for the handler outside the entry permit and
496
+ // then reads its stored reply, which wins even when post-reply handler
497
+ // cleanup failed.
498
+ return Effect.gen(function* () {
499
+ const exit = yield* Fiber.await(fiber);
500
+ const reply = yield* loadNextReply(sql, requestId);
501
+ if (reply !== undefined) return [reply.reply];
502
+ if (Exit.isFailure(exit)) return yield* Effect.failCause(exit.cause);
503
+ return yield* Effect.die("Persisted unary handler finished without a reply");
504
+ });
505
+ }
506
+ }
507
+ const scheduled = runOptions?.scheduled === true;
508
+ if (discard || scheduled) {
509
+ if (persisted) {
510
+ if (pendingRuns.has(requestId)) return Effect.succeed([]);
511
+ pendingRuns.add(requestId);
512
+ yield* startPersisted;
513
+ }
514
+ // No stream acks on this path, so the runtime's plain permit is enough.
515
+ const execute = entityRuntime.run(envelope, lastSentChunk, discard, reply => Effect.gen(function* () {
516
+ const encoded = yield* encodeReplyFor(registration, rpc, reply);
517
+ const save = persisted ? Effect.orDie(write(saveReply(sql, encoded))) : Effect.void;
518
+ if (!scheduled || reply._tag !== "WithExit") return yield* save;
519
+ // Saving the terminal reply, settling the waiters and reading the
520
+ // targets share the entry permit, so a duplicate entry either finds
521
+ // the stored reply or is registered in time to receive it.
522
+ const replyTos = yield* Semaphore.withPermit(semaphore, Effect.andThen(save, settleScheduledReply(requestId, encoded, runOptions?.replyTos)));
523
+ yield* pushScheduledReply(encoded, replyTos);
524
+ }));
525
+ const fiber = yield* Effect.forkDetach(execute.pipe(Effect.onExit(exit => !persisted ? Effect.void : Semaphore.withPermit(semaphore, Effect.suspend(() => {
526
+ pendingRuns.delete(requestId);
527
+ return discard && Exit.isSuccess(exit) ? write(completeTell(sql, requestId)) : Effect.void;
528
+ }).pipe(Effect.ensuring(finishPersisted))))));
529
+ options.waitUntil(Fiber.await(fiber));
530
+ return Effect.as(Fiber.join(fiber), []);
531
+ }
532
+ const queue = yield* Queue.make();
533
+ const completeInterrupt = persisted ? Effect.flatMap(encodeReplyFor(registration, rpc, new Reply.WithExit({
534
+ requestId: envelope.requestId,
535
+ id: crypto.randomUUID(),
536
+ exit: Exit.interrupt()
537
+ })), reply => Effect.orDie(write(Effect.andThen(clearReplies(sql, requestId), saveReply(sql, reply))))) : Effect.void;
538
+ const session = {
539
+ queue,
540
+ completeInterrupt,
541
+ uninterruptible: isUninterruptibleForServer(rpc.annotations),
542
+ cancelling: false,
543
+ ack: undefined,
544
+ fiber: undefined,
545
+ saved: undefined
546
+ };
547
+ sessions.set(requestId, session);
548
+ if (persisted) yield* startPersisted;
549
+ const permit = makeHandlerPermit(entityRuntime.handlerSemaphore);
550
+ const respond = Effect.fnUntraced(function* (reply) {
551
+ const encoded = yield* encodeReplyFor(registration, rpc, reply);
552
+ if (persisted) {
553
+ yield* Effect.orDie(write(saveReply(sql, encoded)));
554
+ signalSaved(session);
555
+ }
556
+ if (reply._tag === "Chunk") {
557
+ const acknowledged = Deferred.makeUnsafe();
558
+ session.ack = {
559
+ replyId: String(reply.id),
560
+ deferred: acknowledged
561
+ };
562
+ // A false offer means the session was interrupted and the queue
563
+ // ended; awaiting the acknowledgement would then never resume.
564
+ const offered = yield* Queue.offer(queue, {
565
+ text: encoded,
566
+ terminal: false
567
+ });
568
+ if (offered) yield* permit.pause(Deferred.await(acknowledged));else session.ack = undefined;
569
+ } else {
570
+ yield* Queue.offer(queue, {
571
+ text: encoded,
572
+ terminal: true
573
+ });
574
+ }
575
+ });
576
+ session.fiber = yield* Effect.forkDetach(entityRuntime.run(envelope, lastSentChunk, discard, respond, permit).pipe(Effect.onExit(exit => {
577
+ const cleanup = Effect.sync(() => {
578
+ if (Exit.isSuccess(exit)) Queue.endUnsafe(queue);else Queue.failCauseUnsafe(queue, exit.cause);
579
+ if (Queue.sizeUnsafe(queue) === 0 && session.ack === undefined) {
580
+ removeSession(requestId, session);
581
+ }
582
+ signalSaved(session);
583
+ });
584
+ return persisted ? Semaphore.withPermit(semaphore, Effect.andThen(cleanup, finishPersisted)) : cleanup;
585
+ })));
586
+ options.waitUntil(Fiber.await(session.fiber));
587
+ return takeReply(requestId, session);
588
+ });
589
+ const runStored = (registration, entityRuntime, envelopeText, lastSentChunk, discard, runOptions) => Effect.flatMap(decodeRequest(registration, envelopeText), envelope => run(registration, entityRuntime, envelope, lastSentChunk, discard, true, runOptions));
590
+ const completeReplayFailure = (registration, row, cause) => Effect.suspend(() => peekEnvelopeTag(row.envelope)).pipe(Effect.flatMap(tag => {
591
+ if (row.discard || tag === undefined) {
592
+ return write(completeTell(sql, row.requestId));
593
+ }
594
+ const rpc = registration.entity.protocol.requests.get(tag);
595
+ if (rpc === undefined) {
596
+ return write(completeTell(sql, row.requestId));
597
+ }
598
+ return Effect.flatMap(encodeReplyFor(registration, rpc, new Reply.WithExit({
599
+ requestId: row.requestId,
600
+ id: crypto.randomUUID(),
601
+ exit: Exit.failCause(cause)
602
+ })), reply => write(saveReply(sql, reply)).pipe(Effect.andThen(deliverScheduledReply(row.requestId, reply, row.replyTos)))).pipe(Effect.catchCause(() => write(completeTell(sql, row.requestId))));
603
+ }));
604
+ // Phase one runs each row up to forking its handler under the entry
605
+ // permit; the returned fibers observe the handler output and route
606
+ // failures through `completeReplayFailure`.
607
+ const replayRows = (registration, entityRuntime, rows) => rows.length === 0 ? Effect.succeed([]) : Effect.forEach(rows, row =>
608
+ // An active session or in-flight run already owns this request;
609
+ // replaying it would start a second execution in the same wake.
610
+ isRunning(row.requestId) ? Effect.succeed(undefined) : runStored(registration, entityRuntime, row.envelope, row.lastSentChunk, row.discard, row.deliverAt === undefined ? undefined : {
611
+ scheduled: true,
612
+ ...(row.replyTos === undefined ? undefined : {
613
+ replyTos: row.replyTos
614
+ })
615
+ }).pipe(Effect.flatMap(continuation => Effect.forkDetach(Effect.catchCause(Effect.asVoid(continuation), cause => completeReplayFailure(registration, row, cause)))), Effect.catchCause(cause => Effect.as(completeReplayFailure(registration, row, cause), undefined)))).pipe(Effect.map(entries => entries.filter(entry => entry !== undefined)), Effect.tap(entries => Effect.sync(() => {
616
+ if (entries.length > 0) options.waitUntil(Fiber.awaitAll(entries));
617
+ })));
618
+ // Registers the waiter eagerly (under the entry permit) and returns the
619
+ // await for the caller to run once the permit is released.
620
+ const delayedOutcome = (requestId, discard, replyTo, clientRequestId = requestId) => {
621
+ if (discard || replyTo !== undefined) return Effect.succeed(success(requestId, []));
622
+ const deferred = Deferred.makeUnsafe();
623
+ const waiters = workerWaiters.get(requestId) ?? [];
624
+ waiters.push({
625
+ clientRequestId,
626
+ deferred
627
+ });
628
+ workerWaiters.set(requestId, waiters);
629
+ return Deferred.await(deferred);
630
+ };
631
+ // Runs the storage entry under the caller-held permit and returns the
632
+ // effect that produces the invoke result after the permit is released.
633
+ const invokeEntry = (envelopeText, discard, delivery) => {
634
+ const registration = getEntityRegistration(options.address.entityType);
635
+ if (registration === undefined) {
636
+ return Effect.die(`No handlers registered for entity type: ${options.address.entityType}`);
637
+ }
638
+ return Effect.gen(function* () {
639
+ const entityRuntime = yield* getRuntime(registration);
640
+ // Replayed rows run on their own fibers. The handler semaphore already
641
+ // orders them ahead of this request, so the caller does not wait for them.
642
+ yield* Effect.flatMap(loadUnprocessed(sql), rows => replayRows(registration, entityRuntime, rows));
643
+ const finish = (requestId, continuation) => Effect.map(continuation, replies => success(requestId, replies));
644
+ // A persisted tell must not pin its caller: the row is durable, so the
645
+ // invoke result is complete once the handler is forked. Failures of the
646
+ // forked machinery leave the row unprocessed for replay on a later
647
+ // wake, so they are only logged here.
648
+ const finishTell = (requestId, continuation) => Effect.as(Effect.forkDetach(Effect.catchCause(Effect.asVoid(continuation), cause => Effect.logError("Entity tell execution failed", cause))), Effect.succeed(success(requestId, [])));
649
+ const envelope = yield* decodeRequest(registration, envelopeText);
650
+ const rpc = registration.entity.protocol.requests.get(envelope.tag);
651
+ const isPersisted = Context.get(rpc.annotations, Persisted);
652
+ if (!isPersisted) {
653
+ const continuation = yield* run(registration, entityRuntime, envelope, undefined, discard, false);
654
+ return finish(String(envelope.requestId), continuation);
655
+ }
656
+ const persistedResult = yield* Effect.result(write(persistRequest(sql, envelopeText, delivery?.primaryKey ?? Envelope.primaryKey(envelope), discard, delivery?.deliverAt, delivery?.replyTo)).pipe(Effect.withSpan("CloudflareCluster.persist", {
657
+ attributes: {
658
+ entityType: registration.entity.type,
659
+ entityId: String(options.address.entityId),
660
+ rpc: envelope.tag
661
+ }
662
+ }, {
663
+ captureStackTrace: false
664
+ }), Effect.provideContext(registration.context)));
665
+ if (Result.isFailure(persistedResult)) {
666
+ return Effect.succeed(persistedResult.failure._tag === "MailboxFull" ? {
667
+ _tag: "MailboxFull"
668
+ } : {
669
+ _tag: "EncodedMessageTooLarge"
670
+ });
671
+ }
672
+ const persisted = persistedResult.success;
673
+ if (persisted._tag === "Duplicate") {
674
+ const original = yield* loadMessage(sql, persisted.originalId);
675
+ if (original === undefined) return yield* Effect.die("Duplicate mailbox row disappeared");
676
+ if (original.discard && !discard) {
677
+ return Effect.succeed({
678
+ _tag: "AskDeduplicatedToTell"
679
+ });
680
+ }
681
+ const nextReply = yield* loadNextReply(sql, persisted.originalId);
682
+ if (nextReply !== undefined) {
683
+ if (nextReply.kind === "WithExit") {
684
+ const session = sessions.get(persisted.originalId);
685
+ if (session !== undefined) removeSession(persisted.originalId, session);
686
+ }
687
+ return Effect.succeed(success(persisted.originalId, [nextReply.reply]));
688
+ }
689
+ if (persisted.processed) {
690
+ return Effect.succeed(success(persisted.originalId, []));
691
+ }
692
+ if (delivery?.deliverAt !== undefined || original.deliverAt !== undefined && original.deliverAt > Date.now()) {
693
+ yield* armEarliestAlarm;
694
+ return delayedOutcome(persisted.originalId, discard, delivery?.replyTo, String(envelope.requestId));
695
+ }
696
+ // A scheduled ask past its deadline replies through its waiters and
697
+ // reply targets, not a session. The duplicate joins that execution,
698
+ // starting it if it is not running yet; the waiter is registered
699
+ // first so a run that settles at once still reaches it.
700
+ if (original.deliverAt !== undefined && !original.discard) {
701
+ const outcome = delayedOutcome(persisted.originalId, discard, delivery?.replyTo, String(envelope.requestId));
702
+ if (!isRunning(persisted.originalId)) {
703
+ yield* replayRows(registration, entityRuntime, [original]);
704
+ }
705
+ return outcome;
706
+ }
707
+ const continuation = yield* runStored(registration, entityRuntime, original.envelope, original.lastSentChunk, original.discard);
708
+ return original.discard ? yield* finishTell(persisted.originalId, continuation) : finish(persisted.originalId, continuation);
709
+ }
710
+ if (delivery?.deliverAt !== undefined) {
711
+ yield* armEarliestAlarm;
712
+ return delayedOutcome(String(envelope.requestId), discard, delivery.replyTo);
713
+ }
714
+ const continuation = yield* run(registration, entityRuntime, envelope, undefined, discard, true);
715
+ return discard ? yield* finishTell(String(envelope.requestId), continuation) : finish(String(envelope.requestId), continuation);
716
+ });
717
+ };
718
+ const invoke = (envelopeText, discard, delivery) => Effect.flatten(Semaphore.withPermit(semaphore, invokeEntry(envelopeText, discard, delivery)));
719
+ // An acknowledgement with no session comes after a restart. The stream is
720
+ // replayed from its last acknowledged chunk so the caller gets its next
721
+ // reply instead of waiting on an alarm replay that waits for this ack.
722
+ const resumeAcknowledged = requestId => Effect.flatten(Semaphore.withPermit(semaphore, Effect.gen(function* () {
723
+ const nextReply = yield* loadNextReply(sql, requestId);
724
+ if (nextReply !== undefined) return Effect.succeed([nextReply.reply]);
725
+ const registration = getEntityRegistration(options.address.entityType);
726
+ if (registration === undefined) return Effect.succeed([]);
727
+ // A session started since the ack was written owns the request.
728
+ const session = sessions.get(requestId);
729
+ if (session !== undefined) return yield* awaitStoredReply(requestId, session);
730
+ if (pendingRuns.has(requestId)) return Effect.succeed([]);
731
+ const row = (yield* loadUnprocessed(sql)).find(row => row.requestId === requestId);
732
+ if (row === undefined || row.discard || row.deliverAt !== undefined) return Effect.succeed([]);
733
+ const entityRuntime = yield* getRuntime(registration);
734
+ return yield* runStored(registration, entityRuntime, row.envelope, row.lastSentChunk, false).pipe(Effect.catchCause(cause => Effect.as(completeReplayFailure(registration, row, cause), storedNextReply(requestId))));
735
+ })));
736
+ const acknowledge = (requestId, replyId) => write(ackChunk(sql, requestId, replyId)).pipe(Effect.flatMap(() => {
737
+ const session = sessions.get(requestId);
738
+ if (session?.ack?.replyId === replyId) {
739
+ const acknowledged = session.ack.deferred;
740
+ session.ack = undefined;
741
+ Deferred.doneUnsafe(acknowledged, Effect.void);
742
+ return takeReply(requestId, session);
743
+ }
744
+ if (session === undefined) return resumeAcknowledged(requestId);
745
+ // A session this ack does not match may be a replay that has not saved
746
+ // its next reply yet; the caller will not ask again, so wait for it.
747
+ return Effect.flatten(awaitStoredReply(requestId, session));
748
+ }));
749
+ const interrupt = (storageRequestId, clientRequestId = storageRequestId) => Effect.suspend(() => {
750
+ const waiters = workerWaiters.get(storageRequestId);
751
+ if (waiters !== undefined) {
752
+ const remaining = waiters.filter(waiter => {
753
+ if (waiter.clientRequestId !== clientRequestId) return true;
754
+ Deferred.doneUnsafe(waiter.deferred, Effect.die(new Error("Delayed entity request interrupted")));
755
+ return false;
756
+ });
757
+ if (remaining.length === 0) workerWaiters.delete(storageRequestId);else workerWaiters.set(storageRequestId, remaining);
758
+ }
759
+ const session = sessions.get(storageRequestId);
760
+ if (session === undefined || session.uninterruptible) return Effect.void;
761
+ // Keep ownership through handler finalization and the terminal write.
762
+ // A failed write must leave the row owned rather than replayable.
763
+ session.cancelling = true;
764
+ if (session.ack !== undefined) {
765
+ Deferred.doneUnsafe(session.ack.deferred, Effect.void);
766
+ session.ack = undefined;
767
+ }
768
+ Queue.endUnsafe(session.queue);
769
+ const stop = session.fiber === undefined ? Effect.void : Effect.asVoid(Fiber.interrupt(session.fiber));
770
+ // The handler finalizer takes this same permit. Interrupt outside it.
771
+ return Effect.andThen(stop, Semaphore.withPermit(semaphore, Effect.andThen(session.completeInterrupt, Effect.suspend(() => {
772
+ if (sessions.get(storageRequestId) === session) sessions.delete(storageRequestId);
773
+ return inFlightPersisted === 0 ? rearm : Effect.void;
774
+ }))));
775
+ });
776
+ const reset = requestId => write(clearReplies(sql, requestId));
777
+ const alarm = Effect.suspend(() => {
778
+ const registration = getEntityRegistration(options.address.entityType);
779
+ if (registration === undefined) {
780
+ // A deploy that removed this entity type leaves its last alarm behind:
781
+ // it fails here, Cloudflare retries it 6 times, and the chain stops. The
782
+ // `cluster_entity_state` row is never cleaned up.
783
+ return Effect.die(`No handlers registered for entity type: ${options.address.entityType}`);
784
+ }
785
+ // The entry permit covers replay setup and alarm arming. The next alarm is
786
+ // armed before awaiting the replayed handlers: a handler that outlives the
787
+ // alarm wall-clock limit kills this invocation, and the chain must not
788
+ // depend on code after it.
789
+ return Effect.flatten(Semaphore.withPermit(semaphore, Effect.gen(function* () {
790
+ const rows = (yield* loadUnprocessed(sql)).filter(row => !isRunning(row.requestId));
791
+ const keepAlive = loadEntityState(sql).keepAlive;
792
+ // A heartbeat left over after keep-alive was released must not
793
+ // rebuild the entity; the `ensuring` below still settles the alarm.
794
+ if (rows.length === 0 && !keepAlive) return Effect.void;
795
+ const entityRuntime = yield* getRuntime(registration);
796
+ const fibers = yield* replayRows(registration, entityRuntime, rows);
797
+ yield* rearm;
798
+ const awaitReplays = Effect.asVoid(Fiber.awaitAll(fibers));
799
+ if (!keepAlive) return awaitReplays;
800
+ // The hold is never awaited: it does not resolve while held.
801
+ return Effect.andThen(Effect.catchCause(entityRuntime.warm, cause => Effect.logError("Entity keep-alive rebuild failed", cause)), awaitReplays);
802
+ }))).pipe(Effect.ensuring(Semaphore.withPermit(semaphore, rearm)), Effect.withSpan("CloudflareCluster.alarm", {
803
+ attributes: {
804
+ entityType: registration.entity.type,
805
+ entityId: String(options.address.entityId)
806
+ }
807
+ }, {
808
+ captureStackTrace: false
809
+ }), Effect.provideContext(registration.context));
810
+ });
811
+ const saveKeepAliveState = wanted => Semaphore.withPermit(semaphore, Effect.andThen(write(saveKeepAlive(sql, wanted)), wanted ? armHeartbeat : rearm));
812
+ const deliverReply = (requestId, reply) => Effect.sync(() => replyRegistry.deliver(requestId, reply));
813
+ return {
814
+ invoke,
815
+ acknowledge,
816
+ interrupt,
817
+ reset,
818
+ alarm,
819
+ deliverReply,
820
+ saveKeepAlive: saveKeepAliveState
821
+ };
822
+ };
823
+ //# sourceMappingURL=entityRuntime.js.map