1/**
2 * Transactional references for coordinating mutable state with Effect
3 * transactions. A `TxRef` stores a current value, but reads and writes inside
4 * `Effect.tx` are recorded in a transaction journal and committed together only
5 * when the outermost transaction succeeds.
6 *
7 * This is the basic building block behind the other transactional collections
8 * in Effect. The module provides effectful and unsafe constructors plus the
9 * core operations for reading, setting, updating, and modifying a transactional
10 * value while returning a separate result.
11 *
12 * @since 4.0.0
13 */
14import * as Effect from "./Effect.ts"
15import { dual } from "./Function.ts"
16import { pipeArguments } from "./Pipeable.ts"
17import type { Pipeable } from "./Pipeable.ts"
18import type { NoInfer } from "./Types.ts"
19
20const TypeId = "~effect/TxRef"
21
22/**
23 * TxRef is a transactional value, it can be read and modified within the body of a transaction.
24 *
25 * **When to use**
26 *
27 * Use to store mutable state that must be read and modified inside Effect
28 * transactions.
29 *
30 * **Details**
31 *
32 * Accessed values are tracked by the transaction in order to detect conflicts and in order to
33 * track changes, a transaction will retry whenever a conflict is detected or whenever the
34 * transaction explicitely calls to `Effect.txRetry` and any of the accessed TxRef values
35 * change.
36 *
37 * **Example** (Using a transactional reference)
38 *
39 * ```ts import.meta.vitest
40 * import { Effect, TxRef } from "effect"
41 *
42 * const program = Effect.gen(function*() {
43 * // Create a transactional reference
44 * const ref: TxRef.TxRef<number> = yield* TxRef.make(0)
45 *
46 * // Use within a transaction
47 * yield* Effect.tx(Effect.gen(function*() {
48 * const current = yield* TxRef.get(ref)
49 * yield* TxRef.set(ref, current + 1)
50 * }))
51 *
52 * return yield* TxRef.get(ref)
53 * })
54 *
55 * await Effect.runPromise(program) // => 1
56 * ```
57 *
58 * @category models
59 * @since 4.0.0
60 */
61export interface TxRef<in out A> extends Pipeable {
62 readonly [TypeId]: typeof TypeId
63
64 version: number
65 pending: Map<unknown, () => void>
66 value: A
67}
68
69/**
70 * Creates a new `TxRef` with the specified initial value.
71 *
72 * **When to use**
73 *
74 * Use to create a `TxRef` inside an `Effect` workflow.
75 *
76 * **Example** (Creating transactional references)
77 *
78 * ```ts import.meta.vitest
79 * import { Effect, TxRef } from "effect"
80 *
81 * const program = Effect.gen(function*() {
82 * // Create a transactional reference with initial value
83 * const counter = yield* TxRef.make(0)
84 * const name = yield* TxRef.make("Alice")
85 *
86 * // Use in transactions
87 * yield* Effect.tx(Effect.gen(function*() {
88 * yield* TxRef.set(counter, 42)
89 * yield* TxRef.set(name, "Bob")
90 * }))
91 *
92 * return [yield* TxRef.get(counter), yield* TxRef.get(name)]
93 * })
94 *
95 * await Effect.runPromise(program) // => [42, "Bob"]
96 * ```
97 *
98 * @category constructors
99 * @since 2.0.0
100 */
101export const make = <A>(initial: A) => Effect.sync(() => makeUnsafe(initial))
102
103/**
104 * Creates a new `TxRef` synchronously with the specified initial value.
105 *
106 * **When to use**
107 *
108 * Use to construct a `TxRef` synchronously when it must be created outside an
109 * `Effect` workflow.
110 *
111 * **Example** (Creating transactional references unsafely)
112 *
113 * ```ts import.meta.vitest
114 * import { TxRef } from "effect"
115 *
116 * // Create a TxRef synchronously (unsafe - use make instead in Effect contexts)
117 * const counter = TxRef.makeUnsafe(0)
118 * const config = TxRef.makeUnsafe({ timeout: 5000, retries: 3 })
119 *
120 * // These are now ready to use in transactions
121 * counter.value // => 0
122 * config.value // => { timeout: 5000, retries: 3 }
123 * ```
124 *
125 * @category constructors
126 * @since 4.0.0
127 */
128export const makeUnsafe = <A>(initial: A): TxRef<A> => ({
129 [TypeId]: TypeId,
130 pending: new Map(),
131 pipe() {
132 return pipeArguments(this, arguments)
133 },
134 version: 0,
135 value: initial
136})
137
138const journalEntry = <A>(state: Effect.Transaction["Service"], self: TxRef<A>) => {
139 let entry = state.journal.get(self)
140 if (entry === undefined) {
141 entry = { version: self.version, value: self.value, written: false }
142 state.journal.set(self, entry)
143 }
144 return entry
145}
146
147/**
148 * Modifies the value of the `TxRef` using the provided function.
149 *
150 * **When to use**
151 *
152 * Use to update a `TxRef` and return a computed result from the same
153 * transaction step.
154 *
155 * **Example** (Modifying transactional references)
156 *
157 * ```ts import.meta.vitest
158 * import { Effect, TxRef } from "effect"
159 *
160 * const program = Effect.gen(function*() {
161 * const counter = yield* TxRef.make(0)
162 *
163 * // Modify and return both old and new value
164 * const result = yield* TxRef.modify(counter, (current) => [current * 2, current + 1])
165 *
166 * return [result, yield* TxRef.get(counter)]
167 * })
168 *
169 * await Effect.runPromise(program) // => [0, 1]
170 * ```
171 *
172 * @category combinators
173 * @since 2.0.0
174 */
175export const modify: {
176 <A, R>(f: (current: NoInfer<A>) => [returnValue: R, newValue: A]): (self: TxRef<A>) => Effect.Effect<R>
177 <A, R>(self: TxRef<A>, f: (current: A) => [returnValue: R, newValue: A]): Effect.Effect<R>
178} = dual(2, <A, R>(
179 self: TxRef<A>,
180 f: (current: A) => [returnValue: R, newValue: A]
181): Effect.Effect<R> =>
182 Effect.Transaction.pipe(
183 Effect.flatMap((state) =>
184 Effect.sync(() => {
185 const current = journalEntry(state, self)
186 const [returnValue, next] = f(current.value)
187 current.value = next
188 current.written = true
189 return returnValue
190 })
191 ),
192 Effect.tx
193 ))
194
195/**
196 * Updates the value of the `TxRef` using the provided function.
197 *
198 * **When to use**
199 *
200 * Use to transform a `TxRef` when no result value is needed.
201 *
202 * **Example** (Updating transactional references)
203 *
204 * ```ts import.meta.vitest
205 * import { Effect, TxRef } from "effect"
206 *
207 * const program = Effect.gen(function*() {
208 * const counter = yield* TxRef.make(10)
209 *
210 * // Update the value using a function
211 * yield* Effect.tx(
212 * TxRef.update(counter, (current) => current * 2)
213 * )
214 *
215 * return yield* TxRef.get(counter)
216 * })
217 *
218 * await Effect.runPromise(program) // => 20
219 * ```
220 *
221 * @category combinators
222 * @since 2.0.0
223 */
224export const update: {
225 <A>(f: (current: NoInfer<A>) => A): (self: TxRef<A>) => Effect.Effect<void>
226 <A>(self: TxRef<A>, f: (current: A) => A): Effect.Effect<void>
227} = dual(2, <A>(
228 self: TxRef<A>,
229 f: (current: A) => A
230): Effect.Effect<void> => modify(self, (current) => [void 0, f(current)]))
231
232/**
233 * Reads the current value of the `TxRef`.
234 *
235 * **When to use**
236 *
237 * Use to read the current value of a `TxRef`.
238 *
239 * **Example** (Reading transactional references)
240 *
241 * ```ts import.meta.vitest
242 * import { Effect, TxRef } from "effect"
243 *
244 * const program = Effect.gen(function*() {
245 * const counter = yield* TxRef.make(42)
246 *
247 * // Read the value within a transaction
248 * const value = yield* Effect.tx(
249 * TxRef.get(counter)
250 * )
251 *
252 * return value
253 * })
254 *
255 * await Effect.runPromise(program) // => 42
256 * ```
257 *
258 * @category combinators
259 * @since 2.0.0
260 */
261export const get = <A>(self: TxRef<A>): Effect.Effect<A> =>
262 Effect.Transaction.pipe(
263 Effect.map((state) => journalEntry(state, self).value),
264 Effect.tx
265 )
266
267/**
268 * Sets the value of the `TxRef`.
269 *
270 * **When to use**
271 *
272 * Use to replace the value of a `TxRef`.
273 *
274 * **Example** (Setting transactional references)
275 *
276 * ```ts import.meta.vitest
277 * import { Effect, TxRef } from "effect"
278 *
279 * const program = Effect.gen(function*() {
280 * const counter = yield* TxRef.make(0)
281 *
282 * // Set a new value within a transaction
283 * yield* Effect.tx(
284 * TxRef.set(counter, 100)
285 * )
286 *
287 * return yield* TxRef.get(counter)
288 * })
289 *
290 * await Effect.runPromise(program) // => 100
291 * ```
292 *
293 * @category combinators
294 * @since 2.0.0
295 */
296export const set: {
297 <A>(value: A): (self: TxRef<A>) => Effect.Effect<void>
298 <A>(self: TxRef<A>, value: A): Effect.Effect<void>
299} = dual(2, <A>(
300 self: TxRef<A>,
301 value: A
302): Effect.Effect<void> => update(self, () => value))