effect 4.0.3 → 4.0.5

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 (449) hide show
  1. package/AGENTS.md +24 -0
  2. package/CLAUDE.md +24 -0
  3. package/ai-docs/package.json +2 -2
  4. package/ai-docs/src/52_graphql/10_basics.ts +117 -0
  5. package/ai-docs/src/52_graphql/20_testing.ts +45 -0
  6. package/ai-docs/src/52_graphql/30_batching.ts +118 -0
  7. package/ai-docs/src/52_graphql/40_aliased-batching.ts +124 -0
  8. package/ai-docs/src/52_graphql/50_errors-and-retries.ts +74 -0
  9. package/ai-docs/src/52_graphql/index.md +6 -0
  10. package/dist/Array.d.ts +90 -6
  11. package/dist/Array.d.ts.map +1 -1
  12. package/dist/Array.js +4 -2
  13. package/dist/Array.js.map +1 -1
  14. package/dist/Channel.d.ts +6 -1
  15. package/dist/Channel.d.ts.map +1 -1
  16. package/dist/Channel.js +17 -3
  17. package/dist/Channel.js.map +1 -1
  18. package/dist/ConfigProvider.d.ts.map +1 -1
  19. package/dist/ConfigProvider.js +33 -32
  20. package/dist/ConfigProvider.js.map +1 -1
  21. package/dist/Context.d.ts.map +1 -1
  22. package/dist/Context.js +9 -0
  23. package/dist/Context.js.map +1 -1
  24. package/dist/Duration.d.ts.map +1 -1
  25. package/dist/Duration.js +2 -5
  26. package/dist/Duration.js.map +1 -1
  27. package/dist/Effect.d.ts +61 -19
  28. package/dist/Effect.d.ts.map +1 -1
  29. package/dist/Effect.js +58 -39
  30. package/dist/Effect.js.map +1 -1
  31. package/dist/FileSystem.d.ts +23 -9
  32. package/dist/FileSystem.d.ts.map +1 -1
  33. package/dist/FileSystem.js.map +1 -1
  34. package/dist/Match.d.ts +20 -6
  35. package/dist/Match.d.ts.map +1 -1
  36. package/dist/Match.js +2 -2
  37. package/dist/Match.js.map +1 -1
  38. package/dist/Path.d.ts +37 -5
  39. package/dist/Path.d.ts.map +1 -1
  40. package/dist/Path.js +36 -5
  41. package/dist/Path.js.map +1 -1
  42. package/dist/RequestResolver.d.ts.map +1 -1
  43. package/dist/RequestResolver.js +4 -10
  44. package/dist/RequestResolver.js.map +1 -1
  45. package/dist/Scheduler.d.ts +9 -0
  46. package/dist/Scheduler.d.ts.map +1 -1
  47. package/dist/Scheduler.js +9 -0
  48. package/dist/Scheduler.js.map +1 -1
  49. package/dist/Schema.d.ts +15 -2
  50. package/dist/Schema.d.ts.map +1 -1
  51. package/dist/Schema.js +16 -2
  52. package/dist/Schema.js.map +1 -1
  53. package/dist/SchemaGetter.d.ts +4 -0
  54. package/dist/SchemaGetter.d.ts.map +1 -1
  55. package/dist/SchemaGetter.js +52 -33
  56. package/dist/SchemaGetter.js.map +1 -1
  57. package/dist/Sink.d.ts.map +1 -1
  58. package/dist/Sink.js +38 -17
  59. package/dist/Sink.js.map +1 -1
  60. package/dist/Stream.d.ts +11 -0
  61. package/dist/Stream.d.ts.map +1 -1
  62. package/dist/Stream.js +16 -1
  63. package/dist/Stream.js.map +1 -1
  64. package/dist/String.d.ts +12 -0
  65. package/dist/String.d.ts.map +1 -1
  66. package/dist/String.js +17 -4
  67. package/dist/String.js.map +1 -1
  68. package/dist/TxRef.d.ts.map +1 -1
  69. package/dist/TxRef.js +3 -2
  70. package/dist/TxRef.js.map +1 -1
  71. package/dist/Utils.js +8 -3
  72. package/dist/Utils.js.map +1 -1
  73. package/dist/ai/AiError.d.ts +10 -10
  74. package/dist/ai/AiError.d.ts.map +1 -1
  75. package/dist/ai/AiError.js +1 -1
  76. package/dist/ai/AiError.js.map +1 -1
  77. package/dist/ai/AnthropicStructuredOutput.js +0 -2
  78. package/dist/ai/AnthropicStructuredOutput.js.map +1 -1
  79. package/dist/ai/Chat.d.ts +2 -2
  80. package/dist/ai/Chat.d.ts.map +1 -1
  81. package/dist/ai/Decision.d.ts +70 -19
  82. package/dist/ai/Decision.d.ts.map +1 -1
  83. package/dist/ai/Decision.js +37 -3
  84. package/dist/ai/Decision.js.map +1 -1
  85. package/dist/ai/DecisionModel.js +1 -1
  86. package/dist/ai/DecisionModel.js.map +1 -1
  87. package/dist/ai/LanguageModel.d.ts +38 -7
  88. package/dist/ai/LanguageModel.d.ts.map +1 -1
  89. package/dist/ai/LanguageModel.js +70 -40
  90. package/dist/ai/LanguageModel.js.map +1 -1
  91. package/dist/ai/McpServer.d.ts +61 -52
  92. package/dist/ai/McpServer.d.ts.map +1 -1
  93. package/dist/ai/McpServer.js +134 -50
  94. package/dist/ai/McpServer.js.map +1 -1
  95. package/dist/ai/Prompt.d.ts.map +1 -1
  96. package/dist/ai/Prompt.js +28 -5
  97. package/dist/ai/Prompt.js.map +1 -1
  98. package/dist/ai/Response.d.ts +5 -4
  99. package/dist/ai/Response.d.ts.map +1 -1
  100. package/dist/ai/Response.js.map +1 -1
  101. package/dist/cli/Completions.d.ts +5 -0
  102. package/dist/cli/Completions.d.ts.map +1 -1
  103. package/dist/cli/Completions.js.map +1 -1
  104. package/dist/cli/Flag.d.ts +100 -0
  105. package/dist/cli/Flag.d.ts.map +1 -1
  106. package/dist/cli/Flag.js +33 -0
  107. package/dist/cli/Flag.js.map +1 -1
  108. package/dist/cli/Param.d.ts +87 -0
  109. package/dist/cli/Param.d.ts.map +1 -1
  110. package/dist/cli/Param.js +30 -0
  111. package/dist/cli/Param.js.map +1 -1
  112. package/dist/cli/internal/command.d.ts.map +1 -1
  113. package/dist/cli/internal/command.js +2 -1
  114. package/dist/cli/internal/command.js.map +1 -1
  115. package/dist/cli/internal/completions/bash.js +4 -3
  116. package/dist/cli/internal/completions/bash.js.map +1 -1
  117. package/dist/cli/internal/completions/descriptor.js +8 -1
  118. package/dist/cli/internal/completions/descriptor.js.map +1 -1
  119. package/dist/cli/internal/completions/fish.js +4 -4
  120. package/dist/cli/internal/completions/fish.js.map +1 -1
  121. package/dist/cli/internal/completions/powershell.js +3 -2
  122. package/dist/cli/internal/completions/powershell.js.map +1 -1
  123. package/dist/cli/internal/completions/zsh.js +7 -3
  124. package/dist/cli/internal/completions/zsh.js.map +1 -1
  125. package/dist/cli/internal/parser.js +11 -3
  126. package/dist/cli/internal/parser.js.map +1 -1
  127. package/dist/cli/internal/wizard.d.ts.map +1 -1
  128. package/dist/cli/internal/wizard.js +2 -1
  129. package/dist/cli/internal/wizard.js.map +1 -1
  130. package/dist/cluster/ClusterWorkflowEngine.d.ts +1 -0
  131. package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
  132. package/dist/cluster/ClusterWorkflowEngine.js +113 -21
  133. package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
  134. package/dist/cluster/MessageStorage.d.ts.map +1 -1
  135. package/dist/cluster/MessageStorage.js +20 -12
  136. package/dist/cluster/MessageStorage.js.map +1 -1
  137. package/dist/cluster/Runners.d.ts.map +1 -1
  138. package/dist/cluster/Runners.js +54 -34
  139. package/dist/cluster/Runners.js.map +1 -1
  140. package/dist/cluster/Sharding.d.ts.map +1 -1
  141. package/dist/cluster/Sharding.js +6 -4
  142. package/dist/cluster/Sharding.js.map +1 -1
  143. package/dist/cluster/internal/entityManager.js +1 -1
  144. package/dist/cluster/internal/entityManager.js.map +1 -1
  145. package/dist/encoding/Sse.d.ts +6 -0
  146. package/dist/encoding/Sse.d.ts.map +1 -1
  147. package/dist/encoding/Sse.js +63 -71
  148. package/dist/encoding/Sse.js.map +1 -1
  149. package/dist/graphql/GraphQL.d.ts +349 -0
  150. package/dist/graphql/GraphQL.d.ts.map +1 -0
  151. package/dist/graphql/GraphQL.js +207 -0
  152. package/dist/graphql/GraphQL.js.map +1 -0
  153. package/dist/graphql/GraphQLClient.d.ts +562 -0
  154. package/dist/graphql/GraphQLClient.d.ts.map +1 -0
  155. package/dist/graphql/GraphQLClient.js +505 -0
  156. package/dist/graphql/GraphQLClient.js.map +1 -0
  157. package/dist/graphql/GraphQLClientError.d.ts +445 -0
  158. package/dist/graphql/GraphQLClientError.d.ts.map +1 -0
  159. package/dist/graphql/GraphQLClientError.js +479 -0
  160. package/dist/graphql/GraphQLClientError.js.map +1 -0
  161. package/dist/graphql/GraphQLGroup.d.ts +118 -0
  162. package/dist/graphql/GraphQLGroup.d.ts.map +1 -0
  163. package/dist/graphql/GraphQLGroup.js +109 -0
  164. package/dist/graphql/GraphQLGroup.js.map +1 -0
  165. package/dist/graphql/GraphQLMiddleware.d.ts +249 -0
  166. package/dist/graphql/GraphQLMiddleware.d.ts.map +1 -0
  167. package/dist/graphql/GraphQLMiddleware.js +130 -0
  168. package/dist/graphql/GraphQLMiddleware.js.map +1 -0
  169. package/dist/graphql/GraphQLProtocol.d.ts +969 -0
  170. package/dist/graphql/GraphQLProtocol.d.ts.map +1 -0
  171. package/dist/graphql/GraphQLProtocol.js +809 -0
  172. package/dist/graphql/GraphQLProtocol.js.map +1 -0
  173. package/dist/graphql/GraphQLResolver.d.ts +453 -0
  174. package/dist/graphql/GraphQLResolver.d.ts.map +1 -0
  175. package/dist/graphql/GraphQLResolver.js +582 -0
  176. package/dist/graphql/GraphQLResolver.js.map +1 -0
  177. package/dist/graphql/index.d.ts +40 -0
  178. package/dist/graphql/index.d.ts.map +1 -0
  179. package/dist/graphql/index.js +41 -0
  180. package/dist/graphql/index.js.map +1 -0
  181. package/dist/http/FetchHttpClient.d.ts +6 -3
  182. package/dist/http/FetchHttpClient.d.ts.map +1 -1
  183. package/dist/http/FetchHttpClient.js +6 -3
  184. package/dist/http/FetchHttpClient.js.map +1 -1
  185. package/dist/http/FindMyWay/internal/router.js +2 -1
  186. package/dist/http/FindMyWay/internal/router.js.map +1 -1
  187. package/dist/http/Headers.d.ts +3 -3
  188. package/dist/http/Headers.d.ts.map +1 -1
  189. package/dist/http/Headers.js +6 -5
  190. package/dist/http/Headers.js.map +1 -1
  191. package/dist/http/HttpClient.d.ts +27 -0
  192. package/dist/http/HttpClient.d.ts.map +1 -1
  193. package/dist/http/HttpClient.js +38 -11
  194. package/dist/http/HttpClient.js.map +1 -1
  195. package/dist/http/HttpClientError.d.ts.map +1 -1
  196. package/dist/http/HttpClientError.js +8 -6
  197. package/dist/http/HttpClientError.js.map +1 -1
  198. package/dist/http/HttpClientRequest.d.ts +147 -4
  199. package/dist/http/HttpClientRequest.d.ts.map +1 -1
  200. package/dist/http/HttpClientRequest.js +47 -1
  201. package/dist/http/HttpClientRequest.js.map +1 -1
  202. package/dist/http/HttpEffect.d.ts.map +1 -1
  203. package/dist/http/HttpEffect.js +14 -7
  204. package/dist/http/HttpEffect.js.map +1 -1
  205. package/dist/http/HttpMiddleware.d.ts +60 -0
  206. package/dist/http/HttpMiddleware.d.ts.map +1 -1
  207. package/dist/http/HttpMiddleware.js +93 -32
  208. package/dist/http/HttpMiddleware.js.map +1 -1
  209. package/dist/http/HttpRouter.d.ts +35 -0
  210. package/dist/http/HttpRouter.d.ts.map +1 -1
  211. package/dist/http/HttpRouter.js +87 -13
  212. package/dist/http/HttpRouter.js.map +1 -1
  213. package/dist/http/HttpServerError.d.ts.map +1 -1
  214. package/dist/http/HttpServerError.js +6 -4
  215. package/dist/http/HttpServerError.js.map +1 -1
  216. package/dist/http/HttpServerRequest.js +2 -2
  217. package/dist/http/HttpServerRequest.js.map +1 -1
  218. package/dist/http/HttpServerResponse.d.ts +8 -6
  219. package/dist/http/HttpServerResponse.d.ts.map +1 -1
  220. package/dist/http/HttpServerResponse.js +27 -16
  221. package/dist/http/HttpServerResponse.js.map +1 -1
  222. package/dist/http/HttpStaticServer.d.ts.map +1 -1
  223. package/dist/http/HttpStaticServer.js +4 -10
  224. package/dist/http/HttpStaticServer.js.map +1 -1
  225. package/dist/http/HttpTraceContext.js +1 -1
  226. package/dist/http/HttpTraceContext.js.map +1 -1
  227. package/dist/http/Multipart.d.ts.map +1 -1
  228. package/dist/http/Multipart.js +15 -1
  229. package/dist/http/Multipart.js.map +1 -1
  230. package/dist/http/Url.d.ts.map +1 -1
  231. package/dist/http/Url.js +2 -1
  232. package/dist/http/Url.js.map +1 -1
  233. package/dist/http/UrlParams.d.ts +112 -8
  234. package/dist/http/UrlParams.d.ts.map +1 -1
  235. package/dist/http/UrlParams.js +141 -14
  236. package/dist/http/UrlParams.js.map +1 -1
  237. package/dist/http/internal/compression.d.ts +1 -1
  238. package/dist/http/internal/compression.d.ts.map +1 -1
  239. package/dist/http/internal/compression.js +2 -10
  240. package/dist/http/internal/compression.js.map +1 -1
  241. package/dist/http/internal/compressionBody.d.ts +2 -0
  242. package/dist/http/internal/compressionBody.d.ts.map +1 -0
  243. package/dist/http/internal/compressionBody.js +36 -0
  244. package/dist/http/internal/compressionBody.js.map +1 -0
  245. package/dist/http/internal/decompression.d.ts +2 -0
  246. package/dist/http/internal/decompression.d.ts.map +1 -0
  247. package/dist/http/internal/decompression.js +162 -0
  248. package/dist/http/internal/decompression.js.map +1 -0
  249. package/dist/http/internal/tracing.js +14 -4
  250. package/dist/http/internal/tracing.js.map +1 -1
  251. package/dist/http/internal/urlParams.d.ts +2 -0
  252. package/dist/http/internal/urlParams.d.ts.map +1 -0
  253. package/dist/http/internal/urlParams.js +4 -0
  254. package/dist/http/internal/urlParams.js.map +1 -0
  255. package/dist/http-api/HttpApiClient.d.ts.map +1 -1
  256. package/dist/http-api/HttpApiClient.js +2 -1
  257. package/dist/http-api/HttpApiClient.js.map +1 -1
  258. package/dist/http-api/HttpApiScalar.d.ts +5 -0
  259. package/dist/http-api/HttpApiScalar.d.ts.map +1 -1
  260. package/dist/http-api/HttpApiScalar.js.map +1 -1
  261. package/dist/internal/core.js +17 -1
  262. package/dist/internal/core.js.map +1 -1
  263. package/dist/internal/dateTime.js +40 -35
  264. package/dist/internal/dateTime.js.map +1 -1
  265. package/dist/internal/effect.js +13 -6
  266. package/dist/internal/effect.js.map +1 -1
  267. package/dist/internal/graphql.d.ts +2 -0
  268. package/dist/internal/graphql.d.ts.map +1 -0
  269. package/dist/internal/graphql.js +205 -0
  270. package/dist/internal/graphql.js.map +1 -0
  271. package/dist/internal/graphqlHeaders.d.ts +4 -0
  272. package/dist/internal/graphqlHeaders.d.ts.map +1 -0
  273. package/dist/internal/graphqlHeaders.js +11 -0
  274. package/dist/internal/graphqlHeaders.js.map +1 -0
  275. package/dist/internal/request.js +20 -1
  276. package/dist/internal/request.js.map +1 -1
  277. package/dist/internal/schema/toCodeDocument.js +4 -1
  278. package/dist/internal/schema/toCodeDocument.js.map +1 -1
  279. package/dist/internal/schema/toJsonSchemaDocument.js +29 -5
  280. package/dist/internal/schema/toJsonSchemaDocument.js.map +1 -1
  281. package/dist/internal/version.js +1 -1
  282. package/dist/net/Host.d.ts +49 -0
  283. package/dist/net/Host.d.ts.map +1 -1
  284. package/dist/net/Host.js +55 -0
  285. package/dist/net/Host.js.map +1 -1
  286. package/dist/net/NetAddress.d.ts +8 -0
  287. package/dist/net/NetAddress.d.ts.map +1 -1
  288. package/dist/net/NetAddress.js +8 -0
  289. package/dist/net/NetAddress.js.map +1 -1
  290. package/dist/observability/Otlp.d.ts +15 -0
  291. package/dist/observability/Otlp.d.ts.map +1 -1
  292. package/dist/observability/Otlp.js +3 -0
  293. package/dist/observability/Otlp.js.map +1 -1
  294. package/dist/observability/OtlpExporter.d.ts +8 -0
  295. package/dist/observability/OtlpExporter.d.ts.map +1 -1
  296. package/dist/observability/OtlpExporter.js +10 -1
  297. package/dist/observability/OtlpExporter.js.map +1 -1
  298. package/dist/observability/OtlpLogger.d.ts +17 -0
  299. package/dist/observability/OtlpLogger.d.ts.map +1 -1
  300. package/dist/observability/OtlpLogger.js +14 -1
  301. package/dist/observability/OtlpLogger.js.map +1 -1
  302. package/dist/observability/OtlpMetrics.d.ts +17 -0
  303. package/dist/observability/OtlpMetrics.d.ts.map +1 -1
  304. package/dist/observability/OtlpMetrics.js +13 -0
  305. package/dist/observability/OtlpMetrics.js.map +1 -1
  306. package/dist/observability/OtlpTracer.d.ts +17 -0
  307. package/dist/observability/OtlpTracer.d.ts.map +1 -1
  308. package/dist/observability/OtlpTracer.js +15 -2
  309. package/dist/observability/OtlpTracer.js.map +1 -1
  310. package/dist/reactivity/AsyncResult.d.ts +6 -0
  311. package/dist/reactivity/AsyncResult.d.ts.map +1 -1
  312. package/dist/reactivity/AsyncResult.js +27 -0
  313. package/dist/reactivity/AsyncResult.js.map +1 -1
  314. package/dist/reactivity/AtomRegistry.js +3 -1
  315. package/dist/reactivity/AtomRegistry.js.map +1 -1
  316. package/dist/rpc/RpcClient.d.ts.map +1 -1
  317. package/dist/rpc/RpcClient.js +6 -1
  318. package/dist/rpc/RpcClient.js.map +1 -1
  319. package/dist/rpc/RpcServer.d.ts.map +1 -1
  320. package/dist/rpc/RpcServer.js +36 -9
  321. package/dist/rpc/RpcServer.js.map +1 -1
  322. package/dist/socket/Socket.d.ts +6 -0
  323. package/dist/socket/Socket.d.ts.map +1 -1
  324. package/dist/socket/Socket.js.map +1 -1
  325. package/dist/sql/SqlClient.d.ts +8 -7
  326. package/dist/sql/SqlClient.d.ts.map +1 -1
  327. package/dist/sql/SqlClient.js +8 -7
  328. package/dist/sql/SqlClient.js.map +1 -1
  329. package/dist/testing/TestClock.d.ts +16 -3
  330. package/dist/testing/TestClock.d.ts.map +1 -1
  331. package/dist/testing/TestClock.js +112 -22
  332. package/dist/testing/TestClock.js.map +1 -1
  333. package/dist/workflow/Activity.js +1 -0
  334. package/dist/workflow/Activity.js.map +1 -1
  335. package/dist/workflow/DurableDeferred.d.ts.map +1 -1
  336. package/dist/workflow/DurableDeferred.js +32 -3
  337. package/dist/workflow/DurableDeferred.js.map +1 -1
  338. package/dist/workflow/Workflow.d.ts +22 -0
  339. package/dist/workflow/Workflow.d.ts.map +1 -1
  340. package/dist/workflow/Workflow.js +15 -3
  341. package/dist/workflow/Workflow.js.map +1 -1
  342. package/dist/workflow/WorkflowEngine.d.ts +44 -3
  343. package/dist/workflow/WorkflowEngine.d.ts.map +1 -1
  344. package/dist/workflow/WorkflowEngine.js +45 -22
  345. package/dist/workflow/WorkflowEngine.js.map +1 -1
  346. package/package.json +6 -3
  347. package/src/Array.ts +95 -6
  348. package/src/Channel.ts +24 -3
  349. package/src/ConfigProvider.ts +40 -44
  350. package/src/Context.ts +9 -0
  351. package/src/Duration.ts +4 -5
  352. package/src/Effect.ts +140 -53
  353. package/src/FileSystem.ts +23 -9
  354. package/src/Match.ts +65 -12
  355. package/src/Path.ts +52 -7
  356. package/src/RequestResolver.ts +4 -10
  357. package/src/Scheduler.ts +9 -0
  358. package/src/Schema.ts +25 -4
  359. package/src/SchemaGetter.ts +47 -35
  360. package/src/Sink.ts +45 -18
  361. package/src/Stream.ts +33 -4
  362. package/src/String.ts +17 -4
  363. package/src/TxRef.ts +4 -2
  364. package/src/Utils.ts +8 -5
  365. package/src/ai/AiError.ts +3 -1
  366. package/src/ai/AnthropicStructuredOutput.ts +2 -2
  367. package/src/ai/Chat.ts +2 -2
  368. package/src/ai/Decision.ts +70 -21
  369. package/src/ai/DecisionModel.ts +1 -1
  370. package/src/ai/LanguageModel.ts +141 -63
  371. package/src/ai/McpServer.ts +259 -145
  372. package/src/ai/Prompt.ts +24 -10
  373. package/src/ai/Response.ts +6 -3
  374. package/src/cli/Completions.ts +5 -0
  375. package/src/cli/Flag.ts +101 -0
  376. package/src/cli/Param.ts +96 -0
  377. package/src/cli/internal/command.ts +2 -1
  378. package/src/cli/internal/completions/bash.ts +4 -3
  379. package/src/cli/internal/completions/descriptor.ts +10 -1
  380. package/src/cli/internal/completions/fish.ts +4 -4
  381. package/src/cli/internal/completions/powershell.ts +5 -2
  382. package/src/cli/internal/completions/zsh.ts +7 -3
  383. package/src/cli/internal/parser.ts +12 -3
  384. package/src/cli/internal/wizard.ts +2 -1
  385. package/src/cluster/ClusterWorkflowEngine.ts +143 -36
  386. package/src/cluster/MessageStorage.ts +14 -3
  387. package/src/cluster/Runners.ts +88 -66
  388. package/src/cluster/Sharding.ts +12 -9
  389. package/src/cluster/internal/entityManager.ts +1 -1
  390. package/src/encoding/Sse.ts +70 -80
  391. package/src/graphql/GraphQL.ts +494 -0
  392. package/src/graphql/GraphQLClient.ts +924 -0
  393. package/src/graphql/GraphQLClientError.ts +542 -0
  394. package/src/graphql/GraphQLGroup.ts +167 -0
  395. package/src/graphql/GraphQLMiddleware.ts +253 -0
  396. package/src/graphql/GraphQLProtocol.ts +1540 -0
  397. package/src/graphql/GraphQLResolver.ts +1038 -0
  398. package/src/graphql/index.ts +48 -0
  399. package/src/http/FetchHttpClient.ts +6 -3
  400. package/src/http/FindMyWay/internal/router.ts +2 -1
  401. package/src/http/Headers.ts +6 -9
  402. package/src/http/HttpClient.ts +72 -11
  403. package/src/http/HttpClientError.ts +10 -6
  404. package/src/http/HttpClientRequest.ts +173 -7
  405. package/src/http/HttpEffect.ts +11 -5
  406. package/src/http/HttpMiddleware.ts +113 -34
  407. package/src/http/HttpRouter.ts +95 -13
  408. package/src/http/HttpServerError.ts +8 -4
  409. package/src/http/HttpServerRequest.ts +2 -2
  410. package/src/http/HttpServerResponse.ts +28 -16
  411. package/src/http/HttpStaticServer.ts +4 -10
  412. package/src/http/HttpTraceContext.ts +1 -1
  413. package/src/http/Multipart.ts +17 -1
  414. package/src/http/Url.ts +2 -1
  415. package/src/http/UrlParams.ts +225 -26
  416. package/src/http/internal/compression.ts +3 -17
  417. package/src/http/internal/compressionBody.ts +56 -0
  418. package/src/http/internal/decompression.ts +226 -0
  419. package/src/http/internal/tracing.ts +21 -4
  420. package/src/http/internal/urlParams.ts +5 -0
  421. package/src/http-api/HttpApiClient.ts +2 -1
  422. package/src/http-api/HttpApiScalar.ts +5 -0
  423. package/src/internal/core.ts +21 -1
  424. package/src/internal/dateTime.ts +13 -10
  425. package/src/internal/effect.ts +13 -7
  426. package/src/internal/graphql.ts +286 -0
  427. package/src/internal/graphqlHeaders.ts +11 -0
  428. package/src/internal/request.ts +31 -3
  429. package/src/internal/schema/toCodeDocument.ts +7 -1
  430. package/src/internal/schema/toJsonSchemaDocument.ts +34 -2
  431. package/src/internal/version.ts +1 -1
  432. package/src/net/Host.ts +72 -0
  433. package/src/net/NetAddress.ts +9 -0
  434. package/src/observability/Otlp.ts +18 -0
  435. package/src/observability/OtlpExporter.ts +14 -3
  436. package/src/observability/OtlpLogger.ts +31 -2
  437. package/src/observability/OtlpMetrics.ts +30 -1
  438. package/src/observability/OtlpTracer.ts +32 -3
  439. package/src/reactivity/AsyncResult.ts +31 -0
  440. package/src/reactivity/AtomRegistry.ts +3 -1
  441. package/src/rpc/RpcClient.ts +6 -1
  442. package/src/rpc/RpcServer.ts +55 -15
  443. package/src/socket/Socket.ts +6 -0
  444. package/src/sql/SqlClient.ts +9 -7
  445. package/src/testing/TestClock.ts +144 -29
  446. package/src/workflow/Activity.ts +1 -0
  447. package/src/workflow/DurableDeferred.ts +43 -5
  448. package/src/workflow/Workflow.ts +31 -5
  449. package/src/workflow/WorkflowEngine.ts +107 -25
@@ -282,7 +282,7 @@ export const make = Effect.gen(function*() {
282
282
  yield* storage.clearAddress(clockAddress)
283
283
  })
284
284
 
285
- const resume = Effect.fnUntraced(function*(workflow: Workflow.Any, executionId: string) {
285
+ const resume = Effect.fnUntraced(function*(workflow: Workflow.Any, executionId: string, mode: WakeMode) {
286
286
  const maybeReply = yield* requestReply({
287
287
  workflow,
288
288
  entityType: `Workflow/${workflow._tag}`,
@@ -291,12 +291,11 @@ export const make = Effect.gen(function*() {
291
291
  id: ""
292
292
  })
293
293
 
294
- const maybeSuspended = Option.filter(
295
- maybeReply,
296
- (reply) => reply.exit._tag === "Success" && reply.exit.value._tag === "Suspended"
297
- )
294
+ const maybeSuspended = Option.filter(maybeReply, isSuspendedReply)
298
295
 
299
296
  if (Option.isNone(maybeSuspended)) return
297
+ // Replies without `failed` retain automatic wakes for compatibility.
298
+ if (mode === "automatic" && isFailedReply(maybeSuspended.value)) return
300
299
  if (
301
300
  !(yield* sharding.reset(
302
301
  Snowflake.Snowflake(maybeSuspended.value.requestId),
@@ -308,6 +307,15 @@ export const make = Effect.gen(function*() {
308
307
  yield* sharding.pollStorage
309
308
  })
310
309
 
310
+ // Activations load a persisted interrupt when they start, so the per-call
311
+ // check reads only memory.
312
+ const interruptPending = (instance: WorkflowEngine.WorkflowInstance["Service"]) =>
313
+ Effect.map(Effect.serviceOption(CurrentActivationScope), (activation) => {
314
+ const pending = Option.isSome(activation) ? pendingByActivation.get(activation.value) : undefined
315
+ return pending?.workflowName === instance.workflow._tag && pending.executionId === instance.executionId &&
316
+ pending.results.has(InterruptSignal.name)
317
+ })
318
+
311
319
  const sendResumeParent = Effect.fnUntraced(function*(options: {
312
320
  readonly workflowName: string
313
321
  readonly executionId: string
@@ -328,10 +336,14 @@ export const make = Effect.gen(function*() {
328
336
  yield* sharding.reset(requestId.value)
329
337
  }, Effect.scoped)
330
338
 
331
- // A child or deferred may complete while a run is still unwinding. Subscribe
332
- // to the run's reply before reading storage so the wake cannot fall between
333
- // the run's last read and its suspended reply.
334
- const waitForRunReply = Effect.fnUntraced(function*(workflow: Workflow.Any, request: Entity.Request<any>) {
339
+ // Subscribe to the run's next reply before reading storage, so a reply
340
+ // cannot fall between the read and the subscription. Returns `true` when the
341
+ // stored reply already settles the wait, `false` when a new reply arrived.
342
+ const awaitRunReply = Effect.fnUntraced(function*(
343
+ workflow: Workflow.Any,
344
+ request: Envelope.Request<any>,
345
+ isSettled: (reply: WithExitEncoded<Workflow.ResultEncoded<any, any>>) => boolean
346
+ ) {
335
347
  const waiter = yield* storage.registerReplyHandler(
336
348
  new Message.OutgoingRequest<any>({
337
349
  envelope: request,
@@ -343,12 +355,45 @@ export const make = Effect.gen(function*() {
343
355
  })
344
356
  ).pipe(Effect.forkScoped({ startImmediately: true }))
345
357
  const reply = yield* replyForRequestId(request.requestId)
346
- if (Option.isSome(reply)) return
347
- yield* Fiber.join(waiter).pipe(
358
+ if (Option.isSome(reply) && isSettled(reply.value)) return true
359
+ yield* Fiber.join(waiter)
360
+ return false
361
+ }, Effect.scoped)
362
+
363
+ // A child or deferred may complete while a run is still unwinding. Wait for
364
+ // the run's reply so the wake cannot fall between the run's last read and
365
+ // its suspended reply.
366
+ const waitForRunReply = (workflow: Workflow.Any, request: Entity.Request<any>) =>
367
+ awaitRunReply(workflow, request, () => true).pipe(
368
+ Effect.asVoid,
348
369
  Effect.catchTag("EntityNotAssignedToRunner", () => ClusterAbandon.interrupt),
349
370
  Effect.catchCause((cause) => ClusterAbandon.isCause(cause) ? ClusterAbandon.interrupt : Effect.failCause(cause))
350
371
  )
351
- }, Effect.scoped)
372
+
373
+ // Wake an external caller once its run publishes a reply other than
374
+ // `Suspended`. Replies are only observed on the runner that saves them, so
375
+ // callers on other runners, and any failure here, leave delivery to the
376
+ // caller's retry schedule.
377
+ const awaitComplete = (workflow: Workflow.Any, executionId: string) =>
378
+ Effect.gen(function*() {
379
+ const address = entityAddressFor({ workflow, entityType: `Workflow/${workflow._tag}`, executionId })
380
+ if (!sharding.hasShardId(address.shardId)) return yield* Effect.never
381
+ const requestId = yield* storage.requestIdForPrimaryKey({ address, tag: "run", id: "" })
382
+ if (Option.isNone(requestId)) return yield* Effect.never
383
+ const request = Envelope.makeRequest<any>({
384
+ requestId: requestId.value,
385
+ address,
386
+ tag: "run",
387
+ payload: {},
388
+ headers: Headers.empty
389
+ })
390
+ // A replay that suspends again re-arms the wait, so it does not spend a
391
+ // step of the caller's retry schedule.
392
+ let settled = false
393
+ while (!settled) {
394
+ settled = yield* awaitRunReply(workflow, request, (reply) => !isSuspendedReply(reply))
395
+ }
396
+ }).pipe(Effect.catchCause(() => Effect.never))
352
397
 
353
398
  const interrupt = Effect.fnUntraced(
354
399
  function*(workflow: Workflow.Any, executionId: string) {
@@ -367,7 +412,7 @@ export const make = Effect.gen(function*() {
367
412
 
368
413
  const nonSuspendedReply = Option.filter(
369
414
  reply,
370
- (reply) => reply.exit._tag !== "Success" || reply.exit.value._tag !== "Suspended"
415
+ (reply) => !isSuspendedReply(reply)
371
416
  )
372
417
  if (Option.isSome(nonSuspendedReply)) {
373
418
  return Option.none()
@@ -402,7 +447,25 @@ export const make = Effect.gen(function*() {
402
447
  const activation = yield* CurrentActivationScope
403
448
  let pending = pendingByActivation.get(activation)
404
449
  if (!pending) {
405
- pending = { workflowName: workflow._tag, executionId, results: new Map() }
450
+ const results = new Map<string, Exit.Exit<unknown, unknown>>()
451
+ // A new owner must see an interrupt persisted before it took over.
452
+ // Publish the cache only once loaded, so a retried build repeats
453
+ // a failed lookup.
454
+ const interrupt = yield* Effect.orDie(requestReply({
455
+ workflow,
456
+ entityType: `Workflow/${workflow._tag}`,
457
+ executionId,
458
+ tag: "deferred",
459
+ id: InterruptSignal.name
460
+ }))
461
+ if (Option.isSome(interrupt)) {
462
+ const decoded = decodeDeferredWithExit(interrupt.value as any)
463
+ results.set(
464
+ InterruptSignal.name,
465
+ (decoded.exit._tag === "Success" ? decoded.exit.value : decoded.exit) as Exit.Exit<unknown, unknown>
466
+ )
467
+ }
468
+ pending = { workflowName: workflow._tag, executionId, results }
406
469
  pendingByActivation.set(activation, pending)
407
470
  }
408
471
  // Retain awaited names through reply persistence, including suspension.
@@ -410,25 +473,30 @@ export const make = Effect.gen(function*() {
410
473
  let currentRun: {
411
474
  readonly request: Entity.Request<any>
412
475
  readonly instance: WorkflowEngine.WorkflowInstance["Service"]
476
+ // Activity awaits must wake the run after suspension without preempting it.
477
+ readonly activityAwaits: Map<string, number>
413
478
  } | undefined
414
479
  // Concurrent wakes share one wait for the current run to publish its reply.
480
+ // Explicit wakes cannot be dropped: operator resume bypasses the gate;
481
+ // InterruptSignal enters it only on a fresh entity, with no run to wait for.
415
482
  const resumeGate = Semaphore.makeUnsafe(1)
416
- const resumeCurrentRun = Effect.suspend(() =>
417
- resumeGate.withPermitsIfAvailable(1)(
418
- currentRun ? waitForRunReply(workflow, currentRun.request) : Effect.void
483
+ const resumeCurrentRun = (mode: WakeMode) =>
484
+ Effect.suspend(() =>
485
+ resumeGate.withPermitsIfAvailable(1)(
486
+ currentRun ? waitForRunReply(workflow, currentRun.request) : Effect.void
487
+ )
488
+ ).pipe(
489
+ // Release the gate before reset can start another run, so later
490
+ // completions can wake that replay.
491
+ Effect.flatMap((waited) => Option.isSome(waited) ? resume(workflow, executionId, mode) : Effect.void),
492
+ ensureSuccess
419
493
  )
420
- ).pipe(
421
- // Release the gate before reset can start another run, so later
422
- // completions can wake that replay.
423
- Effect.flatMap((waited) => Option.isSome(waited) ? resume(workflow, executionId) : Effect.void),
424
- ensureSuccess
425
- )
426
494
  return {
427
495
  run: (request: Entity.Request<any>) => {
428
496
  const instance = WorkflowEngine.WorkflowInstance.initial(workflow, executionId)
429
- currentRun = { request, instance }
497
+ currentRun = { request, instance, activityAwaits: new Map() }
430
498
  const parent = (request.payload as any)[payloadParentKey] as
431
- | { workflowName: string; executionId: string }
499
+ | { workflowName: string; executionId: string; detached?: boolean }
432
500
  | undefined
433
501
  return execute(workflow.payloadSchema.make(request.payload) as object, executionId).pipe(
434
502
  Effect.onExit((exit) => {
@@ -445,7 +513,9 @@ export const make = Effect.gen(function*() {
445
513
  !instance.abandoned &&
446
514
  !(suspendOnFailure && exit._tag === "Failure")
447
515
  ) {
448
- return parent ? ensureSuccess(sendResumeParent(parent, executionId)) : Effect.void
516
+ return parent && !parent.detached
517
+ ? ensureSuccess(sendResumeParent(parent, executionId))
518
+ : Effect.void
449
519
  }
450
520
  return engine.deferredResult(InterruptSignal).pipe(
451
521
  Effect.flatMap((maybeExit) => {
@@ -472,7 +542,9 @@ export const make = Effect.gen(function*() {
472
542
  activity(request: Entity.Request<any>) {
473
543
  const payload = request.payload as { name: string; attempt: number }
474
544
  const activityId = `${executionId}/${payload.name}`
475
- const instance = WorkflowEngine.WorkflowInstance.initial(workflow, executionId)
545
+ const instance = WorkflowEngine.WorkflowInstance.initial(workflow, executionId, {
546
+ awaitedDeferreds: currentRun?.activityAwaits
547
+ })
476
548
  interruptedActivities.delete(activityId)
477
549
  const entry = activities.get(activityId)
478
550
  if (!entry) {
@@ -512,18 +584,24 @@ export const make = Effect.gen(function*() {
512
584
  deferred: (request: Entity.Request<any>) => {
513
585
  const payload = request.payload as any
514
586
  pending.results.set(payload.name, payload.exit)
587
+ const mode: WakeMode = payload.name === InterruptSignal.name ? "explicit" : "automatic"
515
588
  // Await registers before reading the cache. If this run has not
516
589
  // registered yet, a later await will see the result just stored.
517
590
  // Check when the wake runs and skip preemption too: interrupting
518
591
  // a later await would require waiting for its reply before reset.
519
592
  // A run completing its own deferred also reads it from the cache
520
- // when it awaits later, so that completion never wakes the run.
593
+ // when it awaits later, unless an activity is already parked on it.
521
594
  const wake = Effect.suspend(() =>
522
595
  currentRun && !currentRun.instance.awaitedDeferreds.has(payload.name)
523
- ? currentRun.instance.completedDeferreds.has(payload.name)
596
+ ? currentRun.activityAwaits.has(payload.name)
597
+ // Hold an activity wake until the run has published its reply.
598
+ ? resumeCurrentRun(mode)
599
+ : currentRun.instance.completedDeferreds.has(payload.name)
524
600
  ? Effect.void
525
- : ensureSuccess(resume(workflow, executionId))
526
- : deferredState.deferredDone(executionId, payload.name).pipe(Effect.andThen(resumeCurrentRun))
601
+ : ensureSuccess(resume(workflow, executionId, mode))
602
+ : deferredState.deferredDone(executionId, payload.name).pipe(
603
+ Effect.andThen(resumeCurrentRun(mode))
604
+ )
527
605
  )
528
606
  // An asynchronous reply releases the RPC concurrency permit while
529
607
  // the wake waits; the pending request still occupies mailbox capacity.
@@ -538,7 +616,7 @@ export const make = Effect.gen(function*() {
538
616
  )
539
617
  },
540
618
 
541
- resume: () => resumeCurrentRun.pipe(Rpc.wrap({ fork: true, uninterruptible: true }))
619
+ resume: () => resumeCurrentRun("automatic").pipe(Rpc.wrap({ fork: true, uninterruptible: true }))
542
620
  }
543
621
  }),
544
622
  // Alongside the run, let deferred handlers briefly acquire a permit to
@@ -555,7 +633,7 @@ export const make = Effect.gen(function*() {
555
633
  ).pipe(Effect.andThen(registration))
556
634
  }),
557
635
 
558
- execute: (workflow, { discard, executionId, parent, payload }) => {
636
+ execute: (workflow, { detached, discard, executionId, parent, payload }) => {
559
637
  ensureEntity(workflow)
560
638
  return RcMap.get(clients, workflow._tag).pipe(
561
639
  Effect.flatMap((make) =>
@@ -563,7 +641,10 @@ export const make = Effect.gen(function*() {
563
641
  parent ?
564
642
  {
565
643
  ...payload,
566
- [payloadParentKey]: { workflowName: parent.workflow._tag, executionId: parent.executionId }
644
+ // A detached child keeps its parent for observability only.
645
+ [payloadParentKey]: detached
646
+ ? { workflowName: parent.workflow._tag, executionId: parent.executionId, detached: true }
647
+ : { workflowName: parent.workflow._tag, executionId: parent.executionId }
567
648
  } :
568
649
  payload,
569
650
  { discard }
@@ -615,7 +696,9 @@ export const make = Effect.gen(function*() {
615
696
  ))
616
697
  }),
617
698
 
618
- resume: (workflow, executionId) => ensureSuccess(resume(workflow, executionId)),
699
+ resume: (workflow, executionId) => ensureSuccess(resume(workflow, executionId, "explicit")),
700
+
701
+ awaitComplete,
619
702
 
620
703
  activityExecute: Effect.fnUntraced(
621
704
  function*(activity, attempt) {
@@ -623,6 +706,21 @@ export const make = Effect.gen(function*() {
623
706
  const instance = Context.get(services, WorkflowEngine.WorkflowInstance)
624
707
  yield* Effect.annotateCurrentSpan("executionId", instance.executionId)
625
708
  const activityId = `${instance.executionId}/${activity.name}`
709
+ // A pending interrupt replays only to rebuild compensations, so it must
710
+ // not run new activities. Finalizers run once `interrupted` is set.
711
+ if (!instance.interrupted && (yield* interruptPending(instance))) {
712
+ const reply = yield* Effect.orDie(requestReply({
713
+ workflow: instance.workflow,
714
+ entityType: `Workflow/${instance.workflow._tag}`,
715
+ executionId: instance.executionId,
716
+ tag: "activity",
717
+ id: activityPrimaryKey(activity.name, attempt)
718
+ }))
719
+ // Suspend so `onExit` applies the interrupt to the run.
720
+ if (Option.isNone(reply) || isSuspendedReply(reply.value)) {
721
+ return new Workflow.Suspended({})
722
+ }
723
+ }
626
724
  const client = (yield* RcMap.get(clientsPartial, instance.workflow._tag))(instance.executionId)
627
725
  while (true) {
628
726
  if (!activities.has(activityId)) {
@@ -809,6 +907,14 @@ const DeferredRpc = Rpc.make("deferred", {
809
907
  .annotate(ClusterSchema.Persisted, true)
810
908
  .annotate(ClusterSchema.Uninterruptible, true)
811
909
 
910
+ type WakeMode = "explicit" | "automatic"
911
+
912
+ const isSuspendedReply = (reply: WithExitEncoded<Workflow.ResultEncoded<any, any>>) =>
913
+ reply.exit._tag === "Success" && reply.exit.value._tag === "Suspended"
914
+
915
+ const isFailedReply = (reply: WithExitEncoded<Workflow.ResultEncoded<any, any>>) =>
916
+ reply.exit._tag === "Success" && reply.exit.value._tag === "Suspended" && reply.exit.value.failed === true
917
+
812
918
  const decodeDeferredWithExit = Schema.decodeSync(Schema.toCodecJson(Reply.WithExit.schema(DeferredRpc)))
813
919
 
814
920
  const ResumeRpc = Rpc.make("resume", {
@@ -836,7 +942,8 @@ const makeWorkflowEntity = (workflow: Workflow.Any) =>
836
942
  ...workflow.payloadSchema.fields,
837
943
  [payloadParentKey]: Schema.optional(Schema.Struct({
838
944
  workflowName: Schema.String,
839
- executionId: Schema.String
945
+ executionId: Schema.String,
946
+ detached: Schema.optional(Schema.Boolean)
840
947
  }))
841
948
  },
842
949
  primaryKey: () => "",
@@ -1086,8 +1086,8 @@ export class MemoryDriver extends Context.Service<MemoryDriver>()("effect/cluste
1086
1086
  journal.push(envelope)
1087
1087
  return SaveResultEncoded.Success()
1088
1088
  }),
1089
- saveReply: (reply_) =>
1090
- Effect.sync(() => {
1089
+ saveReply: (reply_) => {
1090
+ const save = Effect.sync(() => {
1091
1091
  const reply = JSON.parse(JSON.stringify(reply_)) as Reply.Encoded
1092
1092
  const entry = requests.get(reply.requestId)
1093
1093
  if (!entry || replyIds.has(reply.id)) return
@@ -1098,7 +1098,18 @@ export class MemoryDriver extends Context.Service<MemoryDriver>()("effect/cluste
1098
1098
  entry.replies.push(reply)
1099
1099
  replyIds.add(reply.id)
1100
1100
  replyLatch.openUnsafe()
1101
- }),
1101
+ })
1102
+ // Memory has no transaction isolation. Publish terminal replies at the
1103
+ // outer commit boundary, before their queued handler notifications, so
1104
+ // reply readers cannot deliver a success that may still be rolled back.
1105
+ return TransactionNotifications.use((notifications) =>
1106
+ reply_._tag === "WithExit" && notifications
1107
+ ? Effect.sync(() => {
1108
+ notifications.push(save)
1109
+ })
1110
+ : save
1111
+ )
1112
+ },
1102
1113
  clearReplies: (id, options) =>
1103
1114
  Effect.sync(() => {
1104
1115
  const entry = requests.get(String(id))
@@ -14,6 +14,7 @@
14
14
  import * as Context from "../Context.ts"
15
15
  import * as Effect from "../Effect.ts"
16
16
  import * as Exit from "../Exit.ts"
17
+ import * as Fiber from "../Fiber.ts"
17
18
  import * as Latch from "../Latch.ts"
18
19
  import * as Layer from "../Layer.ts"
19
20
  import * as Option from "../Option.ts"
@@ -247,78 +248,93 @@ export const make: (
247
248
 
248
249
  type StorageRequestEntry = {
249
250
  readonly latch: Latch.Latch
250
- doneLatch: Latch.Latch | undefined
251
251
  readonly messages: Set<Message.OutgoingRequest<any>>
252
+ readonly pump: Fiber.Fiber<void>
252
253
  replies: Array<Reply.Reply<any>>
253
254
  }
254
255
  const storageRequests = new Map<Snowflake.Snowflake, StorageRequestEntry>()
255
256
  const waitingStorageRequests = new Map<Snowflake.Snowflake, Message.OutgoingRequest<any>>()
256
- const replyFromStorage = Effect.fnUntraced(
257
- function*(message: Message.OutgoingRequest<any>) {
258
- let entry = storageRequests.get(message.envelope.requestId)
259
- if (entry) {
260
- entry.messages.add(message)
261
- entry.doneLatch ??= Latch.makeUnsafe(false)
262
- return yield* entry.doneLatch.await
263
- } else {
264
- entry = {
265
- latch: Latch.makeUnsafe(false),
266
- doneLatch: undefined,
267
- replies: [],
268
- messages: new Set([message])
269
- }
270
- storageRequests.set(message.envelope.requestId, entry)
271
- }
272
257
 
273
- while (true) {
274
- // wait for the storage loop to notify us
275
- entry.latch.closeUnsafe()
276
- waitingStorageRequests.set(message.envelope.requestId, message)
277
- storageLatch.openUnsafe()
278
- yield* entry.latch.await
279
-
280
- // send the replies back
281
- for (let i = 0; i < entry.replies.length; i++) {
282
- const reply = entry.replies[i]
283
- // we have reached the end
284
- if (reply._tag === "WithExit") {
285
- // A reply can immediately reset and resend the same request. Do not
286
- // let that sender attach to this completed reply loop.
287
- storageRequests.delete(message.envelope.requestId)
288
- waitingStorageRequests.delete(message.envelope.requestId)
289
- for (const message of entry.messages) {
290
- yield* message.respond(reply)
291
- }
292
- entry.doneLatch?.openUnsafe()
293
- return
294
- }
258
+ // A reset request can register a replacement entry under the same id, which
259
+ // must survive the old entry's cleanup.
260
+ const removeStorageRequest = (requestId: Snowflake.Snowflake, entry: Omit<StorageRequestEntry, "pump">) => {
261
+ if (storageRequests.get(requestId) !== entry) return
262
+ storageRequests.delete(requestId)
263
+ waitingStorageRequests.delete(requestId)
264
+ }
295
265
 
296
- entry.latch.closeUnsafe()
266
+ // The pump is owned by the shared entry rather than the first caller, so any
267
+ // waiter can be interrupted without stopping delivery to the others.
268
+ const pumpReplies = Effect.fnUntraced(function*(
269
+ requestId: Snowflake.Snowflake,
270
+ entry: Omit<StorageRequestEntry, "pump">
271
+ ) {
272
+ while (true) {
273
+ // wait for the storage loop to notify us
274
+ entry.latch.closeUnsafe()
275
+ const message = entry.messages.values().next().value
276
+ if (!message) return
277
+ waitingStorageRequests.set(requestId, message)
278
+ storageLatch.openUnsafe()
279
+ yield* entry.latch.await
280
+
281
+ // send the replies back
282
+ for (let i = 0; i < entry.replies.length; i++) {
283
+ const reply = entry.replies[i]
284
+ // we have reached the end
285
+ if (reply._tag === "WithExit") {
286
+ // A reply can immediately reset and resend the same request. Do not
287
+ // let that sender attach to this completed entry.
288
+ removeStorageRequest(requestId, entry)
297
289
  for (const message of entry.messages) {
298
290
  yield* message.respond(reply)
299
291
  }
300
- // wait for ack
301
- yield* entry.latch.await
292
+ return
302
293
  }
303
- entry.replies = []
294
+
295
+ entry.latch.closeUnsafe()
296
+ for (const message of entry.messages) {
297
+ yield* message.respond(reply)
298
+ }
299
+ // wait for ack
300
+ yield* entry.latch.await
304
301
  }
305
- },
306
- (effect, message) =>
307
- Effect.ensuring(
308
- effect,
309
- Effect.sync(() => {
310
- const entry = storageRequests.get(message.envelope.requestId)
311
- // An old waiter must not clean up a reset request's new reply loop.
312
- if (!entry?.messages.has(message)) return
313
- if (entry.messages.size > 1) {
314
- entry.messages.delete(message)
315
- return
302
+ entry.replies = []
303
+ }
304
+ })
305
+
306
+ const replyFromStorage = (message: Message.OutgoingRequest<any>) =>
307
+ Effect.uninterruptibleMask((restore) =>
308
+ Effect.gen(function*() {
309
+ const requestId = message.envelope.requestId
310
+ let entry = storageRequests.get(requestId)
311
+ if (!entry) {
312
+ const state = {
313
+ latch: Latch.makeUnsafe(false),
314
+ messages: new Set<Message.OutgoingRequest<any>>(),
315
+ replies: [] as Array<Reply.Reply<any>>
316
316
  }
317
- storageRequests.delete(message.envelope.requestId)
318
- waitingStorageRequests.delete(message.envelope.requestId)
319
- })
320
- )
321
- )
317
+ entry = Object.assign(state, { pump: yield* Effect.forkIn(pumpReplies(requestId, state), runnersScope) })
318
+ storageRequests.set(requestId, entry)
319
+ }
320
+ const current = entry
321
+ current.messages.add(message)
322
+ return yield* restore(Fiber.join(current.pump)).pipe(
323
+ Effect.ensuring(Effect.suspend(() => {
324
+ current.messages.delete(message)
325
+ if (current.messages.size === 0) {
326
+ removeStorageRequest(requestId, current)
327
+ return Fiber.interrupt(current.pump)
328
+ }
329
+ // The storage loop decodes replies using the waiting message.
330
+ if (waitingStorageRequests.get(requestId) === message) {
331
+ waitingStorageRequests.set(requestId, current.messages.values().next().value!)
332
+ }
333
+ return Effect.void
334
+ }))
335
+ )
336
+ })
337
+ )
322
338
 
323
339
  const storageLatch = Latch.makeUnsafe(false)
324
340
  if (storage !== MessageStorage.noop) {
@@ -415,22 +431,28 @@ export const make: (
415
431
  })
416
432
  },
417
433
  notifyLocal(options) {
418
- return notifyWith(options.message, (message, duplicate) => {
434
+ return notifyWith(options.message, (message) => {
419
435
  if (options.discard || message._tag === "OutgoingEnvelope") {
420
436
  return Effect.catchTag(
421
437
  options.notify(Message.incomingLocalFromOutgoing(message)),
422
438
  "EntityNotAssignedToRunner",
423
439
  () => Effect.void
424
440
  )
425
- } else if (!duplicate && options.storageOnly !== true) {
426
- return options.notify(Message.incomingLocalFromOutgoing(message)).pipe(
427
- Effect.andThen(storage.registerReplyHandler(message)),
428
- Effect.catchTag("EntityNotAssignedToRunner", () => replyFromStorage(message))
429
- )
430
441
  }
442
+ // The request can be processed as soon as it is saved, so replies are
443
+ // always read back from storage. The local reply handler only wakes the
444
+ // storage loop, so streams do not have to wait for the next poll.
445
+ const readReplies = options.storageOnly
446
+ ? replyFromStorage(message)
447
+ : Effect.raceFirst(
448
+ replyFromStorage(message),
449
+ storage.registerReplyHandler(
450
+ new Message.OutgoingRequest({ ...message, respond: () => storageLatch.open })
451
+ ).pipe(Effect.ignore, Effect.andThen(Effect.never))
452
+ )
431
453
  return options.notify(Message.incomingLocalFromOutgoing(message)).pipe(
432
454
  Effect.catchTag("EntityNotAssignedToRunner", () => Effect.void),
433
- Effect.andThen(replyFromStorage(message))
455
+ Effect.andThen(readReplies)
434
456
  )
435
457
  })
436
458
  }
@@ -692,6 +692,12 @@ const make = Effect.gen(function*() {
692
692
  const storageReadLatch = yield* Latch.make(true)
693
693
  const openStorageReadLatch = constant(Effect.asVoid(storageReadLatch.open))
694
694
 
695
+ type PendingNotification = {
696
+ resume: (_: Effect.Effect<void, EntityNotAssignedToRunner>) => void
697
+ readonly message: Message.IncomingRequest<any>
698
+ }
699
+ const pendingNotifications = new Map<Snowflake.Snowflake, PendingNotification>()
700
+
695
701
  const storageReadLock = Semaphore.makeUnsafe(1)
696
702
  const withStorageReadLock = storageReadLock.withPermits(1)
697
703
 
@@ -725,7 +731,7 @@ const make = Effect.gen(function*() {
725
731
  if (storageEnabled && initialRunnerAddress) {
726
732
  const selfAddress = initialRunnerAddress
727
733
 
728
- yield* Effect.gen(function*() {
734
+ const storageReadLoop = Effect.gen(function*() {
729
735
  yield* Effect.logDebug("Starting")
730
736
  yield* Effect.addFinalizer(() => Effect.logDebug("Shutting down"))
731
737
 
@@ -982,8 +988,7 @@ const make = Effect.gen(function*() {
982
988
  module: "effect/cluster/Sharding",
983
989
  fiber: "Storage read loop",
984
990
  runner: selfAddress
985
- }),
986
- Effect.forkIn(shardingScope)
991
+ })
987
992
  )
988
993
 
989
994
  // open the storage latch every poll interval
@@ -1110,6 +1115,9 @@ const make = Effect.gen(function*() {
1110
1115
  ),
1111
1116
  Effect.forkIn(shardingScope)
1112
1117
  )
1118
+
1119
+ // fork after the state the loop reads has been initialized
1120
+ yield* Effect.forkIn(storageReadLoop, shardingScope)
1113
1121
  }
1114
1122
 
1115
1123
  // --- Sending messages ---
@@ -1150,11 +1158,6 @@ const make = Effect.gen(function*() {
1150
1158
  return isRequest ? Effect.andThen(awaitShardLock(address.shardId), send) : send
1151
1159
  }
1152
1160
 
1153
- type PendingNotification = {
1154
- resume: (_: Effect.Effect<void, EntityNotAssignedToRunner>) => void
1155
- readonly message: Message.IncomingRequest<any>
1156
- }
1157
- const pendingNotifications = new Map<Snowflake.Snowflake, PendingNotification>()
1158
1161
  const notifyLocal = <M extends Message.Outgoing<any> | Message.Incoming<any>>(
1159
1162
  message: M,
1160
1163
  discard: boolean,
@@ -1172,7 +1175,7 @@ const make = Effect.gen(function*() {
1172
1175
  const state = entityManagers.get(address.entityType)
1173
1176
  if (!state) {
1174
1177
  return Effect.flatMap(waitForEntityManager(address.entityType), loop)
1175
- } else if (state.status === "closed" || !isEntityOnLocalShards(address)) {
1178
+ } else if (state.status === "closed") {
1176
1179
  return Effect.fail(new EntityNotAssignedToRunner({ address }))
1177
1180
  }
1178
1181
 
@@ -250,7 +250,7 @@ export const make = Effect.fnUntraced(function*<
250
250
  ) as Effect.Effect<Context.Context<Rpc.ToHandler<Rpcs>>>)
251
251
 
252
252
  const server = yield* RpcServer.makeNoSerialization(entity.protocol, {
253
- spanPrefix: `${entity.type}(${address.entityId})`,
253
+ spanPrefix: entity.type,
254
254
  spanAttributes: {
255
255
  ...options.spanAttributes,
256
256
  "entity.type": entity.type,