/** * Creates refreshable references to a single layer-built service context. * * A `LayerRef` builds one `Layer` into a cached `Context`, exposes it * back as a layer or scoped effect, and supports invalidation so later users can * acquire a fresh context. * * @stability unstable * @since 4.0.0 */ import * as Context from "./Context.ts" import type * as Duration from "./Duration.ts" import * as Effect from "./Effect.ts" import { identity } from "./Function.ts" import { getStackTraceLimit, setStackTraceLimit } from "./internal/stackTraceLimit.ts" import * as Layer from "./Layer.ts" import * as RcRef from "./RcRef.ts" import type * as Schedule from "./Schedule.ts" import * as Scope from "./Scope.ts" import type { Mutable } from "./Types.ts" const TypeId = "~effect/LayerRef" /** * A refreshable reference to a single layer-built service context. * * **When to use** * * Use when you want to share one scoped layer resource across many users while * retaining the ability to invalidate it and rebuild it later. * * **Details** * * A `LayerRef` is the unkeyed counterpart to a layer cache: it lazily builds the * layer on first use, reuses the resulting context while it is borrowed or kept * idle, and can invalidate the cached context so the next use rebuilds it. * * @see {@link make} for constructing a `LayerRef` from a layer * @see {@link Service} for defining a `LayerRef` as a service class * * @stability unstable * @category models * @since 4.0.0 */ export interface LayerRef { readonly [TypeId]: typeof TypeId /** * The underlying reference-counted cache that stores the built context. */ readonly rcRef: RcRef.RcRef, E> /** * Layer that provides the currently cached context, acquiring it if needed. */ readonly get: Layer.Layer /** * Scoped effect that retrieves the currently cached context, acquiring it if needed. */ readonly contextEffect: Effect.Effect, E, Scope.Scope> /** * Invalidates the cached context so the next use rebuilds the layer. */ readonly invalidate: Effect.Effect /** * Invalidates the cached context so the next use rebuilds the layer, and * reacquires it. */ readonly refresh: Effect.Effect } /** * Creates a `LayerRef` from a `Layer`. * * **When to use** * * Use when you have one layer-built resource that should be shared, optionally * kept alive while idle, and refreshed on demand. * * **Details** * * The layer is built lazily on first use unless `preload` is `true`. * `idleTimeToLive` keeps the context cached after it stops being used, and * `invalidationSchedule` can periodically invalidate it. When `preload` is * `true`, scheduled invalidation also reacquires the context. * * **Gotchas** * * Invalidation does not revoke contexts already borrowed by active scopes; those * contexts remain usable until their scopes close. * * **Example** (Sharing one layer-built service) * * ```ts import.meta.vitest * import { Context, Effect, Layer, LayerRef } from "effect" * * class Database extends Context.Service * }>()("Database") {} * * const databaseLayer = Layer.succeed(Database, { * query: Effect.succeed("result") * }) * * const query = Effect.gen(function*() { * const database = yield* Database * return yield* database.query * }) * * const program = Effect.scoped( * Effect.gen(function*() { * const ref = yield* LayerRef.make(databaseLayer, { * idleTimeToLive: "5 seconds" * }) * * const result = yield* Effect.provide(query, ref.get) * * yield* ref.invalidate * * return result * }) * ) * * await Effect.runPromise(program) // => "result" * ``` * * @see {@link Service} for defining a reusable service class around a `LayerRef` * * @stability unstable * @category constructors * @since 4.0.0 */ export const make = Effect.fnUntraced( function*( layer: Layer.Layer, options?: { /** * Duration to keep the resource alive after it is no longer used. */ readonly idleTimeToLive?: Duration.Input | undefined /** * Whether to acquire the resource during creation. */ readonly preload?: Preload | undefined /** * Schedule used to invalidate the cached resource. When `preload` is * `true`, each scheduled invalidation also reacquires the resource. */ readonly invalidationSchedule?: Schedule.Schedule | undefined } | undefined ): Effect.fn.Return< LayerRef, [Preload] extends [true] ? E : never, Scope.Scope | R | SR > { const context = yield* Effect.context() const memoMap = Layer.CurrentMemoMap.forkOrCreate(context) const rcRef = yield* RcRef.make({ acquire: Effect.contextWith((_: Context.Context) => Layer.buildWithMemoMap(layer, memoMap, Context.get(_, Scope.Scope)) ), idleTimeToLive: options?.idleTimeToLive }) const refresh = RcRef.invalidate(rcRef).pipe( Effect.andThen(Effect.scoped(RcRef.get(rcRef))), Effect.asVoid ) if (options?.preload) { yield* refresh as Effect.Effect } if (options?.invalidationSchedule) { const onRefresh = options.preload ? refresh : RcRef.invalidate(rcRef) yield* onRefresh.pipe( Effect.ignoreCause, Effect.schedule(options.invalidationSchedule), Effect.forkScoped ) } return identity>({ [TypeId]: TypeId, rcRef, get: Layer.effectContext(RcRef.get(rcRef)), contextEffect: RcRef.get(rcRef), invalidate: RcRef.invalidate(rcRef), refresh }) } ) /** * Service class shape produced by `LayerRef.Service`. * * **When to use** * * Use as the public type for classes returned by `LayerRef.Service` when an API * needs to accept, return, or alias the generated service class and its static * helpers. * * **Details** * * It combines a `Context.Service` tag for the `LayerRef` with default layers and * helper accessors for retrieving, using, and invalidating the cached resource. * * @see {@link Service} for creating concrete `LayerRef` service classes * * @stability unstable * @category services * @since 4.0.0 */ export interface TagClass< in out Self, in out Id extends string, in out I, in out E, in out R, in out LE, in out Deps extends Layer.Layer > extends Context.ServiceClass> { /** * Default layer for the `LayerRef` service, with dependencies applied. */ readonly layer: Layer.Layer< Self, (Deps extends Layer.Layer ? _E : never) | LE, | Exclude ? _A : never)> | (Deps extends Layer.Layer ? _R : never) > /** * Default layer for the `LayerRef` service without provided dependencies. */ readonly layerNoDeps: Layer.Layer /** * Layer that provides the currently cached context, requiring this service. */ readonly get: Layer.Layer /** * Scoped effect that retrieves the currently cached context through this service. */ readonly contextEffect: Effect.Effect, E, Scope.Scope | Self> /** * Invalidates the cached context through this service. */ readonly invalidate: Effect.Effect /** * Invalidates the cached context through this service, and reacquires it. */ readonly refresh: Effect.Effect } /** * Creates a service class for a `LayerRef`. * * **When to use** * * Use when you want to name a shared layer reference as an application service * and expose static helpers for providing, retrieving, and invalidating it. * * **Details** * * The returned class is a `Context.Service` whose value is a `LayerRef`. It also * includes `.layer`, `.layerNoDeps`, `.get`, `.contextEffect`, and `.invalidate` * helpers so callers do not need to access the `LayerRef` value directly. * * **Example** (Defining a refreshable service) * * ```ts import.meta.vitest * import { Context, Effect, Layer, LayerRef } from "effect" * * class Database extends Context.Service * }>()("Database") {} * * const databaseLayer = Layer.succeed(Database, { * query: Effect.succeed("result") * }) * * class DatabaseRef extends LayerRef.Service()("DatabaseRef", { * layer: databaseLayer, * preload: true * }) {} * * const program = Effect.gen(function*() { * const database = yield* Database * return yield* database.query * }).pipe( * Effect.provide(DatabaseRef.get), * Effect.provide(DatabaseRef.layer) * ) * * await Effect.runPromise(program) // => "result" * ``` * * @see {@link make} for creating a `LayerRef` value without defining a service class * * @stability unstable * @category services * @since 4.0.0 */ export const Service = () => < const Id extends string, I, E, R, X, const Deps extends ReadonlyArray> = [], const Preload extends boolean = never, SE = never, SR = never >( id: Id, options: { /** * Layer used to build the cached context. */ readonly layer: Layer.Layer /** * Layers used to satisfy dependencies of the `LayerRef` service layer. */ readonly dependencies?: Deps | undefined /** * Duration to keep the resource alive after it is no longer used. */ readonly idleTimeToLive?: Duration.Input | undefined /** * Whether to acquire the resource during creation. */ readonly preload?: Preload | undefined /** * Schedule used to invalidate the cached resource. When `preload` is * `true`, each scheduled invalidation also reacquires the resource. */ readonly invalidationSchedule?: Schedule.Schedule | undefined } ): TagClass< Self, Id, I, E, R | SR, [Preload] extends [true] ? E : never, Deps[number] > => { const limit = getStackTraceLimit() let creationError: Error | undefined if (limit !== 0) { setStackTraceLimit(2) creationError = new globalThis.Error() setStackTraceLimit(limit) } function TagClass() {} const TagClass_ = TagClass as any as Mutable> Object.setPrototypeOf(TagClass, Object.getPrototypeOf(Context.Service(id))) TagClass.key = id Object.defineProperty(TagClass, "stack", { get() { return creationError?.stack } }) TagClass_.layerNoDeps = Layer.effect(TagClass_)( make(options.layer, options) ) TagClass_.layer = options.dependencies && options.dependencies.length > 0 ? Layer.provide(TagClass_.layerNoDeps, options.dependencies as any) : TagClass_.layerNoDeps TagClass_.get = Layer.unwrap(TagClass_.useSync((ref) => ref.get)) TagClass_.contextEffect = TagClass_.use((ref) => ref.contextEffect) TagClass_.invalidate = TagClass_.use((ref) => ref.invalidate) TagClass_.refresh = TagClass_.use((ref) => ref.refresh) return TagClass as any }