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
@@ -50,7 +50,7 @@ import type { Primitive } from "./core.ts"
50
50
  import {
51
51
  args,
52
52
  causeAnnotate,
53
- causeDie,
53
+ causeCombine,
54
54
  causeEmpty,
55
55
  causeFromReasons,
56
56
  CauseImpl,
@@ -238,48 +238,6 @@ export const causeAnnotations = <E>(
238
238
  return Context.makeUnsafe(map)
239
239
  }
240
240
 
241
- const dedupeReasons = <E>(
242
- self: ReadonlyArray<Cause.Reason<E>>,
243
- that: ReadonlyArray<Cause.Reason<E>>
244
- ): Array<Cause.Reason<E>> => {
245
- // Keep deduplication local so causeCombine does not retain Array.ts in the core bundle.
246
- // Snapshot both arrays before invoking user-defined hash or equality methods.
247
- const buckets = new Map<number, Array<Cause.Reason<E>>>()
248
- const out: Array<Cause.Reason<E>> = []
249
- for (const reason of self.concat(that)) {
250
- const hash = Hash.hash(reason)
251
- const bucket = buckets.get(hash)
252
- if (bucket === undefined) {
253
- buckets.set(hash, [reason])
254
- } else if (bucket.some((previous) => Equal.equals(previous, reason))) {
255
- continue
256
- } else {
257
- bucket.push(reason)
258
- }
259
- out.push(reason)
260
- }
261
- return out
262
- }
263
-
264
- /** @internal */
265
- export const causeCombine: {
266
- <E2>(that: Cause.Cause<E2>): <E>(self: Cause.Cause<E>) => Cause.Cause<E | E2>
267
- <E, E2>(self: Cause.Cause<E>, that: Cause.Cause<E2>): Cause.Cause<E | E2>
268
- } = dual(
269
- 2,
270
- <E, E2>(self: Cause.Cause<E>, that: Cause.Cause<E2>): Cause.Cause<E | E2> => {
271
- if (self.reasons.length === 0) {
272
- return that as Cause.Cause<E | E2>
273
- } else if (that.reasons.length === 0) {
274
- return self as Cause.Cause<E | E2>
275
- }
276
- const newCause = new CauseImpl<E | E2>(
277
- dedupeReasons<E | E2>(self.reasons, that.reasons)
278
- )
279
- return Equal.equals(self, newCause) ? self : newCause
280
- }
281
- )
282
-
283
241
  /** @internal */
284
242
  export const causeMap: {
285
243
  <E, E2>(f: (error: NoInfer<E>) => E2): (self: Cause.Cause<E>) => Cause.Cause<E2>
@@ -504,6 +462,21 @@ const fiberVariance = {
504
462
 
505
463
  const fiberIdStore = { id: 0 }
506
464
 
465
+ interface AsyncContext {
466
+ runInAsyncScope<This, Arg, R>(fn: (this: This, arg: Arg) => R, thisArg: This, arg: Arg): R
467
+ }
468
+
469
+ const AsyncResource: (new(type: string) => AsyncContext) | undefined = (() => {
470
+ try {
471
+ return (globalThis as any).process?.getBuiltinModule?.("node:async_hooks")?.AsyncResource
472
+ } catch {
473
+ return undefined
474
+ }
475
+ })()
476
+
477
+ const captureAsyncContext = (): AsyncContext | undefined =>
478
+ AsyncResource === undefined ? undefined : new AsyncResource("effect/Fiber")
479
+
507
480
  /** @internal */
508
481
  export const getCurrentFiber = (): Fiber.Fiber<any, any> | undefined => (globalThis as any)[currentFiberTypeId]
509
482
 
@@ -526,6 +499,7 @@ export class FiberImpl<A = any, E = any> implements Fiber.Fiber<A, E> {
526
499
  this._running = false
527
500
  this._deferredInterrupt = false
528
501
  this._parent = undefined
502
+ this._asyncContext = undefined
529
503
  this.cache.runtimeMetrics?.recordFiberStart(this.context)
530
504
  }
531
505
 
@@ -545,6 +519,7 @@ export class FiberImpl<A = any, E = any> implements Fiber.Fiber<A, E> {
545
519
  declare _running: boolean
546
520
  declare _deferredInterrupt: boolean
547
521
  declare _parent: FiberImpl<any, any> | undefined
522
+ declare _asyncContext: AsyncContext | undefined
548
523
 
549
524
  // set in setContext
550
525
  declare context: Context.Context<never>
@@ -605,6 +580,10 @@ export class FiberImpl<A = any, E = any> implements Fiber.Fiber<A, E> {
605
580
  evaluate(effect: Primitive): void {
606
581
  if (this._exit) {
607
582
  return
583
+ } else if (this._asyncContext !== undefined) {
584
+ const asyncContext = this._asyncContext
585
+ this._asyncContext = undefined
586
+ return asyncContext.runInAsyncScope(this.evaluate, this, effect)
608
587
  } else if (this._yielded !== undefined) {
609
588
  const yielded = this._yielded as () => void
610
589
  this._yielded = undefined
@@ -612,6 +591,7 @@ export class FiberImpl<A = any, E = any> implements Fiber.Fiber<A, E> {
612
591
  }
613
592
  const exit = this.runLoop(effect)
614
593
  if (exit === Yield) {
594
+ this._asyncContext = captureAsyncContext()
615
595
  return
616
596
  }
617
597
  // the interruptChildren middleware is added in Effect.forkChild, so it can be
@@ -1122,20 +1102,22 @@ export const tryPromise = <A, E = Cause.UnknownError>(
1122
1102
  ? ((cause: unknown) => new UnknownError(cause, "An error occurred in Effect.tryPromise"))
1123
1103
  : options.catch
1124
1104
  return callbackOptions<A, E>(function(resume, signal) {
1125
- const failWithCatch = (cause: unknown) => {
1126
- try {
1127
- resume(fail(internalCall(() => catcher(cause)) as E))
1128
- } catch (err) {
1129
- resume(die(err))
1130
- }
1131
- }
1105
+ // Defer the catcher until the fiber restores its async context.
1106
+ const failWithCatch = (cause: unknown) =>
1107
+ suspend(() => {
1108
+ try {
1109
+ return fail(internalCall(() => catcher(cause)) as E)
1110
+ } catch (err) {
1111
+ return die(err)
1112
+ }
1113
+ })
1132
1114
  try {
1133
1115
  f(signal!).then(
1134
1116
  (a) => resume(succeed(a)),
1135
- failWithCatch
1117
+ (e) => resume(failWithCatch(e))
1136
1118
  )
1137
1119
  } catch (err) {
1138
- failWithCatch(err)
1120
+ resume(failWithCatch(err))
1139
1121
  }
1140
1122
  }, f.length !== 0)
1141
1123
  }
@@ -4457,48 +4439,78 @@ export const cachedInvalidateWithTTL: {
4457
4439
  self: Effect.Effect<A, E, R>,
4458
4440
  ttl: Duration.Input | ((exit: Exit.Exit<A, E>) => Duration.Input)
4459
4441
  ): Effect.Effect<[Effect.Effect<A, E, R>, Effect.Effect<void>]> =>
4460
- sync(() => {
4461
- const ttlMillis = typeof ttl === "function"
4462
- ? (exit: Exit.Exit<A, E>) => Duration.toMillis(Duration.fromInputUnsafe(ttl(exit)))
4463
- : constant(Duration.toMillis(Duration.fromInputUnsafe(ttl)))
4464
- const latch = makeLatchUnsafe(false)
4465
- let expiresAt = 0
4466
- let running = false
4467
- let exit: Exit.Exit<A, E> | undefined
4468
- const wait = flatMap(latch.await, () => exit!)
4469
- return [
4470
- withFiber((fiber) => {
4471
- const clock = fiber.getRef(ClockRef)
4472
- const now = expiresAt === Infinity ? 0 : clock.currentTimeMillisUnsafe()
4473
- if (running || now < expiresAt) return exit ?? wait
4474
- running = true
4475
- latch.closeUnsafe()
4476
- exit = undefined
4477
- onExitUnsafe<A, E>(fiber, (exit_) =>
4478
- sync(() => {
4479
- try {
4480
- const duration = ttlMillis(exit_)
4481
- expiresAt = clock.currentTimeMillisUnsafe() + duration
4482
- exit = exit_
4483
- } catch (error) {
4484
- const cause = causeDie(error)
4485
- // Publish the same combined cause that onExit returns to the owner.
4486
- exit = exitFailCause(exitIsFailure(exit_) ? causeCombine(exit_.cause, cause) : cause)
4487
- throw error
4488
- } finally {
4489
- running = false
4490
- latch.openUnsafe()
4491
- }
4492
- }))
4493
- return self
4494
- }),
4495
- sync(() => {
4496
- expiresAt = 0
4497
- latch.closeUnsafe()
4498
- exit = undefined
4499
- })
4500
- ]
4501
- }))
4442
+ sync(() =>
4443
+ makeCachedUnsafe(
4444
+ self,
4445
+ typeof ttl === "function"
4446
+ ? (exit: Exit.Exit<A, E>) => Duration.toMillis(Duration.fromInputUnsafe(ttl(exit)))
4447
+ : constant(Duration.toMillis(Duration.fromInputUnsafe(ttl)))
4448
+ )
4449
+ ))
4450
+
4451
+ const infiniteTTL = constant(Infinity)
4452
+
4453
+ interface CachedRun<A, E> {
4454
+ fiber: FiberImpl<A, E> | undefined
4455
+ awaiters: number
4456
+ }
4457
+
4458
+ const makeCachedUnsafe = <A, E, R>(
4459
+ self: Effect.Effect<A, E, R>,
4460
+ ttlMillis: (exit: Exit.Exit<A, E>) => number
4461
+ ): [Effect.Effect<A, E, R>, Effect.Effect<void>] => {
4462
+ let expiresAt = 0
4463
+ let exit: Exit.Exit<A, E> | undefined
4464
+ let current: CachedRun<A, E> | undefined
4465
+
4466
+ const join = (fiber: Fiber.Fiber<unknown, unknown>, run: CachedRun<A, E>): Effect.Effect<A, E> => {
4467
+ run.awaiters++
4468
+ onExitUnsafe(fiber, () => {
4469
+ // Abandon the run once every caller has left, unless it already finished.
4470
+ if (--run.awaiters > 0 || current !== run) return
4471
+ // Detach it first so new callers start a fresh run instead of joining
4472
+ // one that is being interrupted.
4473
+ current = undefined
4474
+ return fiberInterrupt(run.fiber!)
4475
+ })
4476
+ return fiberJoin(run.fiber!)
4477
+ }
4478
+
4479
+ return [
4480
+ withFiber((fiber) => {
4481
+ if (current !== undefined) return join(fiber, current)
4482
+ if (
4483
+ exit !== undefined &&
4484
+ (expiresAt === Infinity || fiber.getRef(ClockRef).currentTimeMillisUnsafe() < expiresAt)
4485
+ ) {
4486
+ return exit
4487
+ }
4488
+ exit = undefined
4489
+ const clock = fiber.getRef(ClockRef)
4490
+ const run: CachedRun<A, E> = { fiber: undefined, awaiters: 0 }
4491
+ current = run
4492
+ run.fiber = forkUnsafe(
4493
+ fiber,
4494
+ onExitPrimitive(self, (exit_) => {
4495
+ // An abandoned run must not overwrite or clear a replacement run.
4496
+ if (current !== run) return
4497
+ current = undefined
4498
+ // Interruption is abandonment, so it is never cached.
4499
+ if (exitHasInterrupts(exit_)) return
4500
+ const duration = ttlMillis(exit_)
4501
+ expiresAt = duration === Infinity ? Infinity : clock.currentTimeMillisUnsafe() + duration
4502
+ exit = exit_
4503
+ }),
4504
+ true,
4505
+ true
4506
+ )
4507
+ return run.fiber._exit ?? join(fiber, run)
4508
+ }),
4509
+ sync(() => {
4510
+ exit = undefined
4511
+ })
4512
+ ]
4513
+ }
4502
4514
 
4503
4515
  /** @internal */
4504
4516
  export const cachedWithTTL: {
@@ -4525,23 +4537,7 @@ export const cachedWithTTL: {
4525
4537
 
4526
4538
  /** @internal */
4527
4539
  export const cached = <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<Effect.Effect<A, E, R>> =>
4528
- sync(() => {
4529
- const latch = makeLatchUnsafe(false)
4530
- let started = false
4531
- let exit: Exit.Exit<A, E> | undefined
4532
- const wait = flatMap(latch.await, () => exit!)
4533
- return withFiber((fiber) => {
4534
- if (exit !== undefined) return exit
4535
- if (started) return wait
4536
- started = true
4537
- onExitUnsafe<A, E>(fiber, (result) =>
4538
- sync(() => {
4539
- exit = result
4540
- latch.openUnsafe()
4541
- }))
4542
- return self
4543
- })
4544
- })
4540
+ sync(() => makeCachedUnsafe(self, infiniteTTL)[0])
4545
4541
 
4546
4542
  // ----------------------------------------------------------------------------
4547
4543
  // interruption
@@ -5531,6 +5527,8 @@ export const forkUnsafe = <FA, FE, A, E, R>(
5531
5527
  if (immediate) {
5532
5528
  child.evaluate(effect as any)
5533
5529
  } else {
5530
+ // Preserve the fork context rather than the dispatcher's context.
5531
+ child._asyncContext = captureAsyncContext()
5534
5532
  parentRuntime.currentDispatcher.scheduleTask(() => child.evaluate(effect as any), 0)
5535
5533
  }
5536
5534
  if (!daemon && !child._exit) {
@@ -7,17 +7,26 @@ import { identity } from "../Function.ts"
7
7
  import { pipeArguments } from "../Pipeable.ts"
8
8
  import type * as RcRef from "../RcRef.ts"
9
9
  import * as Scope from "../Scope.ts"
10
- import * as Semaphore from "../Semaphore.ts"
11
10
 
12
11
  const TypeId = "~effect/RcRef"
13
12
 
14
- type State<A> = State.Empty | State.Acquired<A> | State.Closed
13
+ type State<A> = State.Empty | State.Acquiring<A> | State.Acquired<A> | State.Closed
15
14
 
16
15
  declare namespace State {
17
16
  interface Empty {
18
17
  readonly _tag: "Empty"
19
18
  }
20
19
 
20
+ interface Acquiring<A> {
21
+ readonly _tag: "Acquiring"
22
+ // Assigned as soon as the acquisition is forked.
23
+ fiber: Fiber.Fiber<Acquired<A>, unknown>
24
+ awaiters: number
25
+ // Set once references are allocated to the waiters, which can happen
26
+ // before the acquisition fiber exits.
27
+ acquired: Acquired<A> | undefined
28
+ }
29
+
21
30
  interface Acquired<A> {
22
31
  readonly _tag: "Acquired"
23
32
  readonly value: A
@@ -48,7 +57,6 @@ class RcRefImpl<A, E> implements RcRef.RcRef<A, E> {
48
57
  }
49
58
 
50
59
  state: State<A> = stateEmpty
51
- readonly semaphore = Semaphore.makeUnsafe(1)
52
60
  readonly acquire: Effect.Effect<A, E>
53
61
  readonly context: Context.Context<never>
54
62
  readonly scope: Scope.Scope
@@ -85,11 +93,11 @@ export const make = <A, E, R>(options: {
85
93
  )
86
94
  return Effect.as(
87
95
  Scope.addFinalizerExit(scope, () => {
88
- const close = ref.state._tag === "Acquired"
89
- ? Scope.close(ref.state.scope, Exit.void)
90
- : Effect.void
96
+ const state = ref.state
91
97
  ref.state = stateClosed
92
- return close
98
+ if (state._tag === "Acquired") return Scope.close(state.scope, Exit.void)
99
+ if (state._tag === "Acquiring") return Fiber.interrupt(state.fiber)
100
+ return Effect.void
93
101
  }),
94
102
  ref
95
103
  )
@@ -110,40 +118,82 @@ const getState = <A, E>(
110
118
  ? Effect.as(Fiber.interrupt(state.fiber), state)
111
119
  : Effect.succeed(state)
112
120
  }
121
+ case "Acquiring": {
122
+ const state = self.state
123
+ state.awaiters++
124
+ return awaitAcquiring(self, state, restore)
125
+ }
113
126
  case "Empty": {
114
- return self.semaphore.withPermit(
115
- Effect.suspend(() => {
116
- if (self.state._tag !== "Empty") {
117
- return getState(self, restore)
127
+ // The caller that starts the acquisition counts as its first awaiter.
128
+ const acquiring: State.Acquiring<A> = {
129
+ _tag: "Acquiring",
130
+ fiber: undefined as any,
131
+ awaiters: 1,
132
+ acquired: undefined
133
+ }
134
+ self.state = acquiring
135
+ const scope = Scope.makeUnsafe()
136
+ // Acquire on a detached fiber so the acquisition is shared by every
137
+ // caller and only interrupted once all of them have been interrupted.
138
+ const acquire = Effect.provideContext(
139
+ self.acquire as Effect.Effect<A, E>,
140
+ Context.add(self.context, Scope.Scope, scope)
141
+ ).pipe(
142
+ Effect.flatMap((value) => {
143
+ // The ref was closed, or the acquisition was abandoned.
144
+ if (self.state !== acquiring) {
145
+ return Effect.interrupt
146
+ }
147
+ const state: State.Acquired<A> = {
148
+ _tag: "Acquired",
149
+ value,
150
+ scope,
151
+ fiber: undefined,
152
+ // Every caller still waiting holds a reference.
153
+ refCount: acquiring.awaiters,
154
+ invalidated: false
118
155
  }
119
- const scope = Scope.makeUnsafe()
120
- return restore(Effect.provideContext(
121
- self.acquire as Effect.Effect<A, E>,
122
- Context.add(self.context, Scope.Scope, scope)
123
- )).pipe(
124
- Effect.flatMap((value) => {
125
- if (self.state._tag === "Closed") {
126
- return Effect.interrupt
127
- }
128
- const state: State.Acquired<A> = {
129
- _tag: "Acquired",
130
- value,
131
- scope,
132
- fiber: undefined,
133
- refCount: 1,
134
- invalidated: false
135
- }
136
- self.state = state
137
- return Effect.succeed(state)
138
- }),
139
- Effect.onExit((exit) => Exit.isFailure(exit) ? Scope.close(scope, exit) : Effect.void)
140
- )
156
+ self.state = state
157
+ acquiring.acquired = state
158
+ return Effect.succeed(state)
159
+ }),
160
+ Effect.onExit((exit) => {
161
+ if (Exit.isSuccess(exit)) return Effect.void
162
+ if (self.state === acquiring) self.state = stateEmpty
163
+ return Scope.close(scope, exit)
141
164
  })
142
165
  )
166
+ acquiring.fiber = Effect.runForkWith(Fiber.getCurrent()!.context)(acquire)
167
+ return awaitAcquiring(self, acquiring, restore)
143
168
  }
144
169
  }
145
170
  }
146
171
 
172
+ const awaitAcquiring = <A, E>(
173
+ self: RcRefImpl<A, E>,
174
+ acquiring: State.Acquiring<A>,
175
+ restore: <AX, EX, RX>(effect: Effect.Effect<AX, EX, RX>) => Effect.Effect<AX, EX, RX>
176
+ ): Effect.Effect<State.Acquired<A>, E> =>
177
+ Effect.onExit(
178
+ restore(Fiber.join(acquiring.fiber)) as Effect.Effect<State.Acquired<A>, E>,
179
+ (exit) => {
180
+ const acquired = acquiring.acquired
181
+ if (acquired !== undefined) {
182
+ // The acquisition counted this caller as a reference, possibly before
183
+ // its fiber exited. Release it if the caller was interrupted before it
184
+ // could register its finalizer.
185
+ return Exit.isFailure(exit) ? release(self, acquired) : Effect.void
186
+ }
187
+ // Abandon the acquisition once every caller has left, unless it already
188
+ // failed (a failed acquisition allocated no references).
189
+ if (--acquiring.awaiters > 0 || self.state !== acquiring) return Effect.void
190
+ // Detach it first so new callers start a fresh acquisition instead of
191
+ // joining one that is being interrupted.
192
+ self.state = stateEmpty
193
+ return Fiber.interrupt(acquiring.fiber)
194
+ }
195
+ )
196
+
147
197
  /** @internal */
148
198
  export const get = <A, E>(self_: RcRef.RcRef<A, E>): Effect.Effect<A, E, Scope.Scope> => {
149
199
  const self = self_ as RcRefImpl<A, E>
@@ -1067,13 +1067,15 @@ function translateJsonSchemaMultiDocument(
1067
1067
  )
1068
1068
  case "ignore":
1069
1069
  return undefined
1070
- case "apply":
1070
+ case "apply": {
1071
+ let regex: RegExp
1071
1072
  try {
1072
- globalThis.RegExp(pattern, jsonSchemaPatternFlags)
1073
+ regex = globalThis.RegExp(pattern, jsonSchemaPatternFlags)
1073
1074
  } catch {
1074
1075
  throw errorWithPath("Cannot import pattern using ECMAScript Unicode mode.", path)
1075
1076
  }
1076
- return jsonSchemaFilter("effect/schema/isPattern", { source: pattern, flags: jsonSchemaPatternFlags })
1077
+ return jsonSchemaFilter("effect/schema/isPattern", { source: regex.source, flags: regex.flags })
1078
+ }
1077
1079
  }
1078
1080
  }
1079
1081
 
@@ -454,9 +454,11 @@ export const makeNoSerialization: <Rpcs extends Rpc.Any, E, const Flatten extend
454
454
  const id = generateRequestId()
455
455
 
456
456
  const scope = Context.getUnsafe(fiber.context, Scope.Scope)
457
+ const queue = yield* Queue.bounded<any, any>(streamBufferSize)
457
458
  yield* Scope.addFinalizerExit(
458
459
  scope,
459
460
  (exit) => {
461
+ Queue.shutdownUnsafe(queue)
460
462
  if (!entries.has(id)) return Effect.void
461
463
  entries.delete(id)
462
464
  return sendInterrupt(
@@ -469,7 +471,6 @@ export const makeNoSerialization: <Rpcs extends Rpc.Any, E, const Flatten extend
469
471
  }
470
472
  )
471
473
 
472
- const queue = yield* Queue.bounded<any, any>(streamBufferSize)
473
474
  entries.set(id, {
474
475
  _tag: "Queue",
475
476
  rpc,
@@ -671,6 +672,12 @@ export const make: <Rpcs extends Rpc.Any, const Flatten extends boolean = false>
671
672
  }
672
673
  const entries = new Map<RequestId, ClientEntry>()
673
674
 
675
+ const interruptRequest = (requestId: RequestId): Effect.Effect<void, RpcClientError> => {
676
+ if (!entries.has(requestId)) return Effect.void
677
+ entries.delete(requestId)
678
+ return send(clientId, { _tag: "Interrupt", requestId }) as Effect.Effect<void, RpcClientError>
679
+ }
680
+
674
681
  const { client, write } = yield* makeNoSerialization(group, {
675
682
  ...options,
676
683
  supportsAck,
@@ -711,13 +718,7 @@ export const make: <Rpcs extends Rpc.Any, const Flatten extends boolean = false>
711
718
  }) as Effect.Effect<void, RpcClientError>
712
719
  }
713
720
  case "Interrupt": {
714
- const entry = entries.get(message.requestId)
715
- if (!entry) return Effect.void
716
- entries.delete(message.requestId)
717
- return send(clientId, {
718
- _tag: "Interrupt",
719
- requestId: message.requestId
720
- }) as Effect.Effect<void, RpcClientError>
721
+ return interruptRequest(message.requestId)
721
722
  }
722
723
  case "Eof": {
723
724
  return Effect.void
@@ -732,19 +733,22 @@ export const make: <Rpcs extends Rpc.Any, const Flatten extends boolean = false>
732
733
  const requestId = RequestId(message.requestId)
733
734
  const entry = entries.get(requestId)
734
735
  if (!entry || Option.isNone(entry.schemas.decodeChunk)) return Effect.void
735
- return entry.schemas.decodeChunk.value(message.values).pipe(
736
- Effect.provideContext(entry.context),
737
- Effect.orDie,
738
- Effect.flatMap((chunk) =>
739
- write({ _tag: "Chunk", clientId: 0, requestId: RequestId(message.requestId), values: chunk })
740
- ),
741
- Effect.onError((cause) =>
742
- write({
743
- _tag: "Exit",
744
- clientId: 0,
745
- requestId: RequestId(message.requestId),
746
- exit: Exit.failCause(cause)
747
- })
736
+ const decodeChunk = entry.schemas.decodeChunk.value
737
+ return Effect.uninterruptibleMask((restore) =>
738
+ restore(
739
+ decodeChunk(message.values).pipe(
740
+ Effect.provideContext(entry.context),
741
+ Effect.orDie,
742
+ Effect.flatMap((chunk) => write({ _tag: "Chunk", clientId: 0, requestId, values: chunk }))
743
+ )
744
+ ).pipe(
745
+ Effect.onError((cause) =>
746
+ write({ _tag: "Exit", clientId: 0, requestId, exit: Exit.failCause(cause) }).pipe(
747
+ Effect.andThen(Cause.hasInterrupts(cause) ? Effect.void : Effect.ignore(interruptRequest(requestId)))
748
+ )
749
+ ),
750
+ // a decode failure only ends its own request; interruption still propagates
751
+ Effect.catchCauseIf((cause) => !Cause.hasInterrupts(cause), () => Effect.void)
748
752
  )
749
753
  ) as Effect.Effect<void>
750
754
  }
@@ -768,7 +772,7 @@ export const make: <Rpcs extends Rpc.Any, const Flatten extends boolean = false>
768
772
  case "ClientProtocolError": {
769
773
  const exit = Exit.fail(message.error)
770
774
  return Effect.forEach(
771
- entries.keys(),
775
+ Array.from(entries.keys()),
772
776
  (requestId) => write({ _tag: "Exit", clientId: 0, requestId, exit: exit as any })
773
777
  )
774
778
  }
@@ -1392,13 +1392,14 @@ export const makeProtocolStdio = Effect.gen(function*() {
1392
1392
  Effect.sandbox,
1393
1393
  Effect.tapError(Effect.logError),
1394
1394
  Effect.retry(Schedule.spaced(500)),
1395
- Effect.ensuring(Effect.forkDetach(Fiber.interrupt(fiber), { startImmediately: true })),
1395
+ Effect.andThen(() => writeRequest(0, constEof)),
1396
1396
  Effect.forkScoped
1397
1397
  )
1398
1398
 
1399
1399
  yield* Stream.fromQueue(queue).pipe(
1400
1400
  Stream.run(stdio.stdout()),
1401
1401
  Effect.retry(Schedule.spaced(500)),
1402
+ Effect.ensuring(Effect.forkDetach(Fiber.interrupt(fiber), { startImmediately: true })),
1402
1403
  Effect.forkScoped
1403
1404
  )
1404
1405
 
package/src/rpc/Utils.ts CHANGED
@@ -1,10 +1,5 @@
1
1
  /**
2
- * Helps RPC protocol services buffer messages until their receive loop starts.
3
- *
4
- * Client and server protocol constructors use these helpers to expose a stable
5
- * service before the active receiver is installed. Writes made before `run`
6
- * starts are buffered with their current `Context`, then replayed once the
7
- * receiver is ready.
2
+ * Buffers RPC protocol messages until their receive loop starts.
8
3
  *
9
4
  * @stability unstable
10
5
  * @since 4.0.0
@@ -16,9 +11,8 @@ import type { Protocol } from "./RpcClient.ts"
16
11
  import type { FromServerEncoded } from "./RpcMessage.ts"
17
12
 
18
13
  /**
19
- * Builds a service with a `run` method that buffers writes until `run` installs
20
- * a writer, replays buffered writes with their original contexts, and restores
21
- * the previous writer when the run ends.
14
+ * Buffers writes until `run` replays them with their original contexts and
15
+ * installs the writer. Restores the previous writer when `run` ends.
22
16
  *
23
17
  * @stability unstable
24
18
  * @category services
@@ -43,12 +37,12 @@ export const withRun = <
43
37
  run(f) {
44
38
  return semaphore.withPermits(1)(Effect.gen(function*() {
45
39
  const prev = write
46
- write = f
47
40
 
48
41
  for (const [args, context] of buffer) {
49
42
  yield* Effect.provideContext(Effect.suspend(() => f(...args)), context)
50
43
  }
51
44
  buffer = []
45
+ write = f
52
46
 
53
47
  return yield* Effect.onExit(Effect.never, () => {
54
48
  write = prev
@@ -274,6 +274,9 @@ export class WorkflowInstance extends Context.Service<
274
274
  /** Deferred names this run parked on; their completions preempt the run. */
275
275
  readonly awaitedDeferreds: Set<string>
276
276
 
277
+ /** Deferred names this run completed itself; their completions need not wake it. */
278
+ readonly completedDeferreds: Set<string>
279
+
277
280
  readonly activityState: {
278
281
  count: number
279
282
  readonly latch: Latch.Latch
@@ -294,6 +297,7 @@ export class WorkflowInstance extends Context.Service<
294
297
  abandoned: false,
295
298
  cause: undefined,
296
299
  awaitedDeferreds: new Set(),
300
+ completedDeferreds: new Set(),
297
301
  activityState: {
298
302
  count: 0,
299
303
  latch: Latch.makeUnsafe()