| 1 | /** |
| 2 | * Creates refreshable references to a single layer-built service context. |
| 3 | * |
| 4 | * A `LayerRef<I, E>` builds one `Layer` into a cached `Context<I>`, exposes it |
| 5 | * back as a layer or scoped effect, and supports invalidation so later users can |
| 6 | * acquire a fresh context. |
| 7 | * |
| 8 | * @since 4.0.0 |
| 9 | */ |
| 10 | import * as Context from "./Context.ts" |
| 11 | import type * as Duration from "./Duration.ts" |
| 12 | import * as Effect from "./Effect.ts" |
| 13 | import { identity } from "./Function.ts" |
| 14 | import { getStackTraceLimit, setStackTraceLimit } from "./internal/stackTraceLimit.ts" |
| 15 | import * as Layer from "./Layer.ts" |
| 16 | import * as RcRef from "./RcRef.ts" |
| 17 | import type * as Schedule from "./Schedule.ts" |
| 18 | import * as Scope from "./Scope.ts" |
| 19 | import type { Mutable } from "./Types.ts" |
| 20 | |
| 21 | const TypeId = "~effect/LayerRef" |
| 22 | |
| 23 | /** |
| 24 | * A refreshable reference to a single layer-built service context. |
| 25 | * |
| 26 | * **When to use** |
| 27 | * |
| 28 | * Use when you want to share one scoped layer resource across many users while |
| 29 | * retaining the ability to invalidate it and rebuild it later. |
| 30 | * |
| 31 | * **Details** |
| 32 | * |
| 33 | * A `LayerRef` is the unkeyed counterpart to a layer cache: it lazily builds the |
| 34 | * layer on first use, reuses the resulting context while it is borrowed or kept |
| 35 | * idle, and can invalidate the cached context so the next use rebuilds it. |
| 36 | * |
| 37 | * @see {@link make} for constructing a `LayerRef` from a layer |
| 38 | * @see {@link Service} for defining a `LayerRef` as a service class |
| 39 | * |
| 40 | * @category models |
| 41 | * @since 4.0.0 |
| 42 | */ |
| 43 | export interface LayerRef<in out I, in out E = never> { |
| 44 | readonly [TypeId]: typeof TypeId |
| 45 | |
| 46 | /** |
| 47 | * The underlying reference-counted cache that stores the built context. |
| 48 | */ |
| 49 | readonly rcRef: RcRef.RcRef<Context.Context<I>, E> |
| 50 | |
| 51 | /** |
| 52 | * Layer that provides the currently cached context, acquiring it if needed. |
| 53 | */ |
| 54 | readonly get: Layer.Layer<I, E> |
| 55 | |
| 56 | /** |
| 57 | * Scoped effect that retrieves the currently cached context, acquiring it if needed. |
| 58 | */ |
| 59 | readonly contextEffect: Effect.Effect<Context.Context<I>, E, Scope.Scope> |
| 60 | |
| 61 | /** |
| 62 | * Invalidates the cached context so the next use rebuilds the layer. |
| 63 | */ |
| 64 | readonly invalidate: Effect.Effect<void> |
| 65 | |
| 66 | /** |
| 67 | * Invalidates the cached context so the next use rebuilds the layer, and |
| 68 | * reacquires it. |
| 69 | */ |
| 70 | readonly refresh: Effect.Effect<void, E> |
| 71 | } |
| 72 | |
| 73 | /** |
| 74 | * Creates a `LayerRef` from a `Layer`. |
| 75 | * |
| 76 | * **When to use** |
| 77 | * |
| 78 | * Use when you have one layer-built resource that should be shared, optionally |
| 79 | * kept alive while idle, and refreshed on demand. |
| 80 | * |
| 81 | * **Details** |
| 82 | * |
| 83 | * The layer is built lazily on first use unless `preload` is `true`. |
| 84 | * `idleTimeToLive` keeps the context cached after it stops being used, and |
| 85 | * `invalidationSchedule` can periodically invalidate it. When `preload` is |
| 86 | * `true`, scheduled invalidation also reacquires the context. |
| 87 | * |
| 88 | * **Gotchas** |
| 89 | * |
| 90 | * Invalidation does not revoke contexts already borrowed by active scopes; those |
| 91 | * contexts remain usable until their scopes close. |
| 92 | * |
| 93 | * **Example** (Sharing one layer-built service) |
| 94 | * |
| 95 | * ```ts import.meta.vitest |
| 96 | * import { Context, Effect, Layer, LayerRef } from "effect" |
| 97 | * |
| 98 | * class Database extends Context.Service<Database, { |
| 99 | * readonly query: Effect.Effect<string> |
| 100 | * }>()("Database") {} |
| 101 | * |
| 102 | * const databaseLayer = Layer.succeed(Database, { |
| 103 | * query: Effect.succeed("result") |
| 104 | * }) |
| 105 | * |
| 106 | * const query = Effect.gen(function*() { |
| 107 | * const database = yield* Database |
| 108 | * return yield* database.query |
| 109 | * }) |
| 110 | * |
| 111 | * const program = Effect.scoped( |
| 112 | * Effect.gen(function*() { |
| 113 | * const ref = yield* LayerRef.make(databaseLayer, { |
| 114 | * idleTimeToLive: "5 seconds" |
| 115 | * }) |
| 116 | * |
| 117 | * const result = yield* Effect.provide(query, ref.get) |
| 118 | * |
| 119 | * yield* ref.invalidate |
| 120 | * |
| 121 | * return result |
| 122 | * }) |
| 123 | * ) |
| 124 | * |
| 125 | * await Effect.runPromise(program) // => "result" |
| 126 | * ``` |
| 127 | * |
| 128 | * @see {@link Service} for defining a reusable service class around a `LayerRef` |
| 129 | * |
| 130 | * @category constructors |
| 131 | * @since 4.0.0 |
| 132 | */ |
| 133 | export const make = Effect.fnUntraced( |
| 134 | function*<I, E, R, X, const Preload extends boolean = never, SE = never, SR = never>( |
| 135 | layer: Layer.Layer<I, E, R>, |
| 136 | options?: { |
| 137 | /** |
| 138 | * Duration to keep the resource alive after it is no longer used. |
| 139 | */ |
| 140 | readonly idleTimeToLive?: Duration.Input | undefined |
| 141 | /** |
| 142 | * Whether to acquire the resource during creation. |
| 143 | */ |
| 144 | readonly preload?: Preload | undefined |
| 145 | /** |
| 146 | * Schedule used to invalidate the cached resource. When `preload` is |
| 147 | * `true`, each scheduled invalidation also reacquires the resource. |
| 148 | */ |
| 149 | readonly invalidationSchedule?: Schedule.Schedule<X, unknown, SE, SR> | undefined |
| 150 | } | undefined |
| 151 | ): Effect.fn.Return< |
| 152 | LayerRef<I, E>, |
| 153 | [Preload] extends [true] ? E : never, |
| 154 | Scope.Scope | R | SR |
| 155 | > { |
| 156 | const context = yield* Effect.context<never>() |
| 157 | const memoMap = Layer.CurrentMemoMap.forkOrCreate(context) |
| 158 | |
| 159 | const rcRef = yield* RcRef.make({ |
| 160 | acquire: Effect.contextWith((_: Context.Context<Scope.Scope>) => |
| 161 | Layer.buildWithMemoMap(layer, memoMap, Context.get(_, Scope.Scope)) |
| 162 | ), |
| 163 | idleTimeToLive: options?.idleTimeToLive |
| 164 | }) |
| 165 | |
| 166 | const refresh = RcRef.invalidate(rcRef).pipe( |
| 167 | Effect.andThen(Effect.scoped(RcRef.get(rcRef))), |
| 168 | Effect.asVoid |
| 169 | ) |
| 170 | |
| 171 | if (options?.preload) { |
| 172 | yield* refresh as Effect.Effect<void> |
| 173 | } |
| 174 | |
| 175 | if (options?.invalidationSchedule) { |
| 176 | const onRefresh = options.preload ? refresh : RcRef.invalidate(rcRef) |
| 177 | yield* onRefresh.pipe( |
| 178 | Effect.ignoreCause, |
| 179 | Effect.schedule(options.invalidationSchedule), |
| 180 | Effect.forkScoped |
| 181 | ) |
| 182 | } |
| 183 | |
| 184 | return identity<LayerRef<I, any>>({ |
| 185 | [TypeId]: TypeId, |
| 186 | rcRef, |
| 187 | get: Layer.effectContext(RcRef.get(rcRef)), |
| 188 | contextEffect: RcRef.get(rcRef), |
| 189 | invalidate: RcRef.invalidate(rcRef), |
| 190 | refresh |
| 191 | }) |
| 192 | } |
| 193 | ) |
| 194 | |
| 195 | /** |
| 196 | * Service class shape produced by `LayerRef.Service`. |
| 197 | * |
| 198 | * **When to use** |
| 199 | * |
| 200 | * Use as the public type for classes returned by `LayerRef.Service` when an API |
| 201 | * needs to accept, return, or alias the generated service class and its static |
| 202 | * helpers. |
| 203 | * |
| 204 | * **Details** |
| 205 | * |
| 206 | * It combines a `Context.Service` tag for the `LayerRef` with default layers and |
| 207 | * helper accessors for retrieving, using, and invalidating the cached resource. |
| 208 | * |
| 209 | * @see {@link Service} for creating concrete `LayerRef` service classes |
| 210 | * |
| 211 | * @category services |
| 212 | * @since 4.0.0 |
| 213 | */ |
| 214 | export interface TagClass< |
| 215 | in out Self, |
| 216 | in out Id extends string, |
| 217 | in out I, |
| 218 | in out E, |
| 219 | in out R, |
| 220 | in out LE, |
| 221 | in out Deps extends Layer.Layer<any, any, any> |
| 222 | > extends Context.ServiceClass<Self, Id, LayerRef<I, E>> { |
| 223 | /** |
| 224 | * Default layer for the `LayerRef` service, with dependencies applied. |
| 225 | */ |
| 226 | readonly layer: Layer.Layer< |
| 227 | Self, |
| 228 | (Deps extends Layer.Layer<infer _A, infer _E, infer _R> ? _E : never) | LE, |
| 229 | | Exclude<R, (Deps extends Layer.Layer<infer _A, infer _E, infer _R> ? _A : never)> |
| 230 | | (Deps extends Layer.Layer<infer _A, infer _E, infer _R> ? _R : never) |
| 231 | > |
| 232 | |
| 233 | /** |
| 234 | * Default layer for the `LayerRef` service without provided dependencies. |
| 235 | */ |
| 236 | readonly layerNoDeps: Layer.Layer<Self, LE, R> |
| 237 | |
| 238 | /** |
| 239 | * Layer that provides the currently cached context, requiring this service. |
| 240 | */ |
| 241 | readonly get: Layer.Layer<I, E, Self> |
| 242 | |
| 243 | /** |
| 244 | * Scoped effect that retrieves the currently cached context through this service. |
| 245 | */ |
| 246 | readonly contextEffect: Effect.Effect<Context.Context<I>, E, Scope.Scope | Self> |
| 247 | |
| 248 | /** |
| 249 | * Invalidates the cached context through this service. |
| 250 | */ |
| 251 | readonly invalidate: Effect.Effect<void, never, Self> |
| 252 | |
| 253 | /** |
| 254 | * Invalidates the cached context through this service, and reacquires it. |
| 255 | */ |
| 256 | readonly refresh: Effect.Effect<void, E, Self> |
| 257 | } |
| 258 | |
| 259 | /** |
| 260 | * Creates a service class for a `LayerRef`. |
| 261 | * |
| 262 | * **When to use** |
| 263 | * |
| 264 | * Use when you want to name a shared layer reference as an application service |
| 265 | * and expose static helpers for providing, retrieving, and invalidating it. |
| 266 | * |
| 267 | * **Details** |
| 268 | * |
| 269 | * The returned class is a `Context.Service` whose value is a `LayerRef`. It also |
| 270 | * includes `.layer`, `.layerNoDeps`, `.get`, `.contextEffect`, and `.invalidate` |
| 271 | * helpers so callers do not need to access the `LayerRef` value directly. |
| 272 | * |
| 273 | * **Example** (Defining a refreshable service) |
| 274 | * |
| 275 | * ```ts import.meta.vitest |
| 276 | * import { Context, Effect, Layer, LayerRef } from "effect" |
| 277 | * |
| 278 | * class Database extends Context.Service<Database, { |
| 279 | * readonly query: Effect.Effect<string> |
| 280 | * }>()("Database") {} |
| 281 | * |
| 282 | * const databaseLayer = Layer.succeed(Database, { |
| 283 | * query: Effect.succeed("result") |
| 284 | * }) |
| 285 | * |
| 286 | * class DatabaseRef extends LayerRef.Service<DatabaseRef>()("DatabaseRef", { |
| 287 | * layer: databaseLayer, |
| 288 | * preload: true |
| 289 | * }) {} |
| 290 | * |
| 291 | * const program = Effect.gen(function*() { |
| 292 | * const database = yield* Database |
| 293 | * return yield* database.query |
| 294 | * }).pipe( |
| 295 | * Effect.provide(DatabaseRef.get), |
| 296 | * Effect.provide(DatabaseRef.layer) |
| 297 | * ) |
| 298 | * |
| 299 | * await Effect.runPromise(program) // => "result" |
| 300 | * ``` |
| 301 | * |
| 302 | * @see {@link make} for creating a `LayerRef` value without defining a service class |
| 303 | * |
| 304 | * @category services |
| 305 | * @since 4.0.0 |
| 306 | */ |
| 307 | export const Service = <Self>() => |
| 308 | < |
| 309 | const Id extends string, |
| 310 | I, |
| 311 | E, |
| 312 | R, |
| 313 | X, |
| 314 | const Deps extends ReadonlyArray<Layer.Layer<any, any, any>> = [], |
| 315 | const Preload extends boolean = never, |
| 316 | SE = never, |
| 317 | SR = never |
| 318 | >( |
| 319 | id: Id, |
| 320 | options: { |
| 321 | /** |
| 322 | * Layer used to build the cached context. |
| 323 | */ |
| 324 | readonly layer: Layer.Layer<I, E, R> |
| 325 | /** |
| 326 | * Layers used to satisfy dependencies of the `LayerRef` service layer. |
| 327 | */ |
| 328 | readonly dependencies?: Deps | undefined |
| 329 | /** |
| 330 | * Duration to keep the resource alive after it is no longer used. |
| 331 | */ |
| 332 | readonly idleTimeToLive?: Duration.Input | undefined |
| 333 | /** |
| 334 | * Whether to acquire the resource during creation. |
| 335 | */ |
| 336 | readonly preload?: Preload | undefined |
| 337 | /** |
| 338 | * Schedule used to invalidate the cached resource. When `preload` is |
| 339 | * `true`, each scheduled invalidation also reacquires the resource. |
| 340 | */ |
| 341 | readonly invalidationSchedule?: Schedule.Schedule<X, unknown, SE, SR> | undefined |
| 342 | } |
| 343 | ): TagClass< |
| 344 | Self, |
| 345 | Id, |
| 346 | I, |
| 347 | E, |
| 348 | R | SR, |
| 349 | [Preload] extends [true] ? E : never, |
| 350 | Deps[number] |
| 351 | > => { |
| 352 | const limit = getStackTraceLimit() |
| 353 | let creationError: Error | undefined |
| 354 | if (limit !== 0) { |
| 355 | setStackTraceLimit(2) |
| 356 | creationError = new globalThis.Error() |
| 357 | setStackTraceLimit(limit) |
| 358 | } |
| 359 | |
| 360 | function TagClass() {} |
| 361 | const TagClass_ = TagClass as any as Mutable<TagClass<Self, Id, any, any, any, any, any>> |
| 362 | Object.setPrototypeOf(TagClass, Object.getPrototypeOf(Context.Service<Self, any>(id))) |
| 363 | TagClass.key = id |
| 364 | Object.defineProperty(TagClass, "stack", { |
| 365 | get() { |
| 366 | return creationError?.stack |
| 367 | } |
| 368 | }) |
| 369 | |
| 370 | TagClass_.layerNoDeps = Layer.effect(TagClass_)( |
| 371 | make(options.layer, options) |
| 372 | ) |
| 373 | TagClass_.layer = options.dependencies && options.dependencies.length > 0 ? |
| 374 | Layer.provide(TagClass_.layerNoDeps, options.dependencies as any) : |
| 375 | TagClass_.layerNoDeps |
| 376 | |
| 377 | TagClass_.get = Layer.unwrap(TagClass_.useSync((ref) => ref.get)) |
| 378 | TagClass_.contextEffect = TagClass_.use((ref) => ref.contextEffect) |
| 379 | TagClass_.invalidate = TagClass_.use((ref) => ref.invalidate) |
| 380 | TagClass_.refresh = TagClass_.use((ref) => ref.refresh) |
| 381 | |
| 382 | return TagClass as any |
| 383 | } |