1import { assert, describe, it } from "@effect/vitest"
2import { Effect, Stream, TxQueue, TxSubscriptionRef } from "effect"
3
4describe("TxSubscriptionRef", () => {
5 describe("constructors", () => {
6 it.effect("make creates ref with initial value", () =>
7 Effect.tx(Effect.gen(function*() {
8 const ref = yield* TxSubscriptionRef.make(42)
9 const value = yield* TxSubscriptionRef.get(ref)
10 assert.strictEqual(value, 42)
11 })))
12 })
13
14 describe("mutations", () => {
15 it.effect("set updates the value", () =>
16 Effect.tx(Effect.gen(function*() {
17 const ref = yield* TxSubscriptionRef.make(0)
18 yield* TxSubscriptionRef.set(ref, 10)
19 assert.strictEqual(yield* TxSubscriptionRef.get(ref), 10)
20 })))
21
22 it.effect("update applies a function", () =>
23 Effect.tx(Effect.gen(function*() {
24 const ref = yield* TxSubscriptionRef.make(5)
25 yield* TxSubscriptionRef.update(ref, (n) => n * 2)
26 assert.strictEqual(yield* TxSubscriptionRef.get(ref), 10)
27 })))
28
29 it.effect("modify returns a value and updates", () =>
30 Effect.tx(Effect.gen(function*() {
31 const ref = yield* TxSubscriptionRef.make(10)
32 const result = yield* TxSubscriptionRef.modify(ref, (n) => [`was ${n}`, n + 1])
33 assert.strictEqual(result, "was 10")
34 assert.strictEqual(yield* TxSubscriptionRef.get(ref), 11)
35 })))
36
37 it.effect("getAndSet returns old value and sets new", () =>
38 Effect.tx(Effect.gen(function*() {
39 const ref = yield* TxSubscriptionRef.make("a")
40 const old = yield* TxSubscriptionRef.getAndSet(ref, "b")
41 assert.strictEqual(old, "a")
42 assert.strictEqual(yield* TxSubscriptionRef.get(ref), "b")
43 })))
44
45 it.effect("getAndUpdate returns old value and applies function", () =>
46 Effect.tx(Effect.gen(function*() {
47 const ref = yield* TxSubscriptionRef.make(1)
48 const old = yield* TxSubscriptionRef.getAndUpdate(ref, (n) => n + 10)
49 assert.strictEqual(old, 1)
50 assert.strictEqual(yield* TxSubscriptionRef.get(ref), 11)
51 })))
52
53 it.effect("updateAndGet applies function and returns new value", () =>
54 Effect.tx(Effect.gen(function*() {
55 const ref = yield* TxSubscriptionRef.make(3)
56 const result = yield* TxSubscriptionRef.updateAndGet(ref, (n) => n * 3)
57 assert.strictEqual(result, 9)
58 assert.strictEqual(yield* TxSubscriptionRef.get(ref), 9)
59 })))
60 })
61
62 describe("subscriptions", () => {
63 it.effect("changes includes current value as first element", () =>
64 Effect.gen(function*() {
65 const ref = yield* Effect.tx(TxSubscriptionRef.make(100))
66
67 yield* Effect.scoped(
68 Effect.gen(function*() {
69 const sub = yield* TxSubscriptionRef.changes(ref)
70 const first = yield* Effect.tx(TxQueue.take(sub))
71 assert.strictEqual(first, 100)
72 })
73 )
74 }))
75
76 it.effect("changes receives updates in order", () =>
77 Effect.gen(function*() {
78 const ref = yield* Effect.tx(TxSubscriptionRef.make(0))
79
80 yield* Effect.scoped(
81 Effect.gen(function*() {
82 const sub = yield* TxSubscriptionRef.changes(ref)
83 // Take the initial value
84 const initial = yield* Effect.tx(TxQueue.take(sub))
85 assert.strictEqual(initial, 0)
86
87 yield* Effect.tx(TxSubscriptionRef.set(ref, 1))
88 yield* Effect.tx(TxSubscriptionRef.set(ref, 2))
89 yield* Effect.tx(TxSubscriptionRef.set(ref, 3))
90
91 assert.strictEqual(yield* Effect.tx(TxQueue.take(sub)), 1)
92 assert.strictEqual(yield* Effect.tx(TxQueue.take(sub)), 2)
93 assert.strictEqual(yield* Effect.tx(TxQueue.take(sub)), 3)
94 })
95 )
96 }))
97
98 it.effect("multiple subscribers each see all changes", () =>
99 Effect.gen(function*() {
100 const ref = yield* Effect.tx(TxSubscriptionRef.make(0))
101
102 yield* Effect.scoped(
103 Effect.gen(function*() {
104 const sub1 = yield* TxSubscriptionRef.changes(ref)
105 const sub2 = yield* TxSubscriptionRef.changes(ref)
106
107 // Both get the initial value
108 assert.strictEqual(yield* Effect.tx(TxQueue.take(sub1)), 0)
109 assert.strictEqual(yield* Effect.tx(TxQueue.take(sub2)), 0)
110
111 yield* Effect.tx(TxSubscriptionRef.set(ref, 42))
112
113 assert.strictEqual(yield* Effect.tx(TxQueue.take(sub1)), 42)
114 assert.strictEqual(yield* Effect.tx(TxQueue.take(sub2)), 42)
115 })
116 )
117 }))
118
119 it.effect("failed transactions do not publish rolled back values", () =>
120 Effect.gen(function*() {
121 const ref = yield* Effect.tx(TxSubscriptionRef.make(0))
122
123 // Collect initial value only (take 1 before any updates)
124 const initial = yield* Stream.runCollect(
125 TxSubscriptionRef.changesStream(ref).pipe(Stream.take(1))
126 )
127 assert.deepStrictEqual(initial, [0])
128 }))
129
130 it.effect("changesStream starts from the current value for each subscriber", () =>
131 Effect.gen(function*() {
132 const ref = yield* Effect.tx(TxSubscriptionRef.make(0))
133
134 yield* Effect.tx(TxSubscriptionRef.set(ref, 1))
135 yield* Effect.tx(TxSubscriptionRef.set(ref, 2))
136
137 const values = yield* Stream.runCollect(
138 TxSubscriptionRef.changesStream(ref).pipe(Stream.take(1))
139 )
140 // Stream starts fresh, so takes the current value (2)
141 assert.deepStrictEqual(values, [2])
142 }))
143
144 it.effect("scope close releases the subscriber queue", () =>
145 Effect.gen(function*() {
146 const ref = yield* Effect.tx(TxSubscriptionRef.make(0))
147
148 yield* Effect.scoped(
149 Effect.gen(function*() {
150 yield* TxSubscriptionRef.changes(ref)
151 // Subscriber exists within scope
152 })
153 )
154
155 // After scope closes, setting values should not fail
156 yield* Effect.tx(TxSubscriptionRef.set(ref, 1))
157 assert.strictEqual(yield* Effect.tx(TxSubscriptionRef.get(ref)), 1)
158 }))
159 })
160
161 describe("pipe support", () => {
162 it.effect("set data-last style", () =>
163 Effect.tx(Effect.gen(function*() {
164 const ref = yield* TxSubscriptionRef.make(0)
165 yield* ref.pipe(TxSubscriptionRef.set(99))
166 assert.strictEqual(yield* TxSubscriptionRef.get(ref), 99)
167 })))
168
169 it.effect("update data-last style", () =>
170 Effect.tx(Effect.gen(function*() {
171 const ref = yield* TxSubscriptionRef.make(5)
172 yield* ref.pipe(TxSubscriptionRef.update((n: number) => n + 1))
173 assert.strictEqual(yield* TxSubscriptionRef.get(ref), 6)
174 })))
175
176 it.effect("modify data-last style", () =>
177 Effect.tx(Effect.gen(function*() {
178 const ref = yield* TxSubscriptionRef.make(10)
179 const result = yield* ref.pipe(
180 TxSubscriptionRef.modify((n: number) => [n * 2, n + 1])
181 )
182 assert.strictEqual(result, 20)
183 assert.strictEqual(yield* TxSubscriptionRef.get(ref), 11)
184 })))
185 })
186
187 describe("type guards", () => {
188 it.effect("isTxSubscriptionRef identifies instances", () =>
189 Effect.tx(Effect.gen(function*() {
190 const ref = yield* TxSubscriptionRef.make(0)
191 assert.isTrue(TxSubscriptionRef.isTxSubscriptionRef(ref))
192 assert.isFalse(TxSubscriptionRef.isTxSubscriptionRef({}))
193 assert.isFalse(TxSubscriptionRef.isTxSubscriptionRef(null))
194 assert.isFalse(TxSubscriptionRef.isTxSubscriptionRef(undefined))
195 })))
196 })
197})