1import { describe, expect, it } from "@effect/vitest"
2import { Cause, Deferred, Duration, Effect, Exit, Fiber, Scope } from "effect"
3import { TestClock } from "effect/testing"
4
5describe("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})