| 1 | import { describe, expect, it } from "@effect/vitest" |
| 2 | import { Cause, Deferred, Duration, Effect, Exit, Fiber, Scope } from "effect" |
| 3 | import { TestClock } from "effect/testing" |
| 4 | |
| 5 | describe("Scope", () => { |
| 6 | describe("parallel finalization", () => { |
| 7 | it.effect("executes finalizers in parallel", () => |
| 8 | Effect.gen(function*() { |
| 9 | const scope = Scope.makeUnsafe("parallel") |
| 10 | yield* Scope.addFinalizer(scope, Effect.sleep(Duration.seconds(1))) |
| 11 | yield* Scope.addFinalizer(scope, Effect.sleep(Duration.seconds(1))) |
| 12 | yield* Scope.addFinalizer(scope, Effect.sleep(Duration.seconds(1))) |
| 13 | const fiber = yield* Effect.forkChild(Scope.close(scope, Exit.void), { startImmediately: true }) |
| 14 | expect(fiber.pollUnsafe()).toBeUndefined() |
| 15 | yield* TestClock.adjust(Duration.seconds(1)) |
| 16 | expect(fiber.pollUnsafe()).toBeDefined() |
| 17 | })) |
| 18 | }) |
| 19 | |
| 20 | describe("interrupted close", () => { |
| 21 | for (const strategy of ["sequential", "parallel"] as const) { |
| 22 | it.effect(strategy + ": completes direct close after interruption", () => |
| 23 | Effect.gen(function*() { |
| 24 | const scope = Scope.makeUnsafe(strategy) |
| 25 | const started = yield* Deferred.make<void>() |
| 26 | const release = yield* Deferred.make<void>() |
| 27 | const otherStarted = yield* Deferred.make<void>() |
| 28 | const ran: Array<string> = [] |
| 29 | |
| 30 | yield* Scope.addFinalizer( |
| 31 | scope, |
| 32 | Effect.gen(function*() { |
| 33 | if (strategy === "parallel") { |
| 34 | yield* Deferred.succeed(otherStarted, undefined) |
| 35 | yield* Deferred.await(release) |
| 36 | } |
| 37 | ran.push("second") |
| 38 | }) |
| 39 | ) |
| 40 | yield* Scope.addFinalizer( |
| 41 | scope, |
| 42 | Effect.gen(function*() { |
| 43 | yield* Deferred.succeed(started, undefined) |
| 44 | yield* Deferred.await(release) |
| 45 | ran.push("first") |
| 46 | }) |
| 47 | ) |
| 48 | |
| 49 | const closing = yield* Effect.forkChild(Scope.close(scope, Exit.void), { startImmediately: true }) |
| 50 | yield* Deferred.await(started) |
| 51 | if (strategy === "parallel") yield* Deferred.await(otherStarted) |
| 52 | const interrupting = yield* Effect.forkChild(Fiber.interrupt(closing), { startImmediately: true }) |
| 53 | yield* Effect.yieldNow |
| 54 | expect(interrupting.pollUnsafe()).toBeUndefined() |
| 55 | yield* Deferred.succeed(release, undefined) |
| 56 | yield* Fiber.join(interrupting) |
| 57 | yield* Fiber.await(closing) |
| 58 | yield* Scope.close(scope, Exit.void) |
| 59 | if (strategy === "parallel") ran.sort() |
| 60 | expect(ran).toEqual(["first", "second"]) |
| 61 | })) |
| 62 | } |
| 63 | |
| 64 | it.effect("closeUnsafe runs finalizers when its returned effect is masked by the caller", () => |
| 65 | Effect.gen(function*() { |
| 66 | const scope = Scope.makeUnsafe() |
| 67 | const started = yield* Deferred.make<void>() |
| 68 | const release = yield* Deferred.make<void>() |
| 69 | const ran: Array<string> = [] |
| 70 | yield* Scope.addFinalizer( |
| 71 | scope, |
| 72 | Effect.sync(() => { |
| 73 | ran.push("second") |
| 74 | }) |
| 75 | ) |
| 76 | yield* Scope.addFinalizer( |
| 77 | scope, |
| 78 | Effect.gen(function*() { |
| 79 | yield* Deferred.succeed(started, undefined) |
| 80 | yield* Deferred.await(release) |
| 81 | ran.push("first") |
| 82 | }) |
| 83 | ) |
| 84 | |
| 85 | const finalizers = Scope.closeUnsafe(scope, Exit.void) |
| 86 | expect(finalizers).toBeDefined() |
| 87 | if (finalizers === undefined) throw new Error("expected finalizers") |
| 88 | const closing = yield* Effect.forkChild(Effect.uninterruptible(finalizers), { startImmediately: true }) |
| 89 | yield* Deferred.await(started) |
| 90 | const interrupting = yield* Effect.forkChild(Fiber.interrupt(closing), { startImmediately: true }) |
| 91 | yield* Deferred.succeed(release, undefined) |
| 92 | yield* Fiber.join(interrupting) |
| 93 | expect(ran).toEqual(["first", "second"]) |
| 94 | })) |
| 95 | }) |
| 96 | |
| 97 | describe("finalizers that throw", () => { |
| 98 | const reasons = (exit: Exit.Exit<unknown, unknown>) => |
| 99 | Exit.isSuccess(exit) ? [] : exit.cause.reasons.map((reason) => |
| 100 | Cause.isFailReason(reason) |
| 101 | ? `fail:${String(reason.error)}` |
| 102 | : Cause.isDieReason(reason) |
| 103 | ? `die:${String(reason.defect)}` |
| 104 | : "interrupt" |
| 105 | ) |
| 106 | for (const strategy of ["sequential", "parallel"] as const) { |
| 107 | it.effect(`${strategy}: continues and awaits remaining finalizers after a throw`, () => |
| 108 | Effect.gen(function*() { |
| 109 | const scope = Scope.makeUnsafe(strategy) |
| 110 | const log: Array<string> = [] |
| 111 | yield* Scope.addFinalizer( |
| 112 | scope, |
| 113 | Effect.gen(function*() { |
| 114 | yield* Effect.yieldNow |
| 115 | log.push("remaining") |
| 116 | }) |
| 117 | ) |
| 118 | yield* Scope.addFinalizerExit(scope, () => { |
| 119 | log.push("throwing") |
| 120 | throw "release" |
| 121 | }) |
| 122 | const exit = yield* Effect.exit(Scope.close(scope, Exit.void)) |
| 123 | expect(log).toEqual(["throwing", "remaining"]) |
| 124 | expect(reasons(exit)).toEqual(["die:release"]) |
| 125 | })) |
| 126 | } |
| 127 | |
| 128 | it.effect("a sole throwing finalizer keeps the scoped effect's failure", () => |
| 129 | Effect.gen(function*() { |
| 130 | const exit = yield* Effect.gen(function*() { |
| 131 | const scope = yield* Effect.scope |
| 132 | yield* Scope.addFinalizerExit(scope, () => { |
| 133 | throw "release" |
| 134 | }) |
| 135 | return yield* Effect.fail("error") |
| 136 | }).pipe(Effect.scoped, Effect.exit) |
| 137 | expect(reasons(exit)).toEqual(["fail:error", "die:release"]) |
| 138 | })) |
| 139 | }) |
| 140 | |
| 141 | describe("fork", () => { |
| 142 | it.effect("closing a child removes it from the parent", () => |
| 143 | Effect.gen(function*() { |
| 144 | const parent = Scope.makeUnsafe() |
| 145 | const child = Scope.forkUnsafe(parent) |
| 146 | expect(child.state._tag).toBe("Empty") |
| 147 | expect(parent.state._tag).toBe("Open") |
| 148 | yield* Scope.close(child, Exit.void) |
| 149 | expect(parent.state._tag).toBe("Empty") |
| 150 | })) |
| 151 | |
| 152 | it.effect("a throwing child finalizer does not leave the child in its parent", () => |
| 153 | Effect.gen(function*() { |
| 154 | const parent = Scope.makeUnsafe() |
| 155 | const child = Scope.forkUnsafe(parent) |
| 156 | yield* Scope.addFinalizerExit(child, () => { |
| 157 | throw new Error("boom") |
| 158 | }) |
| 159 | const result = yield* Effect.exit(Scope.close(child, Exit.void)) |
| 160 | expect(Exit.isFailure(result)).toBe(true) |
| 161 | expect(child.state._tag).toBe("Closed") |
| 162 | expect(parent.state._tag).toBe("Empty") |
| 163 | })) |
| 164 | |
| 165 | it.effect("an interrupted close does not leave the child in its parent", () => |
| 166 | Effect.gen(function*() { |
| 167 | const parent = Scope.makeUnsafe() |
| 168 | const child = Scope.forkUnsafe(parent) |
| 169 | const entered = yield* Deferred.make<void>() |
| 170 | const release = yield* Deferred.make<void>() |
| 171 | yield* Scope.addFinalizer( |
| 172 | child, |
| 173 | Deferred.succeed(entered, void 0).pipe(Effect.andThen(Deferred.await(release))) |
| 174 | ) |
| 175 | const fiber = yield* Effect.forkChild(Scope.close(child, Exit.void), { startImmediately: true }) |
| 176 | yield* Deferred.await(entered) |
| 177 | const interrupting = yield* Effect.forkChild(Fiber.interrupt(fiber), { startImmediately: true }) |
| 178 | yield* Deferred.succeed(release, undefined) |
| 179 | yield* Fiber.join(interrupting) |
| 180 | expect(child.state._tag).toBe("Closed") |
| 181 | expect(parent.state._tag).toBe("Empty") |
| 182 | })) |
| 183 | |
| 184 | it.effect("closing the parent closes the child with the parent's exit only once", () => |
| 185 | Effect.gen(function*() { |
| 186 | const parent = Scope.makeUnsafe() |
| 187 | const child = Scope.forkUnsafe(parent) |
| 188 | const exits: Array<Exit.Exit<unknown, unknown>> = [] |
| 189 | yield* Scope.addFinalizerExit(child, (exit) => |
| 190 | Effect.sync(() => { |
| 191 | exits.push(exit) |
| 192 | })) |
| 193 | const failure = Exit.fail("parent failed") |
| 194 | yield* Scope.close(parent, failure) |
| 195 | yield* Scope.close(child, Exit.void) |
| 196 | expect(exits).toEqual([failure]) |
| 197 | })) |
| 198 | }) |
| 199 | }) |