effect 4.0.2 → 4.0.4

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 (389) 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 +111 -0
  5. package/ai-docs/src/52_graphql/20_testing.ts +45 -0
  6. package/ai-docs/src/52_graphql/30_batching.ts +116 -0
  7. package/ai-docs/src/52_graphql/40_aliased-batching.ts +122 -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/Context.d.ts.map +1 -1
  11. package/dist/Context.js +12 -3
  12. package/dist/Context.js.map +1 -1
  13. package/dist/Crypto.d.ts +33 -5
  14. package/dist/Crypto.d.ts.map +1 -1
  15. package/dist/Crypto.js +8 -2
  16. package/dist/Crypto.js.map +1 -1
  17. package/dist/DateTime.d.ts +9 -6
  18. package/dist/DateTime.d.ts.map +1 -1
  19. package/dist/DateTime.js +9 -6
  20. package/dist/DateTime.js.map +1 -1
  21. package/dist/Duration.d.ts +6 -6
  22. package/dist/Duration.d.ts.map +1 -1
  23. package/dist/Duration.js +27 -8
  24. package/dist/Duration.js.map +1 -1
  25. package/dist/FileSystem.d.ts +44 -2
  26. package/dist/FileSystem.d.ts.map +1 -1
  27. package/dist/FileSystem.js.map +1 -1
  28. package/dist/RequestResolver.d.ts.map +1 -1
  29. package/dist/RequestResolver.js +4 -10
  30. package/dist/RequestResolver.js.map +1 -1
  31. package/dist/Scheduler.d.ts +9 -0
  32. package/dist/Scheduler.d.ts.map +1 -1
  33. package/dist/Scheduler.js +9 -0
  34. package/dist/Scheduler.js.map +1 -1
  35. package/dist/Schema.d.ts +481 -1
  36. package/dist/Schema.d.ts.map +1 -1
  37. package/dist/Schema.js +362 -4
  38. package/dist/Schema.js.map +1 -1
  39. package/dist/SchemaGetter.d.ts +4 -0
  40. package/dist/SchemaGetter.d.ts.map +1 -1
  41. package/dist/SchemaGetter.js +52 -33
  42. package/dist/SchemaGetter.js.map +1 -1
  43. package/dist/Sink.d.ts.map +1 -1
  44. package/dist/Sink.js +38 -17
  45. package/dist/Sink.js.map +1 -1
  46. package/dist/Stdio.d.ts +7 -1
  47. package/dist/Stdio.d.ts.map +1 -1
  48. package/dist/Stdio.js +1 -0
  49. package/dist/Stdio.js.map +1 -1
  50. package/dist/Stream.d.ts +22 -11
  51. package/dist/Stream.d.ts.map +1 -1
  52. package/dist/Stream.js +21 -6
  53. package/dist/Stream.js.map +1 -1
  54. package/dist/Utils.js +8 -3
  55. package/dist/Utils.js.map +1 -1
  56. package/dist/ai/AnthropicStructuredOutput.js +0 -2
  57. package/dist/ai/AnthropicStructuredOutput.js.map +1 -1
  58. package/dist/ai/Chat.d.ts +2 -2
  59. package/dist/ai/Chat.d.ts.map +1 -1
  60. package/dist/ai/LanguageModel.d.ts +38 -7
  61. package/dist/ai/LanguageModel.d.ts.map +1 -1
  62. package/dist/ai/LanguageModel.js +25 -7
  63. package/dist/ai/LanguageModel.js.map +1 -1
  64. package/dist/ai/McpServer.d.ts +61 -0
  65. package/dist/ai/McpServer.d.ts.map +1 -1
  66. package/dist/ai/McpServer.js +196 -44
  67. package/dist/ai/McpServer.js.map +1 -1
  68. package/dist/ai/Prompt.d.ts.map +1 -1
  69. package/dist/ai/Prompt.js +28 -5
  70. package/dist/ai/Prompt.js.map +1 -1
  71. package/dist/ai/Response.d.ts +4 -3
  72. package/dist/ai/Response.d.ts.map +1 -1
  73. package/dist/ai/Response.js.map +1 -1
  74. package/dist/ai/internal/mcpProtocol/v2026_07_28.js +5 -0
  75. package/dist/ai/internal/mcpProtocol/v2026_07_28.js.map +1 -1
  76. package/dist/ai/internal/mcpProtocol.js.map +1 -1
  77. package/dist/ai/internal/mcpRuntime.js +1 -0
  78. package/dist/ai/internal/mcpRuntime.js.map +1 -1
  79. package/dist/cli/CliOutput.d.ts +27 -0
  80. package/dist/cli/CliOutput.d.ts.map +1 -1
  81. package/dist/cli/CliOutput.js +33 -10
  82. package/dist/cli/CliOutput.js.map +1 -1
  83. package/dist/cli/Command.d.ts.map +1 -1
  84. package/dist/cli/Command.js +6 -3
  85. package/dist/cli/Command.js.map +1 -1
  86. package/dist/cli/Completions.d.ts +3 -3
  87. package/dist/cli/Completions.d.ts.map +1 -1
  88. package/dist/cli/Completions.js +8 -5
  89. package/dist/cli/Completions.js.map +1 -1
  90. package/dist/cli/GlobalFlag.d.ts +5 -4
  91. package/dist/cli/GlobalFlag.d.ts.map +1 -1
  92. package/dist/cli/GlobalFlag.js +3 -3
  93. package/dist/cli/GlobalFlag.js.map +1 -1
  94. package/dist/cli/Prompt.d.ts +17 -1
  95. package/dist/cli/Prompt.d.ts.map +1 -1
  96. package/dist/cli/Prompt.js +119 -114
  97. package/dist/cli/Prompt.js.map +1 -1
  98. package/dist/cli/internal/ansi.d.ts +0 -1
  99. package/dist/cli/internal/ansi.d.ts.map +1 -1
  100. package/dist/cli/internal/ansi.js +20 -3
  101. package/dist/cli/internal/ansi.js.map +1 -1
  102. package/dist/cli/internal/completions/powershell.d.ts +2 -0
  103. package/dist/cli/internal/completions/powershell.d.ts.map +1 -0
  104. package/dist/cli/internal/completions/powershell.js +273 -0
  105. package/dist/cli/internal/completions/powershell.js.map +1 -0
  106. package/dist/cli/internal/completions/zsh.js +3 -2
  107. package/dist/cli/internal/completions/zsh.js.map +1 -1
  108. package/dist/cli/internal/wizard.d.ts +3 -3
  109. package/dist/cli/internal/wizard.d.ts.map +1 -1
  110. package/dist/cli/internal/wizard.js +21 -18
  111. package/dist/cli/internal/wizard.js.map +1 -1
  112. package/dist/cluster/ClusterSchema.d.ts +28 -0
  113. package/dist/cluster/ClusterSchema.d.ts.map +1 -1
  114. package/dist/cluster/ClusterSchema.js +30 -0
  115. package/dist/cluster/ClusterSchema.js.map +1 -1
  116. package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
  117. package/dist/cluster/ClusterWorkflowEngine.js +56 -26
  118. package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
  119. package/dist/cluster/MessageStorage.d.ts +18 -3
  120. package/dist/cluster/MessageStorage.d.ts.map +1 -1
  121. package/dist/cluster/MessageStorage.js +31 -11
  122. package/dist/cluster/MessageStorage.js.map +1 -1
  123. package/dist/cluster/Runners.d.ts.map +1 -1
  124. package/dist/cluster/Runners.js +8 -2
  125. package/dist/cluster/Runners.js.map +1 -1
  126. package/dist/cluster/Sharding.d.ts.map +1 -1
  127. package/dist/cluster/Sharding.js +187 -66
  128. package/dist/cluster/Sharding.js.map +1 -1
  129. package/dist/cluster/SqlMessageStorage.d.ts +59 -19
  130. package/dist/cluster/SqlMessageStorage.d.ts.map +1 -1
  131. package/dist/cluster/SqlMessageStorage.js +84 -55
  132. package/dist/cluster/SqlMessageStorage.js.map +1 -1
  133. package/dist/cluster/SqlRunnerStorage.d.ts +68 -19
  134. package/dist/cluster/SqlRunnerStorage.d.ts.map +1 -1
  135. package/dist/cluster/SqlRunnerStorage.js +191 -113
  136. package/dist/cluster/SqlRunnerStorage.js.map +1 -1
  137. package/dist/cluster/internal/entityManager.js +104 -37
  138. package/dist/cluster/internal/entityManager.js.map +1 -1
  139. package/dist/encoding/Sse.d.ts +6 -0
  140. package/dist/encoding/Sse.d.ts.map +1 -1
  141. package/dist/encoding/Sse.js +62 -70
  142. package/dist/encoding/Sse.js.map +1 -1
  143. package/dist/graphql/GraphQL.d.ts +349 -0
  144. package/dist/graphql/GraphQL.d.ts.map +1 -0
  145. package/dist/graphql/GraphQL.js +207 -0
  146. package/dist/graphql/GraphQL.js.map +1 -0
  147. package/dist/graphql/GraphQLClient.d.ts +562 -0
  148. package/dist/graphql/GraphQLClient.d.ts.map +1 -0
  149. package/dist/graphql/GraphQLClient.js +498 -0
  150. package/dist/graphql/GraphQLClient.js.map +1 -0
  151. package/dist/graphql/GraphQLClientError.d.ts +445 -0
  152. package/dist/graphql/GraphQLClientError.d.ts.map +1 -0
  153. package/dist/graphql/GraphQLClientError.js +479 -0
  154. package/dist/graphql/GraphQLClientError.js.map +1 -0
  155. package/dist/graphql/GraphQLGroup.d.ts +118 -0
  156. package/dist/graphql/GraphQLGroup.d.ts.map +1 -0
  157. package/dist/graphql/GraphQLGroup.js +109 -0
  158. package/dist/graphql/GraphQLGroup.js.map +1 -0
  159. package/dist/graphql/GraphQLMiddleware.d.ts +249 -0
  160. package/dist/graphql/GraphQLMiddleware.d.ts.map +1 -0
  161. package/dist/graphql/GraphQLMiddleware.js +130 -0
  162. package/dist/graphql/GraphQLMiddleware.js.map +1 -0
  163. package/dist/graphql/GraphQLProtocol.d.ts +731 -0
  164. package/dist/graphql/GraphQLProtocol.d.ts.map +1 -0
  165. package/dist/graphql/GraphQLProtocol.js +719 -0
  166. package/dist/graphql/GraphQLProtocol.js.map +1 -0
  167. package/dist/graphql/GraphQLResolver.d.ts +453 -0
  168. package/dist/graphql/GraphQLResolver.d.ts.map +1 -0
  169. package/dist/graphql/GraphQLResolver.js +552 -0
  170. package/dist/graphql/GraphQLResolver.js.map +1 -0
  171. package/dist/graphql/index.d.ts +40 -0
  172. package/dist/graphql/index.d.ts.map +1 -0
  173. package/dist/graphql/index.js +41 -0
  174. package/dist/graphql/index.js.map +1 -0
  175. package/dist/http/FindMyWay/internal/router.js +3 -6
  176. package/dist/http/FindMyWay/internal/router.js.map +1 -1
  177. package/dist/http/HttpClient.d.ts +21 -0
  178. package/dist/http/HttpClient.d.ts.map +1 -1
  179. package/dist/http/HttpClient.js +63 -12
  180. package/dist/http/HttpClient.js.map +1 -1
  181. package/dist/http/HttpEffect.d.ts.map +1 -1
  182. package/dist/http/HttpEffect.js +14 -7
  183. package/dist/http/HttpEffect.js.map +1 -1
  184. package/dist/http/HttpMiddleware.d.ts.map +1 -1
  185. package/dist/http/HttpMiddleware.js +31 -32
  186. package/dist/http/HttpMiddleware.js.map +1 -1
  187. package/dist/http/HttpRouter.d.ts +35 -0
  188. package/dist/http/HttpRouter.d.ts.map +1 -1
  189. package/dist/http/HttpRouter.js +35 -0
  190. package/dist/http/HttpRouter.js.map +1 -1
  191. package/dist/http/HttpServerResponse.d.ts +8 -6
  192. package/dist/http/HttpServerResponse.d.ts.map +1 -1
  193. package/dist/http/HttpServerResponse.js +27 -16
  194. package/dist/http/HttpServerResponse.js.map +1 -1
  195. package/dist/http/HttpTraceContext.d.ts.map +1 -1
  196. package/dist/http/HttpTraceContext.js +4 -2
  197. package/dist/http/HttpTraceContext.js.map +1 -1
  198. package/dist/http/Multipart.d.ts.map +1 -1
  199. package/dist/http/Multipart.js +15 -1
  200. package/dist/http/Multipart.js.map +1 -1
  201. package/dist/http-api/HttpApiScalar.d.ts +5 -0
  202. package/dist/http-api/HttpApiScalar.d.ts.map +1 -1
  203. package/dist/http-api/HttpApiScalar.js.map +1 -1
  204. package/dist/internal/dateTime.js +40 -35
  205. package/dist/internal/dateTime.js.map +1 -1
  206. package/dist/internal/effect.js +19 -9
  207. package/dist/internal/effect.js.map +1 -1
  208. package/dist/internal/graphql.d.ts +2 -0
  209. package/dist/internal/graphql.d.ts.map +1 -0
  210. package/dist/internal/graphql.js +195 -0
  211. package/dist/internal/graphql.js.map +1 -0
  212. package/dist/internal/graphqlHeaders.d.ts +4 -0
  213. package/dist/internal/graphqlHeaders.d.ts.map +1 -0
  214. package/dist/internal/graphqlHeaders.js +11 -0
  215. package/dist/internal/graphqlHeaders.js.map +1 -0
  216. package/dist/internal/request.js +20 -1
  217. package/dist/internal/request.js.map +1 -1
  218. package/dist/internal/schema/toCodeDocument.js +4 -1
  219. package/dist/internal/schema/toCodeDocument.js.map +1 -1
  220. package/dist/internal/schema/toJsonSchemaDocument.js +29 -5
  221. package/dist/internal/schema/toJsonSchemaDocument.js.map +1 -1
  222. package/dist/internal/version.js +1 -1
  223. package/dist/net/AddressResolver.d.ts +145 -0
  224. package/dist/net/AddressResolver.d.ts.map +1 -0
  225. package/dist/net/AddressResolver.js +114 -0
  226. package/dist/net/AddressResolver.js.map +1 -0
  227. package/dist/net/Dns.d.ts +596 -0
  228. package/dist/net/Dns.d.ts.map +1 -0
  229. package/dist/net/Dns.js +549 -0
  230. package/dist/net/Dns.js.map +1 -0
  231. package/dist/net/Host.d.ts +369 -0
  232. package/dist/net/Host.d.ts.map +1 -0
  233. package/dist/net/Host.js +397 -0
  234. package/dist/net/Host.js.map +1 -0
  235. package/dist/net/IpInterface.d.ts +76 -3
  236. package/dist/net/IpInterface.d.ts.map +1 -1
  237. package/dist/net/IpInterface.js +53 -4
  238. package/dist/net/IpInterface.js.map +1 -1
  239. package/dist/net/IpNetwork.d.ts +82 -8
  240. package/dist/net/IpNetwork.d.ts.map +1 -1
  241. package/dist/net/IpNetwork.js +60 -4
  242. package/dist/net/IpNetwork.js.map +1 -1
  243. package/dist/net/NetAddress.d.ts +396 -35
  244. package/dist/net/NetAddress.d.ts.map +1 -1
  245. package/dist/net/NetAddress.js +358 -34
  246. package/dist/net/NetAddress.js.map +1 -1
  247. package/dist/net/index.d.ts +15 -0
  248. package/dist/net/index.d.ts.map +1 -1
  249. package/dist/net/index.js +15 -0
  250. package/dist/net/index.js.map +1 -1
  251. package/dist/observability/OtlpMetrics.d.ts.map +1 -1
  252. package/dist/observability/OtlpMetrics.js +15 -21
  253. package/dist/observability/OtlpMetrics.js.map +1 -1
  254. package/dist/observability/PrometheusMetrics.js +1 -0
  255. package/dist/observability/PrometheusMetrics.js.map +1 -1
  256. package/dist/persistence/PersistedQueue.d.ts +32 -3
  257. package/dist/persistence/PersistedQueue.d.ts.map +1 -1
  258. package/dist/persistence/PersistedQueue.js +75 -16
  259. package/dist/persistence/PersistedQueue.js.map +1 -1
  260. package/dist/rpc/RpcClient.d.ts +22 -6
  261. package/dist/rpc/RpcClient.d.ts.map +1 -1
  262. package/dist/rpc/RpcClient.js +12 -5
  263. package/dist/rpc/RpcClient.js.map +1 -1
  264. package/dist/rpc/RpcServer.d.ts +6 -0
  265. package/dist/rpc/RpcServer.d.ts.map +1 -1
  266. package/dist/rpc/RpcServer.js +78 -29
  267. package/dist/rpc/RpcServer.js.map +1 -1
  268. package/dist/socket/DatagramSocket.d.ts +456 -0
  269. package/dist/socket/DatagramSocket.d.ts.map +1 -0
  270. package/dist/socket/DatagramSocket.js +535 -0
  271. package/dist/socket/DatagramSocket.js.map +1 -0
  272. package/dist/socket/Socket.d.ts +20 -1
  273. package/dist/socket/Socket.d.ts.map +1 -1
  274. package/dist/socket/Socket.js +9 -1
  275. package/dist/socket/Socket.js.map +1 -1
  276. package/dist/socket/index.d.ts +5 -0
  277. package/dist/socket/index.d.ts.map +1 -1
  278. package/dist/socket/index.js +5 -0
  279. package/dist/socket/index.js.map +1 -1
  280. package/dist/sql/Migrator.d.ts.map +1 -1
  281. package/dist/sql/Migrator.js +7 -2
  282. package/dist/sql/Migrator.js.map +1 -1
  283. package/dist/sql/SqlClient.d.ts +7 -1
  284. package/dist/sql/SqlClient.d.ts.map +1 -1
  285. package/dist/sql/SqlClient.js +31 -6
  286. package/dist/sql/SqlClient.js.map +1 -1
  287. package/dist/testing/TestCrypto.d.ts +73 -0
  288. package/dist/testing/TestCrypto.d.ts.map +1 -0
  289. package/dist/testing/TestCrypto.js +87 -0
  290. package/dist/testing/TestCrypto.js.map +1 -0
  291. package/dist/testing/index.d.ts +5 -0
  292. package/dist/testing/index.d.ts.map +1 -1
  293. package/dist/testing/index.js +5 -0
  294. package/dist/testing/index.js.map +1 -1
  295. package/dist/workflow/DurableDeferred.d.ts +54 -0
  296. package/dist/workflow/DurableDeferred.d.ts.map +1 -1
  297. package/dist/workflow/DurableDeferred.js +108 -50
  298. package/dist/workflow/DurableDeferred.js.map +1 -1
  299. package/dist/workflow/WorkflowEngine.d.ts +24 -2
  300. package/dist/workflow/WorkflowEngine.d.ts.map +1 -1
  301. package/dist/workflow/WorkflowEngine.js +13 -3
  302. package/dist/workflow/WorkflowEngine.js.map +1 -1
  303. package/package.json +6 -3
  304. package/src/Context.ts +12 -3
  305. package/src/Crypto.ts +43 -9
  306. package/src/DateTime.ts +9 -6
  307. package/src/Duration.ts +36 -16
  308. package/src/FileSystem.ts +48 -2
  309. package/src/RequestResolver.ts +4 -10
  310. package/src/Scheduler.ts +9 -0
  311. package/src/Schema.ts +677 -5
  312. package/src/SchemaGetter.ts +47 -35
  313. package/src/Sink.ts +45 -18
  314. package/src/Stdio.ts +9 -2
  315. package/src/Stream.ts +44 -15
  316. package/src/Utils.ts +8 -5
  317. package/src/ai/AnthropicStructuredOutput.ts +2 -2
  318. package/src/ai/Chat.ts +2 -2
  319. package/src/ai/LanguageModel.ts +96 -24
  320. package/src/ai/McpServer.ts +288 -94
  321. package/src/ai/Prompt.ts +24 -10
  322. package/src/ai/Response.ts +6 -3
  323. package/src/ai/internal/mcpProtocol/v2026_07_28.ts +5 -0
  324. package/src/ai/internal/mcpProtocol.ts +3 -0
  325. package/src/ai/internal/mcpRuntime.ts +2 -0
  326. package/src/cli/CliOutput.ts +33 -16
  327. package/src/cli/Command.ts +4 -3
  328. package/src/cli/Completions.ts +9 -6
  329. package/src/cli/GlobalFlag.ts +19 -11
  330. package/src/cli/Prompt.ts +159 -149
  331. package/src/cli/internal/ansi.ts +26 -3
  332. package/src/cli/internal/completions/powershell.ts +322 -0
  333. package/src/cli/internal/completions/zsh.ts +4 -2
  334. package/src/cli/internal/wizard.ts +34 -26
  335. package/src/cluster/ClusterSchema.ts +32 -0
  336. package/src/cluster/ClusterWorkflowEngine.ts +64 -36
  337. package/src/cluster/MessageStorage.ts +45 -14
  338. package/src/cluster/Runners.ts +8 -2
  339. package/src/cluster/Sharding.ts +219 -58
  340. package/src/cluster/SqlMessageStorage.ts +118 -84
  341. package/src/cluster/SqlRunnerStorage.ts +224 -128
  342. package/src/cluster/internal/entityManager.ts +151 -55
  343. package/src/encoding/Sse.ts +69 -79
  344. package/src/graphql/GraphQL.ts +494 -0
  345. package/src/graphql/GraphQLClient.ts +912 -0
  346. package/src/graphql/GraphQLClientError.ts +542 -0
  347. package/src/graphql/GraphQLGroup.ts +167 -0
  348. package/src/graphql/GraphQLMiddleware.ts +253 -0
  349. package/src/graphql/GraphQLProtocol.ts +1286 -0
  350. package/src/graphql/GraphQLResolver.ts +1010 -0
  351. package/src/graphql/index.ts +48 -0
  352. package/src/http/FindMyWay/internal/router.ts +4 -7
  353. package/src/http/HttpClient.ts +110 -12
  354. package/src/http/HttpEffect.ts +11 -5
  355. package/src/http/HttpMiddleware.ts +33 -34
  356. package/src/http/HttpRouter.ts +35 -0
  357. package/src/http/HttpServerResponse.ts +28 -16
  358. package/src/http/HttpTraceContext.ts +8 -2
  359. package/src/http/Multipart.ts +17 -1
  360. package/src/http-api/HttpApiScalar.ts +5 -0
  361. package/src/internal/dateTime.ts +13 -10
  362. package/src/internal/effect.ts +22 -15
  363. package/src/internal/graphql.ts +263 -0
  364. package/src/internal/graphqlHeaders.ts +11 -0
  365. package/src/internal/request.ts +31 -3
  366. package/src/internal/schema/toCodeDocument.ts +7 -1
  367. package/src/internal/schema/toJsonSchemaDocument.ts +34 -2
  368. package/src/internal/version.ts +1 -1
  369. package/src/net/AddressResolver.ts +222 -0
  370. package/src/net/Dns.ts +1035 -0
  371. package/src/net/Host.ts +544 -0
  372. package/src/net/IpInterface.ts +117 -7
  373. package/src/net/IpNetwork.ts +116 -17
  374. package/src/net/NetAddress.ts +620 -76
  375. package/src/net/index.ts +18 -0
  376. package/src/observability/OtlpMetrics.ts +12 -23
  377. package/src/observability/PrometheusMetrics.ts +1 -0
  378. package/src/persistence/PersistedQueue.ts +146 -53
  379. package/src/rpc/RpcClient.ts +41 -19
  380. package/src/rpc/RpcServer.ts +115 -40
  381. package/src/socket/DatagramSocket.ts +919 -0
  382. package/src/socket/Socket.ts +28 -2
  383. package/src/socket/index.ts +6 -0
  384. package/src/sql/Migrator.ts +21 -5
  385. package/src/sql/SqlClient.ts +39 -15
  386. package/src/testing/TestCrypto.ts +94 -0
  387. package/src/testing/index.ts +6 -0
  388. package/src/workflow/DurableDeferred.ts +160 -47
  389. package/src/workflow/WorkflowEngine.ts +50 -5
@@ -277,6 +277,13 @@ const make = Effect.gen(function*() {
277
277
  // the active shards are the ones that we have acquired the lock for
278
278
  const acquiredShards = MutableHashSet.empty<ShardId>()
279
279
  const activeShardsLatch = yield* Latch.make(false)
280
+ // open once shutdown starts handing every shard off
281
+ const handoffLatch = Latch.makeUnsafe(false)
282
+ // open once the handoff has released every shard lock
283
+ const handedOffLatch = Latch.makeUnsafe(false)
284
+ // serializes runner registration with unregistration, so a registration
285
+ // in flight cannot re-advertise a runner that is shutting down
286
+ const withRegistrationLock = Semaphore.makeUnsafe(1).withPermits(1)
280
287
 
281
288
  const events = yield* PubSub.unbounded<ShardingRegistrationEvent>()
282
289
  const getRegistrationEvents: Stream.Stream<ShardingRegistrationEvent> = Stream.fromPubSub(events)
@@ -295,6 +302,33 @@ const make = Effect.gen(function*() {
295
302
  return MutableHashSet.has(acquiredShards, address.shardId)
296
303
  }
297
304
 
305
+ function isAwaitingShardLock(shardId: ShardId): boolean {
306
+ return !MutableRef.get(isShutdown) &&
307
+ MutableHashSet.has(selfShards, shardId) &&
308
+ !MutableHashSet.has(acquiredShards, shardId)
309
+ }
310
+
311
+ // Requests for a shard assigned to this runner wait for its lock, for at most
312
+ // `shardLockExpiration`, instead of bouncing while the previous owner drains.
313
+ // Each notification opens the current latch and replaces it, so a waiter that
314
+ // checked its shard before a notification cannot miss it.
315
+ let shardLocksChanged = Latch.makeUnsafe(false)
316
+ const notifyShardLockWaiters = Effect.suspend(() => {
317
+ const latch = shardLocksChanged
318
+ shardLocksChanged = Latch.makeUnsafe(false)
319
+ return latch.open
320
+ })
321
+ const awaitShardLock = (shardId: ShardId): Effect.Effect<void> => {
322
+ const loop: Effect.Effect<void> = Effect.suspend(() =>
323
+ isAwaitingShardLock(shardId) ? Effect.andThen(shardLocksChanged.await, loop) : Effect.void
324
+ )
325
+ return Effect.suspend(() =>
326
+ isAwaitingShardLock(shardId)
327
+ ? Effect.asVoid(Effect.timeoutOption(loop, config.shardLockExpiration))
328
+ : Effect.void
329
+ )
330
+ }
331
+
298
332
  yield* Scope.addFinalizer(
299
333
  shardingScope,
300
334
  Effect.logDebug("Shutdown complete").pipe(Effect.annotateLogs({
@@ -310,6 +344,8 @@ const make = Effect.gen(function*() {
310
344
  // allow them to move to another runner.
311
345
 
312
346
  const releasingShards = MutableHashSet.empty<ShardId>()
347
+ // Hands every held shard off on shutdown, set when this is a runner
348
+ let handoff: Effect.Effect<void> = Effect.void
313
349
  // Shards whose entities must be force interrupted and locks fully released
314
350
  // before normal reacquisition.
315
351
  const forceReleasingShards = MutableHashSet.empty<ShardId>()
@@ -321,8 +357,37 @@ const make = Effect.gen(function*() {
321
357
  return Effect.ignore(runnerStorage.releaseAll(selfAddress))
322
358
  })
323
359
 
360
+ // Shard releases fork into a child scope that closes after the shutdown
361
+ // handoff, as forks into the closing sharding scope never run.
362
+ const releaseScope = yield* Scope.fork(shardingScope)
363
+ // Interrupt the shards' entities on every entity manager that is open
364
+ const forkInterruptShards = Effect.fnUntraced(function*(shardIds: Iterable<ShardId>, force: boolean) {
365
+ const fibers = Arr.empty<Fiber.Fiber<void>>()
366
+ for (const shardId of shardIds) {
367
+ for (const state of entityManagers.values()) {
368
+ if (state.status === "closed") continue
369
+ fibers.push(
370
+ yield* Effect.forkIn(state.manager.interruptShard(shardId, { force }), releaseScope, {
371
+ startImmediately: true
372
+ })
373
+ )
374
+ }
375
+ }
376
+ return fibers
377
+ })
324
378
  const releaseShardsMap = yield* FiberMap.make<ShardId>()
325
379
  let forcedShardReleaseRunning = false
380
+ // Called from the acquisition loop only, so no `acquire` is in flight
381
+ const checkHandedOff = () => {
382
+ if (
383
+ handoffLatch.isOpen() &&
384
+ MutableHashSet.size(acquiredShards) === 0 &&
385
+ MutableHashSet.size(releasingShards) === 0 &&
386
+ !forcedShardReleasePending()
387
+ ) {
388
+ handedOffLatch.openUnsafe()
389
+ }
390
+ }
326
391
  // Interrupt the shards' entities, wait for lock health, run the storage
327
392
  // release, then clear the shards' bookkeeping.
328
393
  const runShardRelease = Effect.fnUntraced(function*<E>(
@@ -330,14 +395,11 @@ const make = Effect.gen(function*() {
330
395
  force: boolean,
331
396
  release: Effect.Effect<void, E>
332
397
  ) {
333
- const fibers = Arr.empty<Fiber.Fiber<void>>()
334
- for (const shardId of shardIds) {
335
- for (const state of entityManagers.values()) {
336
- if (state.status === "closed") continue
337
- fibers.push(yield* Effect.forkScoped(state.manager.interruptShard(shardId, { force })))
338
- }
339
- }
340
- yield* Fiber.joinAll(fibers)
398
+ yield* Fiber.joinAll(yield* forkInterruptShards(shardIds, force))
399
+ // stop the singletons of shards that are no longer acquired, and wait
400
+ // for those on the released shards
401
+ yield* startSyncSingletons
402
+ yield* awaitStoppingSingletons(shardIds)
341
403
  yield* shardLocksHealthyLatch.await
342
404
  yield* release
343
405
  for (const shardId of shardIds) {
@@ -351,6 +413,8 @@ const make = Effect.gen(function*() {
351
413
  interrupt: MutableRef.get(isShutdown)
352
414
  })
353
415
  }
416
+ // let the acquisition loop check whether the handoff has finished
417
+ if (handoffLatch.isOpen()) activeShardsLatch.openUnsafe()
354
418
  })
355
419
  const retryShardRelease =
356
420
  (annotations: { readonly fiber: string; readonly shardId?: ShardId }) =>
@@ -402,7 +466,7 @@ const make = Effect.gen(function*() {
402
466
  forcedShardReleaseRunning = false
403
467
  activeShardsLatch.openUnsafe()
404
468
  })),
405
- Effect.forkIn(shardingScope),
469
+ Effect.forkIn(releaseScope),
406
470
  Effect.asVoid
407
471
  )
408
472
  })
@@ -422,9 +486,11 @@ const make = Effect.gen(function*() {
422
486
  yield* activeShardsLatch.await
423
487
  activeShardsLatch.closeUnsafe()
424
488
 
425
- // if a shard is no longer assigned to this runner, we release it
489
+ // if a shard is no longer assigned to this runner, or the runner is
490
+ // handing every shard off, we release it
491
+ const handingOff = handoffLatch.isOpen()
426
492
  for (const shardId of acquiredShards) {
427
- if (MutableHashSet.has(selfShards, shardId)) continue
493
+ if (!handingOff && MutableHashSet.has(selfShards, shardId)) continue
428
494
  MutableHashSet.remove(acquiredShards, shardId)
429
495
  MutableHashSet.add(releasingShards, shardId)
430
496
  }
@@ -433,6 +499,7 @@ const make = Effect.gen(function*() {
433
499
  yield* Effect.forkIn(syncSingletons, shardingScope)
434
500
  yield* releaseShards
435
501
  }
502
+ checkHandedOff()
436
503
 
437
504
  if (!shardLocksHealthyLatch.isOpen()) {
438
505
  continue
@@ -444,6 +511,11 @@ const make = Effect.gen(function*() {
444
511
  continue
445
512
  }
446
513
 
514
+ // a runner that is shutting down takes no new shards
515
+ if (isShutdown.current) {
516
+ continue
517
+ }
518
+
447
519
  // if a shard has been assigned to this runner, we acquire it
448
520
  const unacquiredShards = MutableHashSet.empty<ShardId>()
449
521
  for (const shardId of selfShards) {
@@ -484,13 +556,15 @@ const make = Effect.gen(function*() {
484
556
  MutableHashSet.add(acquiredShards, shardId)
485
557
  }
486
558
  if (acquired.length > 0) {
559
+ yield* notifyShardLockWaiters
487
560
  yield* storageReadLatch.open
488
561
  yield* Effect.forkIn(syncSingletons, shardingScope)
489
562
 
490
563
  // update metrics
491
564
  ClusterMetrics.shards.updateUnsafe(BigInt(MutableHashSet.size(acquiredShards)), Context.empty())
492
565
  }
493
- yield* Effect.sleep(1000)
566
+ // the shutdown handoff releases the shards acquired here without waiting
567
+ yield* Effect.raceFirst(Effect.sleep(1000), handoffLatch.await)
494
568
  activeShardsLatch.openUnsafe()
495
569
  }
496
570
  }).pipe(
@@ -519,19 +593,11 @@ const make = Effect.gen(function*() {
519
593
  activeShardsLatch.openUnsafe()
520
594
 
521
595
  return Effect.gen(function*() {
596
+ yield* notifyShardLockWaiters
522
597
  yield* Effect.logError("Shard lock storage is unhealthy", cause)
523
598
  yield* Effect.forkIn(syncSingletons, shardingScope, { startImmediately: true })
524
599
 
525
- for (const shardId of affectedShards) {
526
- for (const state of entityManagers.values()) {
527
- if (state.status === "closed") continue
528
- yield* Effect.forkIn(
529
- state.manager.interruptShard(shardId, { force: true }),
530
- shardingScope,
531
- { startImmediately: true }
532
- )
533
- }
534
- }
600
+ yield* forkInterruptShards(affectedShards, true)
535
601
  activeShardsLatch.openUnsafe()
536
602
  })
537
603
  })
@@ -598,6 +664,18 @@ const make = Effect.gen(function*() {
598
664
  Effect.forever,
599
665
  Effect.forkIn(shardingScope)
600
666
  )
667
+
668
+ // Release every held shard through the per-shard path. Their entities are
669
+ // forced to stop, so the handoff does not wait out graceful termination.
670
+ // This is best effort: the wait is bounded, and if it times out, the final
671
+ // `releaseAll` attempts to release the remaining locks.
672
+ handoff = Effect.suspend(() => {
673
+ if (handoffLatch.openUnsafe()) activeShardsLatch.openUnsafe()
674
+ return forkInterruptShards([...acquiredShards, ...releasingShards], true)
675
+ }).pipe(
676
+ Effect.andThen(Effect.timeoutOption(handedOffLatch.await, shardLockInterval)),
677
+ Effect.asVoid
678
+ )
601
679
  }
602
680
 
603
681
  // --- Storage inbox ---
@@ -614,6 +692,12 @@ const make = Effect.gen(function*() {
614
692
  const storageReadLatch = yield* Latch.make(true)
615
693
  const openStorageReadLatch = constant(Effect.asVoid(storageReadLatch.open))
616
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
+
617
701
  const storageReadLock = Semaphore.makeUnsafe(1)
618
702
  const withStorageReadLock = storageReadLock.withPermits(1)
619
703
 
@@ -647,7 +731,7 @@ const make = Effect.gen(function*() {
647
731
  if (storageEnabled && initialRunnerAddress) {
648
732
  const selfAddress = initialRunnerAddress
649
733
 
650
- yield* Effect.gen(function*() {
734
+ const storageReadLoop = Effect.gen(function*() {
651
735
  yield* Effect.logDebug("Starting")
652
736
  yield* Effect.addFinalizer(() => Effect.logDebug("Shutting down"))
653
737
 
@@ -904,8 +988,7 @@ const make = Effect.gen(function*() {
904
988
  module: "effect/cluster/Sharding",
905
989
  fiber: "Storage read loop",
906
990
  runner: selfAddress
907
- }),
908
- Effect.forkIn(shardingScope)
991
+ })
909
992
  )
910
993
 
911
994
  // open the storage latch every poll interval
@@ -1032,20 +1115,28 @@ const make = Effect.gen(function*() {
1032
1115
  ),
1033
1116
  Effect.forkIn(shardingScope)
1034
1117
  )
1118
+
1119
+ // fork after the state the loop reads has been initialized
1120
+ yield* Effect.forkIn(storageReadLoop, shardingScope)
1035
1121
  }
1036
1122
 
1037
1123
  // --- Sending messages ---
1038
1124
 
1039
- const sendLocal = <M extends Message.Outgoing<any> | Message.Incoming<any>>(message: M) =>
1040
- Effect.suspend(function loop(): Effect.Effect<
1125
+ const sendLocal = <M extends Message.Outgoing<any> | Message.Incoming<any>>(message: M) => {
1126
+ const address = message.envelope.address
1127
+ const isRequest = message._tag === "IncomingRequest" || message._tag === "OutgoingRequest"
1128
+ const send = Effect.suspend(function loop(): Effect.Effect<
1041
1129
  void,
1042
1130
  | EntityNotAssignedToRunner
1043
1131
  | MailboxFull
1044
1132
  | AlreadyProcessingMessage
1045
1133
  | (M extends Message.Incoming<any> ? never : PersistenceError)
1046
1134
  > {
1047
- const address = message.envelope.address
1048
- if (!isEntityOnLocalShards(address)) {
1135
+ if (
1136
+ !isEntityOnLocalShards(address) &&
1137
+ // Entities draining from a released shard still accept interrupts and acks
1138
+ (isRequest || !MutableHashSet.has(releasingShards, address.shardId))
1139
+ ) {
1049
1140
  return Effect.fail(new EntityNotAssignedToRunner({ address }))
1050
1141
  }
1051
1142
  const state = entityManagers.get(address.entityType)
@@ -1064,12 +1155,9 @@ const make = Effect.gen(function*() {
1064
1155
  simulateRemoteSerialization: config.simulateRemoteSerialization
1065
1156
  }) as any
1066
1157
  })
1067
-
1068
- type PendingNotification = {
1069
- resume: (_: Effect.Effect<void, EntityNotAssignedToRunner>) => void
1070
- readonly message: Message.IncomingRequest<any>
1158
+ return isRequest ? Effect.andThen(awaitShardLock(address.shardId), send) : send
1071
1159
  }
1072
- const pendingNotifications = new Map<Snowflake.Snowflake, PendingNotification>()
1160
+
1073
1161
  const notifyLocal = <M extends Message.Outgoing<any> | Message.Incoming<any>>(
1074
1162
  message: M,
1075
1163
  discard: boolean,
@@ -1087,7 +1175,7 @@ const make = Effect.gen(function*() {
1087
1175
  const state = entityManagers.get(address.entityType)
1088
1176
  if (!state) {
1089
1177
  return Effect.flatMap(waitForEntityManager(address.entityType), loop)
1090
- } else if (state.status === "closed" || !isEntityOnLocalShards(address)) {
1178
+ } else if (state.status === "closed") {
1091
1179
  return Effect.fail(new EntityNotAssignedToRunner({ address }))
1092
1180
  }
1093
1181
 
@@ -1129,7 +1217,10 @@ const make = Effect.gen(function*() {
1129
1217
  function sendOutgoing(
1130
1218
  message: Message.Outgoing<any>,
1131
1219
  discard: boolean,
1132
- retries?: number
1220
+ retries?: number,
1221
+ // The runner serving the request, for its interrupts and acks. Later
1222
+ // attempts fall back to the current shard assignment.
1223
+ runner?: RunnerAddress
1133
1224
  ): Effect.Effect<
1134
1225
  void,
1135
1226
  MailboxFull | AlreadyProcessingMessage | PersistenceError | EntityNotAssignedToRunner
@@ -1172,7 +1263,9 @@ const make = Effect.gen(function*() {
1172
1263
  if (shouldFail && MutableRef.get(isShutdown)) {
1173
1264
  return Effect.fail(new EntityNotAssignedToRunner({ address }))
1174
1265
  }
1175
- const maybeRunner = MutableHashMap.get(shardAssignments, address.shardId)
1266
+ const maybeRunner = runner !== undefined
1267
+ ? Option.some(runner)
1268
+ : MutableHashMap.get(shardAssignments, address.shardId)
1176
1269
  const runnerIsLocal = Option.isSome(maybeRunner) && isLocalRunner(maybeRunner.value)
1177
1270
  if (isPersisted) {
1178
1271
  return runnerIsLocal
@@ -1180,6 +1273,9 @@ const make = Effect.gen(function*() {
1180
1273
  : runnersService.notify({ address: maybeRunner, message, discard })
1181
1274
  } else if (Option.isNone(maybeRunner)) {
1182
1275
  return Effect.fail(new EntityNotAssignedToRunner({ address }))
1276
+ } else if (message._tag === "OutgoingRequest") {
1277
+ const entry = clientRequests.get(message.envelope.requestId)
1278
+ if (entry) entry.runner = maybeRunner.value
1183
1279
  }
1184
1280
  return runnerIsLocal
1185
1281
  ? sendLocal(message)
@@ -1250,9 +1346,12 @@ const make = Effect.gen(function*() {
1250
1346
  while (true) {
1251
1347
  // Ensure the current runner is registered
1252
1348
  if (selfRunner && !isShutdown.current && !MutableHashMap.has(allRunners, selfRunner)) {
1253
- yield* Effect.logDebug("Registering runner", selfRunner)
1254
- const machineId = yield* withTimeout(runnerStorage.register(selfRunner, true))
1255
- yield* snowflakeGen.setMachineId(machineId)
1349
+ yield* withRegistrationLock(Effect.gen(function*() {
1350
+ if (isShutdown.current) return
1351
+ yield* Effect.logDebug("Registering runner", selfRunner)
1352
+ const machineId = yield* withTimeout(runnerStorage.register(selfRunner, true))
1353
+ yield* snowflakeGen.setMachineId(machineId)
1354
+ }))
1256
1355
  }
1257
1356
 
1258
1357
  const runners = yield* withTimeout(runnerStorage.getRunners)
@@ -1324,6 +1423,7 @@ const make = Effect.gen(function*() {
1324
1423
  })
1325
1424
  yield* Effect.logDebug("New shard assignments", selfShards)
1326
1425
  activeShardsLatch.openUnsafe()
1426
+ yield* notifyShardLockWaiters
1327
1427
 
1328
1428
  // update metrics
1329
1429
  if (selfRunner) {
@@ -1361,6 +1461,8 @@ const make = Effect.gen(function*() {
1361
1461
  readonly context: Context.Context<never>
1362
1462
  readonly message: Message.OutgoingRequest<any>
1363
1463
  lastChunkId?: Snowflake.Snowflake
1464
+ // the runner a volatile request was last sent to
1465
+ runner?: RunnerAddress
1364
1466
  }
1365
1467
  const clientRequests = new Map<Snowflake.Snowflake, ClientRequestEntry>()
1366
1468
 
@@ -1445,7 +1547,9 @@ const make = Effect.gen(function*() {
1445
1547
  }),
1446
1548
  rpc: entry.rpc
1447
1549
  }),
1448
- false
1550
+ false,
1551
+ undefined,
1552
+ entry.runner
1449
1553
  )
1450
1554
  }
1451
1555
  case "Interrupt": {
@@ -1476,7 +1580,8 @@ const make = Effect.gen(function*() {
1476
1580
  rpc: entry.rpc
1477
1581
  }),
1478
1582
  false,
1479
- 3
1583
+ 3,
1584
+ entry.runner
1480
1585
  ))
1481
1586
  }
1482
1587
  }
@@ -1569,6 +1674,47 @@ const make = Effect.gen(function*() {
1569
1674
  const singletonFibers = yield* FiberMap.make<SingletonAddress>()
1570
1675
  const withSingletonLock = Semaphore.makeUnsafe(1).withPermits(1)
1571
1676
 
1677
+ // Singletons that are stopping. They count as running, so they are not
1678
+ // restarted, and shard releases wait for them. Their teardown is awaited
1679
+ // without the singleton lock, as it can register or stop other singletons.
1680
+ const stoppingSingletons = MutableHashMap.empty<SingletonAddress, Fiber.Fiber<void>>()
1681
+ const isSingletonRegistered = (address: SingletonAddress) => {
1682
+ const map = singletons.get(address.shardId)
1683
+ return map !== undefined && MutableHashMap.has(map, address)
1684
+ }
1685
+
1686
+ // Called with the singleton lock held. Starts stopping the singleton and
1687
+ // returns the wait for its teardown, to run without the lock.
1688
+ const stopSingletonUnsafe = (address: SingletonAddress, activeTeardown: boolean): Effect.Effect<void> => {
1689
+ const stopping = MutableHashMap.get(stoppingSingletons, address)
1690
+ if (Option.isSome(stopping)) return Fiber.join(stopping.value)
1691
+ const fiber = FiberMap.getUnsafe(singletonFibers, address)
1692
+ if (Option.isNone(fiber)) return Effect.void
1693
+ const interrupt = Fiber.interrupt(fiber.value)
1694
+ const stop = Effect.runForkWith(services)(
1695
+ activeTeardown ? ActiveTeardown.aroundShard(address.shardId, interrupt) : interrupt
1696
+ )
1697
+ MutableHashMap.set(stoppingSingletons, address, stop)
1698
+ stop.addObserver(() => {
1699
+ MutableHashMap.remove(stoppingSingletons, address)
1700
+ // a registration made while the previous singleton was stopping
1701
+ if (isSingletonRegistered(address)) runFork(syncSingletons)
1702
+ })
1703
+ return Fiber.join(stop)
1704
+ }
1705
+
1706
+ // Waits for the singletons of the given shards that are still stopping
1707
+ const awaitStoppingSingletons = (shardIds: ReadonlyArray<ShardId>) =>
1708
+ Effect.suspend(() => {
1709
+ const stops = Arr.empty<Fiber.Fiber<void>>()
1710
+ MutableHashMap.forEach(stoppingSingletons, (stop, address) => {
1711
+ if (shardIds.some((shardId) => Equal.equals(shardId, address.shardId))) {
1712
+ stops.push(stop)
1713
+ }
1714
+ })
1715
+ return Fiber.joinAll(stops)
1716
+ })
1717
+
1572
1718
  const registerSingleton: Sharding["Service"]["registerSingleton"] = Effect.fnUntraced(
1573
1719
  function*(name, run, options) {
1574
1720
  const shardGroup = options?.shardGroup ?? "default"
@@ -1599,32 +1745,38 @@ const make = Effect.gen(function*() {
1599
1745
 
1600
1746
  yield* PubSub.publish(events, SingletonRegistered({ address }))
1601
1747
 
1602
- // start if we are on the right shard
1603
- if (MutableHashSet.has(acquiredShards, address.shardId)) {
1748
+ // start if we are on the right shard, unless a previous registration of
1749
+ // this singleton is still stopping
1750
+ if (
1751
+ MutableHashSet.has(acquiredShards, address.shardId) &&
1752
+ !MutableHashMap.has(stoppingSingletons, address)
1753
+ ) {
1604
1754
  yield* Effect.logDebug("Starting singleton", address)
1605
1755
  yield* FiberMap.run(singletonFibers, address, wrappedRun)
1606
1756
  }
1607
1757
 
1608
- yield* Effect.addFinalizer(() => {
1609
- const map = singletons.get(address.shardId)!
1610
- MutableHashMap.remove(map, address)
1611
- return FiberMap.remove(singletonFibers, address)
1612
- })
1758
+ yield* Effect.addFinalizer(() =>
1759
+ withSingletonLock(Effect.sync(() => {
1760
+ MutableHashMap.remove(singletons.get(address.shardId)!, address)
1761
+ return stopSingletonUnsafe(address, false)
1762
+ })).pipe(Effect.flatten)
1763
+ )
1613
1764
  },
1614
1765
  withSingletonLock
1615
1766
  )
1616
1767
 
1617
- const syncSingletons = withSingletonLock(Effect.gen(function*() {
1768
+ // Starts and stops singletons to match the acquired shards, returning the
1769
+ // waits for the stopped singletons
1770
+ const startSyncSingletons = withSingletonLock(Effect.gen(function*() {
1771
+ const stops = Arr.empty<Effect.Effect<void>>()
1618
1772
  for (const [shardId, map] of singletons) {
1619
1773
  for (const [address, run] of map) {
1620
- const running = FiberMap.hasUnsafe(singletonFibers, address)
1774
+ const stopping = MutableHashMap.has(stoppingSingletons, address)
1775
+ const running = stopping || FiberMap.hasUnsafe(singletonFibers, address)
1621
1776
  const shouldBeRunning = MutableHashSet.has(acquiredShards, shardId)
1622
1777
  if (running && !shouldBeRunning) {
1623
- yield* Effect.logDebug("Stopping singleton", address)
1624
- yield* ActiveTeardown.aroundShard(
1625
- address.shardId,
1626
- FiberMap.remove(singletonFibers, address)
1627
- )
1778
+ if (!stopping) yield* Effect.logDebug("Stopping singleton", address)
1779
+ stops.push(stopSingletonUnsafe(address, true))
1628
1780
  } else if (!running && shouldBeRunning) {
1629
1781
  yield* Effect.logDebug("Starting singleton", address)
1630
1782
  yield* FiberMap.run(singletonFibers, address, run)
@@ -1635,8 +1787,15 @@ const make = Effect.gen(function*() {
1635
1787
  BigInt(yield* FiberMap.size(singletonFibers)),
1636
1788
  Context.empty()
1637
1789
  )
1790
+ return stops
1638
1791
  }))
1639
1792
 
1793
+ const syncSingletons: Effect.Effect<void> = Effect.flatMap(
1794
+ startSyncSingletons,
1795
+ // wait for the stopped singletons without the lock
1796
+ (stops) => Effect.all(stops, { discard: true })
1797
+ )
1798
+
1640
1799
  // --- Entities ---
1641
1800
 
1642
1801
  const reaper = yield* EntityReaper
@@ -1777,12 +1936,14 @@ const make = Effect.gen(function*() {
1777
1936
 
1778
1937
  if (isShutdown.current) return
1779
1938
  MutableRef.set(isShutdown, true)
1939
+ yield* notifyShardLockWaiters
1780
1940
  if (selfRunner) {
1781
- yield* Effect.ignore(runnerStorage.unregister(selfRunner.address))
1941
+ yield* Effect.ignore(withRegistrationLock(runnerStorage.unregister(selfRunner.address)))
1782
1942
  }
1783
1943
  })
1784
1944
 
1785
- yield* Scope.addFinalizerExit(shardingScope, shutdown)
1945
+ // hand the shards off before the loops stop
1946
+ yield* Scope.addFinalizerExit(shardingScope, (exit) => Effect.andThen(shutdown(exit), handoff))
1786
1947
 
1787
1948
  const activeEntityCount = Effect.gen(function*() {
1788
1949
  let count = 0