1import { assert, describe, it } from "@effect/vitest"
2import { Deferred, Effect, Exit, Fiber, Scope } from "effect"
3import { ResourceMap } from "effect/cluster/internal/resourceMap"
4
5describe("ResourceMap", () => {
6 it.effect("closes a failed lookup scope", () =>
7 Effect.scoped(Effect.gen(function*() {
8 let finalized = 0
9 const map = yield* ResourceMap.make((_key: string) =>
10 Effect.gen(function*() {
11 yield* Effect.addFinalizer(() => Effect.sync(() => finalized++))
12 return yield* Effect.fail("failed")
13 })
14 )
15 yield* Effect.exit(map.get("key"))
16 assert.strictEqual(finalized, 1)
17 })))
18
19 it.effect("shares a lookup until every get is interrupted, then starts fresh", () =>
20 Effect.scoped(Effect.gen(function*() {
21 let lookups = 0
22 const started = yield* Deferred.make<void>()
23 const finalizing = yield* Deferred.make<void>()
24 const finishFinalizer = yield* Deferred.make<void>()
25 const map = yield* ResourceMap.make((_key: string) =>
26 Effect.suspend(() => {
27 if (++lookups > 1) return Effect.succeed(lookups)
28 return Deferred.succeed(started, void 0).pipe(
29 Effect.andThen(Effect.never),
30 Effect.onInterrupt(() =>
31 Deferred.succeed(finalizing, void 0).pipe(Effect.andThen(Deferred.await(finishFinalizer)))
32 )
33 )
34 })
35 )
36
37 const first = yield* Effect.forkChild(map.get("key"), { startImmediately: true })
38 yield* Deferred.await(started)
39 const second = yield* Effect.forkChild(map.get("key"), { startImmediately: true })
40 yield* Fiber.interrupt(first)
41 assert.isFalse(yield* Deferred.isDone(finalizing))
42
43 // A get made while the abandoned lookup finalizes starts a fresh one.
44 const interruptSecond = yield* Effect.forkChild(Fiber.interrupt(second), { startImmediately: true })
45 yield* Deferred.await(finalizing)
46 const fresh = yield* Effect.forkChild(map.get("key"), { startImmediately: true })
47 yield* Deferred.succeed(finishFinalizer, void 0)
48 yield* Fiber.join(interruptSecond)
49 assert.deepStrictEqual(yield* Fiber.await(fresh), Exit.succeed(2))
50 })))
51
52 it.effect("an abandoned lookup does not remove a replacement entry", () =>
53 Effect.scoped(Effect.gen(function*() {
54 let lookups = 0
55 const started = yield* Deferred.make<void>()
56 const release = yield* Deferred.make<void>()
57 const map = yield* ResourceMap.make((_key: string) =>
58 Effect.suspend(() => {
59 const lookup = ++lookups
60 if (lookup === 1) return Deferred.succeed(started, void 0).pipe(Effect.andThen(Effect.never))
61 return Deferred.await(release).pipe(Effect.as(lookup))
62 })
63 )
64
65 const abandoned = yield* Effect.forkChild(map.get("key"), { startImmediately: true })
66 yield* Deferred.await(started)
67 yield* map.remove("key")
68 const replacement = yield* Effect.forkChild(map.get("key"), { startImmediately: true })
69 yield* Fiber.interrupt(abandoned)
70 const joiner = yield* Effect.forkChild(map.get("key"), { startImmediately: true })
71
72 yield* Deferred.succeed(release, void 0)
73 assert.deepStrictEqual(yield* Fiber.await(replacement), Exit.succeed(2))
74 assert.deepStrictEqual(yield* Fiber.await(joiner), Exit.succeed(2))
75 })))
76
77 it.effect("closing the map interrupts a pending lookup and closes its scope", () =>
78 Effect.gen(function*() {
79 let finalized = 0
80 const started = yield* Deferred.make<void>()
81 const mapScope = yield* Scope.make()
82 const map = yield* ResourceMap.make((_key: string) =>
83 Effect.gen(function*() {
84 yield* Effect.addFinalizer(() =>
85 Effect.sync(() => {
86 finalized++
87 })
88 )
89 yield* Deferred.succeed(started, void 0)
90 return yield* Effect.never
91 })
92 ).pipe(Scope.provide(mapScope))
93
94 const getter = yield* Effect.forkChild(map.get("key"), { startImmediately: true })
95 yield* Deferred.await(started)
96 yield* Scope.close(mapScope, Exit.void)
97
98 assert.strictEqual(finalized, 1)
99 assert.isTrue(Exit.hasInterrupts(yield* Fiber.await(getter)))
100 }))
101})