| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import { assertFalse, assertTrue, strictEqual } from "@effect/vitest/utils" |
| 3 | import { Deferred, Effect, Exit, Fiber, FiberHandle, Option, pipe, Ref } from "effect" |
| 4 | import { TestClock } from "effect/testing" |
| 5 | |
| 6 | const 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 | |
| 20 | describe("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 | }) |