1import { assert, describe, it, test } from "@effect/vitest"
2import { strictEqual } from "@effect/vitest/utils"
3import { Context, Deferred, Effect, Exit, Fiber, Layer, ManagedRuntime, Pool, Scope } from "effect"
4
5describe("ManagedRuntime", () => {
6 test("memoizes the layer build", async () => {
7 let count = 0
8 const layer = Layer.effectDiscard(Effect.sync(() => {
9 count++
10 }))
11 const runtime = ManagedRuntime.make(layer)
12 await runtime.runPromise(Effect.void)
13 await runtime.runPromise(Effect.void)
14 await runtime.dispose()
15 strictEqual(count, 1)
16 })
17
18 test("provides context", async () => {
19 const tag = Context.Service<string>("string")
20 const layer = Layer.succeed(tag)("test")
21 const runtime = ManagedRuntime.make(layer)
22 const result = await runtime.runPromise(tag)
23 await runtime.dispose()
24 strictEqual(result, "test")
25 })
26
27 test("allows sharing a MemoMap", async () => {
28 let count = 0
29 const layer = Layer.effectDiscard(Effect.sync(() => {
30 count++
31 }))
32 const runtimeA = ManagedRuntime.make(layer)
33 const runtimeB = ManagedRuntime.make(layer, { memoMap: runtimeA.memoMap })
34 await runtimeA.runPromise(Effect.void)
35 await runtimeB.runPromise(Effect.void)
36 await runtimeA.dispose()
37 await runtimeB.dispose()
38 strictEqual(count, 1)
39 })
40
41 it("can be built synchronously", () => {
42 const tag = Context.Service<string>("string")
43 const layer = Layer.succeed(tag)("test")
44 const managedRuntime = ManagedRuntime.make(layer)
45 const services = Effect.runSync(managedRuntime.contextEffect)
46 const result = Context.get(services, tag)
47 strictEqual(result, "test")
48 })
49
50 test("supports await using", async () => {
51 let count = 0
52 const layer = Layer.effectDiscard(Effect.addFinalizer(() =>
53 Effect.sync(() => {
54 count++
55 })
56 ))
57 {
58 await using runtime = ManagedRuntime.make(layer)
59 await runtime.runPromise(Effect.void)
60 strictEqual(count, 0)
61 }
62 strictEqual(count, 1)
63 })
64
65 it("fibers are interrupted on dispose", async () => {
66 const runtime = ManagedRuntime.make(Layer.empty)
67 const fiber = runtime.runFork(Effect.never)
68 await runtime.dispose()
69 const exit = fiber.pollUnsafe()
70 assert(exit)
71 assert.isTrue(Exit.hasInterrupts(exit))
72 })
73
74 for (const method of ["disposeEffect", "dispose"] as const) {
75 it(`finishes request cleanup before releasing layer resources with ${method}`, async () => {
76 const events = await Effect.runPromise(Effect.gen(function*() {
77 const Database = Context.Service<Pool.Pool<number>>("Database")
78 const gate = yield* Deferred.make<void>()
79 const started = yield* Deferred.make<void>()
80 const events: Array<string> = []
81 const layer = Layer.effect(
82 Database,
83 Pool.make({
84 size: 1,
85 acquire: Effect.acquireRelease(
86 Effect.succeed(1),
87 () =>
88 Effect.sync(() => {
89 events.push("pool closed")
90 })
91 )
92 })
93 )
94 const runtime = ManagedRuntime.make(layer)
95 yield* runtime.contextEffect
96
97 runtime.runFork(Effect.gen(function*() {
98 const pool = yield* Database
99 yield* Deferred.succeed(started, undefined)
100 return yield* Effect.ensuring(
101 Effect.never,
102 Effect.gen(function*() {
103 yield* Deferred.await(gate)
104 const exit = yield* Effect.exit(Effect.scoped(Pool.get(pool)))
105 events.push(`request cleanup: ${exit._tag}`)
106 })
107 )
108 }))
109 yield* Deferred.await(started)
110
111 if (method === "disposeEffect") {
112 const disposing = yield* Effect.forkChild(runtime.disposeEffect, { startImmediately: true })
113 yield* Deferred.succeed(gate, undefined)
114 yield* Fiber.join(disposing)
115 } else {
116 const disposing = runtime.dispose()
117 yield* Deferred.succeed(gate, undefined)
118 yield* Effect.promise(() => disposing)
119 }
120 return events
121 }))
122
123 assert.deepEqual(events, ["request cleanup: Success", "pool closed"])
124 })
125 }
126
127 it("completes sequential layer finalizers when disposeEffect is interrupted", async () => {
128 const ran = await Effect.runPromise(Effect.gen(function*() {
129 const started = yield* Deferred.make<void>()
130 const release = yield* Deferred.make<void>()
131 const ran: Array<string> = []
132 const layer = Layer.effectDiscard(Effect.gen(function*() {
133 const scope = yield* Effect.scope
134 yield* Scope.addFinalizer(
135 scope,
136 Effect.sync(() => {
137 ran.push("close database")
138 })
139 )
140 yield* Scope.addFinalizer(
141 scope,
142 Effect.gen(function*() {
143 yield* Deferred.succeed(started, undefined)
144 yield* Deferred.await(release)
145 ran.push("flush logs")
146 })
147 )
148 }))
149 const runtime = ManagedRuntime.make(layer)
150 yield* runtime.contextEffect
151
152 const closing = yield* Effect.forkChild(runtime.disposeEffect, { startImmediately: true })
153 yield* Deferred.await(started)
154 // Interrupt from a separate fiber so this fiber can release the finalizer.
155 const interrupting = yield* Effect.forkChild(Fiber.interrupt(closing), { startImmediately: true })
156 yield* Deferred.succeed(release, undefined)
157 yield* Fiber.join(interrupting)
158 yield* Fiber.await(closing)
159 yield* runtime.disposeEffect
160 return ran
161 }))
162
163 assert.deepEqual(ran, ["flush logs", "close database"])
164 })
165})