effect 4.0.0 → 4.0.1

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 (148) hide show
  1. package/dist/Cache.d.ts.map +1 -1
  2. package/dist/Cache.js +20 -16
  3. package/dist/Cache.js.map +1 -1
  4. package/dist/Cause.d.ts.map +1 -1
  5. package/dist/Cause.js +1 -1
  6. package/dist/Cause.js.map +1 -1
  7. package/dist/Effect.d.ts +150 -22
  8. package/dist/Effect.d.ts.map +1 -1
  9. package/dist/Effect.js +11 -5
  10. package/dist/Effect.js.map +1 -1
  11. package/dist/HashRing.d.ts.map +1 -1
  12. package/dist/HashRing.js +13 -5
  13. package/dist/HashRing.js.map +1 -1
  14. package/dist/Pool.d.ts +9 -5
  15. package/dist/Pool.d.ts.map +1 -1
  16. package/dist/Pool.js +34 -41
  17. package/dist/Pool.js.map +1 -1
  18. package/dist/RcMap.d.ts.map +1 -1
  19. package/dist/RcMap.js +16 -1
  20. package/dist/RcMap.js.map +1 -1
  21. package/dist/ScopedCache.d.ts.map +1 -1
  22. package/dist/ScopedCache.js +24 -16
  23. package/dist/ScopedCache.js.map +1 -1
  24. package/dist/Stream.d.ts +45 -6
  25. package/dist/Stream.d.ts.map +1 -1
  26. package/dist/Stream.js +55 -44
  27. package/dist/Stream.js.map +1 -1
  28. package/dist/ai/LanguageModel.d.ts +8 -0
  29. package/dist/ai/LanguageModel.d.ts.map +1 -1
  30. package/dist/ai/LanguageModel.js +13 -2
  31. package/dist/ai/LanguageModel.js.map +1 -1
  32. package/dist/ai/McpSchema.d.ts +508 -508
  33. package/dist/ai/McpSchema.d.ts.map +1 -1
  34. package/dist/ai/McpSchema.js +1 -1
  35. package/dist/ai/McpSchema.js.map +1 -1
  36. package/dist/ai/McpServer.d.ts.map +1 -1
  37. package/dist/ai/McpServer.js +11 -3
  38. package/dist/ai/McpServer.js.map +1 -1
  39. package/dist/ai/Response.d.ts.map +1 -1
  40. package/dist/ai/Response.js +22 -12
  41. package/dist/ai/Response.js.map +1 -1
  42. package/dist/ai/internal/mcpProtocol/v2024_11_05.js +1 -0
  43. package/dist/ai/internal/mcpProtocol/v2024_11_05.js.map +1 -1
  44. package/dist/ai/internal/mcpProtocol/v2025_03_26.js +1 -0
  45. package/dist/ai/internal/mcpProtocol/v2025_03_26.js.map +1 -1
  46. package/dist/ai/internal/mcpProtocol/v2025_06_18.js +1 -0
  47. package/dist/ai/internal/mcpProtocol/v2025_06_18.js.map +1 -1
  48. package/dist/ai/internal/mcpProtocol/v2025_11_25.js +1 -0
  49. package/dist/ai/internal/mcpProtocol/v2025_11_25.js.map +1 -1
  50. package/dist/ai/internal/mcpSchema/v2024_11_05.js +3 -1
  51. package/dist/ai/internal/mcpSchema/v2024_11_05.js.map +1 -1
  52. package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
  53. package/dist/cluster/ClusterWorkflowEngine.js +11 -2
  54. package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
  55. package/dist/cluster/Sharding.d.ts.map +1 -1
  56. package/dist/cluster/Sharding.js +14 -7
  57. package/dist/cluster/Sharding.js.map +1 -1
  58. package/dist/cluster/SqlMessageStorage.d.ts.map +1 -1
  59. package/dist/cluster/SqlMessageStorage.js +21 -12
  60. package/dist/cluster/SqlMessageStorage.js.map +1 -1
  61. package/dist/cluster/SqlRunnerStorage.d.ts.map +1 -1
  62. package/dist/cluster/SqlRunnerStorage.js +30 -44
  63. package/dist/cluster/SqlRunnerStorage.js.map +1 -1
  64. package/dist/cluster/internal/entityManager.js +63 -62
  65. package/dist/cluster/internal/entityManager.js.map +1 -1
  66. package/dist/cluster/internal/interruptors.js +8 -0
  67. package/dist/cluster/internal/interruptors.js.map +1 -1
  68. package/dist/cluster/internal/resourceMap.js +39 -14
  69. package/dist/cluster/internal/resourceMap.js.map +1 -1
  70. package/dist/cluster/internal/resourceRef.js +168 -87
  71. package/dist/cluster/internal/resourceRef.js.map +1 -1
  72. package/dist/http/HttpMiddleware.d.ts.map +1 -1
  73. package/dist/http/HttpMiddleware.js +3 -2
  74. package/dist/http/HttpMiddleware.js.map +1 -1
  75. package/dist/http/HttpStaticServer.d.ts.map +1 -1
  76. package/dist/http/HttpStaticServer.js +14 -0
  77. package/dist/http/HttpStaticServer.js.map +1 -1
  78. package/dist/http/HttpTraceContext.d.ts +1 -1
  79. package/dist/http/HttpTraceContext.d.ts.map +1 -1
  80. package/dist/http/HttpTraceContext.js +4 -4
  81. package/dist/http/HttpTraceContext.js.map +1 -1
  82. package/dist/http-api/HttpApiError.d.ts +1 -0
  83. package/dist/http-api/HttpApiError.d.ts.map +1 -1
  84. package/dist/http-api/HttpApiError.js +1 -0
  85. package/dist/http-api/HttpApiError.js.map +1 -1
  86. package/dist/internal/core.js +43 -2
  87. package/dist/internal/core.js.map +1 -1
  88. package/dist/internal/dateTime.js +1 -1
  89. package/dist/internal/dateTime.js.map +1 -1
  90. package/dist/internal/effect.js +63 -81
  91. package/dist/internal/effect.js.map +1 -1
  92. package/dist/internal/rcRef.js +59 -23
  93. package/dist/internal/rcRef.js.map +1 -1
  94. package/dist/internal/schema/fromJsonSchemaDocument.js +11 -8
  95. package/dist/internal/schema/fromJsonSchemaDocument.js.map +1 -1
  96. package/dist/rpc/RpcClient.d.ts.map +1 -1
  97. package/dist/rpc/RpcClient.js +20 -14
  98. package/dist/rpc/RpcClient.js.map +1 -1
  99. package/dist/rpc/RpcServer.d.ts.map +1 -1
  100. package/dist/rpc/RpcServer.js +2 -2
  101. package/dist/rpc/RpcServer.js.map +1 -1
  102. package/dist/rpc/Utils.d.ts +2 -3
  103. package/dist/rpc/Utils.d.ts.map +1 -1
  104. package/dist/rpc/Utils.js +3 -4
  105. package/dist/rpc/Utils.js.map +1 -1
  106. package/dist/workflow/WorkflowEngine.d.ts +2 -0
  107. package/dist/workflow/WorkflowEngine.d.ts.map +1 -1
  108. package/dist/workflow/WorkflowEngine.js +1 -0
  109. package/dist/workflow/WorkflowEngine.js.map +1 -1
  110. package/package.json +1 -1
  111. package/src/Cache.ts +22 -16
  112. package/src/Cause.ts +1 -1
  113. package/src/Effect.ts +153 -22
  114. package/src/HashRing.ts +14 -5
  115. package/src/Pool.ts +28 -36
  116. package/src/RcMap.ts +26 -2
  117. package/src/ScopedCache.ts +27 -16
  118. package/src/Stream.ts +158 -88
  119. package/src/ai/LanguageModel.ts +27 -2
  120. package/src/ai/McpSchema.ts +3 -3
  121. package/src/ai/McpServer.ts +12 -3
  122. package/src/ai/Response.ts +27 -12
  123. package/src/ai/internal/mcpProtocol/v2024_11_05.ts +1 -0
  124. package/src/ai/internal/mcpProtocol/v2025_03_26.ts +1 -0
  125. package/src/ai/internal/mcpProtocol/v2025_06_18.ts +1 -0
  126. package/src/ai/internal/mcpProtocol/v2025_11_25.ts +1 -0
  127. package/src/ai/internal/mcpSchema/v2024_11_05.ts +4 -2
  128. package/src/cluster/ClusterWorkflowEngine.ts +18 -4
  129. package/src/cluster/Sharding.ts +24 -24
  130. package/src/cluster/SqlMessageStorage.ts +26 -13
  131. package/src/cluster/SqlRunnerStorage.ts +85 -113
  132. package/src/cluster/internal/entityManager.ts +68 -80
  133. package/src/cluster/internal/interruptors.ts +13 -0
  134. package/src/cluster/internal/resourceMap.ts +55 -17
  135. package/src/cluster/internal/resourceRef.ts +189 -96
  136. package/src/http/HttpMiddleware.ts +8 -2
  137. package/src/http/HttpStaticServer.ts +16 -0
  138. package/src/http/HttpTraceContext.ts +8 -4
  139. package/src/http-api/HttpApiError.ts +1 -0
  140. package/src/internal/core.ts +56 -2
  141. package/src/internal/dateTime.ts +3 -1
  142. package/src/internal/effect.ts +109 -111
  143. package/src/internal/rcRef.ts +83 -33
  144. package/src/internal/schema/fromJsonSchemaDocument.ts +5 -3
  145. package/src/rpc/RpcClient.ts +26 -22
  146. package/src/rpc/RpcServer.ts +2 -1
  147. package/src/rpc/Utils.ts +4 -10
  148. package/src/workflow/WorkflowEngine.ts +4 -0
package/src/Effect.ts CHANGED
@@ -13909,6 +13909,11 @@ export const onExitFilter: {
13909
13909
  * evaluations of the same effect will return the cached result without
13910
13910
  * re-executing the logic.
13911
13911
  *
13912
+ * Concurrent callers share the pending computation, which is interrupted only
13913
+ * once every caller waiting on it has been interrupted. Interrupted
13914
+ * computations are never cached, so the next evaluation starts a fresh
13915
+ * computation.
13916
+ *
13912
13917
  * **Example** (Memoizing an effect until invalidated)
13913
13918
  *
13914
13919
  * ```ts import.meta.vitest
@@ -13970,11 +13975,12 @@ export const cached: <A, E, R>(self: Effect<A, E, R>) => Effect<Effect<A, E, R>>
13970
13975
  * `Duration.Input`. The function runs once after each fresh computation,
13971
13976
  * including failures, so successes and failures can have different TTLs. It
13972
13977
  * does not run when the cache is created or when a cached result is reused.
13973
- * The callback also receives interruption exits, which are cached for the
13974
- * returned duration.
13978
+ * Interrupted computations are never cached and do not call the function.
13975
13979
  *
13976
13980
  * The TTL starts when the computation completes. Concurrent callers share the
13977
- * pending computation. A zero TTL expires immediately, and an infinite TTL
13981
+ * pending computation, which is interrupted only once every caller waiting on
13982
+ * it has been interrupted. The next evaluation then starts a fresh
13983
+ * computation. A zero TTL expires immediately, and an infinite TTL
13978
13984
  * keeps the result indefinitely.
13979
13985
  *
13980
13986
  * **Example** (Memoizing an effect with TTL)
@@ -14054,11 +14060,12 @@ export const cachedWithTTL: {
14054
14060
  * `Duration.Input`. The function runs once after each fresh computation,
14055
14061
  * including failures, so successes and failures can have different TTLs. It
14056
14062
  * does not run when the cache is created or when a cached result is reused.
14057
- * The callback also receives interruption exits, which are cached for the
14058
- * returned duration.
14063
+ * Interrupted computations are never cached and do not call the function.
14059
14064
  *
14060
14065
  * The TTL starts when the computation completes. Concurrent callers share the
14061
- * pending computation. A zero TTL expires immediately, and an infinite TTL
14066
+ * pending computation, which is interrupted only once every caller waiting on
14067
+ * it has been interrupted. The next evaluation then starts a fresh
14068
+ * computation. A zero TTL expires immediately, and an infinite TTL
14062
14069
  * keeps the result indefinitely.
14063
14070
  *
14064
14071
  * **Example** (Memoizing an effect with TTL)
@@ -14138,11 +14145,12 @@ export const cachedWithTTL: {
14138
14145
  * `Duration.Input`. The function runs once after each fresh computation,
14139
14146
  * including failures, so successes and failures can have different TTLs. It
14140
14147
  * does not run when the cache is created or when a cached result is reused.
14141
- * The callback also receives interruption exits, which are cached for the
14142
- * returned duration.
14148
+ * Interrupted computations are never cached and do not call the function.
14143
14149
  *
14144
14150
  * The TTL starts when the computation completes. Concurrent callers share the
14145
- * pending computation. A zero TTL expires immediately, and an infinite TTL
14151
+ * pending computation, which is interrupted only once every caller waiting on
14152
+ * it has been interrupted. The next evaluation then starts a fresh
14153
+ * computation. A zero TTL expires immediately, and an infinite TTL
14146
14154
  * keeps the result indefinitely.
14147
14155
  *
14148
14156
  * **Example** (Memoizing an effect with TTL)
@@ -14222,11 +14230,12 @@ export const cachedWithTTL: {
14222
14230
  * `Duration.Input`. The function runs once after each fresh computation,
14223
14231
  * including failures, so successes and failures can have different TTLs. It
14224
14232
  * does not run when the cache is created or when a cached result is reused.
14225
- * The callback also receives interruption exits, which are cached for the
14226
- * returned duration.
14233
+ * Interrupted computations are never cached and do not call the function.
14227
14234
  *
14228
14235
  * The TTL starts when the computation completes. Concurrent callers share the
14229
- * pending computation. A zero TTL expires immediately, and an infinite TTL
14236
+ * pending computation, which is interrupted only once every caller waiting on
14237
+ * it has been interrupted. The next evaluation then starts a fresh
14238
+ * computation. A zero TTL expires immediately, and an infinite TTL
14230
14239
  * keeps the result indefinitely.
14231
14240
  *
14232
14241
  * **Example** (Memoizing an effect with TTL)
@@ -14306,11 +14315,12 @@ export const cachedWithTTL: {
14306
14315
  * `Duration.Input`. The function runs once after each fresh computation,
14307
14316
  * including failures, so successes and failures can have different TTLs. It
14308
14317
  * does not run when the cache is created or when a cached result is reused.
14309
- * The callback also receives interruption exits, which are cached for the
14310
- * returned duration.
14318
+ * Interrupted computations are never cached and do not call the function.
14311
14319
  *
14312
14320
  * The TTL starts when the computation completes. Concurrent callers share the
14313
- * pending computation. A zero TTL expires immediately, and an infinite TTL
14321
+ * pending computation, which is interrupted only once every caller waiting on
14322
+ * it has been interrupted. The next evaluation then starts a fresh
14323
+ * computation. A zero TTL expires immediately, and an infinite TTL
14314
14324
  * keeps the result indefinitely.
14315
14325
  *
14316
14326
  * **Example** (Memoizing an effect with TTL)
@@ -14371,8 +14381,8 @@ export const cachedWithTTL: {
14371
14381
  } = internal.cachedWithTTL
14372
14382
 
14373
14383
  /**
14374
- * Creates a cached effect result for a specified duration and allows manual
14375
- * invalidation before expiration.
14384
+ * Creates a cached effect result for a fixed duration or a duration computed
14385
+ * from its `Exit` and allows manual invalidation before expiration.
14376
14386
  *
14377
14387
  * **When to use**
14378
14388
  *
@@ -14430,8 +14440,67 @@ export const cachedWithTTL: {
14430
14440
  */
14431
14441
  export const cachedInvalidateWithTTL: {
14432
14442
  /**
14433
- * Creates a cached effect result for a specified duration and allows manual
14434
- * invalidation before expiration.
14443
+ * Creates a cached effect result for a fixed duration or a duration computed
14444
+ * from its `Exit` and allows manual invalidation before expiration.
14445
+ *
14446
+ * **When to use**
14447
+ *
14448
+ * Use when an effect result should be cached for a bounded time and callers
14449
+ * also need a manual invalidation effect to force recomputation before
14450
+ * expiration.
14451
+ *
14452
+ * **Details**
14453
+ *
14454
+ * This function behaves similarly to {@link cachedWithTTL} by caching the
14455
+ * result of an effect for a specified period of time. However, it introduces an
14456
+ * additional feature: it provides an effect that allows you to manually
14457
+ * invalidate the cached result before it naturally expires.
14458
+ *
14459
+ * This gives you more control over the cache, allowing you to refresh the
14460
+ * result when needed, even if the original cache has not yet expired.
14461
+ *
14462
+ * Once the cache is invalidated, the next time the effect is evaluated, the
14463
+ * result will be recomputed, and the cache will be refreshed.
14464
+ *
14465
+ * **Example** (Memoizing with TTL and invalidation)
14466
+ *
14467
+ * ```ts import.meta.vitest
14468
+ * import { Effect } from "effect"
14469
+ * const output: Array<unknown> = []
14470
+ * const record = (value: unknown) => Effect.sync(() => { output.push(value) })
14471
+ *
14472
+ * let i = 1
14473
+ * const expensiveTask = Effect.sync(() => {
14474
+ * void output.push("expensive task...")
14475
+ * return `result ${i++}`
14476
+ * })
14477
+ *
14478
+ * const program = Effect.gen(function*() {
14479
+ * const [cached, invalidate] = yield* Effect.cachedInvalidateWithTTL(
14480
+ * expensiveTask,
14481
+ * "1 hour"
14482
+ * )
14483
+ * yield* cached.pipe(Effect.andThen(record))
14484
+ * yield* cached.pipe(Effect.andThen(record))
14485
+ * yield* invalidate
14486
+ * yield* cached.pipe(Effect.andThen(record))
14487
+ * })
14488
+ *
14489
+ * Effect.runSync(program)
14490
+ * output // => ["expensive task...", "result 1", "result 1", "expensive task...", "result 2"]
14491
+ * ```
14492
+ *
14493
+ * @see {@link cached} for a similar function that caches the result
14494
+ * indefinitely.
14495
+ * @see {@link cachedWithTTL} for a similar function that caches the result for
14496
+ * a specified duration but does not include an effect for manual invalidation.
14497
+ * @category caching
14498
+ * @since 2.0.0
14499
+ */
14500
+ <A, E>(timeToLive: (exit: Exit.Exit<A, E>) => Duration.Input): <R>(self: Effect<A, E, R>) => Effect<[Effect<A, E, R>, Effect<void>]>
14501
+ /**
14502
+ * Creates a cached effect result for a fixed duration or a duration computed
14503
+ * from its `Exit` and allows manual invalidation before expiration.
14435
14504
  *
14436
14505
  * **When to use**
14437
14506
  *
@@ -14489,8 +14558,67 @@ export const cachedInvalidateWithTTL: {
14489
14558
  */
14490
14559
  (timeToLive: Duration.Input): <A, E, R>(self: Effect<A, E, R>) => Effect<[Effect<A, E, R>, Effect<void>]>
14491
14560
  /**
14492
- * Creates a cached effect result for a specified duration and allows manual
14493
- * invalidation before expiration.
14561
+ * Creates a cached effect result for a fixed duration or a duration computed
14562
+ * from its `Exit` and allows manual invalidation before expiration.
14563
+ *
14564
+ * **When to use**
14565
+ *
14566
+ * Use when an effect result should be cached for a bounded time and callers
14567
+ * also need a manual invalidation effect to force recomputation before
14568
+ * expiration.
14569
+ *
14570
+ * **Details**
14571
+ *
14572
+ * This function behaves similarly to {@link cachedWithTTL} by caching the
14573
+ * result of an effect for a specified period of time. However, it introduces an
14574
+ * additional feature: it provides an effect that allows you to manually
14575
+ * invalidate the cached result before it naturally expires.
14576
+ *
14577
+ * This gives you more control over the cache, allowing you to refresh the
14578
+ * result when needed, even if the original cache has not yet expired.
14579
+ *
14580
+ * Once the cache is invalidated, the next time the effect is evaluated, the
14581
+ * result will be recomputed, and the cache will be refreshed.
14582
+ *
14583
+ * **Example** (Memoizing with TTL and invalidation)
14584
+ *
14585
+ * ```ts import.meta.vitest
14586
+ * import { Effect } from "effect"
14587
+ * const output: Array<unknown> = []
14588
+ * const record = (value: unknown) => Effect.sync(() => { output.push(value) })
14589
+ *
14590
+ * let i = 1
14591
+ * const expensiveTask = Effect.sync(() => {
14592
+ * void output.push("expensive task...")
14593
+ * return `result ${i++}`
14594
+ * })
14595
+ *
14596
+ * const program = Effect.gen(function*() {
14597
+ * const [cached, invalidate] = yield* Effect.cachedInvalidateWithTTL(
14598
+ * expensiveTask,
14599
+ * "1 hour"
14600
+ * )
14601
+ * yield* cached.pipe(Effect.andThen(record))
14602
+ * yield* cached.pipe(Effect.andThen(record))
14603
+ * yield* invalidate
14604
+ * yield* cached.pipe(Effect.andThen(record))
14605
+ * })
14606
+ *
14607
+ * Effect.runSync(program)
14608
+ * output // => ["expensive task...", "result 1", "result 1", "expensive task...", "result 2"]
14609
+ * ```
14610
+ *
14611
+ * @see {@link cached} for a similar function that caches the result
14612
+ * indefinitely.
14613
+ * @see {@link cachedWithTTL} for a similar function that caches the result for
14614
+ * a specified duration but does not include an effect for manual invalidation.
14615
+ * @category caching
14616
+ * @since 2.0.0
14617
+ */
14618
+ <A, E>(timeToLive: Duration.Input | ((exit: Exit.Exit<A, E>) => Duration.Input)): <R>(self: Effect<A, E, R>) => Effect<[Effect<A, E, R>, Effect<void>]>
14619
+ /**
14620
+ * Creates a cached effect result for a fixed duration or a duration computed
14621
+ * from its `Exit` and allows manual invalidation before expiration.
14494
14622
  *
14495
14623
  * **When to use**
14496
14624
  *
@@ -14546,7 +14674,10 @@ export const cachedInvalidateWithTTL: {
14546
14674
  * @category caching
14547
14675
  * @since 2.0.0
14548
14676
  */
14549
- <A, E, R>(self: Effect<A, E, R>, timeToLive: Duration.Input): Effect<[Effect<A, E, R>, Effect<void>]>
14677
+ <A, E, R>(
14678
+ self: Effect<A, E, R>,
14679
+ timeToLive: Duration.Input | ((exit: Exit.Exit<A, E>) => Duration.Input)
14680
+ ): Effect<[Effect<A, E, R>, Effect<void>]>
14550
14681
  } = internal.cachedInvalidateWithTTL
14551
14682
 
14552
14683
  // -----------------------------------------------------------------------------
package/src/HashRing.ts CHANGED
@@ -179,12 +179,9 @@ export const addMany: {
179
179
  if (entry[1] === weight) continue
180
180
  toRemove ??= new Set()
181
181
  toRemove.add(key)
182
- self.totalWeightCache -= entry[1]
183
- self.totalWeightCache += weight
184
182
  entry[1] = weight
185
183
  } else {
186
184
  self.nodes.set(key, [node, weight])
187
- self.totalWeightCache += weight
188
185
  }
189
186
  keys.push(key)
190
187
  }
@@ -192,6 +189,7 @@ export const addMany: {
192
189
  self.ring = self.ring.filter(([, n]) => !toRemove.has(n))
193
190
  }
194
191
  addNodesToRing(self, keys, Math.round(weight * self.baseWeight))
192
+ updateTotalWeight(self)
195
193
  return self
196
194
  }
197
195
  )
@@ -206,7 +204,18 @@ function addNodesToRing<A extends PrimaryKey.PrimaryKey>(self: HashRing<A>, keys
206
204
  ])
207
205
  }
208
206
  }
209
- self.ring.sort((a, b) => a[0] - b[0])
207
+ // Break hash ties by node key to avoid insertion-order dependence.
208
+ self.ring.sort((a, b) => a[0] - b[0] || (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0))
209
+ }
210
+
211
+ // Sum in key order to avoid history-dependent floating-point rounding.
212
+ function updateTotalWeight<A extends PrimaryKey.PrimaryKey>(self: HashRing<A>) {
213
+ const keys = Array.from(self.nodes.keys()).sort()
214
+ let total = 0
215
+ for (let i = 0; i < keys.length; i++) {
216
+ total += self.nodes.get(keys[i])![1]
217
+ }
218
+ self.totalWeightCache = total
210
219
  }
211
220
 
212
221
  /**
@@ -380,7 +389,7 @@ export const remove: {
380
389
  if (entry) {
381
390
  self.nodes.delete(key)
382
391
  self.ring = self.ring.filter(([, n]) => n !== key)
383
- self.totalWeightCache -= entry[1]
392
+ updateTotalWeight(self)
384
393
  }
385
394
  return self
386
395
  })
package/src/Pool.ts CHANGED
@@ -19,7 +19,6 @@ import * as Fiber from "./Fiber.ts"
19
19
  import { constant, dual, identity } from "./Function.ts"
20
20
  import * as core from "./internal/core.ts"
21
21
  import * as internal from "./internal/effect.ts"
22
- import * as Iterable from "./Iterable.ts"
23
22
  import { type Pipeable, pipeArguments } from "./Pipeable.ts"
24
23
  import { hasProperty } from "./Predicate.ts"
25
24
  import * as Queue from "./Queue.ts"
@@ -31,10 +30,12 @@ const TypeId = "~effect/Pool"
31
30
 
32
31
  const Acquire = Symbol()
33
32
  const AcquireContext = Symbol()
33
+ const PoolClock = Symbol()
34
34
 
35
35
  interface PoolImpl<A, E> extends Pool<A, E> {
36
36
  readonly [Acquire]: Effect.Effect<A, E, Scope.Scope>
37
37
  readonly [AcquireContext]: Context.Context<Scope.Scope>
38
+ readonly [PoolClock]: Clock
38
39
  }
39
40
 
40
41
  /**
@@ -119,7 +120,6 @@ export interface State<A, E> {
119
120
  isShuttingDown: boolean
120
121
  usage: number
121
122
  readonly resizeSemaphore: Semaphore.Semaphore
122
- // Insertion order determines usage-TTL retirement order; reclaimed items move to the back.
123
123
  readonly items: Set<PoolItem<A, E>>
124
124
  availableHead: PoolItem<A, E> | undefined
125
125
  availableTail: PoolItem<A, E> | undefined
@@ -139,8 +139,8 @@ export interface State<A, E> {
139
139
  * **Details**
140
140
  *
141
141
  * Each item stores the acquisition `Exit`, its finalizer, the current
142
- * reference count, and whether automatic reclaiming has been disabled because
143
- * the item was invalidated.
142
+ * reference count, when it last became idle, and whether automatic reclaiming
143
+ * has been disabled because the item was invalidated.
144
144
  *
145
145
  * @see {@link Strategy} for the custom strategy callbacks that receive and return pool items
146
146
  * @see {@link State} for the runtime sets that store active, available, and invalidated pool items
@@ -152,6 +152,8 @@ export interface PoolItem<A, E> {
152
152
  readonly exit: Exit.Exit<A, E>
153
153
  finalizer: Effect.Effect<void>
154
154
  refCount: number
155
+ /** Clock time at which the item was acquired or its last borrower released it. */
156
+ idleSince: number
155
157
  disableReclaim: boolean
156
158
  isAvailable: boolean
157
159
  availablePrevious: PoolItem<A, E> | undefined
@@ -257,9 +259,11 @@ export const make = <A, E, R>(options: {
257
259
  * pool implementation. A value of `1` waits until existing items are fully
258
260
  * utilized before creating more items.
259
261
  *
260
- * `timeToLiveStrategy` controls when excess items expire: `"creation"` measures
261
- * from item creation, while `"usage"` measures from pool usage. The default is
262
- * `"usage"`.
262
+ * `timeToLiveStrategy` defaults to `"usage"`, which expires idle, unreserved
263
+ * excess items after `timeToLive` since their last borrower released them
264
+ * (or acquisition if never borrowed). It checks once per `timeToLive`, so
265
+ * retirement can take up to twice that duration. `"creation"` measures from
266
+ * item creation instead.
263
267
  *
264
268
  * **Example** (Creating a connection pool)
265
269
  *
@@ -374,6 +378,7 @@ export const makeWithStrategy = <A, E, R>(options: {
374
378
  [TypeId]: TypeId,
375
379
  [Acquire]: options.acquire as Effect.Effect<A, E, Scope.Scope>,
376
380
  [AcquireContext]: services as Context.Context<Scope.Scope>,
381
+ [PoolClock]: yield* Clock,
377
382
  config,
378
383
  state,
379
384
  pipe() {
@@ -676,6 +681,9 @@ const releaseItem = <A, E>(self: Pool<A, E>, item: PoolItem<A, E>): Effect.Effec
676
681
  const state = self.state
677
682
  item.refCount--
678
683
  state.usage--
684
+ if (item.refCount === 0) {
685
+ item.idleSince = (self as PoolImpl<A, E>)[PoolClock].currentTimeMillisUnsafe()
686
+ }
679
687
  if (state.invalidated.has(item)) {
680
688
  return invalidatePoolItem(self, item)
681
689
  }
@@ -1014,6 +1022,7 @@ const allocate = <A, E>(self: Pool<A, E>): Effect.Effect<PoolItem<A, E>> =>
1014
1022
  exit,
1015
1023
  finalizer: Effect.catchCause(Scope.close(scope, exit), reportUnhandledError),
1016
1024
  refCount: 0,
1025
+ idleSince: impl[PoolClock].currentTimeMillisUnsafe(),
1017
1026
  disableReclaim: false,
1018
1027
  isAvailable: false,
1019
1028
  availablePrevious: undefined,
@@ -1110,19 +1119,21 @@ const strategyCreationTTL = Effect.fnUntraced(function*<A, E>(ttl: Duration.Inpu
1110
1119
  })
1111
1120
  })
1112
1121
 
1113
- const strategyUsageTTL = <A, E>(ttl: Duration.Input): Effect.Effect<Strategy<A, E>> =>
1114
- Effect.succeed<Strategy<A, E>>({
1122
+ const strategyUsageTTL = Effect.fnUntraced(function*<A, E>(ttl: Duration.Input) {
1123
+ const clock = yield* Clock
1124
+ const ttlMillis = Duration.toMillis(Duration.fromInputUnsafe(ttl))
1125
+ return identity<Strategy<A, E>>({
1115
1126
  run: (pool) => {
1116
- // `state.items` iterates in insertion order, so its oldest live entry is
1117
- // the next to retire. Using it directly means an item stops being
1118
- // referenced by this strategy as soon as it leaves the pool.
1119
1127
  const process: Effect.Effect<void> = Effect.suspend(() => {
1120
1128
  if (activeSize(pool) <= targetSize(pool)) return Effect.void
1129
+ const expired = clock.currentTimeMillisUnsafe() - ttlMillis
1130
+ let oldest: PoolItem<A, E> | undefined
1121
1131
  for (const item of pool.state.items) {
1122
- if (pool.state.invalidated.has(item)) continue
1123
- return Effect.flatMap(invalidatePoolItem(pool, item), () => process)
1132
+ if (item.refCount > 0 || item.idleSince > expired) continue
1133
+ if (pool.state.invalidated.has(item) || reservations.has(item)) continue
1134
+ if (oldest === undefined || item.idleSince < oldest.idleSince) oldest = item
1124
1135
  }
1125
- return Effect.void
1136
+ return oldest === undefined ? Effect.void : Effect.flatMap(invalidatePoolItem(pool, oldest), () => process)
1126
1137
  })
1127
1138
  return process.pipe(
1128
1139
  Effect.delay(ttl),
@@ -1130,28 +1141,9 @@ const strategyUsageTTL = <A, E>(ttl: Duration.Input): Effect.Effect<Strategy<A,
1130
1141
  )
1131
1142
  },
1132
1143
  onAcquire: (_) => Effect.void,
1133
- reclaim(pool) {
1134
- return Effect.suspend((): Effect.Effect<PoolItem<A, E> | undefined> => {
1135
- if (pool.state.invalidated.size === 0) {
1136
- return Effect.undefined
1137
- }
1138
- const item = Iterable.head(
1139
- Iterable.filter(pool.state.invalidated, (item) => !item.disableReclaim && !reservations.has(item))
1140
- )
1141
- if (item._tag === "None") {
1142
- return Effect.undefined
1143
- }
1144
- pool.state.invalidated.delete(item.value)
1145
- // Re-adding moves the reclaimed item to the back of the retirement order.
1146
- pool.state.items.delete(item.value)
1147
- pool.state.items.add(item.value)
1148
- if (item.value.refCount < pool.config.concurrency) {
1149
- addAvailable(pool, item.value)
1150
- }
1151
- return Effect.succeed(item.value)
1152
- })
1153
- }
1144
+ reclaim: (_) => Effect.undefined
1154
1145
  })
1146
+ })
1155
1147
 
1156
1148
  const reportUnhandledError = <E>(cause: Cause.Cause<E>) =>
1157
1149
  Effect.withFiber<void>((fiber) => {
package/src/RcMap.ts CHANGED
@@ -566,10 +566,16 @@ export const get: {
566
566
  context.set(key, value)
567
567
  })
568
568
  context.set(Scope.Scope.key, entry.scope)
569
+ const lookupContext: Context.Context<Scope.Scope> = Context.makeUnsafe(context)
569
570
  Effect.suspend(() => self.lookup(key)).pipe(
570
- Effect.runForkWith(Context.makeUnsafe(context)),
571
+ Effect.runForkWith(lookupContext),
571
572
  Fiber.runIn(entry.scope)
572
- ).addObserver((exit) => Deferred.doneUnsafe(entry.deferred, exit))
573
+ ).addObserver((exit) => {
574
+ // Interruption is abandonment, so the entry is dropped and the next
575
+ // get starts a fresh lookup.
576
+ if (Exit.hasInterrupts(exit)) removeInterrupted(self, key, entry, lookupContext)
577
+ Deferred.doneUnsafe(entry.deferred, exit)
578
+ })
573
579
  }
574
580
  const scope = Context.getUnsafe(parent.context, Scope.Scope)
575
581
  return Scope.addFinalizer(scope, entry.finalizer).pipe(
@@ -733,6 +739,24 @@ export const getOption: {
733
739
  })
734
740
  )
735
741
 
742
+ const removeInterrupted = <K, A, E>(
743
+ self: RcMap<K, A, E>,
744
+ key: K,
745
+ entry: State.Entry<A, E>,
746
+ context: Context.Context<Scope.Scope>
747
+ ) => {
748
+ if (self.state._tag === "Closed") return
749
+ const o = MutableHashMap.get(self.state.map, key)
750
+ if (o._tag === "None" || o.value !== entry) return
751
+ MutableHashMap.remove(self.state.map, key)
752
+ // Borrowers close the entry on release; an idle entry has none left.
753
+ if (entry.refCount > 0) return
754
+ closeEntry(entry).pipe(
755
+ Effect.runForkWith(context),
756
+ Fiber.runIn(self.scope)
757
+ )
758
+ }
759
+
736
760
  const closeEntry = <A, E>(entry: State.Entry<A, E>) =>
737
761
  entry.fiber
738
762
  ? Fiber.interrupt(entry.fiber).pipe(Effect.andThen(Scope.close(entry.scope, Exit.void)))
@@ -317,7 +317,7 @@ export const get: {
317
317
  // Move the entry to the end of the map to keep it fresh
318
318
  MutableHashMap.remove(state.map, key)
319
319
  MutableHashMap.set(state.map, key, oentry.value)
320
- return awaitEntry(oentry.value, restore)
320
+ return awaitEntry(self, key, oentry.value, restore)
321
321
  }
322
322
  const scope = Scope.makeUnsafe()
323
323
  const deferred = Deferred.makeUnsafe</**
@@ -380,41 +380,46 @@ export const get: {
380
380
  entry.fiber = effect.forkUnsafe(
381
381
  fiber,
382
382
  effect.onExit(effect.suspend(() => Scope.provide(self.lookup(key), scope)), (exit) => {
383
- Deferred.doneUnsafe(deferred, exit)
384
383
  if (effect.exitHasInterrupts(exit)) {
385
- if (self.state._tag === "Open") {
386
- const current = MutableHashMap.get(self.state.map, key)
387
- if (Option.isSome(current) && current.value === entry) MutableHashMap.remove(self.state.map, key)
388
- }
384
+ removeEntry(self, key, entry)
385
+ Deferred.doneUnsafe(deferred, exit)
389
386
  return Scope.close(scope, exit)
390
387
  }
391
- const ttl = self.timeToLive(exit, key)
392
- if (Duration.isFinite(ttl)) {
393
- entry.expiresAt = fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() + Duration.toMillis(ttl)
388
+ try {
389
+ const ttl = self.timeToLive(exit, key)
390
+ if (Duration.isFinite(ttl)) {
391
+ entry.expiresAt = fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() + Duration.toMillis(ttl)
392
+ }
393
+ } finally {
394
+ Deferred.doneUnsafe(deferred, exit)
394
395
  }
395
396
  return effect.void
396
397
  }),
397
398
  true,
398
399
  true
399
400
  )
400
- return awaitEntry(entry, restore)
401
+ return awaitEntry(self, key, entry, restore)
401
402
  })
402
403
  )
403
404
  })
404
405
  )
405
406
  )
406
407
 
407
- const awaitEntry = <A, E>(
408
+ const awaitEntry = <Key, A, E, R>(
409
+ self: ScopedCache<Key, A, E, R>,
410
+ key: Key,
408
411
  entry: Entry<A, E>,
409
412
  restore: <X, Y, Z>(effect: Effect.Effect<X, Y, Z>) => Effect.Effect<X, Y, Z>
410
413
  ): Effect.Effect<A, E> => {
411
- const fiber = entry.fiber
412
- if (fiber === undefined || Deferred.isDoneUnsafe(entry.deferred)) return restore(Deferred.await(entry.deferred))
414
+ if (Deferred.isDoneUnsafe(entry.deferred)) return restore(Deferred.await(entry.deferred))
413
415
  entry.awaiters++
414
416
  // Install cleanup before restore so a pending interrupt cannot skip the decrement.
415
417
  return effect.onExit(restore(Deferred.await(entry.deferred)), () => {
416
418
  entry.awaiters--
417
- if (entry.awaiters > 0 || Deferred.isDoneUnsafe(entry.deferred)) return effect.void
419
+ const fiber = entry.fiber
420
+ if (entry.awaiters > 0 || fiber === undefined || Deferred.isDoneUnsafe(entry.deferred)) return effect.void
421
+ // Detach before interruption so callers arriving during finalization start fresh.
422
+ removeEntry(self, key, entry)
418
423
  return effect.flatMap(effect.fiberInterrupt(fiber), () => {
419
424
  const exit = fiber.pollUnsafe()!
420
425
  return Exit.isFailure(exit) && Cause.hasDies(exit.cause) ? effect.failCause(exit.cause) : effect.void
@@ -422,6 +427,12 @@ const awaitEntry = <A, E>(
422
427
  })
423
428
  }
424
429
 
430
+ const removeEntry = <Key, A, E, R>(self: ScopedCache<Key, A, E, R>, key: Key, entry: Entry<A, E>): void => {
431
+ if (self.state._tag !== "Open") return
432
+ const current = MutableHashMap.get(self.state.map, key)
433
+ if (Option.isSome(current) && current.value === entry) MutableHashMap.remove(self.state.map, key)
434
+ }
435
+
425
436
  const hasExpired = <A, E>(entry: Entry<A, E>, fiber: Fiber.Fiber<unknown, unknown>): boolean => {
426
437
  if (entry.expiresAt === undefined) {
427
438
  return false
@@ -518,7 +529,7 @@ export const getOption: {
518
529
  core.withFiber((fiber) =>
519
530
  effect.flatMap(
520
531
  getImpl(self, key, fiber),
521
- (entry) => entry ? effect.asSome(awaitEntry(entry, restore)) : effect.succeedNone
532
+ (entry) => entry ? effect.asSome(awaitEntry(self, key, entry, restore)) : effect.succeedNone
522
533
  )
523
534
  )
524
535
  )
@@ -1004,7 +1015,7 @@ export const invalidateWhen: {
1004
1015
  if (entry === undefined) {
1005
1016
  return effect.succeed(false)
1006
1017
  }
1007
- return awaitEntry(entry, restore).pipe(
1018
+ return awaitEntry(self, key, entry, restore).pipe(
1008
1019
  effect.flatMap((value) => {
1009
1020
  if (self.state._tag === "Closed") {
1010
1021
  return effect.succeed(false)