1import { assert, describe, it } from "@effect/vitest"
2import { assertFalse, assertTrue, strictEqual } from "@effect/vitest/utils"
3import { Deferred, Effect, Exit, Fiber, FiberHandle, Option, pipe, Ref } 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("FiberHandle", () => {
21 it.effect("run defers startup", () =>
22 Effect.gen(function*() {
23 const container = yield* FiberHandle.make()
24 let started = false
25 const fiber = yield* FiberHandle.run(
26 container,
27 Effect.sync(() => {
28 started = true
29 }),
30 { startImmediately: false }
31 )
32 assert.isFalse(started)
33 yield* Fiber.join(fiber)
34 assert.isTrue(started)
35 }))
36
37 it.effect("interrupts the current fiber when the scope closes", () =>
38 Effect.gen(function*() {
39 const ref = yield* (Ref.make(0))
40 yield* pipe(
41 Effect.gen(function*() {
42 const handle = yield* FiberHandle.make()
43 yield* FiberHandle.run(handle, Effect.onInterrupt(Effect.never, () => Ref.update(ref, (n) => n + 1)))
44 yield* Effect.yieldNow
45 }),
46 Effect.scoped
47 )
48
49 strictEqual(yield* (Ref.get(ref)), 1)
50 }))
51
52 it.effect("runtime", () =>
53 Effect.gen(function*() {
54 const ref = yield* Ref.make(0)
55 yield* pipe(
56 Effect.gen(function*() {
57 const handle = yield* FiberHandle.make()
58 const run = yield* FiberHandle.runtime(handle)<never>()
59 run(Effect.onInterrupt(Effect.never, () => Ref.update(ref, (n) => n + 1)))
60 yield* Effect.yieldNow
61 run(Effect.onInterrupt(Effect.never, () => Ref.update(ref, (n) => n + 1)))
62 yield* Effect.yieldNow
63 run(Effect.onInterrupt(Effect.never, () => Ref.update(ref, (n) => n + 1)), {
64 onlyIfMissing: true
65 })
66 yield* Effect.yieldNow
67 strictEqual(yield* Ref.get(ref), 1)
68 }),
69 Effect.scoped
70 )
71
72 strictEqual(yield* Ref.get(ref), 2)
73 }))
74
75 it.effect("get and getUnsafe", () =>
76 Effect.gen(function*() {
77 const handle = yield* FiberHandle.make<string>()
78
79 assert.deepStrictEqual(FiberHandle.getUnsafe(handle), Option.none())
80 assert.deepStrictEqual(yield* FiberHandle.get(handle), Option.none())
81
82 const fiber = yield* FiberHandle.run(handle, Effect.never)
83
84 const unsafeFiber = FiberHandle.getUnsafe(handle)
85 if (Option.isNone(unsafeFiber)) {
86 assert.fail("expected Option.some from getUnsafe")
87 return
88 }
89 strictEqual(unsafeFiber.value, fiber)
90
91 const safeFiber = yield* FiberHandle.get(handle)
92 if (Option.isNone(safeFiber)) {
93 assert.fail("expected Option.some from get")
94 return
95 }
96 strictEqual(safeFiber.value, fiber)
97 }))
98
99 it.effect("join ignores managed replacement interruptions and fails with child errors", () =>
100 Effect.gen(function*() {
101 const handle = yield* FiberHandle.make()
102 FiberHandle.setUnsafe(handle, Effect.runFork(Effect.void))
103 FiberHandle.setUnsafe(handle, Effect.runFork(Effect.fail("fail")))
104 const result = yield* Effect.flip(FiberHandle.join(handle))
105 strictEqual(result, "fail")
106 }))
107
108 it.effect("onlyIfMissing keeps the current fiber and interrupts rejected starts", () =>
109 Effect.gen(function*() {
110 const handle = yield* FiberHandle.make()
111 const fiberA = yield* FiberHandle.run(handle, Effect.never)
112 const fiberB = yield* FiberHandle.run(handle, Effect.never, { onlyIfMissing: true })
113 const fiberC = yield* FiberHandle.run(handle, Effect.never, { onlyIfMissing: true })
114 yield* Effect.yieldNow
115 assertTrue(Exit.hasInterrupts(yield* Fiber.await(fiberB)))
116 assertTrue(Exit.hasInterrupts(yield* Fiber.await(fiberC)))
117 strictEqual(fiberA.pollUnsafe(), undefined)
118 }))
119
120 it.effect("onlyIfMissing keeps the same registered fiber", () =>
121 Effect.gen(function*() {
122 const handle = yield* FiberHandle.make()
123 const worker = yield* makeWorker
124 FiberHandle.setUnsafe(handle, worker.fiber)
125 FiberHandle.setUnsafe(handle, worker.fiber, { onlyIfMissing: true })
126
127 strictEqual(Option.getOrUndefined(FiberHandle.getUnsafe(handle)), worker.fiber)
128 strictEqual(worker.fiber.pollUnsafe(), undefined)
129 strictEqual(yield* Ref.get(worker.cleanups), 0)
130 }))
131
132 for (const propagateInterruption of [false, true]) {
133 it.effect(`same-fiber registration preserves propagateInterruption: ${propagateInterruption}`, () =>
134 Effect.gen(function*() {
135 const handle = yield* FiberHandle.make()
136 const worker = yield* makeWorker
137 FiberHandle.setUnsafe(handle, worker.fiber, { propagateInterruption })
138 FiberHandle.setUnsafe(handle, worker.fiber, {
139 onlyIfMissing: true,
140 propagateInterruption: !propagateInterruption
141 })
142
143 yield* Fiber.interrupt(worker.fiber)
144 strictEqual(yield* Deferred.isDone(handle.deferred), propagateInterruption)
145 }))
146 }
147
148 it.effect("clear does not remove a newer fiber installed while interrupting the previous one", () =>
149 Effect.gen(function*() {
150 const handle = yield* FiberHandle.make()
151 yield* FiberHandle.run(handle, Effect.uninterruptible(Effect.sleep(200)))
152
153 const clearFiber = yield* Effect.forkChild(FiberHandle.clear(handle), { startImmediately: true })
154 yield* TestClock.adjust(50)
155 const nextFiber = yield* FiberHandle.run(handle, Effect.never)
156 yield* TestClock.adjust(200)
157 yield* Fiber.join(clearFiber)
158
159 const current = FiberHandle.getUnsafe(handle)
160 if (Option.isNone(current)) {
161 assert.fail("expected FiberHandle.clear to preserve the newer fiber")
162 return
163 }
164 strictEqual(current.value, nextFiber)
165 strictEqual(nextFiber.pollUnsafe(), undefined)
166 }))
167
168 it.effect("runtime onlyIfMissing", () =>
169 Effect.gen(function*() {
170 const run = yield* FiberHandle.makeRuntime<never>()
171 const fiberA = run(Effect.never)
172 const fiberB = run(Effect.never, { onlyIfMissing: true })
173 const fiberC = run(Effect.never, { onlyIfMissing: true })
174 yield* Effect.yieldNow
175 assertTrue(Exit.hasInterrupts(yield* Fiber.await(fiberB)))
176 assertTrue(Exit.hasInterrupts(yield* Fiber.await(fiberC)))
177 strictEqual(fiberA.pollUnsafe(), undefined)
178 }))
179
180 it.effect("propagateInterruption: false", () =>
181 Effect.gen(function*() {
182 const handle = yield* FiberHandle.make()
183 const fiber = yield* FiberHandle.run(handle, Effect.never, {
184 propagateInterruption: false
185 })
186 yield* Effect.yieldNow
187 yield* Fiber.interrupt(fiber)
188 assertFalse(yield* Deferred.isDone(handle.deferred))
189 }))
190
191 it.effect("propagateInterruption: true", () =>
192 Effect.gen(function*() {
193 const handle = yield* FiberHandle.make()
194 const fiber = yield* FiberHandle.run(handle, Effect.never, {
195 propagateInterruption: true
196 })
197 yield* Effect.yieldNow
198 yield* Fiber.interrupt(fiber)
199 assertTrue(Exit.hasInterrupts(
200 yield* FiberHandle.join(handle).pipe(
201 Effect.exit
202 )
203 ))
204 }))
205
206 it.effect("awaitEmpty", () =>
207 Effect.gen(function*() {
208 const handle = yield* FiberHandle.make()
209 yield* FiberHandle.run(handle, Effect.sleep(1000))
210
211 const fiber = yield* Effect.forkChild(FiberHandle.awaitEmpty(handle))
212 yield* TestClock.adjust(500)
213 assert.isUndefined(fiber.pollUnsafe())
214 yield* TestClock.adjust(500)
215 assert.isDefined(fiber.pollUnsafe())
216 }))
217
218 it.effect("makeRuntimePromise", () =>
219 Effect.gen(function*() {
220 const run = yield* FiberHandle.makeRuntimePromise()
221 const result = yield* Effect.promise(() => run(Effect.succeed("done")))
222 strictEqual(result, "done")
223 }))
224})