1/**
2 * Stores a current value together with the scope that owns it.
3 *
4 * A `ScopedRef<A>` is useful for resource-backed values such as clients,
5 * connections, subscriptions, or handles. Replacing the value acquires the
6 * replacement in a new scope and releases the resources owned by the previous
7 * value. Reads can be effectful or synchronous, and updates are synchronized so
8 * only one replacement happens at a time.
9 *
10 * @since 2.0.0
11 */
12import * as Effect from "./Effect.ts"
13import * as Exit from "./Exit.ts"
14import { dual, type LazyArg } from "./Function.ts"
15import { PipeInspectableProto } from "./internal/core.ts"
16import type { Pipeable } from "./Pipeable.ts"
17import * as Scope from "./Scope.ts"
18import * as Synchronized from "./SynchronizedRef.ts"
19
20const TypeId = "~effect/ScopedRef"
21
22/**
23 * A `ScopedRef` is a reference whose value is associated with resources,
24 * which must be released properly. You can both get the current value of any
25 * `ScopedRef`, as well as set it to a new value (which may require new
26 * resources). The reference itself takes care of properly releasing resources
27 * for the old value whenever a new value is obtained.
28 *
29 * **When to use**
30 *
31 * Use when an application needs to keep a current resource-backed value and
32 * later replace it with another acquired value while ensuring the previous
33 * value is released.
34 *
35 * @category models
36 * @since 2.0.0
37 */
38export interface ScopedRef<in out A> extends Pipeable {
39 readonly [TypeId]: typeof TypeId
40 readonly backing: Synchronized.SynchronizedRef<readonly [Scope.Closeable, A]>
41}
42
43const Proto = {
44 ...PipeInspectableProto,
45 [TypeId]: TypeId,
46 toJSON(this: ScopedRef<any>) {
47 return {
48 _id: "ScopedRef",
49 value: this.backing.backing.ref.current[1]
50 }
51 }
52}
53
54interface ScopedRefImpl<A> extends ScopedRef<A> {
55 // Keep every generation at the ref's creation slot in the owner's finalizer order.
56 readonly scope: Scope.Scope
57}
58
59const makeUnsafe = <A>(
60 scope: Scope.Scope,
61 generation: Scope.Closeable,
62 value: A
63): ScopedRefImpl<A> => {
64 const self = Object.create(Proto)
65 self.scope = scope
66 self.backing = Synchronized.makeUnsafe([generation, value] as const)
67 return self
68}
69
70const isClosed = (scope: Scope.Scope): boolean => scope.state._tag === "Closed"
71
72/**
73 * Creates a new `ScopedRef` from an effect that acquires the initial value.
74 *
75 * **When to use**
76 *
77 * Use when creating a `ScopedRef` whose initial value requires acquiring
78 * resources that must be released.
79 *
80 * @see {@link make} for creating a `ScopedRef` from a value that does not require resource acquisition
81 * @category constructors
82 * @since 2.0.0
83 */
84export const fromAcquire: <A, E, R>(
85 acquire: Effect.Effect<A, E, R>
86) => Effect.Effect<ScopedRef<A>, E, Scope.Scope | R> = Effect.fnUntraced(function*<A, E, R>(
87 acquire: Effect.Effect<A, E, R>
88) {
89 const scope = Scope.forkUnsafe(yield* Effect.scope)
90 const generation = Scope.forkUnsafe(scope)
91 const value = yield* acquire.pipe(
92 Scope.provide(generation),
93 Effect.tapCause((cause) => Scope.close(scope, Exit.failCause(cause)))
94 )
95 if (isClosed(generation)) return yield* Effect.interrupt
96 return makeUnsafe(scope, generation, value)
97}, Effect.uninterruptible)
98
99/**
100 * Retrieves the current value of the scoped reference synchronously.
101 *
102 * **When to use**
103 *
104 * Use when you need immediate synchronous access to the current `ScopedRef`
105 * value and can guarantee that reading outside the `Effect` API is safe.
106 *
107 * @see {@link get} for Effect-wrapped access in Effect programs
108 *
109 * @category getters
110 * @since 4.0.0
111 */
112export const getUnsafe = <A>(self: ScopedRef<A>): A => self.backing.backing.ref.current[1]
113
114/**
115 * Retrieves the current value of the scoped reference effectfully.
116 *
117 * **When to use**
118 *
119 * Use to read the value currently stored in a `ScopedRef` inside an `Effect`
120 * workflow.
121 *
122 * @see {@link getUnsafe} for reading the current value synchronously when an unsafe read is acceptable
123 *
124 * @category getters
125 * @since 2.0.0
126 */
127export const get = <A>(self: ScopedRef<A>): Effect.Effect<A> => Effect.sync(() => getUnsafe(self))
128
129/**
130 * Creates a new `ScopedRef` from the specified value.
131 *
132 * **When to use**
133 *
134 * Use to create a `ScopedRef` when the initial value is already available or
135 * can be produced without acquiring resources.
136 *
137 * **Details**
138 *
139 * The `evaluate` function runs when the returned effect runs. The returned
140 * effect requires a `Scope`, and the reference closes the currently stored
141 * value's scope when that outer scope closes.
142 *
143 * **Gotchas**
144 *
145 * Do not use `make` for an initial value whose creation acquires resources; use
146 * `fromAcquire` so acquisition and finalization are tracked.
147 *
148 * @see {@link fromAcquire} for creating a `ScopedRef` from an effect that acquires the initial value
149 * @see {@link set} for replacing the current value with a newly acquired value
150 *
151 * @category constructors
152 * @since 2.0.0
153 */
154export const make = <A>(evaluate: LazyArg<A>): Effect.Effect<ScopedRef<A>, never, Scope.Scope> =>
155 fromAcquire(Effect.sync(evaluate))
156
157/**
158 * Sets the value of this reference to a newly acquired scoped value, releasing
159 * any resources associated with the old value.
160 *
161 * **When to use**
162 *
163 * Use to replace the current value of an existing `ScopedRef` with a newly
164 * acquired scoped value while releasing resources for the previous value.
165 *
166 * **Details**
167 *
168 * This method will not return until either the reference is successfully
169 * changed to the new value, with old resources released, or until the attempt
170 * to acquire a new value fails.
171 *
172 * @category mutations
173 * @since 2.0.0
174 */
175export const set: {
176 <A, R, E>(acquire: Effect.Effect<A, E, R>): (self: ScopedRef<A>) => Effect.Effect<void, E, Exclude<R, Scope.Scope>>
177 <A, R, E>(self: ScopedRef<A>, acquire: Effect.Effect<A, E, R>): Effect.Effect<void, E, Exclude<R, Scope.Scope>>
178} = dual(
179 2,
180 Effect.fnUntraced(
181 function*<A, R, E>(
182 self: ScopedRef<A>,
183 acquire: Effect.Effect<A, E, R>
184 ) {
185 const generation = Scope.forkUnsafe((self as ScopedRefImpl<A>).scope)
186 const value = yield* acquire.pipe(
187 Scope.provide(generation),
188 Effect.tapCause((cause) => Scope.close(generation, Exit.failCause(cause)))
189 )
190 yield* Scope.close(self.backing.backing.ref.current[0], Exit.void).pipe(
191 Effect.tapCause((cause) => Scope.close(generation, Exit.failCause(cause)))
192 )
193 if (isClosed(generation)) return yield* Effect.interrupt
194 self.backing.backing.ref.current = [generation, value]
195 },
196 Effect.uninterruptible,
197 (effect, self) => self.backing.semaphore.withPermit(effect)
198 )
199)