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
@@ -86,6 +86,10 @@ export interface Residency {
86
86
  readonly releaseUnsafe: () => void
87
87
  }
88
88
 
89
+ interface RequestTransaction {
90
+ settle?: ((outcome: Exit.Exit<unknown, unknown>) => Effect.Effect<void>) | undefined
91
+ }
92
+
89
93
  // Represents the entities managed by this entity manager
90
94
  /**
91
95
  * @internal
@@ -100,10 +104,13 @@ export type EntityState = {
100
104
  /** Excludes requests awaiting their first dispatch from replay. */
101
105
  delivered: boolean
102
106
  sentExit: boolean
107
+ /** Treat early termination interrupts like shutdown interrupts. */
108
+ terminating?: boolean | undefined
103
109
  lastSentChunk: Option.Option<Reply.Chunk<Rpc.Any>>
104
110
  sequence: number
105
111
  /** Set when the request should not outlive its caller. */
106
112
  callerScope?: Scope.Scope | undefined
113
+ transaction?: RequestTransaction | undefined
107
114
  }>
108
115
  lastActiveCheck: number
109
116
  write: RpcServer.RpcServer<any>["write"]
@@ -152,9 +159,13 @@ export const make = Effect.fnUntraced(function*<
152
159
  entityRpcs.set(KeepAliveRpc._tag, KeepAliveRpc as any)
153
160
 
154
161
  const activeServers = new Map<EntityId, EntityState>()
162
+ // Entities finishing their in-flight requests after removal, which still
163
+ // accept interrupts and acks
164
+ const drainingServers = new Map<EntityId, EntityState>()
155
165
  const serverCloseLatches = new Map<EntityAddress, {
156
166
  readonly closed: Latch.Latch
157
167
  readonly force: Latch.Latch
168
+ closing: boolean
158
169
  }>()
159
170
  const processedRequestIds = new Set<Snowflake.Snowflake>()
160
171
 
@@ -181,7 +192,8 @@ export const make = Effect.fnUntraced(function*<
181
192
  const keepAliveLatch = Latch.makeUnsafe()
182
193
  const closeLatches = {
183
194
  closed: Latch.makeUnsafe(),
184
- force: Latch.makeUnsafe()
195
+ force: Latch.makeUnsafe(),
196
+ closing: false
185
197
  }
186
198
 
187
199
  yield* Scope.addFinalizer(
@@ -264,22 +276,23 @@ export const make = Effect.fnUntraced(function*<
264
276
  // They will be retried when the entity is restarted.
265
277
  // Also, if the request is uninterruptible, we ignore the
266
278
  // interrupt.
279
+ const persisted = storageEnabled && Context.get(request.message.annotations, Persisted)
267
280
  if (
268
- storageEnabled &&
269
- Context.get(request.message.annotations, Persisted) &&
281
+ persisted &&
270
282
  Exit.hasInterrupts(response.exit) &&
271
- (isShuttingDown || isUninterruptibleForServer(request.message.annotations))
283
+ (isShuttingDown || request.terminating || isUninterruptibleForServer(request.message.annotations))
272
284
  ) {
273
- if (!isShuttingDown) {
285
+ if (!isShuttingDown && !request.terminating) {
274
286
  request.sentExit = false
275
287
  return server.write(0, requestEnvelope(request), requestWriteOptions(request)).pipe(
288
+ Effect.setContext(handlerContext),
276
289
  Effect.forkIn(handlerScope)
277
290
  )
278
291
  }
279
292
  activeRequests.delete(Snowflake.Snowflake(response.requestId))
280
293
  return options.storage.unregisterReplyHandler(request.message.envelope.requestId)
281
294
  }
282
- return retryRespond(
295
+ const save = retryRespond(
283
296
  4,
284
297
  Effect.suspend(() =>
285
298
  request.message.respond(
@@ -290,22 +303,53 @@ export const make = Effect.fnUntraced(function*<
290
303
  })
291
304
  )
292
305
  )
293
- ).pipe(
294
- Effect.flatMap(() => {
295
- if (storageEnabled) {
296
- processedRequestIds.add(request.message.envelope.requestId)
297
- }
298
- activeRequests.delete(Snowflake.Snowflake(response.requestId))
306
+ )
307
+ const complete = Effect.sync(() => {
308
+ if (storageEnabled) {
309
+ processedRequestIds.add(request.message.envelope.requestId)
310
+ }
311
+ activeRequests.delete(Snowflake.Snowflake(response.requestId))
299
312
 
300
- // Start the idle timer when the last request completes.
301
- if (activeRequests.size === 0) {
302
- state.lastActiveCheck = clock.currentTimeMillisUnsafe()
303
- }
313
+ // Start the idle timer when the last request completes.
314
+ if (activeRequests.size === 0) {
315
+ state.lastActiveCheck = clock.currentTimeMillisUnsafe()
316
+ }
317
+ })
318
+ const respond = Effect.orDie(Effect.andThen(save, complete))
319
+ const transaction = request.transaction
320
+ if (!transaction) return respond
321
+ const exit = response.exit
322
+
323
+ if (!persisted) {
324
+ if (Exit.isSuccess(exit)) return respond
325
+ transaction.settle = () => respond
326
+ return Effect.void
327
+ }
304
328
 
305
- return Effect.void
306
- }),
307
- Effect.orDie
308
- )
329
+ if (Exit.isSuccess(exit)) {
330
+ transaction.settle = (outcome) =>
331
+ Exit.isSuccess(outcome) ? complete : Effect.flatMap(
332
+ hasStoredExit(request.message.envelope.requestId),
333
+ (committed) => {
334
+ if (!committed) return restartFrom(outcome.cause)
335
+ // Commit succeeded despite the error; let callers read the saved reply.
336
+ return Effect.andThen(
337
+ complete,
338
+ options.storage.unregisterReplyHandler(request.message.envelope.requestId)
339
+ )
340
+ }
341
+ )
342
+ return Effect.orDie(save)
343
+ }
344
+ transaction.settle = (outcome) =>
345
+ // Transaction wrappers can add defects, but not typed errors; interrupts may be repeated.
346
+ Exit.isSuccess(outcome) || !outcome.cause.reasons.some((reason) =>
347
+ Cause.isDieReason(reason) &&
348
+ !exit.cause.reasons.some((r) => Cause.isDieReason(r) && r.defect === reason.defect)
349
+ )
350
+ ? respond
351
+ : restartFrom(outcome.cause)
352
+ return Effect.void
309
353
  }
310
354
  case "Chunk": {
311
355
  const request = activeRequests.get(Snowflake.Snowflake(response.requestId))
@@ -330,10 +374,7 @@ export const make = Effect.fnUntraced(function*<
330
374
  ))
331
375
  }
332
376
  case "Defect": {
333
- if (!isActive()) return endLatch.open
334
- const rebuild = writeRef.rebuildUnsafe({ from: server.write, prepare: replay })
335
- if (!rebuild) return Effect.void
336
- return Effect.forkIn(restart(Cause.die(response.defect), rebuild), managerScope)
377
+ return restartFrom(Cause.die(response.defect))
337
378
  }
338
379
  case "ClientEnd": {
339
380
  return endLatch.open
@@ -345,6 +386,21 @@ export const make = Effect.fnUntraced(function*<
345
386
  Effect.setContext(Context.merge(handlerContext, handlers))
346
387
  )
347
388
 
389
+ // Replay if the stored reply cannot be read.
390
+ const hasStoredExit = (requestId: Snowflake.Snowflake): Effect.Effect<boolean> =>
391
+ options.storage.repliesForUnfiltered([requestId]).pipe(
392
+ Effect.map((replies) => replies.some((reply) => reply._tag === "WithExit")),
393
+ Effect.catchCause(() => Effect.succeed(false))
394
+ )
395
+
396
+ // Do not inherit the failed handler's transaction context.
397
+ const restartFrom = (cause: Cause.Cause<unknown>): Effect.Effect<void> => {
398
+ if (!isActive()) return endLatch.open
399
+ const rebuild = writeRef.rebuildUnsafe({ from: server.write, prepare: replay })
400
+ if (!rebuild) return Effect.void
401
+ return Effect.forkIn(Effect.setContext(restart(cause, rebuild), handlerContext), managerScope)
402
+ }
403
+
348
404
  yield* Scope.addFinalizer(
349
405
  handlerScope,
350
406
  Effect.sync(() => {
@@ -415,15 +471,32 @@ export const make = Effect.fnUntraced(function*<
415
471
  Effect.suspend(() => {
416
472
  activeServers.delete(address.entityId)
417
473
  retired.openUnsafe()
474
+ closeLatches.closing = true
418
475
  acquireEntity(address)
476
+ drainingServers.set(address.entityId, state)
419
477
  return Effect.raceFirst(
420
- state.write(0, { _tag: "Eof" }).pipe(
478
+ Effect.gen(function*() {
479
+ for (const [requestId, request] of activeRequests) {
480
+ if (
481
+ !request.delivered || request.sentExit ||
482
+ !Context.get(request.message.annotations, ClusterSchema.InterruptOnTermination) ||
483
+ isUninterruptibleForServer(request.message.annotations)
484
+ ) continue
485
+ request.terminating = true
486
+ yield* state.write(0, { _tag: "Interrupt", requestId: requestId as any, interruptors: [] })
487
+ }
488
+ yield* state.write(0, { _tag: "Eof" })
489
+ }).pipe(
421
490
  Effect.andThen(endLatch.await),
422
491
  Effect.timeoutOption(config.entityTerminationTimeout),
423
492
  Effect.interruptible
424
493
  ),
425
494
  Effect.interruptible(closeLatches.force.await)
426
- )
495
+ ).pipe(Effect.ensuring(Effect.sync(() => {
496
+ if (drainingServers.get(address.entityId) === state) {
497
+ drainingServers.delete(address.entityId)
498
+ }
499
+ })))
427
500
  })
428
501
  )
429
502
  if (!options.sharding.hasShardId(address.shardId)) {
@@ -458,9 +531,38 @@ export const make = Effect.fnUntraced(function*<
458
531
  Effect.forkIn(managerScope)
459
532
  )
460
533
 
534
+ function sendEnvelope(server: EntityState, message: Message.IncomingEnvelope): Effect.Effect<void> {
535
+ const entry = server.activeRequests.get(message.envelope.requestId)
536
+ if (!entry) {
537
+ return Effect.void
538
+ } else if (
539
+ message.envelope._tag === "AckChunk" &&
540
+ Option.isSome(entry.lastSentChunk) &&
541
+ message.envelope.replyId !== entry.lastSentChunk.value.id
542
+ ) {
543
+ return Effect.void
544
+ }
545
+ return server.write(
546
+ 0,
547
+ message.envelope._tag === "AckChunk"
548
+ ? { _tag: "Ack", requestId: message.envelope.requestId as any }
549
+ : {
550
+ _tag: "Interrupt",
551
+ requestId: message.envelope.requestId as any,
552
+ interruptors: []
553
+ }
554
+ )
555
+ }
556
+
461
557
  function sendLocal<R extends Rpc.Any>(
462
558
  message: Message.IncomingLocal<R>
463
559
  ): Effect.Effect<void, EntityNotAssignedToRunner | MailboxFull | AlreadyProcessingMessage> {
560
+ if (message._tag === "IncomingEnvelope") {
561
+ const draining = drainingServers.get(message.envelope.address.entityId)
562
+ if (draining?.activeRequests.has(message.envelope.requestId)) {
563
+ return sendEnvelope(draining, message)
564
+ }
565
+ }
464
566
  return Effect.provideService(
465
567
  Effect.flatMap(
466
568
  entities.get(message.envelope.address),
@@ -557,26 +659,7 @@ export const make = Effect.fnUntraced(function*<
557
659
  return server.write(0, requestEnvelope(entry), requestWriteOptions(entry))
558
660
  }
559
661
  case "IncomingEnvelope": {
560
- const entry = server.activeRequests.get(message.envelope.requestId)
561
- if (!entry) {
562
- return Effect.void
563
- } else if (
564
- message.envelope._tag === "AckChunk" &&
565
- Option.isSome(entry.lastSentChunk) &&
566
- message.envelope.replyId !== entry.lastSentChunk.value.id
567
- ) {
568
- return Effect.void
569
- }
570
- return server.write(
571
- 0,
572
- message.envelope._tag === "AckChunk"
573
- ? { _tag: "Ack", requestId: message.envelope.requestId as any }
574
- : {
575
- _tag: "Interrupt",
576
- requestId: message.envelope.requestId as any,
577
- interruptors: []
578
- }
579
- )
662
+ return sendEnvelope(server, message)
580
663
  }
581
664
  }
582
665
  }
@@ -596,11 +679,24 @@ export const make = Effect.fnUntraced(function*<
596
679
  })
597
680
 
598
681
  const requestWriteOptions = (
599
- entry: { readonly message: Message.IncomingRequestLocal<any>; readonly callerScope?: Scope.Scope | undefined }
682
+ entry: {
683
+ readonly message: Message.IncomingRequestLocal<any>
684
+ readonly callerScope?: Scope.Scope | undefined
685
+ transaction?: RequestTransaction | undefined
686
+ }
600
687
  ): Parameters<EntityState["write"]>[2] => {
601
- const onTransaction = Context.get(entry.message.annotations, WithTransaction)
602
- ? options.storage.withTransaction
603
- : undefined
688
+ const onTransaction = !Context.get(entry.message.annotations, WithTransaction)
689
+ ? undefined
690
+ : <A, E, R>(effect: Effect.Effect<A, E, R>) =>
691
+ // Interrupts during commit must not hide the outcome from settle.
692
+ Effect.uninterruptibleMask((restore) => {
693
+ const transaction: RequestTransaction = {}
694
+ entry.transaction = transaction
695
+ return Effect.onExit(options.storage.withTransaction(restore(effect)), (outcome) => {
696
+ if (entry.transaction === transaction) entry.transaction = undefined
697
+ return transaction.settle ? transaction.settle(outcome) : Effect.void
698
+ })
699
+ })
604
700
  const onCaller = entry.callerScope && bindToCaller(entry.callerScope)
605
701
  if (!onCaller) return onTransaction && { onRequest: onTransaction }
606
702
  return { onRequest: onTransaction ? (effect) => onCaller(onTransaction(effect)) : onCaller }
@@ -621,13 +717,13 @@ export const make = Effect.fnUntraced(function*<
621
717
  }
622
718
  })
623
719
  }
624
- activeServers.forEach((state) => {
625
- if (shardId[Equal.symbol](state.address.shardId)) {
626
- fibers.push(runFork(entities.removeIgnore(state.address)))
627
- }
628
- })
720
+ // Look entities up by address: `activeServers` is keyed by entity id,
721
+ // so an entity with the same id on another shard can hide this one.
629
722
  serverCloseLatches.forEach((latches, address) => {
630
723
  if (shardId[Equal.symbol](address.shardId)) {
724
+ if (!latches.closing) {
725
+ fibers.push(runFork(entities.removeIgnore(address)))
726
+ }
631
727
  fibers.push(runFork(latches.closed.await))
632
728
  }
633
729
  })
@@ -338,13 +338,6 @@ class RouterImpl<A> implements Router.Router<A> {
338
338
  path = path.replace(FULL_PATH_REGEXP, "/")
339
339
  }
340
340
 
341
- // This must be run before sanitizeUrl as the resulting function
342
- // .sliceParameter must be constructed with same URL string used
343
- // throughout the rest of this function.
344
- if (this.options.ignoreDuplicateSlashes) {
345
- path = removeDuplicateSlashes(path)
346
- }
347
-
348
341
  let sanitizedUrl
349
342
  try {
350
343
  sanitizedUrl = safeDecodeURI(path)
@@ -355,6 +348,10 @@ class RouterImpl<A> implements Router.Router<A> {
355
348
  return undefined
356
349
  }
357
350
 
351
+ if (this.options.ignoreDuplicateSlashes) {
352
+ path = removeDuplicateSlashes(path)
353
+ }
354
+
358
355
  if (this.options.ignoreTrailingSlash) {
359
356
  path = trimLastSlash(path)
360
357
  }
@@ -1466,6 +1466,8 @@ export declare namespace WithRateLimiter {
1466
1466
  * It can update limits by inspecting common rate limit response headers and
1467
1467
  * automatically retries HTTP `429` responses (or `HttpClientError` values
1468
1468
  * wrapping a `429` response) by forcing the retry back through the limiter.
1469
+ * When a response reports the remaining budget, requests wait for the
1470
+ * reported reset once that budget is exhausted.
1469
1471
  *
1470
1472
  * **Gotchas**
1471
1473
  *
@@ -1486,6 +1488,8 @@ export const withRateLimiter: {
1486
1488
  * It can update limits by inspecting common rate limit response headers and
1487
1489
  * automatically retries HTTP `429` responses (or `HttpClientError` values
1488
1490
  * wrapping a `429` response) by forcing the retry back through the limiter.
1491
+ * When a response reports the remaining budget, requests wait for the
1492
+ * reported reset once that budget is exhausted.
1489
1493
  *
1490
1494
  * **Gotchas**
1491
1495
  *
@@ -1508,6 +1512,8 @@ export const withRateLimiter: {
1508
1512
  * It can update limits by inspecting common rate limit response headers and
1509
1513
  * automatically retries HTTP `429` responses (or `HttpClientError` values
1510
1514
  * wrapping a `429` response) by forcing the retry back through the limiter.
1515
+ * When a response reports the remaining budget, requests wait for the
1516
+ * reported reset once that budget is exhausted.
1511
1517
  *
1512
1518
  * **Gotchas**
1513
1519
  *
@@ -1551,14 +1557,36 @@ export const withRateLimiter: {
1551
1557
  return initialState
1552
1558
  }
1553
1559
 
1560
+ const budgets = new Map<string, RateLimitBudget>()
1561
+ const getBudget = (key: string): RateLimitBudget => {
1562
+ let budget = budgets.get(key)
1563
+ if (budget === undefined) {
1564
+ budget = { sent: 0, observed: 0, remaining: 0, resetAt: 0 }
1565
+ budgets.set(key, budget)
1566
+ }
1567
+ return budget
1568
+ }
1569
+
1554
1570
  const onResponse = options.disableResponseInspection
1555
1571
  ? undefined
1556
- : (clock: Clock, key: string, headers: Headers.Headers, tokens: number) => {
1572
+ : (clock: Clock, key: string, headers: Headers.Headers, tokens: number, sent: number) => {
1557
1573
  const current = getState(key)
1558
1574
  const next = parseRateLimiterState(current, clock, headers, tokens, headerNames)
1559
1575
  if (next.limit !== current.limit || !Duration.equals(next.window, current.window)) {
1560
1576
  states.set(key, next)
1561
1577
  }
1578
+ const remaining = parseRateLimitRemaining(headers, headerNames)
1579
+ const budget = getBudget(key)
1580
+ // responses to earlier requests carry stale counts
1581
+ if (remaining === undefined || sent <= budget.observed) {
1582
+ return
1583
+ }
1584
+ const resetAfter = parseRateLimitWindow(clock, headers, headerNames)
1585
+ budget.observed = sent
1586
+ budget.remaining = remaining
1587
+ if (resetAfter !== undefined) {
1588
+ budget.resetAt = clock.currentTimeMillisUnsafe() + Duration.toMillis(resetAfter)
1589
+ }
1562
1590
  }
1563
1591
 
1564
1592
  return transform(self, function loop(effect, request, retries = 0): Effect.Effect<
@@ -1570,6 +1598,15 @@ export const withRateLimiter: {
1570
1598
  const clock = fiber.getRef(Clock)
1571
1599
  const key = resolveKey(request)
1572
1600
  const tokens = Math.max(resolveTokens(request), 1)
1601
+ const budget = getBudget(key)
1602
+ const now = clock.currentTimeMillisUnsafe()
1603
+ // requests admitted after the observed one are not reflected in its count
1604
+ if (budget.resetAt > now && budget.remaining - (budget.sent - budget.observed) < tokens) {
1605
+ return Effect.flatMap(Effect.sleep(budget.resetAt - now), () => loop(effect, request, retries))
1606
+ }
1607
+ // counted at admission, so requests already admitted are sent even if a
1608
+ // later response exhausts the budget
1609
+ const sent = budget.sent += tokens
1573
1610
  const current = getState(key)
1574
1611
  const canRetry = options.times === undefined || retries < options.times
1575
1612
  function retry(retryAfter: Duration.Duration | undefined) {
@@ -1581,7 +1618,7 @@ export const withRateLimiter: {
1581
1618
  response: HttpClientResponse.HttpClientResponse,
1582
1619
  adaptive: RateLimiter.AdaptiveConsumeResult | undefined
1583
1620
  ) => {
1584
- onResponse?.(clock, key, response.headers, tokens)
1621
+ onResponse?.(clock, key, response.headers, tokens, sent)
1585
1622
  if (options.disableResponseInspection || response.status !== 429) {
1586
1623
  return Effect.succeed</**
1587
1624
  * Applies request rate limiting using the `RateLimiter` service.
@@ -1591,6 +1628,8 @@ export const withRateLimiter: {
1591
1628
  * It can update limits by inspecting common rate limit response headers and
1592
1629
  * automatically retries HTTP `429` responses (or `HttpClientError` values
1593
1630
  * wrapping a `429` response) by forcing the retry back through the limiter.
1631
+ * When a response reports the remaining budget, requests wait for the
1632
+ * reported reset once that budget is exhausted.
1594
1633
  *
1595
1634
  * **Gotchas**
1596
1635
  *
@@ -1614,6 +1653,8 @@ export const withRateLimiter: {
1614
1653
  * It can update limits by inspecting common rate limit response headers and
1615
1654
  * automatically retries HTTP `429` responses (or `HttpClientError` values
1616
1655
  * wrapping a `429` response) by forcing the retry back through the limiter.
1656
+ * When a response reports the remaining budget, requests wait for the
1657
+ * reported reset once that budget is exhausted.
1617
1658
  *
1618
1659
  * **Gotchas**
1619
1660
  *
@@ -1637,6 +1678,8 @@ export const withRateLimiter: {
1637
1678
  * It can update limits by inspecting common rate limit response headers and
1638
1679
  * automatically retries HTTP `429` responses (or `HttpClientError` values
1639
1680
  * wrapping a `429` response) by forcing the retry back through the limiter.
1681
+ * When a response reports the remaining budget, requests wait for the
1682
+ * reported reset once that budget is exhausted.
1640
1683
  *
1641
1684
  * **Gotchas**
1642
1685
  *
@@ -1756,6 +1799,17 @@ interface RateLimiterState {
1756
1799
  readonly initial: boolean
1757
1800
  }
1758
1801
 
1802
+ interface RateLimitBudget {
1803
+ /** Total tokens admitted for the key. */
1804
+ sent: number
1805
+ /** The `sent` total of the request whose remaining count was last applied. */
1806
+ observed: number
1807
+ /** The remaining count reported in response to the `observed` request. */
1808
+ remaining: number
1809
+ /** When the server budget resets, in epoch milliseconds. */
1810
+ resetAt: number
1811
+ }
1812
+
1759
1813
  const parseRateLimiterState = (
1760
1814
  state: RateLimiterState,
1761
1815
  clock: Clock,
@@ -93,7 +93,7 @@ export const b3: FromHeaders = (headers) => {
93
93
  return Option.none()
94
94
  }
95
95
  const parts = headers["b3"].split("-")
96
- if (parts.length < 2) {
96
+ if (parts.length < 2 || isB3Ids(parts[0], parts[1]) === false) {
97
97
  return Option.none()
98
98
  }
99
99
  return Option.some(Tracer.externalSpan({
@@ -116,7 +116,10 @@ export const b3: FromHeaders = (headers) => {
116
116
  * @since 4.0.0
117
117
  */
118
118
  export const xb3: FromHeaders = (headers) => {
119
- if (!(headers["x-b3-traceid"]) || !(headers["x-b3-spanid"])) {
119
+ if (
120
+ !(headers["x-b3-traceid"]) || !(headers["x-b3-spanid"]) ||
121
+ isB3Ids(headers["x-b3-traceid"], headers["x-b3-spanid"]) === false
122
+ ) {
120
123
  return Option.none()
121
124
  }
122
125
  return Option.some(Tracer.externalSpan({
@@ -130,6 +133,9 @@ export const xb3: FromHeaders = (headers) => {
130
133
  const w3cTraceId = /^[0-9a-f]{32}$/i
131
134
  const w3cSpanId = /^[0-9a-f]{16}$/i
132
135
 
136
+ const b3TraceId = /^(?:[0-9a-f]{16}|[0-9a-f]{32})$/i
137
+ const isB3Ids = (traceId: string, spanId: string): boolean => b3TraceId.test(traceId) && w3cSpanId.test(spanId)
138
+
133
139
  /**
134
140
  * Decodes an external span safely from the W3C `traceparent` header.
135
141
  *
@@ -1507,6 +1507,7 @@ const succeedPayload = function(this: { readonly payload: any }, _value: unknown
1507
1507
  // V8 includes the property name of a stored continuation in its stack trace.
1508
1508
  // Other engines need an explicit frame for the stack cleaner to cut at.
1509
1509
  const continuationMarksStack = (() => {
1510
+ if (getStackTraceLimit() === 0) return false
1510
1511
  const marker = "~effect/Effect/stackProbe"
1511
1512
  const probe = {
1512
1513
  [marker]: function stackProbe() {
@@ -4453,7 +4454,7 @@ export const cachedInvalidateWithTTL: {
4453
4454
  const infiniteTTL = constant(Infinity)
4454
4455
 
4455
4456
  interface CachedRun<A, E> {
4456
- fiber: FiberImpl<A, E> | undefined
4457
+ readonly fiber: FiberImpl<A, E>
4457
4458
  awaiters: number
4458
4459
  }
4459
4460
 
@@ -4470,17 +4471,19 @@ const makeCachedUnsafe = <A, E, R>(
4470
4471
  onExitUnsafe(fiber, () => {
4471
4472
  // Abandon the run once every caller has left, unless it already finished.
4472
4473
  if (--run.awaiters > 0 || current !== run) return
4473
- // Detach it first so new callers start a fresh run instead of joining
4474
- // one that is being interrupted.
4474
+ // Detach before interruption so new callers start a fresh run.
4475
4475
  current = undefined
4476
- return fiberInterrupt(run.fiber!)
4476
+ return fiberInterrupt(run.fiber)
4477
4477
  })
4478
- return fiberJoin(run.fiber!)
4478
+ return fiberJoin(run.fiber)
4479
4479
  }
4480
4480
 
4481
4481
  return [
4482
4482
  withFiber((fiber) => {
4483
- if (current !== undefined) return join(fiber, current)
4483
+ if (current !== undefined) {
4484
+ // A run joining itself is not an awaiter, so external callers can still abandon it.
4485
+ return fiber === current.fiber ? fiberJoin(current.fiber) : join(fiber, current)
4486
+ }
4484
4487
  if (
4485
4488
  exit !== undefined &&
4486
4489
  (expiresAt === Infinity || fiber.getRef(ClockRef).currentTimeMillisUnsafe() < expiresAt)
@@ -4489,10 +4492,11 @@ const makeCachedUnsafe = <A, E, R>(
4489
4492
  }
4490
4493
  exit = undefined
4491
4494
  const clock = fiber.getRef(ClockRef)
4492
- const run: CachedRun<A, E> = { fiber: undefined, awaiters: 0 }
4495
+ const run: CachedRun<A, E> = { fiber: new FiberImpl(fiber.context), awaiters: 0 }
4493
4496
  current = run
4494
- run.fiber = forkUnsafe(
4495
- fiber,
4497
+ // Join before evaluating, which may synchronously re-enter and interrupt other awaiters.
4498
+ const joined = join(fiber, run)
4499
+ run.fiber.evaluate(
4496
4500
  onExitPrimitive(self, (exit_) => {
4497
4501
  // An abandoned run must not overwrite or clear a replacement run.
4498
4502
  if (current !== run) return
@@ -4502,11 +4506,9 @@ const makeCachedUnsafe = <A, E, R>(
4502
4506
  const duration = ttlMillis(exit_)
4503
4507
  expiresAt = duration === Infinity ? Infinity : clock.currentTimeMillisUnsafe() + duration
4504
4508
  exit = exit_
4505
- }),
4506
- true,
4507
- true
4509
+ }) as any
4508
4510
  )
4509
- return run.fiber._exit ?? join(fiber, run)
4511
+ return run.fiber._exit ?? joined
4510
4512
  }),
4511
4513
  sync(() => {
4512
4514
  exit = undefined
@@ -1,2 +1,2 @@
1
1
  /** @internal */
2
- export const version = "4.0.2"
2
+ export const version = "4.0.3"