| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import { assertFalse, assertTrue, strictEqual } from "@effect/vitest/utils" |
| 3 | import { Array, Deferred, Effect, Exit, Fiber, FiberMap, Option, pipe, Ref, Scope } 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("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 | }) |