1/**
2 * Caches scoped services selected by key and built from layers.
3 *
4 * A `LayerMap<K, I, E>` turns a key into a cached service `Context<I>` and
5 * exposes that context as either a `Layer` or a scoped effect. Entries can be
6 * invalidated explicitly or released after they sit unused. This is useful for
7 * keyed resource families such as tenant clients, regional connections, or
8 * environment-specific services.
9 *
10 * @since 3.14.0
11 */
12import * as Context from "./Context.ts"
13import * as Duration from "./Duration.ts"
14import * as Effect from "./Effect.ts"
15import { identity } from "./Function.ts"
16import { getStackTraceLimit, setStackTraceLimit } from "./internal/stackTraceLimit.ts"
17import * as Layer from "./Layer.ts"
18import type * as Option from "./Option.ts"
19import * as RcMap from "./RcMap.ts"
20import * as Scope from "./Scope.ts"
21import type { Mutable, NoExcessProperties } from "./Types.ts"
22
23const TypeId = "~effect/LayerMap"
24
25type IdleTimeToLiveInput<K> = Duration.Input | ((key: K) => Duration.Input)
26
27/**
28 * A scoped, keyed map of layer-built service contexts.
29 *
30 * **Details**
31 *
32 * A `LayerMap` builds resources for a key on demand, exposes them as a `Layer`
33 * or scoped `Context`, and can invalidate cached resources for a key.
34 *
35 * **Example** (Managing keyed layers)
36 *
37 * ```ts import.meta.vitest
38 * import { Context, Effect, Layer, LayerMap } from "effect"
39 *
40 * // Define a service key
41 * const DatabaseService = Context.Service<{
42 * readonly query: (sql: string) => Effect.Effect<string>
43 * }>("Database")
44 *
45 * // Create a LayerMap that provides different database configurations
46 * const createDatabaseLayerMap = LayerMap.make((env: string) =>
47 * Layer.succeed(DatabaseService)({
48 * query: Effect.fn("DatabaseService.query")((sql) => Effect.succeed(`${env}: ${sql}`))
49 * })
50 * )
51 *
52 * // Use the LayerMap
53 * const program = Effect.gen(function*() {
54 * const layerMap = yield* createDatabaseLayerMap
55 *
56 * // Get a layer for a specific environment
57 * const development = yield* Effect.provide(
58 * DatabaseService.use((database) => database.query("SELECT 1")),
59 * layerMap.get("development")
60 * )
61 *
62 * // Get context directly
63 * const productionContext = yield* layerMap.contextEffect("production")
64 * const production = yield* Context.get(productionContext, DatabaseService).query("SELECT 1")
65 *
66 * // Invalidate a cached layer
67 * yield* layerMap.invalidate("development")
68 *
69 * return { development, production }
70 * })
71 *
72 * await Effect.runPromise(Effect.scoped(program)) // => { development: "development: SELECT 1", production: "production: SELECT 1" }
73 * ```
74 *
75 * @category models
76 * @since 3.14.0
77 */
78export interface LayerMap<in out K, in out I, in out E = never> {
79 readonly [TypeId]: typeof TypeId
80
81 /**
82 * The internal RcMap that stores the resources.
83 */
84 readonly rcMap: RcMap.RcMap<K, Context.Context<I>, E>
85
86 /**
87 * Retrieves a Layer for the resources associated with the key.
88 */
89 get(key: K): Layer.Layer<I, E>
90
91 /**
92 * Retrieves the context associated with the key.
93 */
94 contextEffect(key: K): Effect.Effect<Context.Context<I>, E, Scope.Scope>
95
96 /**
97 * Retains and returns the context for a key only when it is currently cached.
98 *
99 * **Details**
100 *
101 * `Option.none` means no entry is currently cached or the `LayerMap` is closed;
102 * no layer is built for a missing key. An existing in-flight entry is awaited.
103 *
104 * @since 4.0.0
105 */
106 contextEffectOption(key: K): Effect.Effect<Option.Option<Context.Context<I>>, E, Scope.Scope>
107
108 /**
109 * Invalidates the resource associated with the key.
110 */
111 invalidate(key: K): Effect.Effect<void>
112}
113
114/**
115 * Creates a `LayerMap` that dynamically provides resources based on a key.
116 *
117 * **Example** (Creating a layer map)
118 *
119 * ```ts import.meta.vitest
120 * import { Context, Effect, Layer, LayerMap } from "effect"
121 *
122 * // Define a service key
123 * const DatabaseService = Context.Service<{
124 * readonly query: (sql: string) => Effect.Effect<string>
125 * }>("Database")
126 *
127 * // Create a LayerMap that provides different database configurations
128 * const program = Effect.gen(function*() {
129 * const layerMap = yield* LayerMap.make(
130 * (env: string) =>
131 * Layer.succeed(DatabaseService)({
132 * query: Effect.fn("DatabaseService.query")((sql) => Effect.succeed(`${env}: ${sql}`))
133 * }),
134 * { idleTimeToLive: "5 seconds" }
135 * )
136 *
137 * // Get a layer for a specific environment
138 * const devLayer = layerMap.get("development")
139 *
140 * // Use the layer to provide the service
141 * return yield* Effect.provide(
142 * Effect.gen(function*() {
143 * const db = yield* DatabaseService
144 * return yield* db.query("SELECT * FROM users")
145 * }),
146 * devLayer
147 * )
148 * })
149 *
150 * await Effect.runPromise(Effect.scoped(program)) // => "development: SELECT * FROM users"
151 * ```
152 *
153 * @category constructors
154 * @since 3.14.0
155 */
156export const make: <
157 K,
158 L extends Layer.Layer<any, any, any>,
159 PreloadKeys extends Iterable<K> | undefined = undefined
160>(
161 lookup: (key: K) => L,
162 options?: {
163 readonly idleTimeToLive?: IdleTimeToLiveInput<K> | undefined
164 /**
165 * Preloaded entries are retained only for their idle TTL. Keys whose idle
166 * TTL is zero are not preloaded (including when no TTL is specified).
167 */
168 readonly preloadKeys?: PreloadKeys
169 } | undefined
170) => Effect.Effect<
171 LayerMap<K, Layer.Success<L>, Layer.Error<L>>,
172 PreloadKeys extends undefined ? never : Layer.Error<L>,
173 Scope.Scope | Layer.Services<L>
174> = Effect.fnUntraced(function*<I, K, EL, RL>(
175 lookup: (key: K) => Layer.Layer<I, EL, RL>,
176 options?: {
177 readonly idleTimeToLive?: IdleTimeToLiveInput<K> | undefined
178 readonly preloadKeys?: Iterable<K> | undefined
179 } | undefined
180) {
181 const context = yield* Effect.context<never>()
182 const memoMap = Layer.CurrentMemoMap.forkOrCreate(context)
183
184 const rcMap = yield* RcMap.make({
185 lookup: (key: K) =>
186 Effect.contextWith((_: Context.Context<Scope.Scope>) =>
187 Layer.buildWithMemoMap(lookup(key), memoMap, Context.get(_, Scope.Scope))
188 ),
189 idleTimeToLive: options?.idleTimeToLive
190 })
191
192 if (options?.preloadKeys) {
193 for (const key of options.preloadKeys) {
194 if (!Duration.isZero(rcMap.idleTimeToLive(key))) {
195 yield* Effect.scoped(RcMap.get(rcMap, key))
196 }
197 }
198 }
199
200 return identity<LayerMap<K, I, any>>({
201 [TypeId]: TypeId,
202 rcMap,
203 get: (key) => Layer.effectContext(RcMap.get(rcMap, key)),
204 contextEffect: (key) => RcMap.get(rcMap, key),
205 contextEffectOption: (key) => RcMap.getOption(rcMap, key),
206 invalidate: (key) => RcMap.invalidate(rcMap, key)
207 })
208})
209
210/**
211 * Creates a `LayerMap` from a record of predefined layers.
212 *
213 * **Details**
214 *
215 * The record keys become the keys accepted by the returned `LayerMap`, and the
216 * record values are the layers built for those keys.
217 *
218 * **Example** (Creating a layer map from a record)
219 *
220 * ```ts import.meta.vitest
221 * import { Context, Effect, Layer, LayerMap } from "effect"
222 *
223 * // Define a service key
224 * const Database = Context.Service<{
225 * readonly query: (sql: string) => Effect.Effect<string>
226 * }>("Database")
227 *
228 * // Create predefined layers
229 * const layers = {
230 * development: Layer.succeed(Database)({
231 * query: Effect.fn("DevDatabase.query")((sql) => Effect.succeed(`DEV: ${sql}`))
232 * }),
233 * production: Layer.succeed(Database)({
234 * query: Effect.fn("ProdDatabase.query")((sql) => Effect.succeed(`PROD: ${sql}`))
235 * })
236 * } as const
237 *
238 * // Create a LayerMap from the record
239 * const program = Effect.gen(function*() {
240 * const layerMap = yield* LayerMap.fromRecord(layers, {
241 * idleTimeToLive: "10 seconds"
242 * })
243 *
244 * const development = yield* Effect.provide(
245 * Database.use((database) => database.query("SELECT 1")),
246 * layerMap.get("development")
247 * )
248 * const production = yield* Effect.provide(
249 * Database.use((database) => database.query("SELECT 1")),
250 * layerMap.get("production")
251 * )
252 *
253 * return { development, production }
254 * })
255 *
256 * await Effect.runPromise(Effect.scoped(program)) // => { development: "DEV: SELECT 1", production: "PROD: SELECT 1" }
257 * ```
258 *
259 * @category constructors
260 * @since 3.14.0
261 */
262export const fromRecord = <
263 const Layers extends Record<string, Layer.Layer<any, any, any>>,
264 const Preload extends boolean = false
265>(
266 layers: Layers,
267 options?: {
268 readonly idleTimeToLive?: IdleTimeToLiveInput<keyof Layers> | undefined
269 /**
270 * Preloaded entries are retained only for their idle TTL. Keys whose idle
271 * TTL is zero are not preloaded (including when no TTL is specified).
272 */
273 readonly preload?: Preload | undefined
274 } | undefined
275): Effect.Effect<
276 LayerMap<
277 keyof Layers,
278 Layer.Success<Layers[keyof Layers]>,
279 Layer.Error<Layers[keyof Layers]>
280 >,
281 Preload extends true ? Layer.Error<Layers[keyof Layers]> : never,
282 Scope.Scope | (Layers[keyof Layers] extends Layer.Layer<infer _A, infer _E, infer _R> ? _R : never)
283> =>
284 make((key: keyof Layers) => layers[key], {
285 ...options,
286 preloadKeys: options?.preload ? Object.keys(layers) : undefined
287 }) as any
288
289/**
290 * Service class shape produced by `LayerMap.Service`.
291 *
292 * **When to use**
293 *
294 * Use as the public type for classes returned by `LayerMap.Service` when an API
295 * needs to accept, return, or alias the generated service class and its static
296 * helpers.
297 *
298 * **Details**
299 *
300 * It combines a `Context.Service` tag for the `LayerMap` with default layers
301 * and helper accessors for retrieving, using, and invalidating keyed resources.
302 *
303 * @see {@link Service} for creating concrete `LayerMap` service classes
304 *
305 * @category services
306 * @since 3.14.0
307 */
308export interface TagClass<
309 in out Self,
310 in out Id extends string,
311 in out K,
312 in out I,
313 in out E,
314 in out R,
315 in out LE,
316 in out Deps extends Layer.Layer<any, any, any>
317> extends Context.ServiceClass<Self, Id, LayerMap<K, I, E>> {
318 /**
319 * A default layer for the `LayerMap` service.
320 */
321 readonly layer: Layer.Layer<
322 Self,
323 (Deps extends Layer.Layer<infer _A, infer _E, infer _R> ? _E : never) | LE,
324 | Exclude<R, (Deps extends Layer.Layer<infer _A, infer _E, infer _R> ? _A : never)>
325 | (Deps extends Layer.Layer<infer _A, infer _E, infer _R> ? _R : never)
326 >
327
328 /**
329 * A default layer for the `LayerMap` service without the dependencies provided.
330 */
331 readonly layerNoDeps: Layer.Layer<Self, LE, R>
332
333 /**
334 * Retrieves a Layer for the resources associated with the key.
335 */
336 readonly get: (key: K) => Layer.Layer<I, E, Self>
337
338 /**
339 * Retrieves the context associated with the key.
340 */
341 readonly contextEffect: (key: K) => Effect.Effect<Context.Context<I>, E, Scope.Scope | Self>
342
343 /**
344 * Retains and returns the context for a key only when it is currently cached.
345 *
346 * **Details**
347 *
348 * `Option.none` means no entry is currently cached or the `LayerMap` is closed;
349 * no layer is built for a missing key. An existing in-flight entry is awaited.
350 *
351 * @since 4.0.0
352 */
353 readonly contextEffectOption: (
354 key: K
355 ) => Effect.Effect<Option.Option<Context.Context<I>>, E, Scope.Scope | Self>
356
357 /**
358 * Invalidates the resource associated with the key.
359 */
360 readonly invalidate: (key: K) => Effect.Effect<void, never, Self>
361}
362
363/**
364 * Create a `LayerMap` service that provides a dynamic set of resources based on
365 * a key.
366 *
367 * **Example** (Defining a layer map service)
368 *
369 * ```ts import.meta.vitest
370 * import { Context, Effect, Layer, LayerMap } from "effect"
371 *
372 * // Define a service key
373 * const Greeter = Context.Service<{
374 * readonly greet: Effect.Effect<string>
375 * }>("Greeter")
376 *
377 * // Create a service that wraps a LayerMap
378 * class GreeterMap extends LayerMap.Service<GreeterMap>()("GreeterMap", {
379 * // Define the lookup function for the layer map
380 * lookup: (name: string) =>
381 * Layer.succeed(Greeter)({
382 * greet: Effect.succeed(`Hello, ${name}!`)
383 * }),
384 *
385 * // If a layer is not used for a certain amount of time, it can be removed
386 * idleTimeToLive: "5 seconds"
387 * }) {}
388 *
389 * // Usage
390 * const program = Effect.gen(function*() {
391 * // Access and use the Greeter service
392 * const greeter = yield* Greeter
393 * return yield* greeter.greet
394 * }).pipe(
395 * // Use the GreeterMap service to provide a variant of the Greeter service
396 * Effect.provide(GreeterMap.get("John"))
397 * ).pipe(
398 * // Provide the GreeterMap layer
399 * Effect.provide(GreeterMap.layer)
400 * )
401 *
402 * await Effect.runPromise(program) // => "Hello, John!"
403 * ```
404 *
405 * @category services
406 * @since 3.14.0
407 */
408export const Service = <Self>() =>
409<
410 const Id extends string,
411 const Options extends
412 | NoExcessProperties<{
413 readonly lookup: (key: any) => Layer.Layer<any, any, any>
414 readonly dependencies?: ReadonlyArray<Layer.Layer<any, any, any>> | undefined
415 readonly idleTimeToLive?: IdleTimeToLiveInput<any> | undefined
416 /**
417 * Preloaded entries are retained only for their idle TTL. Keys whose idle
418 * TTL is zero are not preloaded (including when no TTL is specified).
419 */
420 readonly preloadKeys?:
421 | Iterable<Options extends { readonly lookup: (key: infer K) => any } ? K : never>
422 | undefined
423 }, Options>
424 | NoExcessProperties<{
425 readonly layers: Record<string, Layer.Layer<any, any, any>>
426 readonly dependencies?: ReadonlyArray<Layer.Layer<any, any, any>> | undefined
427 readonly idleTimeToLive?: IdleTimeToLiveInput<any> | undefined
428 /**
429 * Preloaded entries are retained only for their idle TTL. Keys whose idle
430 * TTL is zero are not preloaded (including when no TTL is specified).
431 */
432 readonly preload?: boolean | undefined
433 }, Options>
434>(
435 id: Id,
436 options: Options
437): TagClass<
438 Self,
439 Id,
440 Options extends { readonly lookup: (key: infer K) => any } ? K
441 : Options extends { readonly layers: infer Layers } ? keyof Layers
442 : never,
443 Service.Success<Options>,
444 Service.Error<Options>,
445 Service.Services<Options>,
446 Options extends { readonly preload: true } ? Service.Error<Options>
447 : Options extends { readonly preloadKeys: Iterable<any> } ? Service.Error<Options>
448 : never,
449 Options extends { readonly dependencies: ReadonlyArray<Layer.Layer<any, any, any>> } ? Options["dependencies"][number]
450 : never
451> => {
452 const limit = getStackTraceLimit()
453 let creationError: Error | undefined
454 if (limit !== 0) {
455 setStackTraceLimit(2)
456 creationError = new globalThis.Error()
457 setStackTraceLimit(limit)
458 }
459
460 function TagClass() {}
461 const TagClass_ = TagClass as any as Mutable<TagClass<Self, Id, string, any, any, any, any, any>>
462 Object.setPrototypeOf(TagClass, Object.getPrototypeOf(Context.Service<Self, any>(id)))
463 TagClass.key = id
464 Object.defineProperty(TagClass, "stack", {
465 get() {
466 return creationError?.stack
467 }
468 })
469
470 TagClass_.layerNoDeps = Layer.effect(TagClass_)(
471 "lookup" in options
472 ? make(options.lookup, options)
473 : fromRecord(options.layers as any, options) as any
474 )
475 TagClass_.layer = options.dependencies && options.dependencies.length > 0 ?
476 Layer.provide(TagClass_.layerNoDeps, options.dependencies as any) :
477 TagClass_.layerNoDeps
478
479 TagClass_.get = (key: string) => Layer.unwrap(Effect.map(TagClass_, (layerMap) => layerMap.get(key)))
480 TagClass_.contextEffect = (key: string) => Effect.flatMap(TagClass_, (layerMap) => layerMap.contextEffect(key))
481 TagClass_.contextEffectOption = (key: string) =>
482 Effect.flatMap(TagClass_, (layerMap) => layerMap.contextEffectOption(key))
483 TagClass_.invalidate = (key: string) => Effect.flatMap(TagClass_, (layerMap) => layerMap.invalidate(key))
484
485 return TagClass as any
486}
487
488/**
489 * Type helpers for values created with `LayerMap.Service`.
490 *
491 * @since 3.14.0
492 */
493export declare namespace Service {
494 /**
495 * Extracts the key type accepted by a `LayerMap.Service` definition.
496 *
497 * @category utility types
498 * @since 3.14.0
499 */
500 export type Key<Options> = Options extends { readonly lookup: (key: infer K) => any } ? K
501 : Options extends { readonly layers: infer Layers } ? keyof Layers
502 : never
503
504 /**
505 * Extracts the layer type produced by a `LayerMap.Service` definition.
506 *
507 * @category utility types
508 * @since 3.14.0
509 */
510 export type Layers<Options> = Options extends { readonly lookup: (key: infer _K) => infer Layers } ? Layers
511 : Options extends { readonly layers: infer Layers } ? Layers[keyof Layers]
512 : never
513
514 /**
515 * Extracts the services provided by the layers in a `LayerMap.Service`
516 * definition.
517 *
518 * @category utility types
519 * @since 3.14.0
520 */
521 export type Success<Options> = Layers<Options> extends Layer.Layer<infer _A, infer _E, infer _R> ? _A : never
522
523 /**
524 * Extracts the error type of the layers in a `LayerMap.Service` definition.
525 *
526 * @category utility types
527 * @since 3.14.0
528 */
529 export type Error<Options> = Layers<Options> extends Layer.Layer<infer _A, infer _E, infer _R> ? _E : never
530
531 /**
532 * Extracts the service requirements of the layers in a `LayerMap.Service`
533 * definition.
534 *
535 * @category utility types
536 * @since 4.0.0
537 */
538 export type Services<Options> = Layers<Options> extends Layer.Layer<infer _A, infer _E, infer _R> ? _R : never
539}