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
@@ -97,6 +97,8 @@ export type EntityState = {
97
97
  readonly rpc: Rpc.AnyWithProps
98
98
  readonly message: Message.IncomingRequestLocal<any>
99
99
  sentReply: boolean
100
+ /** Excludes requests awaiting their first dispatch from replay. */
101
+ delivered: boolean
100
102
  sentExit: boolean
101
103
  lastSentChunk: Option.Option<Reply.Chunk<Rpc.Any>>
102
104
  sequence: number
@@ -200,11 +202,19 @@ export const make = Effect.fnUntraced(function*<
200
202
  )
201
203
 
202
204
  const activeRequests: EntityState["activeRequests"] = new Map()
203
- let defectRequestIds = new Set<Snowflake.Snowflake>()
204
- let isRestartingDueToDefect = false
205
+ const retired = Latch.makeUnsafe()
206
+ const isActive = () => !retired.isOpen()
207
+
208
+ // Replay previously dispatched requests before admitting new work.
209
+ const replay = Effect.fnUntraced(function*(write: EntityState["write"]) {
210
+ if (!isActive()) return
211
+ for (const request of activeRequests.values()) {
212
+ if (!request.delivered) continue
213
+ request.sentExit = false
214
+ yield* write(0, requestEnvelope(request), requestWriteOptions(request))
215
+ }
216
+ })
205
217
 
206
- // the server is stored in a ref, so if there is a defect, we can
207
- // swap the server without losing the active requests
208
218
  const writeRef = yield* ResourceRef.from(
209
219
  scope,
210
220
  Effect.fnUntraced(function*(handlerScope) {
@@ -245,11 +255,8 @@ export const make = Effect.fnUntraced(function*<
245
255
  request.sentReply = true
246
256
  request.sentExit = true
247
257
 
248
- if (
249
- isShuttingDown &&
250
- Exit.hasInterrupts(response.exit) &&
251
- defectRequestIds.has(Snowflake.Snowflake(response.requestId))
252
- ) {
258
+ // Rebuild interrupts are not replies; replacement handlers replay the request.
259
+ if (isShuttingDown && Exit.hasInterrupts(response.exit) && isActive()) {
253
260
  return Effect.void
254
261
  }
255
262
 
@@ -265,19 +272,7 @@ export const make = Effect.fnUntraced(function*<
265
272
  ) {
266
273
  if (!isShuttingDown) {
267
274
  request.sentExit = false
268
- return server.write(
269
- 0,
270
- {
271
- ...request.message.envelope,
272
- id: request.message.envelope.requestId as any,
273
- tag: request.message.envelope.tag as any,
274
- payload: new Request({
275
- ...request.message.envelope,
276
- lastSentChunk: request.lastSentChunk
277
- } as any) as any
278
- },
279
- requestWriteOptions(request)
280
- ).pipe(
275
+ return server.write(0, requestEnvelope(request), requestWriteOptions(request)).pipe(
281
276
  Effect.forkIn(handlerScope)
282
277
  )
283
278
  }
@@ -335,7 +330,10 @@ export const make = Effect.fnUntraced(function*<
335
330
  ))
336
331
  }
337
332
  case "Defect": {
338
- return Effect.forkIn(onDefect(Cause.die(response.defect)), managerScope)
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)
339
337
  }
340
338
  case "ClientEnd": {
341
339
  return endLatch.open
@@ -354,56 +352,25 @@ export const make = Effect.fnUntraced(function*<
354
352
  })
355
353
  )
356
354
 
357
- if (defectRequestIds.size > 0) {
358
- for (const id of defectRequestIds) {
359
- const request = activeRequests.get(id)
360
- if (!request) continue
361
- const { lastSentChunk, message } = request
362
- request.sentExit = false
363
- yield* server.write(
364
- 0,
365
- {
366
- ...message.envelope,
367
- id: message.envelope.requestId as any,
368
- tag: message.envelope.tag as any,
369
- payload: new Request({
370
- ...message.envelope,
371
- lastSentChunk
372
- } as any) as any
373
- },
374
- requestWriteOptions(request)
375
- )
376
- }
377
- defectRequestIds.clear()
378
- }
379
-
380
355
  return server.write
381
356
  }),
382
357
  address
383
358
  )
384
359
 
385
- function onDefect(cause: Cause.Cause<never>): Effect.Effect<void> {
386
- if (!activeServers.has(address.entityId)) {
387
- return endLatch.open
388
- }
389
- if (isRestartingDueToDefect) {
390
- return Effect.void
391
- }
392
- defectRequestIds = new Set(activeRequests.keys())
393
- isRestartingDueToDefect = true
394
- const effect = writeRef.rebuildUnsafe()
360
+ function restart(cause: Cause.Cause<unknown>, rebuild: Effect.Effect<void>): Effect.Effect<void> {
395
361
  return Effect.logError("Defect in entity, restarting", cause).pipe(
396
362
  Effect.andThen(Effect.ignore(retryDriver(void 0))),
397
- Effect.flatMap(() => activeServers.has(address.entityId) ? effect : endLatch.open),
398
- Effect.ensuring(Effect.sync(() => {
399
- isRestartingDueToDefect = false
400
- })),
363
+ Effect.flatMap(() => isActive() ? rebuild : endLatch.open),
401
364
  Effect.annotateLogs({
402
365
  module: "EntityManager",
403
366
  address,
404
367
  runner: options.runnerAddress
405
368
  }),
406
- Effect.catchCause(onDefect)
369
+ Effect.catchCause((cause) => {
370
+ if (Cause.hasInterruptsOnly(cause)) return Effect.void
371
+ const retry = isActive() ? writeRef.rebuildUnsafe({ prepare: replay }) : undefined
372
+ return retry ? restart(cause, retry) : Effect.void
373
+ })
407
374
  )
408
375
  }
409
376
 
@@ -411,10 +378,27 @@ export const make = Effect.fnUntraced(function*<
411
378
  scope,
412
379
  address,
413
380
  write(clientId, message, writeOptions) {
414
- if (writeRef.state.current._tag !== "Acquired") {
415
- return Effect.flatMap(writeRef.await, (write) => write(clientId, message, writeOptions))
416
- }
417
- return writeRef.state.current.value(clientId, message, writeOptions)
381
+ return Effect.suspend(() => {
382
+ if (message._tag !== "Request") {
383
+ // Interrupts, acks and EOF reach the handlers even during shutdown.
384
+ const write = writeRef.getUnsafe()
385
+ return write
386
+ ? write(clientId, message, writeOptions)
387
+ : Effect.flatMap(writeRef.await, (write) => write(clientId, message, writeOptions))
388
+ }
389
+ if (!isActive()) return Effect.interrupt
390
+ const write = writeRef.getUnsafe()
391
+ if (write === undefined) {
392
+ return Effect.flatMap(
393
+ Effect.raceFirst(writeRef.await, retired.await),
394
+ () => state.write(clientId, message, writeOptions)
395
+ )
396
+ }
397
+ const request = activeRequests.get(Snowflake.Snowflake(message.id))
398
+ if (!request) return Effect.void
399
+ request.delivered = true
400
+ return write(clientId, message, writeOptions)
401
+ })
418
402
  },
419
403
  activeRequests,
420
404
  lastActiveCheck: clock.currentTimeMillisUnsafe(),
@@ -430,6 +414,7 @@ export const make = Effect.fnUntraced(function*<
430
414
  scope,
431
415
  Effect.suspend(() => {
432
416
  activeServers.delete(address.entityId)
417
+ retired.openUnsafe()
433
418
  acquireEntity(address)
434
419
  return Effect.raceFirst(
435
420
  state.write(0, { _tag: "Eof" }).pipe(
@@ -441,6 +426,9 @@ export const make = Effect.fnUntraced(function*<
441
426
  )
442
427
  })
443
428
  )
429
+ if (!options.sharding.hasShardId(address.shardId)) {
430
+ return yield* new EntityNotAssignedToRunner({ address })
431
+ }
444
432
  // Do not make shard interruption wait for an entity that is still building.
445
433
  serverCloseLatches.set(address, closeLatches)
446
434
  activeServers.set(address.entityId, state)
@@ -544,6 +532,7 @@ export const make = Effect.fnUntraced(function*<
544
532
  rpc,
545
533
  message,
546
534
  sentReply: false,
535
+ delivered: false,
547
536
  sentExit: false,
548
537
  lastSentChunk: Option.filter(
549
538
  message.lastSentReply,
@@ -565,21 +554,7 @@ export const make = Effect.fnUntraced(function*<
565
554
  }
566
555
  }))
567
556
  }
568
- return server.write(
569
- 0,
570
- {
571
- ...message.envelope,
572
- id: message.envelope.requestId as any,
573
- payload: new Request({
574
- ...message.envelope,
575
- lastSentChunk: Option.filter(
576
- message.lastSentReply,
577
- (reply): reply is Reply.Chunk<R> => reply._tag === "Chunk"
578
- )
579
- })
580
- },
581
- requestWriteOptions(entry)
582
- )
557
+ return server.write(0, requestEnvelope(entry), requestWriteOptions(entry))
583
558
  }
584
559
  case "IncomingEnvelope": {
585
560
  const entry = server.activeRequests.get(message.envelope.requestId)
@@ -795,6 +770,19 @@ const makeMessageDecode = <Rpcs extends Rpc.Any>(entityRpcs: Map<string, Rpcs>)
795
770
  }
796
771
  }
797
772
 
773
+ const requestEnvelope = (entry: {
774
+ readonly message: Message.IncomingRequestLocal<any>
775
+ readonly lastSentChunk: Option.Option<Reply.Chunk<Rpc.Any>>
776
+ }): Parameters<EntityState["write"]>[1] => ({
777
+ ...entry.message.envelope,
778
+ id: entry.message.envelope.requestId as any,
779
+ tag: entry.message.envelope.tag as any,
780
+ payload: new Request({
781
+ ...entry.message.envelope,
782
+ lastSentChunk: entry.lastSentChunk
783
+ } as any) as any
784
+ })
785
+
798
786
  const retryRespond = <A, E, R>(times: number, effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
799
787
  times === 0 ?
800
788
  effect :
@@ -48,6 +48,14 @@ export const acquireEntityType = (entityType: string): void => acquire(entityTyp
48
48
  */
49
49
  export const releaseEntityType = (entityType: string): void => release(entityTypeKey(entityType))
50
50
 
51
+ /**
52
+ * @internal
53
+ */
54
+ export const aroundEntity = <A, E, R>(
55
+ address: EntityAddress,
56
+ effect: Effect.Effect<A, E, R>
57
+ ): Effect.Effect<A, E, R> => around(entityKey(address), effect)
58
+
51
59
  /**
52
60
  * @internal
53
61
  */
@@ -71,3 +79,8 @@ export const isActive = (address: EntityAddress): boolean =>
71
79
  counts.has(entityKey(address)) ||
72
80
  counts.has(shardKey(address.shardId)) ||
73
81
  counts.has(entityTypeKey(address.entityType))
82
+
83
+ /**
84
+ * @internal
85
+ */
86
+ export const isShardActive = (shardId: ShardId): boolean => counts.has(shardKey(shardId))
@@ -2,6 +2,7 @@ import * as Context from "../../Context.ts"
2
2
  import * as Deferred from "../../Deferred.ts"
3
3
  import * as Effect from "../../Effect.ts"
4
4
  import * as Exit from "../../Exit.ts"
5
+ import * as Fiber from "../../Fiber.ts"
5
6
  import * as MutableHashMap from "../../MutableHashMap.ts"
6
7
  import * as MutableRef from "../../MutableRef.ts"
7
8
  import * as Scope from "../../Scope.ts"
@@ -44,9 +45,9 @@ export class ResourceMap<K, A, E> {
44
45
  scope,
45
46
  (exit) => {
46
47
  MutableRef.set(isClosed, true)
47
- return Effect.forEach(entries.map, ([key, { scope }]) => {
48
+ return Effect.forEach(entries.map, ([key, { fiber, scope }]) => {
48
49
  backingDelete(entries, key)
49
- return Effect.exit(Scope.close(scope, exit))
50
+ return Effect.exit(Effect.andThen(Fiber.interrupt(fiber), Scope.close(scope, exit)))
50
51
  }, { concurrency: "unbounded", discard: true })
51
52
  }
52
53
  )
@@ -67,30 +68,64 @@ export class ResourceMap<K, A, E> {
67
68
  }
68
69
 
69
70
  get(key: K): Effect.Effect<A, E> {
70
- return Effect.suspend(() => {
71
+ return Effect.uninterruptibleMask((restore) => {
71
72
  if (MutableRef.get(this.isClosed)) {
72
73
  return Effect.interrupt
73
74
  }
74
75
  const existing = backingGet(this.entries, key)
75
76
  if (existing) {
76
- return Deferred.await(existing.deferred)
77
+ return this.awaitEntry(key, existing, restore)
78
+ }
79
+ const entry: Entry<A, E> = {
80
+ scope: Scope.makeUnsafe(),
81
+ deferred: Deferred.makeUnsafe<A, E>(),
82
+ fiber: undefined as any,
83
+ awaiters: 0
84
+ }
85
+ backingSet(this.entries, key, entry)
86
+ // Run the lookup on a detached fiber so it is shared by every caller and
87
+ // only interrupted once all of them have been interrupted.
88
+ entry.fiber = Effect.runForkWith(Fiber.getCurrent()!.context)(
89
+ Effect.onExit(this.lookup(key, entry.scope), (exit) => {
90
+ if (exit._tag === "Success") {
91
+ return Deferred.done(entry.deferred, exit)
92
+ }
93
+ this.deleteEntry(key, entry)
94
+ return Effect.andThen(
95
+ Deferred.done(entry.deferred, exit),
96
+ Scope.close(entry.scope, exit)
97
+ )
98
+ })
99
+ )
100
+ return this.awaitEntry(key, entry, restore)
101
+ })
102
+ }
103
+
104
+ private awaitEntry(
105
+ key: K,
106
+ entry: Entry<A, E>,
107
+ restore: <AX, EX, RX>(effect: Effect.Effect<AX, EX, RX>) => Effect.Effect<AX, EX, RX>
108
+ ): Effect.Effect<A, E> {
109
+ if (Deferred.isDoneUnsafe(entry.deferred)) return Deferred.await(entry.deferred)
110
+ entry.awaiters++
111
+ return Effect.onExit(restore(Deferred.await(entry.deferred)), () => {
112
+ if (--entry.awaiters > 0 || Deferred.isDoneUnsafe(entry.deferred)) {
113
+ return Effect.void
77
114
  }
78
- const scope = Scope.makeUnsafe()
79
- const deferred = Deferred.makeUnsafe<A, E>()
80
- backingSet(this.entries, key, { scope, deferred })
81
- return Effect.onExit(this.lookup(key, scope), (exit) => {
82
- if (exit._tag === "Success") {
83
- return Deferred.done(deferred, exit)
84
- }
85
- backingDelete(this.entries, key)
86
- return Effect.andThen(
87
- Deferred.done(deferred, exit),
88
- Scope.close(scope, exit)
89
- )
90
- })
115
+ // Detach the abandoned entry first so new callers start a fresh lookup
116
+ // instead of joining one that is being interrupted.
117
+ this.deleteEntry(key, entry)
118
+ return Fiber.interrupt(entry.fiber)
91
119
  })
92
120
  }
93
121
 
122
+ private deleteEntry(key: K, entry: Entry<A, E>): void {
123
+ // Never delete a replacement entry created after this one was removed.
124
+ if (backingGet(this.entries, key) === entry) {
125
+ backingDelete(this.entries, key)
126
+ }
127
+ }
128
+
94
129
  remove(key: K): Effect.Effect<void> {
95
130
  return Effect.suspend(() => {
96
131
  const entry = backingGet(this.entries, key)
@@ -123,6 +158,9 @@ type BackingMap<K, A, E> = {
123
158
  type Entry<A, E> = {
124
159
  readonly scope: Scope.Closeable
125
160
  readonly deferred: Deferred.Deferred<A, E>
161
+ // Assigned as soon as the lookup is forked.
162
+ fiber: Fiber.Fiber<unknown, unknown>
163
+ awaiters: number
126
164
  }
127
165
 
128
166
  const backingGet = <K, A, E>(map: BackingMap<K, A, E>, key: K): Entry<A, E> | undefined => {