effect 4.0.2 → 4.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/dist/Context.js +3 -3
  2. package/dist/Context.js.map +1 -1
  3. package/dist/Crypto.d.ts +33 -5
  4. package/dist/Crypto.d.ts.map +1 -1
  5. package/dist/Crypto.js +8 -2
  6. package/dist/Crypto.js.map +1 -1
  7. package/dist/DateTime.d.ts +9 -6
  8. package/dist/DateTime.d.ts.map +1 -1
  9. package/dist/DateTime.js +9 -6
  10. package/dist/DateTime.js.map +1 -1
  11. package/dist/Duration.d.ts +6 -6
  12. package/dist/Duration.d.ts.map +1 -1
  13. package/dist/Duration.js +27 -8
  14. package/dist/Duration.js.map +1 -1
  15. package/dist/FileSystem.d.ts +44 -2
  16. package/dist/FileSystem.d.ts.map +1 -1
  17. package/dist/FileSystem.js.map +1 -1
  18. package/dist/Schema.d.ts +473 -1
  19. package/dist/Schema.d.ts.map +1 -1
  20. package/dist/Schema.js +355 -4
  21. package/dist/Schema.js.map +1 -1
  22. package/dist/Stdio.d.ts +7 -1
  23. package/dist/Stdio.d.ts.map +1 -1
  24. package/dist/Stdio.js +1 -0
  25. package/dist/Stdio.js.map +1 -1
  26. package/dist/Stream.d.ts +11 -11
  27. package/dist/Stream.js +5 -5
  28. package/dist/ai/McpServer.d.ts +6 -0
  29. package/dist/ai/McpServer.d.ts.map +1 -1
  30. package/dist/ai/McpServer.js +65 -23
  31. package/dist/ai/McpServer.js.map +1 -1
  32. package/dist/ai/internal/mcpProtocol/v2026_07_28.js +5 -0
  33. package/dist/ai/internal/mcpProtocol/v2026_07_28.js.map +1 -1
  34. package/dist/ai/internal/mcpProtocol.js.map +1 -1
  35. package/dist/ai/internal/mcpRuntime.js +1 -0
  36. package/dist/ai/internal/mcpRuntime.js.map +1 -1
  37. package/dist/cli/CliOutput.d.ts +27 -0
  38. package/dist/cli/CliOutput.d.ts.map +1 -1
  39. package/dist/cli/CliOutput.js +33 -10
  40. package/dist/cli/CliOutput.js.map +1 -1
  41. package/dist/cli/Command.d.ts.map +1 -1
  42. package/dist/cli/Command.js +6 -3
  43. package/dist/cli/Command.js.map +1 -1
  44. package/dist/cli/Completions.d.ts +3 -3
  45. package/dist/cli/Completions.d.ts.map +1 -1
  46. package/dist/cli/Completions.js +8 -5
  47. package/dist/cli/Completions.js.map +1 -1
  48. package/dist/cli/GlobalFlag.d.ts +5 -4
  49. package/dist/cli/GlobalFlag.d.ts.map +1 -1
  50. package/dist/cli/GlobalFlag.js +3 -3
  51. package/dist/cli/GlobalFlag.js.map +1 -1
  52. package/dist/cli/Prompt.d.ts +17 -1
  53. package/dist/cli/Prompt.d.ts.map +1 -1
  54. package/dist/cli/Prompt.js +119 -114
  55. package/dist/cli/Prompt.js.map +1 -1
  56. package/dist/cli/internal/ansi.d.ts +0 -1
  57. package/dist/cli/internal/ansi.d.ts.map +1 -1
  58. package/dist/cli/internal/ansi.js +20 -3
  59. package/dist/cli/internal/ansi.js.map +1 -1
  60. package/dist/cli/internal/completions/powershell.d.ts +2 -0
  61. package/dist/cli/internal/completions/powershell.d.ts.map +1 -0
  62. package/dist/cli/internal/completions/powershell.js +273 -0
  63. package/dist/cli/internal/completions/powershell.js.map +1 -0
  64. package/dist/cli/internal/completions/zsh.js +3 -2
  65. package/dist/cli/internal/completions/zsh.js.map +1 -1
  66. package/dist/cli/internal/wizard.d.ts +3 -3
  67. package/dist/cli/internal/wizard.d.ts.map +1 -1
  68. package/dist/cli/internal/wizard.js +21 -18
  69. package/dist/cli/internal/wizard.js.map +1 -1
  70. package/dist/cluster/ClusterSchema.d.ts +28 -0
  71. package/dist/cluster/ClusterSchema.d.ts.map +1 -1
  72. package/dist/cluster/ClusterSchema.js +30 -0
  73. package/dist/cluster/ClusterSchema.js.map +1 -1
  74. package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
  75. package/dist/cluster/ClusterWorkflowEngine.js +10 -18
  76. package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
  77. package/dist/cluster/MessageStorage.d.ts +18 -3
  78. package/dist/cluster/MessageStorage.d.ts.map +1 -1
  79. package/dist/cluster/MessageStorage.js +31 -11
  80. package/dist/cluster/MessageStorage.js.map +1 -1
  81. package/dist/cluster/Runners.d.ts.map +1 -1
  82. package/dist/cluster/Runners.js +8 -2
  83. package/dist/cluster/Runners.js.map +1 -1
  84. package/dist/cluster/Sharding.d.ts.map +1 -1
  85. package/dist/cluster/Sharding.js +181 -62
  86. package/dist/cluster/Sharding.js.map +1 -1
  87. package/dist/cluster/SqlMessageStorage.d.ts +59 -19
  88. package/dist/cluster/SqlMessageStorage.d.ts.map +1 -1
  89. package/dist/cluster/SqlMessageStorage.js +84 -55
  90. package/dist/cluster/SqlMessageStorage.js.map +1 -1
  91. package/dist/cluster/SqlRunnerStorage.d.ts +68 -19
  92. package/dist/cluster/SqlRunnerStorage.d.ts.map +1 -1
  93. package/dist/cluster/SqlRunnerStorage.js +191 -113
  94. package/dist/cluster/SqlRunnerStorage.js.map +1 -1
  95. package/dist/cluster/internal/entityManager.js +104 -37
  96. package/dist/cluster/internal/entityManager.js.map +1 -1
  97. package/dist/http/FindMyWay/internal/router.js +3 -6
  98. package/dist/http/FindMyWay/internal/router.js.map +1 -1
  99. package/dist/http/HttpClient.d.ts +6 -0
  100. package/dist/http/HttpClient.d.ts.map +1 -1
  101. package/dist/http/HttpClient.js +39 -2
  102. package/dist/http/HttpClient.js.map +1 -1
  103. package/dist/http/HttpTraceContext.d.ts.map +1 -1
  104. package/dist/http/HttpTraceContext.js +4 -2
  105. package/dist/http/HttpTraceContext.js.map +1 -1
  106. package/dist/internal/effect.js +12 -7
  107. package/dist/internal/effect.js.map +1 -1
  108. package/dist/internal/version.js +1 -1
  109. package/dist/net/AddressResolver.d.ts +145 -0
  110. package/dist/net/AddressResolver.d.ts.map +1 -0
  111. package/dist/net/AddressResolver.js +114 -0
  112. package/dist/net/AddressResolver.js.map +1 -0
  113. package/dist/net/Dns.d.ts +596 -0
  114. package/dist/net/Dns.d.ts.map +1 -0
  115. package/dist/net/Dns.js +549 -0
  116. package/dist/net/Dns.js.map +1 -0
  117. package/dist/net/Host.d.ts +320 -0
  118. package/dist/net/Host.d.ts.map +1 -0
  119. package/dist/net/Host.js +342 -0
  120. package/dist/net/Host.js.map +1 -0
  121. package/dist/net/IpInterface.d.ts +76 -3
  122. package/dist/net/IpInterface.d.ts.map +1 -1
  123. package/dist/net/IpInterface.js +53 -4
  124. package/dist/net/IpInterface.js.map +1 -1
  125. package/dist/net/IpNetwork.d.ts +82 -8
  126. package/dist/net/IpNetwork.d.ts.map +1 -1
  127. package/dist/net/IpNetwork.js +60 -4
  128. package/dist/net/IpNetwork.js.map +1 -1
  129. package/dist/net/NetAddress.d.ts +388 -35
  130. package/dist/net/NetAddress.d.ts.map +1 -1
  131. package/dist/net/NetAddress.js +350 -34
  132. package/dist/net/NetAddress.js.map +1 -1
  133. package/dist/net/index.d.ts +15 -0
  134. package/dist/net/index.d.ts.map +1 -1
  135. package/dist/net/index.js +15 -0
  136. package/dist/net/index.js.map +1 -1
  137. package/dist/observability/OtlpMetrics.d.ts.map +1 -1
  138. package/dist/observability/OtlpMetrics.js +15 -21
  139. package/dist/observability/OtlpMetrics.js.map +1 -1
  140. package/dist/observability/PrometheusMetrics.js +1 -0
  141. package/dist/observability/PrometheusMetrics.js.map +1 -1
  142. package/dist/persistence/PersistedQueue.d.ts +32 -3
  143. package/dist/persistence/PersistedQueue.d.ts.map +1 -1
  144. package/dist/persistence/PersistedQueue.js +75 -16
  145. package/dist/persistence/PersistedQueue.js.map +1 -1
  146. package/dist/rpc/RpcClient.d.ts +22 -6
  147. package/dist/rpc/RpcClient.d.ts.map +1 -1
  148. package/dist/rpc/RpcClient.js +12 -5
  149. package/dist/rpc/RpcClient.js.map +1 -1
  150. package/dist/rpc/RpcServer.d.ts +6 -0
  151. package/dist/rpc/RpcServer.d.ts.map +1 -1
  152. package/dist/rpc/RpcServer.js +42 -20
  153. package/dist/rpc/RpcServer.js.map +1 -1
  154. package/dist/socket/DatagramSocket.d.ts +456 -0
  155. package/dist/socket/DatagramSocket.d.ts.map +1 -0
  156. package/dist/socket/DatagramSocket.js +535 -0
  157. package/dist/socket/DatagramSocket.js.map +1 -0
  158. package/dist/socket/Socket.d.ts +14 -1
  159. package/dist/socket/Socket.d.ts.map +1 -1
  160. package/dist/socket/Socket.js +9 -1
  161. package/dist/socket/Socket.js.map +1 -1
  162. package/dist/socket/index.d.ts +5 -0
  163. package/dist/socket/index.d.ts.map +1 -1
  164. package/dist/socket/index.js +5 -0
  165. package/dist/socket/index.js.map +1 -1
  166. package/dist/sql/Migrator.d.ts.map +1 -1
  167. package/dist/sql/Migrator.js +7 -2
  168. package/dist/sql/Migrator.js.map +1 -1
  169. package/dist/sql/SqlClient.d.ts +7 -1
  170. package/dist/sql/SqlClient.d.ts.map +1 -1
  171. package/dist/sql/SqlClient.js +31 -6
  172. package/dist/sql/SqlClient.js.map +1 -1
  173. package/dist/testing/TestCrypto.d.ts +73 -0
  174. package/dist/testing/TestCrypto.d.ts.map +1 -0
  175. package/dist/testing/TestCrypto.js +87 -0
  176. package/dist/testing/TestCrypto.js.map +1 -0
  177. package/dist/testing/index.d.ts +5 -0
  178. package/dist/testing/index.d.ts.map +1 -1
  179. package/dist/testing/index.js +5 -0
  180. package/dist/testing/index.js.map +1 -1
  181. package/dist/workflow/DurableDeferred.d.ts +54 -0
  182. package/dist/workflow/DurableDeferred.d.ts.map +1 -1
  183. package/dist/workflow/DurableDeferred.js +78 -49
  184. package/dist/workflow/DurableDeferred.js.map +1 -1
  185. package/dist/workflow/WorkflowEngine.d.ts.map +1 -1
  186. package/dist/workflow/WorkflowEngine.js +2 -1
  187. package/dist/workflow/WorkflowEngine.js.map +1 -1
  188. package/package.json +1 -1
  189. package/src/Context.ts +3 -3
  190. package/src/Crypto.ts +43 -9
  191. package/src/DateTime.ts +9 -6
  192. package/src/Duration.ts +36 -16
  193. package/src/FileSystem.ts +48 -2
  194. package/src/Schema.ts +669 -5
  195. package/src/Stdio.ts +9 -2
  196. package/src/Stream.ts +11 -11
  197. package/src/ai/McpServer.ts +63 -23
  198. package/src/ai/internal/mcpProtocol/v2026_07_28.ts +5 -0
  199. package/src/ai/internal/mcpProtocol.ts +3 -0
  200. package/src/ai/internal/mcpRuntime.ts +2 -0
  201. package/src/cli/CliOutput.ts +33 -16
  202. package/src/cli/Command.ts +4 -3
  203. package/src/cli/Completions.ts +9 -6
  204. package/src/cli/GlobalFlag.ts +19 -11
  205. package/src/cli/Prompt.ts +159 -149
  206. package/src/cli/internal/ansi.ts +26 -3
  207. package/src/cli/internal/completions/powershell.ts +322 -0
  208. package/src/cli/internal/completions/zsh.ts +4 -2
  209. package/src/cli/internal/wizard.ts +34 -26
  210. package/src/cluster/ClusterSchema.ts +32 -0
  211. package/src/cluster/ClusterWorkflowEngine.ts +13 -24
  212. package/src/cluster/MessageStorage.ts +45 -14
  213. package/src/cluster/Runners.ts +8 -2
  214. package/src/cluster/Sharding.ts +207 -49
  215. package/src/cluster/SqlMessageStorage.ts +118 -84
  216. package/src/cluster/SqlRunnerStorage.ts +224 -128
  217. package/src/cluster/internal/entityManager.ts +151 -55
  218. package/src/http/FindMyWay/internal/router.ts +4 -7
  219. package/src/http/HttpClient.ts +56 -2
  220. package/src/http/HttpTraceContext.ts +8 -2
  221. package/src/internal/effect.ts +15 -13
  222. package/src/internal/version.ts +1 -1
  223. package/src/net/AddressResolver.ts +222 -0
  224. package/src/net/Dns.ts +1035 -0
  225. package/src/net/Host.ts +472 -0
  226. package/src/net/IpInterface.ts +117 -7
  227. package/src/net/IpNetwork.ts +116 -17
  228. package/src/net/NetAddress.ts +611 -76
  229. package/src/net/index.ts +18 -0
  230. package/src/observability/OtlpMetrics.ts +12 -23
  231. package/src/observability/PrometheusMetrics.ts +1 -0
  232. package/src/persistence/PersistedQueue.ts +146 -53
  233. package/src/rpc/RpcClient.ts +41 -19
  234. package/src/rpc/RpcServer.ts +60 -25
  235. package/src/socket/DatagramSocket.ts +919 -0
  236. package/src/socket/Socket.ts +22 -2
  237. package/src/socket/index.ts +6 -0
  238. package/src/sql/Migrator.ts +21 -5
  239. package/src/sql/SqlClient.ts +39 -15
  240. package/src/testing/TestCrypto.ts +94 -0
  241. package/src/testing/index.ts +6 -0
  242. package/src/workflow/DurableDeferred.ts +123 -48
  243. package/src/workflow/WorkflowEngine.ts +2 -1
@@ -118,6 +118,13 @@ const make = /*#__PURE__*/Effect.gen(function* () {
118
118
  // the active shards are the ones that we have acquired the lock for
119
119
  const acquiredShards = MutableHashSet.empty();
120
120
  const activeShardsLatch = yield* Latch.make(false);
121
+ // open once shutdown starts handing every shard off
122
+ const handoffLatch = Latch.makeUnsafe(false);
123
+ // open once the handoff has released every shard lock
124
+ const handedOffLatch = Latch.makeUnsafe(false);
125
+ // serializes runner registration with unregistration, so a registration
126
+ // in flight cannot re-advertise a runner that is shutting down
127
+ const withRegistrationLock = Semaphore.makeUnsafe(1).withPermits(1);
121
128
  const events = yield* PubSub.unbounded();
122
129
  const getRegistrationEvents = Stream.fromPubSub(events);
123
130
  const isLocalRunner = address => {
@@ -131,6 +138,23 @@ const make = /*#__PURE__*/Effect.gen(function* () {
131
138
  function isEntityOnLocalShards(address) {
132
139
  return MutableHashSet.has(acquiredShards, address.shardId);
133
140
  }
141
+ function isAwaitingShardLock(shardId) {
142
+ return !MutableRef.get(isShutdown) && MutableHashSet.has(selfShards, shardId) && !MutableHashSet.has(acquiredShards, shardId);
143
+ }
144
+ // Requests for a shard assigned to this runner wait for its lock, for at most
145
+ // `shardLockExpiration`, instead of bouncing while the previous owner drains.
146
+ // Each notification opens the current latch and replaces it, so a waiter that
147
+ // checked its shard before a notification cannot miss it.
148
+ let shardLocksChanged = Latch.makeUnsafe(false);
149
+ const notifyShardLockWaiters = Effect.suspend(() => {
150
+ const latch = shardLocksChanged;
151
+ shardLocksChanged = Latch.makeUnsafe(false);
152
+ return latch.open;
153
+ });
154
+ const awaitShardLock = shardId => {
155
+ const loop = Effect.suspend(() => isAwaitingShardLock(shardId) ? Effect.andThen(shardLocksChanged.await, loop) : Effect.void);
156
+ return Effect.suspend(() => isAwaitingShardLock(shardId) ? Effect.asVoid(Effect.timeoutOption(loop, config.shardLockExpiration)) : Effect.void);
157
+ };
134
158
  yield* Scope.addFinalizer(shardingScope, Effect.logDebug("Shutdown complete").pipe(Effect.annotateLogs({
135
159
  module: "effect/cluster/Sharding"
136
160
  })));
@@ -141,6 +165,8 @@ const make = /*#__PURE__*/Effect.gen(function* () {
141
165
  // This should be shutdown last, when all entities have been shutdown, to
142
166
  // allow them to move to another runner.
143
167
  const releasingShards = MutableHashSet.empty();
168
+ // Hands every held shard off on shutdown, set when this is a runner
169
+ let handoff = Effect.void;
144
170
  // Shards whose entities must be force interrupted and locks fully released
145
171
  // before normal reacquisition.
146
172
  const forceReleasingShards = MutableHashSet.empty();
@@ -151,21 +177,40 @@ const make = /*#__PURE__*/Effect.gen(function* () {
151
177
  // the locks expire over time, so if this fails we ignore it
152
178
  return Effect.ignore(runnerStorage.releaseAll(selfAddress));
153
179
  });
154
- const releaseShardsMap = yield* FiberMap.make();
155
- let forcedShardReleaseRunning = false;
156
- // Interrupt the shards' entities, wait for lock health, run the storage
157
- // release, then clear the shards' bookkeeping.
158
- const runShardRelease = Effect.fnUntraced(function* (shardIds, force, release) {
180
+ // Shard releases fork into a child scope that closes after the shutdown
181
+ // handoff, as forks into the closing sharding scope never run.
182
+ const releaseScope = yield* Scope.fork(shardingScope);
183
+ // Interrupt the shards' entities on every entity manager that is open
184
+ const forkInterruptShards = Effect.fnUntraced(function* (shardIds, force) {
159
185
  const fibers = Arr.empty();
160
186
  for (const shardId of shardIds) {
161
187
  for (const state of entityManagers.values()) {
162
188
  if (state.status === "closed") continue;
163
- fibers.push(yield* Effect.forkScoped(state.manager.interruptShard(shardId, {
189
+ fibers.push(yield* Effect.forkIn(state.manager.interruptShard(shardId, {
164
190
  force
165
- })));
191
+ }), releaseScope, {
192
+ startImmediately: true
193
+ }));
166
194
  }
167
195
  }
168
- yield* Fiber.joinAll(fibers);
196
+ return fibers;
197
+ });
198
+ const releaseShardsMap = yield* FiberMap.make();
199
+ let forcedShardReleaseRunning = false;
200
+ // Called from the acquisition loop only, so no `acquire` is in flight
201
+ const checkHandedOff = () => {
202
+ if (handoffLatch.isOpen() && MutableHashSet.size(acquiredShards) === 0 && MutableHashSet.size(releasingShards) === 0 && !forcedShardReleasePending()) {
203
+ handedOffLatch.openUnsafe();
204
+ }
205
+ };
206
+ // Interrupt the shards' entities, wait for lock health, run the storage
207
+ // release, then clear the shards' bookkeeping.
208
+ const runShardRelease = Effect.fnUntraced(function* (shardIds, force, release) {
209
+ yield* Fiber.joinAll(yield* forkInterruptShards(shardIds, force));
210
+ // stop the singletons of shards that are no longer acquired, and wait
211
+ // for those on the released shards
212
+ yield* startSyncSingletons;
213
+ yield* awaitStoppingSingletons(shardIds);
169
214
  yield* shardLocksHealthyLatch.await;
170
215
  yield* release;
171
216
  for (const shardId of shardIds) {
@@ -179,6 +224,8 @@ const make = /*#__PURE__*/Effect.gen(function* () {
179
224
  interrupt: MutableRef.get(isShutdown)
180
225
  });
181
226
  }
227
+ // let the acquisition loop check whether the handoff has finished
228
+ if (handoffLatch.isOpen()) activeShardsLatch.openUnsafe();
182
229
  });
183
230
  const retryShardRelease = annotations => effect => effect.pipe(Effect.sandbox, Effect.tapError(cause => Effect.logDebug(`Could not release shards, retrying`, cause).pipe(Effect.annotateLogs({
184
231
  module: "effect/cluster/Sharding",
@@ -212,7 +259,7 @@ const make = /*#__PURE__*/Effect.gen(function* () {
212
259
  }), Effect.ensuring(Effect.sync(() => {
213
260
  forcedShardReleaseRunning = false;
214
261
  activeShardsLatch.openUnsafe();
215
- })), Effect.forkIn(shardingScope), Effect.asVoid);
262
+ })), Effect.forkIn(releaseScope), Effect.asVoid);
216
263
  });
217
264
  const releaseShards = Effect.gen(function* () {
218
265
  yield* releaseForcedShards;
@@ -227,9 +274,11 @@ const make = /*#__PURE__*/Effect.gen(function* () {
227
274
  while (true) {
228
275
  yield* activeShardsLatch.await;
229
276
  activeShardsLatch.closeUnsafe();
230
- // if a shard is no longer assigned to this runner, we release it
277
+ // if a shard is no longer assigned to this runner, or the runner is
278
+ // handing every shard off, we release it
279
+ const handingOff = handoffLatch.isOpen();
231
280
  for (const shardId of acquiredShards) {
232
- if (MutableHashSet.has(selfShards, shardId)) continue;
281
+ if (!handingOff && MutableHashSet.has(selfShards, shardId)) continue;
233
282
  MutableHashSet.remove(acquiredShards, shardId);
234
283
  MutableHashSet.add(releasingShards, shardId);
235
284
  }
@@ -237,6 +286,7 @@ const make = /*#__PURE__*/Effect.gen(function* () {
237
286
  yield* Effect.forkIn(syncSingletons, shardingScope);
238
287
  yield* releaseShards;
239
288
  }
289
+ checkHandedOff();
240
290
  if (!shardLocksHealthyLatch.isOpen()) {
241
291
  continue;
242
292
  }
@@ -245,6 +295,10 @@ const make = /*#__PURE__*/Effect.gen(function* () {
245
295
  if (forcedShardReleasePending()) {
246
296
  continue;
247
297
  }
298
+ // a runner that is shutting down takes no new shards
299
+ if (isShutdown.current) {
300
+ continue;
301
+ }
248
302
  // if a shard has been assigned to this runner, we acquire it
249
303
  const unacquiredShards = MutableHashSet.empty();
250
304
  for (const shardId of selfShards) {
@@ -272,12 +326,14 @@ const make = /*#__PURE__*/Effect.gen(function* () {
272
326
  MutableHashSet.add(acquiredShards, shardId);
273
327
  }
274
328
  if (acquired.length > 0) {
329
+ yield* notifyShardLockWaiters;
275
330
  yield* storageReadLatch.open;
276
331
  yield* Effect.forkIn(syncSingletons, shardingScope);
277
332
  // update metrics
278
333
  ClusterMetrics.shards.updateUnsafe(BigInt(MutableHashSet.size(acquiredShards)), Context.empty());
279
334
  }
280
- yield* Effect.sleep(1000);
335
+ // the shutdown handoff releases the shards acquired here without waiting
336
+ yield* Effect.raceFirst(Effect.sleep(1000), handoffLatch.await);
281
337
  activeShardsLatch.openUnsafe();
282
338
  }
283
339
  }).pipe(Effect.catchCause(cause => Effect.logWarning("Could not acquire/release shards", cause)), Effect.repeat(Schedule.spaced(config.entityMessagePollInterval)), Effect.annotateLogs({
@@ -297,20 +353,12 @@ const make = /*#__PURE__*/Effect.gen(function* () {
297
353
  ClusterMetrics.shards.updateUnsafe(BigInt(0), Context.empty());
298
354
  activeShardsLatch.openUnsafe();
299
355
  return Effect.gen(function* () {
356
+ yield* notifyShardLockWaiters;
300
357
  yield* Effect.logError("Shard lock storage is unhealthy", cause);
301
358
  yield* Effect.forkIn(syncSingletons, shardingScope, {
302
359
  startImmediately: true
303
360
  });
304
- for (const shardId of affectedShards) {
305
- for (const state of entityManagers.values()) {
306
- if (state.status === "closed") continue;
307
- yield* Effect.forkIn(state.manager.interruptShard(shardId, {
308
- force: true
309
- }), shardingScope, {
310
- startImmediately: true
311
- });
312
- }
313
- }
361
+ yield* forkInterruptShards(affectedShards, true);
314
362
  activeShardsLatch.openUnsafe();
315
363
  });
316
364
  });
@@ -355,6 +403,14 @@ const make = /*#__PURE__*/Effect.gen(function* () {
355
403
  yield* Effect.suspend(() => shardLocksHealthyLatch.isOpen() ? refreshShardLocks : probeShardLocks).pipe(Effect.repeat(Schedule.fixed(shardLockInterval)), Effect.forever, Effect.forkIn(shardingScope));
356
404
  // open the shard latch every poll interval
357
405
  yield* activeShardsLatch.open.pipe(Effect.delay(config.entityMessagePollInterval), Effect.forever, Effect.forkIn(shardingScope));
406
+ // Release every held shard through the per-shard path. Their entities are
407
+ // forced to stop, so the handoff does not wait out graceful termination.
408
+ // This is best effort: the wait is bounded, and if it times out, the final
409
+ // `releaseAll` attempts to release the remaining locks.
410
+ handoff = Effect.suspend(() => {
411
+ if (handoffLatch.openUnsafe()) activeShardsLatch.openUnsafe();
412
+ return forkInterruptShards([...acquiredShards, ...releasingShards], true);
413
+ }).pipe(Effect.andThen(Effect.timeoutOption(handedOffLatch.await, shardLockInterval)), Effect.asVoid);
358
414
  }
359
415
  // --- Storage inbox ---
360
416
  //
@@ -717,28 +773,34 @@ const make = /*#__PURE__*/Effect.gen(function* () {
717
773
  }), (effect, address) => Effect.ensuring(effect, Effect.sync(() => MutableHashMap.remove(entityResumptionState, address))), Effect.forkIn(shardingScope));
718
774
  }
719
775
  // --- Sending messages ---
720
- const sendLocal = message => Effect.suspend(function loop() {
776
+ const sendLocal = message => {
721
777
  const address = message.envelope.address;
722
- if (!isEntityOnLocalShards(address)) {
723
- return Effect.fail(new EntityNotAssignedToRunner({
724
- address
725
- }));
726
- }
727
- const state = entityManagers.get(address.entityType);
728
- if (!state) {
729
- return Effect.flatMap(waitForEntityManager(address.entityType), loop);
730
- } else if (state.status === "closed" || state.status === "closing" && message._tag === "IncomingRequest") {
731
- // if we are shutting down, we don't accept new requests
732
- return Effect.fail(new EntityNotAssignedToRunner({
733
- address
734
- }));
735
- }
736
- return message._tag === "IncomingRequest" || message._tag === "IncomingEnvelope" ? state.manager.send(message) : runnersService.sendLocal({
737
- message,
738
- send: state.manager.sendLocal,
739
- simulateRemoteSerialization: config.simulateRemoteSerialization
778
+ const isRequest = message._tag === "IncomingRequest" || message._tag === "OutgoingRequest";
779
+ const send = Effect.suspend(function loop() {
780
+ if (!isEntityOnLocalShards(address) && (
781
+ // Entities draining from a released shard still accept interrupts and acks
782
+ isRequest || !MutableHashSet.has(releasingShards, address.shardId))) {
783
+ return Effect.fail(new EntityNotAssignedToRunner({
784
+ address
785
+ }));
786
+ }
787
+ const state = entityManagers.get(address.entityType);
788
+ if (!state) {
789
+ return Effect.flatMap(waitForEntityManager(address.entityType), loop);
790
+ } else if (state.status === "closed" || state.status === "closing" && message._tag === "IncomingRequest") {
791
+ // if we are shutting down, we don't accept new requests
792
+ return Effect.fail(new EntityNotAssignedToRunner({
793
+ address
794
+ }));
795
+ }
796
+ return message._tag === "IncomingRequest" || message._tag === "IncomingEnvelope" ? state.manager.send(message) : runnersService.sendLocal({
797
+ message,
798
+ send: state.manager.sendLocal,
799
+ simulateRemoteSerialization: config.simulateRemoteSerialization
800
+ });
740
801
  });
741
- });
802
+ return isRequest ? Effect.andThen(awaitShardLock(address.shardId), send) : send;
803
+ };
742
804
  const pendingNotifications = new Map();
743
805
  const notifyLocal = (message, discard, options) => Effect.suspend(function loop() {
744
806
  const address = message.envelope.address;
@@ -793,7 +855,10 @@ const make = /*#__PURE__*/Effect.gen(function* () {
793
855
  storageOnly: !isLocal
794
856
  });
795
857
  });
796
- function sendOutgoing(message, discard, retries) {
858
+ function sendOutgoing(message, discard, retries,
859
+ // The runner serving the request, for its interrupts and acks. Later
860
+ // attempts fall back to the current shard assignment.
861
+ runner) {
797
862
  const isPersisted = Context.get(message._tag === "OutgoingRequest" ? message.annotations : message.rpc.annotations, Persisted);
798
863
  const shouldFail = !discard && (message._tag === "OutgoingRequest" || message.envelope._tag === "AckChunk");
799
864
  const abandon = error => {
@@ -818,7 +883,7 @@ const make = /*#__PURE__*/Effect.gen(function* () {
818
883
  address
819
884
  }));
820
885
  }
821
- const maybeRunner = MutableHashMap.get(shardAssignments, address.shardId);
886
+ const maybeRunner = runner !== undefined ? Option.some(runner) : MutableHashMap.get(shardAssignments, address.shardId);
822
887
  const runnerIsLocal = Option.isSome(maybeRunner) && isLocalRunner(maybeRunner.value);
823
888
  if (isPersisted) {
824
889
  return runnerIsLocal ? notifyLocal(message, discard) : runnersService.notify({
@@ -830,6 +895,9 @@ const make = /*#__PURE__*/Effect.gen(function* () {
830
895
  return Effect.fail(new EntityNotAssignedToRunner({
831
896
  address
832
897
  }));
898
+ } else if (message._tag === "OutgoingRequest") {
899
+ const entry = clientRequests.get(message.envelope.requestId);
900
+ if (entry) entry.runner = maybeRunner.value;
833
901
  }
834
902
  return runnerIsLocal ? sendLocal(message) : discard ? runnersService.notify({
835
903
  address: maybeRunner,
@@ -886,9 +954,12 @@ const make = /*#__PURE__*/Effect.gen(function* () {
886
954
  while (true) {
887
955
  // Ensure the current runner is registered
888
956
  if (selfRunner && !isShutdown.current && !MutableHashMap.has(allRunners, selfRunner)) {
889
- yield* Effect.logDebug("Registering runner", selfRunner);
890
- const machineId = yield* withTimeout(runnerStorage.register(selfRunner, true));
891
- yield* snowflakeGen.setMachineId(machineId);
957
+ yield* withRegistrationLock(Effect.gen(function* () {
958
+ if (isShutdown.current) return;
959
+ yield* Effect.logDebug("Registering runner", selfRunner);
960
+ const machineId = yield* withTimeout(runnerStorage.register(selfRunner, true));
961
+ yield* snowflakeGen.setMachineId(machineId);
962
+ }));
892
963
  }
893
964
  const runners = yield* withTimeout(runnerStorage.getRunners);
894
965
  let changed = false;
@@ -957,6 +1028,7 @@ const make = /*#__PURE__*/Effect.gen(function* () {
957
1028
  });
958
1029
  yield* Effect.logDebug("New shard assignments", selfShards);
959
1030
  activeShardsLatch.openUnsafe();
1031
+ yield* notifyShardLockWaiters;
960
1032
  // update metrics
961
1033
  if (selfRunner) {
962
1034
  ClusterMetrics.runnersHealthy.updateUnsafe(BigInt(MutableHashSet.has(healthyRunners, selfRunner) ? 1 : 0), Context.empty());
@@ -1040,7 +1112,7 @@ const make = /*#__PURE__*/Effect.gen(function* () {
1040
1112
  replyId: entry.lastChunkId
1041
1113
  }),
1042
1114
  rpc: entry.rpc
1043
- }), false);
1115
+ }), false, undefined, entry.runner);
1044
1116
  }
1045
1117
  case "Interrupt":
1046
1118
  {
@@ -1066,7 +1138,7 @@ const make = /*#__PURE__*/Effect.gen(function* () {
1066
1138
  requestId
1067
1139
  }),
1068
1140
  rpc: entry.rpc
1069
- }), false, 3));
1141
+ }), false, 3, entry.runner));
1070
1142
  }
1071
1143
  }
1072
1144
  return Effect.void;
@@ -1139,6 +1211,41 @@ const make = /*#__PURE__*/Effect.gen(function* () {
1139
1211
  const singletons = new Map();
1140
1212
  const singletonFibers = yield* FiberMap.make();
1141
1213
  const withSingletonLock = Semaphore.makeUnsafe(1).withPermits(1);
1214
+ // Singletons that are stopping. They count as running, so they are not
1215
+ // restarted, and shard releases wait for them. Their teardown is awaited
1216
+ // without the singleton lock, as it can register or stop other singletons.
1217
+ const stoppingSingletons = MutableHashMap.empty();
1218
+ const isSingletonRegistered = address => {
1219
+ const map = singletons.get(address.shardId);
1220
+ return map !== undefined && MutableHashMap.has(map, address);
1221
+ };
1222
+ // Called with the singleton lock held. Starts stopping the singleton and
1223
+ // returns the wait for its teardown, to run without the lock.
1224
+ const stopSingletonUnsafe = (address, activeTeardown) => {
1225
+ const stopping = MutableHashMap.get(stoppingSingletons, address);
1226
+ if (Option.isSome(stopping)) return Fiber.join(stopping.value);
1227
+ const fiber = FiberMap.getUnsafe(singletonFibers, address);
1228
+ if (Option.isNone(fiber)) return Effect.void;
1229
+ const interrupt = Fiber.interrupt(fiber.value);
1230
+ const stop = Effect.runForkWith(services)(activeTeardown ? ActiveTeardown.aroundShard(address.shardId, interrupt) : interrupt);
1231
+ MutableHashMap.set(stoppingSingletons, address, stop);
1232
+ stop.addObserver(() => {
1233
+ MutableHashMap.remove(stoppingSingletons, address);
1234
+ // a registration made while the previous singleton was stopping
1235
+ if (isSingletonRegistered(address)) runFork(syncSingletons);
1236
+ });
1237
+ return Fiber.join(stop);
1238
+ };
1239
+ // Waits for the singletons of the given shards that are still stopping
1240
+ const awaitStoppingSingletons = shardIds => Effect.suspend(() => {
1241
+ const stops = Arr.empty();
1242
+ MutableHashMap.forEach(stoppingSingletons, (stop, address) => {
1243
+ if (shardIds.some(shardId => Equal.equals(shardId, address.shardId))) {
1244
+ stops.push(stop);
1245
+ }
1246
+ });
1247
+ return Fiber.joinAll(stops);
1248
+ });
1142
1249
  const registerSingleton = Effect.fnUntraced(function* (name, run, options) {
1143
1250
  const shardGroup = options?.shardGroup ?? "default";
1144
1251
  const address = new SingletonAddress({
@@ -1159,25 +1266,29 @@ const make = /*#__PURE__*/Effect.gen(function* () {
1159
1266
  yield* PubSub.publish(events, SingletonRegistered({
1160
1267
  address
1161
1268
  }));
1162
- // start if we are on the right shard
1163
- if (MutableHashSet.has(acquiredShards, address.shardId)) {
1269
+ // start if we are on the right shard, unless a previous registration of
1270
+ // this singleton is still stopping
1271
+ if (MutableHashSet.has(acquiredShards, address.shardId) && !MutableHashMap.has(stoppingSingletons, address)) {
1164
1272
  yield* Effect.logDebug("Starting singleton", address);
1165
1273
  yield* FiberMap.run(singletonFibers, address, wrappedRun);
1166
1274
  }
1167
- yield* Effect.addFinalizer(() => {
1168
- const map = singletons.get(address.shardId);
1169
- MutableHashMap.remove(map, address);
1170
- return FiberMap.remove(singletonFibers, address);
1171
- });
1275
+ yield* Effect.addFinalizer(() => withSingletonLock(Effect.sync(() => {
1276
+ MutableHashMap.remove(singletons.get(address.shardId), address);
1277
+ return stopSingletonUnsafe(address, false);
1278
+ })).pipe(Effect.flatten));
1172
1279
  }, withSingletonLock);
1173
- const syncSingletons = withSingletonLock(Effect.gen(function* () {
1280
+ // Starts and stops singletons to match the acquired shards, returning the
1281
+ // waits for the stopped singletons
1282
+ const startSyncSingletons = withSingletonLock(Effect.gen(function* () {
1283
+ const stops = Arr.empty();
1174
1284
  for (const [shardId, map] of singletons) {
1175
1285
  for (const [address, run] of map) {
1176
- const running = FiberMap.hasUnsafe(singletonFibers, address);
1286
+ const stopping = MutableHashMap.has(stoppingSingletons, address);
1287
+ const running = stopping || FiberMap.hasUnsafe(singletonFibers, address);
1177
1288
  const shouldBeRunning = MutableHashSet.has(acquiredShards, shardId);
1178
1289
  if (running && !shouldBeRunning) {
1179
- yield* Effect.logDebug("Stopping singleton", address);
1180
- yield* ActiveTeardown.aroundShard(address.shardId, FiberMap.remove(singletonFibers, address));
1290
+ if (!stopping) yield* Effect.logDebug("Stopping singleton", address);
1291
+ stops.push(stopSingletonUnsafe(address, true));
1181
1292
  } else if (!running && shouldBeRunning) {
1182
1293
  yield* Effect.logDebug("Starting singleton", address);
1183
1294
  yield* FiberMap.run(singletonFibers, address, run);
@@ -1185,6 +1296,12 @@ const make = /*#__PURE__*/Effect.gen(function* () {
1185
1296
  }
1186
1297
  }
1187
1298
  ClusterMetrics.singletons.updateUnsafe(BigInt(yield* FiberMap.size(singletonFibers)), Context.empty());
1299
+ return stops;
1300
+ }));
1301
+ const syncSingletons = Effect.flatMap(startSyncSingletons,
1302
+ // wait for the stopped singletons without the lock
1303
+ stops => Effect.all(stops, {
1304
+ discard: true
1188
1305
  }));
1189
1306
  // --- Entities ---
1190
1307
  const reaper = yield* EntityReaper;
@@ -1283,11 +1400,13 @@ const make = /*#__PURE__*/Effect.gen(function* () {
1283
1400
  }
1284
1401
  if (isShutdown.current) return;
1285
1402
  MutableRef.set(isShutdown, true);
1403
+ yield* notifyShardLockWaiters;
1286
1404
  if (selfRunner) {
1287
- yield* Effect.ignore(runnerStorage.unregister(selfRunner.address));
1405
+ yield* Effect.ignore(withRegistrationLock(runnerStorage.unregister(selfRunner.address)));
1288
1406
  }
1289
1407
  });
1290
- yield* Scope.addFinalizerExit(shardingScope, shutdown);
1408
+ // hand the shards off before the loops stop
1409
+ yield* Scope.addFinalizerExit(shardingScope, exit => Effect.andThen(shutdown(exit), handoff));
1291
1410
  const activeEntityCount = Effect.gen(function* () {
1292
1411
  let count = 0;
1293
1412
  for (const state of entityManagers.values()) {