| 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 | */ |
| 12 | import * as Effect from "./Effect.ts" |
| 13 | import * as Exit from "./Exit.ts" |
| 14 | import { dual, type LazyArg } from "./Function.ts" |
| 15 | import { PipeInspectableProto } from "./internal/core.ts" |
| 16 | import type { Pipeable } from "./Pipeable.ts" |
| 17 | import * as Scope from "./Scope.ts" |
| 18 | import * as Synchronized from "./SynchronizedRef.ts" |
| 19 | |
| 20 | const 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 | */ |
| 38 | export interface ScopedRef<in out A> extends Pipeable { |
| 39 | readonly [TypeId]: typeof TypeId |
| 40 | readonly backing: Synchronized.SynchronizedRef<readonly [Scope.Closeable, A]> |
| 41 | } |
| 42 | |
| 43 | const 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 | |
| 54 | interface 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 | |
| 59 | const 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 | |
| 70 | const 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 | */ |
| 84 | export 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 | */ |
| 112 | export 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 | */ |
| 127 | export 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 | */ |
| 154 | export 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 | */ |
| 175 | export 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 | ) |