1import { assert, describe, it } from "@effect/vitest"
2import { strictEqual } from "@effect/vitest/utils"
3import { Effect, Exit, Fiber, identity, Latch, pipe, Ref, Scope, ScopedRef } from "effect"
4import * as Counter from "./utils/counter.ts"
5
6describe("ScopedRef", () => {
7 it.effect("single set", () =>
8 Effect.gen(function*() {
9 const counter = yield* Counter.make()
10 const ref = yield* ScopedRef.make(() => 0)
11 yield* ScopedRef.set(ref, counter.acquire())
12 const result = yield* (ScopedRef.get(ref))
13 strictEqual(result, 1)
14 }))
15 it.effect("dual set", () =>
16 Effect.gen(function*() {
17 const counter = yield* Counter.make()
18 const ref = yield* ScopedRef.make(() => 0)
19 yield* pipe(
20 ScopedRef.set(ref, counter.acquire()),
21 Effect.andThen(ScopedRef.set(ref, counter.acquire()))
22 )
23 const result = yield* ScopedRef.get(ref)
24 strictEqual(result, 2)
25 }))
26 it.effect("releases the previous resource when replaced", () =>
27 Effect.gen(function*() {
28 const counter = yield* Counter.make()
29 const ref = yield* ScopedRef.make(() => 0)
30 yield* pipe(
31 ScopedRef.set(ref, counter.acquire()),
32 Effect.andThen(ScopedRef.set(ref, counter.acquire()))
33 )
34
35 const acquired = yield* counter.acquired()
36 const released = yield* counter.released()
37 strictEqual(acquired, 2)
38 strictEqual(released, 1)
39 }))
40 it.effect("releases each previous resource across multiple replacements", () =>
41 Effect.gen(function*() {
42 const counter = yield* Counter.make()
43 const ref = yield* ScopedRef.make(() => 0)
44 yield* (
45 pipe(
46 ScopedRef.set(ref, counter.acquire()),
47 Effect.andThen(ScopedRef.set(ref, counter.acquire())),
48 Effect.andThen(ScopedRef.set(ref, counter.acquire()))
49 )
50 )
51 const acquired = yield* counter.acquired()
52 const released = yield* counter.released()
53 strictEqual(acquired, 3)
54 strictEqual(released, 2)
55 }))
56 it.effect("releases the current resource when the scoped ref scope closes", () =>
57 Effect.gen(function*() {
58 const counter = yield* Counter.make()
59 yield* pipe(
60 ScopedRef.make(() => 0),
61 Effect.flatMap((ref) =>
62 pipe(
63 ScopedRef.set(ref, counter.acquire()),
64 Effect.andThen(ScopedRef.set(ref, counter.acquire())),
65 Effect.andThen(ScopedRef.set(ref, counter.acquire()))
66 )
67 ),
68 Effect.scoped
69 )
70 const acquired = yield* counter.acquired()
71 const released = yield* counter.released()
72 strictEqual(acquired, 3)
73 strictEqual(released, 3)
74 }))
75 it.effect("owner finalizers registered after the ref still see a live value after set", () =>
76 Effect.gen(function*() {
77 const log: Array<string> = []
78 const resource = (n: number) =>
79 Effect.acquireRelease(Effect.succeed(n), () => Effect.sync(() => log.push("release " + n)))
80 const owner = yield* Scope.make()
81 const ref = yield* ScopedRef.fromAcquire(resource(1)).pipe(Scope.provide(owner))
82 yield* Scope.addFinalizer(
83 owner,
84 Effect.flatMap(ScopedRef.get(ref), (n) => Effect.sync(() => log.push("use " + n)))
85 )
86 yield* ScopedRef.set(ref, resource(2))
87 yield* Scope.close(owner, Exit.void)
88 assert.deepStrictEqual(log, ["release 1", "use 2", "release 2"])
89 }))
90 it.effect("keeps the current resource when replacement acquisition fails", () =>
91 Effect.gen(function*() {
92 let released = false
93 const ref = yield* ScopedRef.fromAcquire(
94 Effect.acquireRelease(Effect.succeed(1), () =>
95 Effect.sync(() => {
96 released = true
97 }))
98 )
99
100 yield* ScopedRef.set(ref, Effect.fail("boom")).pipe(Effect.catch(() => Effect.void))
101
102 strictEqual(released, false, "failed replacement must not release the current resource")
103 strictEqual(yield* ScopedRef.get(ref), 1)
104 }))
105 it.effect("releases a replacement when the old finalizer defects", () =>
106 Effect.gen(function*() {
107 const oldReleased = yield* Ref.make(0)
108 const replacementAcquired = yield* Ref.make(0)
109 const replacementReleased = yield* Ref.make(0)
110 const ownerScope = yield* Scope.make()
111 const ref = yield* ScopedRef.fromAcquire(
112 Effect.acquireRelease(
113 Effect.succeed(0),
114 () =>
115 Ref.update(oldReleased, (n) => n + 1).pipe(
116 Effect.andThen(Effect.die("old-release-defect"))
117 )
118 )
119 ).pipe(Scope.provide(ownerScope))
120
121 const setExit = yield* ScopedRef.set(
122 ref,
123 Effect.acquireRelease(
124 Ref.updateAndGet(replacementAcquired, (n) => n + 1),
125 () => Ref.update(replacementReleased, (n) => n + 1)
126 )
127 ).pipe(Effect.exit)
128 strictEqual(yield* ScopedRef.get(ref), 0)
129 const ownerCloseExit = yield* Scope.close(ownerScope, Exit.void).pipe(Effect.exit)
130
131 assert.deepStrictEqual(setExit, Exit.die("old-release-defect"))
132 assert.deepStrictEqual(ownerCloseExit, Exit.void)
133 strictEqual(yield* Ref.get(oldReleased), 1)
134 strictEqual(yield* Ref.get(replacementAcquired), 1)
135 strictEqual(yield* Ref.get(replacementReleased), 1)
136 }))
137 it.effect("a set in flight when the owning scope closes releases its value", () =>
138 Effect.gen(function*() {
139 const released = yield* Ref.make<Array<number>>([])
140 const owner = yield* Scope.make()
141 const ref = yield* ScopedRef.make(() => 0).pipe(Scope.provide(owner))
142 const gate = yield* Latch.make()
143 const setter = yield* Effect.forkDetach(
144 ScopedRef.set(
145 ref,
146 Effect.acquireRelease(gate.await.pipe(Effect.as(1)), (n) => Ref.update(released, (xs) => [...xs, n]))
147 ),
148 { startImmediately: true }
149 )
150 yield* Scope.close(owner, Exit.void)
151 yield* gate.open
152 yield* Fiber.await(setter)
153 assert.deepStrictEqual(yield* Ref.get(released), [1])
154 }))
155 it.effect("fromAcquire tracks the initial resource through replacement and scope close", () =>
156 Effect.gen(function*() {
157 const ref = yield* Effect.scoped(ScopedRef.make(() => 0))
158 strictEqual(ref.pipe(identity), ref)
159 }))
160})