1import { assert, describe, it } from "@effect/vitest"
2import { assertFalse, assertTrue, strictEqual } from "@effect/vitest/utils"
3import { Array, Deferred, Effect, Exit, Fiber, FiberMap, Option, pipe, Ref, Scope } from "effect"
4import { TestClock } from "effect/testing"
5
6const makeWorker = Effect.gen(function*() {
7 const ready = yield* Deferred.make<void>()
8 const cleanups = yield* Ref.make(0)
9 const fiber = yield* Effect.forkScoped(
10 Deferred.succeed(ready, undefined).pipe(
11 Effect.andThen(Effect.never),
12 Effect.ensuring(Ref.update(cleanups, (n) => n + 1))
13 ),
14 { startImmediately: true }
15 )
16 yield* Deferred.await(ready)
17 return { fiber, cleanups }
18})
19
20describe("FiberMap", () => {
21 it.effect("run defers startup", () =>
22 Effect.gen(function*() {
23 const container = yield* FiberMap.make<string>()
24 let started = false
25 const fiber = yield* FiberMap.run(
26 container,
27 "key",
28 Effect.sync(() => {
29 started = true
30 }),
31 { startImmediately: false }
32 )
33 assert.isFalse(started)
34 yield* Fiber.join(fiber)
35 assert.isTrue(started)
36 }))
37
38 it.effect("retains ownership of replacements made by a synchronous finalizer", () =>
39 Effect.gen(function*() {
40 const scope = yield* Scope.make()
41 const map = yield* FiberMap.make<string>().pipe(Scope.provide(scope))
42 const run = yield* FiberMap.runtime(map)()
43 const fibers: Array<Fiber.Fiber<unknown, unknown>> = []
44 yield* Effect.addFinalizer(() => Fiber.interruptAll(fibers))
45
46 const previous = run(
47 "key",
48 Effect.never.pipe(
49 Effect.ensuring(Effect.sync(() => {
50 fibers.push(run("key", Effect.never))
51 }))
52 )
53 )
54 const replacement = run("key", Effect.never)
55 fibers.push(previous, replacement)
56 assert.strictEqual(fibers.length, 3)
57
58 yield* Scope.close(scope, Exit.void)
59
60 for (const fiber of fibers) {
61 assert.isDefined(fiber.pollUnsafe())
62 }
63 }))
64
65 it.effect("removes a synchronously completed replacement", () =>
66 Effect.gen(function*() {
67 const map = yield* FiberMap.make<string>()
68 const previous = yield* FiberMap.run(map, "key", Effect.never)
69 const replacement = yield* FiberMap.run(map, "key", Effect.succeed("done"))
70
71 assert.deepStrictEqual(yield* Fiber.join(replacement), "done")
72 assert.isTrue(Exit.hasInterrupts(yield* Fiber.await(previous)))
73 assert.strictEqual(yield* FiberMap.size(map), 0)
74 }))
75
76 it.effect("interrupts fibers", () =>
77 Effect.gen(function*() {
78 const ref = yield* Ref.make(0)
79 yield* pipe(
80 Effect.gen(function*() {
81 const map = yield* FiberMap.make<number>()
82 yield* (
83 Effect.forEach(Array.range(1, 10), (i) =>
84 Effect.onInterrupt(
85 Effect.never,
86 () => Ref.update(ref, (n) => n + 1)
87 ).pipe(
88 FiberMap.run(map, i)
89 ))
90 )
91 yield* Effect.yieldNow
92 }),
93 Effect.scoped
94 )
95
96 strictEqual(yield* (Ref.get(ref)), 10)
97 }))
98
99 it.effect("runtime", () =>
100 Effect.gen(function*() {
101 const ref = yield* Ref.make(0)
102 yield* pipe(
103 Effect.gen(function*() {
104 const map = yield* FiberMap.make<number>()
105 const run = yield* FiberMap.runtime(map)<never>()
106 Array.range(1, 10).forEach((i) =>
107 run(
108 i,
109 Effect.onInterrupt(
110 Effect.never,
111 () => Ref.update(ref, (n) => n + 1)
112 )
113 )
114 )
115 yield* Effect.yieldNow
116 }),
117 Effect.scoped
118 )
119
120 strictEqual(yield* (Ref.get(ref)), 10)
121 }))
122
123 it.effect("get and getUnsafe", () =>
124 Effect.gen(function*() {
125 const map = yield* FiberMap.make<string, string>()
126
127 assert.deepStrictEqual(FiberMap.getUnsafe(map, "a"), Option.none())
128 assert.deepStrictEqual(yield* FiberMap.get(map, "a"), Option.none())
129
130 const fiber = yield* FiberMap.run(map, "a", Effect.never)
131
132 const unsafeFiber = FiberMap.getUnsafe(map, "a")
133 if (Option.isNone(unsafeFiber)) {
134 assert.fail("expected Option.some from getUnsafe")
135 return
136 }
137 strictEqual(unsafeFiber.value, fiber)
138
139 const safeFiber = yield* FiberMap.get(map, "a")
140 if (Option.isNone(safeFiber)) {
141 assert.fail("expected Option.some from get")
142 return
143 }
144 strictEqual(safeFiber.value, fiber)
145 }))
146
147 it.effect("join", () =>
148 Effect.gen(function*() {
149 const map = yield* FiberMap.make<string>()
150 FiberMap.setUnsafe(map, "a", Effect.runFork(Effect.void))
151 FiberMap.setUnsafe(map, "b", Effect.runFork(Effect.void))
152 FiberMap.setUnsafe(map, "c", Effect.runFork(Effect.fail("fail")))
153 FiberMap.setUnsafe(map, "d", Effect.runFork(Effect.fail("ignored")))
154 const result = yield* pipe(FiberMap.join(map), Effect.flip)
155 strictEqual(result, "fail")
156 }))
157
158 it.effect("size", () =>
159 Effect.gen(function*() {
160 const scope = yield* Scope.make()
161 const set = yield* pipe(FiberMap.make<string>(), Scope.provide(scope))
162 FiberMap.setUnsafe(set, "a", Effect.runFork(Effect.never))
163 FiberMap.setUnsafe(set, "b", Effect.runFork(Effect.never))
164 strictEqual(yield* FiberMap.size(set), 2)
165 yield* Scope.close(scope, Exit.void)
166 strictEqual(yield* FiberMap.size(set), 0)
167 }))
168
169 it.effect("onlyIfMissing", () =>
170 Effect.gen(function*() {
171 const handle = yield* FiberMap.make<string>()
172 const fiberA = yield* FiberMap.run(handle, "a", Effect.never)
173 const fiberB = yield* FiberMap.run(handle, "a", Effect.never, { onlyIfMissing: true })
174 const fiberC = yield* FiberMap.run(handle, "a", Effect.never, { onlyIfMissing: true })
175 yield* Effect.yieldNow
176 assertTrue(Exit.hasInterrupts(yield* Fiber.await(fiberB)))
177 assertTrue(Exit.hasInterrupts(yield* Fiber.await(fiberC)))
178 strictEqual(fiberA.pollUnsafe(), undefined)
179 }))
180
181 it.effect("onlyIfMissing keeps the same registered fiber", () =>
182 Effect.gen(function*() {
183 const map = yield* FiberMap.make<string>()
184 const worker = yield* makeWorker
185 FiberMap.setUnsafe(map, "key", worker.fiber)
186 FiberMap.setUnsafe(map, "key", worker.fiber, { onlyIfMissing: true })
187
188 strictEqual(Option.getOrUndefined(FiberMap.getUnsafe(map, "key")), worker.fiber)
189 strictEqual(worker.fiber.pollUnsafe(), undefined)
190 strictEqual(yield* Ref.get(worker.cleanups), 0)
191 }))
192
193 for (const propagateInterruption of [false, true]) {
194 it.effect(`same-fiber registration preserves propagateInterruption: ${propagateInterruption}`, () =>
195 Effect.gen(function*() {
196 const map = yield* FiberMap.make<string>()
197 const worker = yield* makeWorker
198 FiberMap.setUnsafe(map, "key", worker.fiber, { propagateInterruption })
199 FiberMap.setUnsafe(map, "key", worker.fiber, {
200 onlyIfMissing: true,
201 propagateInterruption: !propagateInterruption
202 })
203
204 yield* Fiber.interrupt(worker.fiber)
205 strictEqual(yield* Deferred.isDone(map.deferred), propagateInterruption)
206 }))
207 }
208
209 it.effect("runtime onlyIfMissing", () =>
210 Effect.gen(function*() {
211 const run = yield* FiberMap.makeRuntime<never, string>()
212 const fiberA = run("a", Effect.never)
213 const fiberB = run("a", Effect.never, { onlyIfMissing: true })
214 const fiberC = run("a", Effect.never, { onlyIfMissing: true })
215 yield* Effect.yieldNow
216 assertTrue(Exit.hasInterrupts(yield* Fiber.await(fiberB)))
217 assertTrue(Exit.hasInterrupts(yield* Fiber.await(fiberC)))
218 strictEqual(fiberA.pollUnsafe(), undefined)
219 }))
220
221 it.effect("propagateInterruption false", () =>
222 Effect.gen(function*() {
223 const map = yield* FiberMap.make<string>()
224 const fiber = yield* FiberMap.run(map, "a", Effect.never, {
225 propagateInterruption: false
226 })
227 yield* Effect.yieldNow
228 yield* Fiber.interrupt(fiber)
229 assertFalse(yield* Deferred.isDone(map.deferred))
230 }))
231
232 it.effect("propagateInterruption true", () =>
233 Effect.gen(function*() {
234 const map = yield* FiberMap.make<string>()
235 const fiber = yield* FiberMap.run(map, "a", Effect.never, {
236 propagateInterruption: true
237 })
238 yield* Effect.yieldNow
239 yield* Fiber.interrupt(fiber)
240 assertTrue(Exit.hasInterrupts(
241 yield* FiberMap.join(map).pipe(
242 Effect.exit
243 )
244 ))
245 }))
246
247 it.effect("awaitEmpty", () =>
248 Effect.gen(function*() {
249 const map = yield* FiberMap.make<string>()
250 yield* FiberMap.run(map, "a", Effect.sleep(1000))
251 yield* FiberMap.run(map, "b", Effect.sleep(1000))
252 yield* FiberMap.run(map, "c", Effect.sleep(1000))
253 yield* FiberMap.run(map, "d", Effect.sleep(1000))
254
255 const fiber = yield* Effect.forkChild(FiberMap.awaitEmpty(map))
256 yield* TestClock.adjust(500)
257 assert.isUndefined(fiber.pollUnsafe())
258 yield* TestClock.adjust(500)
259 assert.isDefined(fiber.pollUnsafe())
260 }))
261
262 it.effect("makeRuntimePromise", () =>
263 Effect.gen(function*() {
264 const run = yield* FiberMap.makeRuntimePromise<never, string>()
265 const result = yield* Effect.promise(() => run("a", Effect.succeed("done")))
266 strictEqual(result, "done")
267 }))
268})