/** * @since 3.14.0 * @experimental */ import * as Context from "./Context.js" import type * as Duration from "./Duration.js" import * as Effect from "./Effect.js" import * as FiberRefsPatch from "./FiberRefsPatch.js" import { identity } from "./Function.js" import * as core from "./internal/core.js" import * as Layer from "./Layer.js" import * as RcMap from "./RcMap.js" import * as Runtime from "./Runtime.js" import * as Scope from "./Scope.js" import type { Mutable, NoExcessProperties } from "./Types.js" /** * @since 3.14.0 * @category Symbols */ export const TypeId: unique symbol = Symbol.for("effect/LayerMap") /** * @since 3.14.0 * @category Symbols */ export type TypeId = typeof TypeId /** * @since 3.14.0 * @category Models * @experimental */ export interface LayerMap { readonly [TypeId]: TypeId /** * The internal RcMap that stores the resources. */ readonly rcMap: RcMap.RcMap readonly runtimeEffect: Effect.Effect, E, Scope.Scope> }, E> /** * Retrieves a Layer for the resources associated with the key. */ get(key: K): Layer.Layer /** * Retrieves a Runtime for the resources associated with the key. */ runtime(key: K): Effect.Effect, E, Scope.Scope> /** * Invalidates the resource associated with the key. */ invalidate(key: K): Effect.Effect } /** * @since 3.14.0 * @category Constructors * @experimental * * A `LayerMap` allows you to create a map of Layer's that can be used to * dynamically access resources based on a key. * * ```ts * import { NodeRuntime } from "@effect/platform-node" * import { Context, Effect, FiberRef, Layer, LayerMap } from "effect" * * class Greeter extends Context.Tag("Greeter") * }>() {} * * // create a service that wraps a LayerMap * class GreeterMap extends LayerMap.Service()("GreeterMap", { * // define the lookup function for the layer map * // * // The returned Layer will be used to provide the Greeter service for the * // given name. * lookup: (name: string) => * Layer.succeed(Greeter, { * greet: Effect.succeed(`Hello, ${name}!`) * }).pipe( * Layer.merge(Layer.locallyScoped(FiberRef.currentConcurrency, 123)) * ), * * // If a layer is not used for a certain amount of time, it can be removed * idleTimeToLive: "5 seconds", * * // Supply the dependencies for the layers in the LayerMap * dependencies: [] * }) {} * * // usage * const program: Effect.Effect = Effect.gen(function*() { * // access and use the Greeter service * const greeter = yield* Greeter * yield* Effect.log(yield* greeter.greet) * }).pipe( * // use the GreeterMap service to provide a variant of the Greeter service * Effect.provide(GreeterMap.get("John")) * ) * * // run the program * program.pipe( * Effect.provide(GreeterMap.Default), * NodeRuntime.runMain * ) * ``` */ export const make: < K, L extends Layer.Layer, PreloadKeys extends Iterable | undefined = undefined >( lookup: (key: K) => L, options?: { readonly idleTimeToLive?: Duration.DurationInput | undefined readonly preloadKeys?: PreloadKeys } | undefined ) => Effect.Effect< LayerMap< K, L extends Layer.Layer ? _A : never, L extends Layer.Layer ? _E : never >, PreloadKeys extends undefined ? never : L extends Layer.Layer ? _E : never, Scope.Scope | (L extends Layer.Layer ? _R : never) > = Effect.fnUntraced(function*( lookup: (key: K) => Layer.Layer, options?: { readonly idleTimeToLive?: Duration.DurationInput | undefined readonly preloadKeys?: Iterable | undefined } | undefined ) { const context = yield* Effect.context() // If we are inside another layer build, use the current memo map, // otherwise create a new one. const memoMap = context.unsafeMap.has(Layer.CurrentMemoMap.key) ? Context.get(context, Layer.CurrentMemoMap) : yield* Layer.makeMemoMap const rcMap = yield* RcMap.make({ lookup: (key: K) => Effect.scopeWith((scope) => Effect.diffFiberRefs(Layer.buildWithMemoMap(lookup(key), memoMap, scope))).pipe( Effect.map(([patch, context]) => ({ layer: Layer.scopedContext( core.withFiberRuntime, any, Scope.Scope>((fiber) => { const scope = Context.unsafeGet(fiber.currentContext, Scope.Scope) const oldRefs = fiber.getFiberRefs() const newRefs = FiberRefsPatch.patch(patch, fiber.id(), oldRefs) const revert = FiberRefsPatch.diff(newRefs, oldRefs) fiber.setFiberRefs(newRefs) return Effect.as( Scope.addFinalizerExit(scope, () => { fiber.setFiberRefs(FiberRefsPatch.patch(revert, fiber.id(), fiber.getFiberRefs())) return Effect.void }), context ) }) ), runtimeEffect: Effect.withFiberRuntime, any, Scope.Scope>((fiber) => { const fiberRefs = FiberRefsPatch.patch(patch, fiber.id(), fiber.getFiberRefs()) return Effect.succeed(Runtime.make({ context, fiberRefs, runtimeFlags: Runtime.defaultRuntime.runtimeFlags })) }) } as const)) ), idleTimeToLive: options?.idleTimeToLive }) if (options?.preloadKeys) { for (const key of options.preloadKeys) { yield* (RcMap.get(rcMap, key) as Effect.Effect) } } return identity, any>>({ [TypeId]: TypeId, rcMap, get: (key) => Layer.unwrapScoped(Effect.map(RcMap.get(rcMap, key), ({ layer }) => layer)), runtime: (key) => Effect.flatMap(RcMap.get(rcMap, key), ({ runtimeEffect }) => runtimeEffect), invalidate: (key) => RcMap.invalidate(rcMap, key) }) }) /** * @since 3.14.0 * @category Constructors * @experimental */ export const fromRecord = < const Layers extends Record>, const Preload extends boolean = false >( layers: Layers, options?: { readonly idleTimeToLive?: Duration.DurationInput | undefined readonly preload?: Preload | undefined } | undefined ): Effect.Effect< LayerMap< keyof Layers, Layers[keyof Layers] extends Layer.Layer ? _A : never, Preload extends true ? never : Layers[keyof Layers] extends Layer.Layer ? _E : never >, Preload extends true ? never : Layers[keyof Layers] extends Layer.Layer ? _E : never, Scope.Scope | (Layers[keyof Layers] extends Layer.Layer ? _R : never) > => make((key: keyof Layers) => layers[key], { ...options, preloadKeys: options?.preload ? Object.keys(layers) : undefined }) as any /** * @since 3.14.0 * @category Service */ export interface TagClass< in out Self, in out Id extends string, in out K, in out I, in out E, in out R, in out LE, in out Deps extends Layer.Layer > extends Context.TagClass> { /** * A default layer for the `LayerMap` service. */ readonly Default: Layer.Layer< Self, (Deps extends Layer.Layer ? _E : never) | LE, | Exclude ? _A : never)> | (Deps extends Layer.Layer ? _R : never) > /** * A default layer for the `LayerMap` service without the dependencies provided. */ readonly DefaultWithoutDependencies: Layer.Layer /** * Retrieves a Layer for the resources associated with the key. */ readonly get: (key: K) => Layer.Layer /** * Retrieves a Runtime for the resources associated with the key. */ readonly runtime: (key: K) => Effect.Effect, E, Scope.Scope | Self> /** * Invalidates the resource associated with the key. */ readonly invalidate: (key: K) => Effect.Effect } /** * @since 3.14.0 * @category Service * @experimental * * Create a `LayerMap` service that provides a dynamic set of resources based on * a key. * * ```ts * import { NodeRuntime } from "@effect/platform-node" * import { Context, Effect, FiberRef, Layer, LayerMap } from "effect" * * class Greeter extends Context.Tag("Greeter") * }>() {} * * // create a service that wraps a LayerMap * class GreeterMap extends LayerMap.Service()("GreeterMap", { * // define the lookup function for the layer map * // * // The returned Layer will be used to provide the Greeter service for the * // given name. * lookup: (name: string) => * Layer.succeed(Greeter, { * greet: Effect.succeed(`Hello, ${name}!`) * }).pipe( * Layer.merge(Layer.locallyScoped(FiberRef.currentConcurrency, 123)) * ), * * // If a layer is not used for a certain amount of time, it can be removed * idleTimeToLive: "5 seconds", * * // Supply the dependencies for the layers in the LayerMap * dependencies: [] * }) {} * * // usage * const program: Effect.Effect = Effect.gen(function*() { * // access and use the Greeter service * const greeter = yield* Greeter * yield* Effect.log(yield* greeter.greet) * }).pipe( * // use the GreeterMap service to provide a variant of the Greeter service * Effect.provide(GreeterMap.get("John")) * ) * * // run the program * program.pipe( * Effect.provide(GreeterMap.Default), * NodeRuntime.runMain * ) * ``` */ export const Service = () => < const Id extends string, Options extends | NoExcessProperties< { readonly lookup: (key: any) => Layer.Layer readonly dependencies?: ReadonlyArray> readonly idleTimeToLive?: Duration.DurationInput | undefined readonly preloadKeys?: | Iterable any } ? K : never> | undefined }, Options > | NoExcessProperties<{ readonly layers: Record> readonly dependencies?: ReadonlyArray> readonly idleTimeToLive?: Duration.DurationInput | undefined readonly preload?: boolean }, Options> >( id: Id, options: Options ): TagClass< Self, Id, Options extends { readonly lookup: (key: infer K) => any } ? K : Options extends { readonly layers: infer Layers } ? keyof Layers : never, Service.Success, Options extends { readonly preload: true } ? never : Service.Error, Service.Context, Options extends { readonly preload: true } ? Service.Error : Options extends { readonly preloadKey: Iterable } ? Service.Error : never, Options extends { readonly dependencies: ReadonlyArray } ? Options["dependencies"][number] : never > => { const Err = globalThis.Error as any const limit = Err.stackTraceLimit Err.stackTraceLimit = 2 const creationError = new Err() Err.stackTraceLimit = limit function TagClass() {} const TagClass_ = TagClass as any as Mutable> Object.setPrototypeOf(TagClass, Object.getPrototypeOf(Context.GenericTag(id))) TagClass.key = id Object.defineProperty(TagClass, "stack", { get() { return creationError.stack } }) TagClass_.DefaultWithoutDependencies = Layer.scoped( TagClass_, "lookup" in options ? make(options.lookup, options) : fromRecord(options.layers as any, options) ) TagClass_.Default = options.dependencies && options.dependencies.length > 0 ? Layer.provide(TagClass_.DefaultWithoutDependencies, options.dependencies as any) : TagClass_.DefaultWithoutDependencies TagClass_.get = (key: string) => Layer.unwrapScoped(Effect.map(TagClass_, (layerMap) => layerMap.get(key))) TagClass_.runtime = (key: string) => Effect.flatMap(TagClass_, (layerMap) => layerMap.runtime(key)) TagClass_.invalidate = (key: string) => Effect.flatMap(TagClass_, (layerMap) => layerMap.invalidate(key)) return TagClass as any } /** * @since 3.14.0 * @category Service * @experimental */ export declare namespace Service { /** * @since 3.14.0 * @category Service * @experimental */ export type Key = Options extends { readonly lookup: (key: infer K) => any } ? K : Options extends { readonly layers: infer Layers } ? keyof Layers : never /** * @since 3.14.0 * @category Service * @experimental */ export type Layers = Options extends { readonly lookup: (key: infer _K) => infer Layers } ? Layers : Options extends { readonly layers: infer Layers } ? Layers[keyof Layers] : never /** * @since 3.14.0 * @category Service * @experimental */ export type Success = Layers extends Layer.Layer ? _A : never /** * @since 3.14.0 * @category Service * @experimental */ export type Error = Layers extends Layer.Layer ? _E : never /** * @since 3.14.0 * @category Service * @experimental */ export type Context = Layers extends Layer.Layer ? _R : never }