LayerMap
Ordinary layers build a fixed set of services. But some applications need a
service instance per key that isn’t known ahead of time: a database client per
tenant, a connection per region, an API client per account. LayerMap is a
scoped, reference-counted cache that builds a layer on demand for each key,
reuses it while it’s live, and finalizes it when it’s no longer needed.
import { Context, Effect, Layer, LayerMap } from "effect"
// The per-key service we want a distinct instance of.class Greeter extends Context.Service<Greeter, { readonly greet: Effect.Effect<string>}>()("myapp/Greeter") {}
// A LayerMap.Service wraps the cache and exposes accessor helpers on the class.// `lookup` is called once per distinct key to produce that key's layer.class GreeterMap extends LayerMap.Service<GreeterMap>()("myapp/GreeterMap", { lookup: (name: string) => Layer.succeed(Greeter, { greet: Effect.succeed(`Hello, ${name}!`) }),
// Entries unused for this long are released automatically. idleTimeToLive: "5 seconds"}) {}To use a keyed resource, provide the layer for a specific key with
GreeterMap.get(key). That layer makes the Greeter service available, built
from the cache:
import { Console, Effect } from "effect"import { Greeter } from "./Greeter.ts"import { GreeterMap } from "./GreeterMap.ts"
const program = Effect.gen(function*() { const greeter = yield* Greeter yield* Console.log(yield* greeter.greet)}).pipe( // `get("John")` provides a Greeter built for the key "John". Asking again for // "John" reuses the cached instance; a different key builds a new one. Effect.provide(GreeterMap.get("John")), // The map itself is a layer too — provide it once at the top. Effect.provide(GreeterMap.layer))
Effect.runFork(program)How it works
Section titled “How it works”LayerMap.Service<Self>()("id", options) produces a class that is both a normal
service (so it has a .layer) and a façade over the underlying cache. The
accessor helpers it generates:
GreeterMap.get(key)— aLayerthat provides the key’s services, building them on first use and reusing the cached instance afterwards.GreeterMap.contextEffect(key)— the acquiredContextdirectly, for when you want the services as a value rather than a layer (requires aScope).GreeterMap.invalidate(key)— finalize the current cached entry for a key; the next access rebuilds it.
The cache is reference-counted: while anything is using a key’s resources they
stay live, and they’re finalized once nobody holds them and idleTimeToLive
elapses.
A realistic example: per-tenant database
Section titled “A realistic example: per-tenant database”The lookup function returns a full layer, so each key can have its own configured resource — including one with dependencies and cleanup:
import { Context, Effect, Layer, LayerMap } from "effect"
class TenantDb extends Context.Service<TenantDb, { query(sql: string): Effect.Effect<ReadonlyArray<unknown>>}>()("myapp/TenantDb") {}
class TenantDbMap extends LayerMap.Service<TenantDbMap>()("myapp/TenantDbMap", { // Build a distinct, resource-owning layer per tenant id. lookup: (tenantId: string) => Layer.effect( TenantDb, Effect.gen(function*() { // Each tenant gets its own connection, released when the entry expires. const conn = yield* Effect.acquireRelease( Effect.sync(() => ({ tenantId, close: () => {} })), (c) => Effect.sync(() => c.close()) )
return TenantDb.of({ query: (sql) => Effect.succeed([{ tenantId: conn.tenantId, sql }]) }) }) ), idleTimeToLive: "30 seconds"}) {}
// Handle a request scoped to one tenant.const handle = (tenantId: string) => Effect.gen(function*() { const db = yield* TenantDb return yield* db.query("SELECT * FROM orders") }).pipe( Effect.provide(TenantDbMap.get(tenantId)), Effect.provide(TenantDbMap.layer) )Fixed sets of keys
Section titled “Fixed sets of keys”When the keys are known and finite, pass a layers record instead of a
lookup function. The key type becomes the union of record keys:
import { Context, Effect, Layer, LayerMap } from "effect"import { Greeter } from "./Greeter.ts"
class RegionGreeters extends LayerMap.Service<RegionGreeters>()("myapp/RegionGreeters", { layers: { us: Layer.succeed(Greeter, { greet: Effect.succeed("Hi from US") }), eu: Layer.succeed(Greeter, { greet: Effect.succeed("Hallo from EU") }) }}) {}
// RegionGreeters.get("us") | "eu" — anything else is a compile error.Dependencies, idle eviction, and preloading
Section titled “Dependencies, idle eviction, and preloading”The service options accept a few more fields that shape how the map is built and how its entries live and die.
dependencies— layers the lookup/record layers need. They are provided to.layer(but not.layerNoDeps), so the resultingLayerMaplayer has no outstanding requirements.idleTimeToLive— how long an entry survives after its reference count hits zero before being finalized. Omit it (the default is0) and an entry is released the instant its last reference is gone. Pass aDuration.Inputfor a fixed window, or a function(key) => Duration.Inputto vary it per key.preloadKeys(lookup form) /preload: true(record form) — eagerly build the listed entries when the map is created. This moves layer construction errors toLayerMapcreation time instead of first use, which is why preloading adds the layers’ error type to.layer’s error channel.
import { Context, Effect, Layer, LayerMap } from "effect"
class Config extends Context.Service<Config, { readonly url: string }>()( "myapp/Config") {}
class ClientMap extends LayerMap.Service<ClientMap>()("myapp/ClientMap", { lookup: (region: "us" | "eu") => Layer.succeed(Config, { url: `https://${region}.example.com` }),
// Provided to `ClientMap.layer`, so the layer requires nothing extra. dependencies: [],
// "us" lives 1 minute idle; "eu" lives 5 minutes idle. idleTimeToLive: (region) => (region === "us" ? "1 minute" : "5 minutes"),
// Build both entries up front; failures surface here, not on first get. preloadKeys: ["us", "eu"]}) {}Reference counting and idle expiration
Section titled “Reference counting and idle expiration”A LayerMap is, under the hood, an RcMap<K, Context<I>, E>: a scoped,
reference-counted map. Each get(key) increments the entry’s reference count for
the current scope and adds a release finalizer to it. When that scope closes
the reference is released; when the count reaches zero the entry is either closed
immediately (default, idleTimeToLive of 0) or kept warm until the idle window
elapses. Building the same key while it is live shares the cached Context
instead of rebuilding it.
idleTimeToLive controls that warm window: a fixed Duration.Input, a
(key) => Duration.Input function, or — when infinite — an entry that never
expires on its own (only invalidate or the map’s scope closing releases it).
Underlying RcMap
Section titled “Underlying RcMap”LayerMap delegates lifecycle management to the RcMap module. You rarely need
it directly, but .rcMap lets you use these combinators on the live entries:
RcMap.keys
Section titled “RcMap.keys”Returns an iterable of all keys that currently have a stored entry. Interrupts if the map’s scope has closed.
import { Effect, RcMap } from "effect"
declare const map: RcMap.RcMap<string, unknown>Effect.gen(function*() { const ks = yield* RcMap.keys(map) console.log([...ks]) // => ["us", "eu"]})RcMap.has
Section titled “RcMap.has”Checks whether a key currently has a stored entry, without running the lookup.
Returns false for both missing keys and closed maps.
import { Effect, RcMap } from "effect"
declare const map: RcMap.RcMap<string, unknown>Effect.gen(function*() { const present = yield* RcMap.has(map, "us") console.log(present) // => true})RcMap.touch
Section titled “RcMap.touch”Resets the idle expiration timer for a key (when idleTimeToLive is configured),
keeping a warm entry alive longer without acquiring a new reference.
import { Effect, RcMap } from "effect"
declare const map: RcMap.RcMap<string, unknown>Effect.gen(function*() { yield* RcMap.touch(map, "us") // => idle timer reset for "us"})RcMap.invalidate
Section titled “RcMap.invalidate”Removes a key and releases it if it has no live references. This is what
LayerMap’s .invalidate(key) calls under the hood.
import { Effect, RcMap } from "effect"
declare const map: RcMap.RcMap<string, unknown>Effect.gen(function*() { yield* RcMap.invalidate(map, "us") // => "us" removed; rebuilt on next get})LayerMap builds on the same caching machinery as
Caching and the scope rules from
Resource Management; reach for it specifically when the
identity of a resource is a runtime key.
API reference
Section titled “API reference”LayerMap.make
Section titled “LayerMap.make”Standalone constructor: builds a scoped LayerMap from a lookup function. Use
this when you want the LayerMap value directly (e.g. inside another layer)
rather than as its own service class. Returns an Effect that yields the map and
requires a Scope.
import { Context, Effect, Layer, LayerMap } from "effect"
class Db extends Context.Service<Db, { readonly query: (sql: string) => Effect.Effect<string>}>()("Db") {}
const program = Effect.gen(function*() { const map = yield* LayerMap.make( (env: string) => Layer.succeed(Db, { query: (sql) => Effect.succeed(`${env}: ${sql}`) }), { idleTimeToLive: "5 seconds" } )
const result = yield* Effect.gen(function*() { const db = yield* Db return yield* db.query("SELECT 1") }).pipe(Effect.provide(map.get("development")))
console.log(result) // => "development: SELECT 1"}).pipe(Effect.scoped)LayerMap.fromRecord
Section titled “LayerMap.fromRecord”Standalone constructor for a fixed set of keys: the record’s keys become the
map’s key type, and its values are the layers built for those keys. Accepts
idleTimeToLive and preload.
import { Context, Effect, Layer, LayerMap } from "effect"import { Db } from "./Db.ts"
const program = Effect.gen(function*() { const map = yield* LayerMap.fromRecord( { dev: Layer.succeed(Db, { query: (s) => Effect.succeed(`DEV: ${s}`) }), prod: Layer.succeed(Db, { query: (s) => Effect.succeed(`PROD: ${s}`) }) }, { idleTimeToLive: "10 seconds" } )
const layer = map.get("dev") // => Layer<Db>}).pipe(Effect.scoped)LayerMap.Service
Section titled “LayerMap.Service”Defines a service class that wraps a LayerMap. Call it as
LayerMap.Service<Self>()(id, options) where options is either the lookup form
{ lookup, dependencies?, idleTimeToLive?, preloadKeys? } or the record form
{ layers, dependencies?, idleTimeToLive?, preload? }. The class is a
Context.Service tag and carries the static accessor helpers below.
import { Context, Effect, Layer, LayerMap } from "effect"
class Greeter extends Context.Service<Greeter, { readonly greet: Effect.Effect<string>}>()("Greeter") {}
class GreeterMap extends LayerMap.Service<GreeterMap>()("GreeterMap", { lookup: (name: string) => Layer.succeed(Greeter, { greet: Effect.succeed(`Hello, ${name}!`) }), idleTimeToLive: "5 seconds"}) {}
// The class is itself a service tag plus the static helpers .layer/.get/etc..get(key)
Section titled “.get(key)”A Layer that provides the key’s services, built on first access and reused
while cached. Provide it to the program that needs the keyed services.
import { Console, Effect } from "effect"import { Greeter } from "./Greeter.ts"import { GreeterMap } from "./GreeterMap.ts"
const useGreeter = Effect.gen(function*() { const greeter = yield* Greeter yield* Console.log(yield* greeter.greet) // => "Hello, John!"}).pipe( Effect.provide(GreeterMap.get("John")), Effect.provide(GreeterMap.layer)).contextEffect(key)
Section titled “.contextEffect(key)”Returns the acquired Context for a key directly, as an effectful value rather
than a layer. Requires a Scope (plus the service tag), since the returned
context’s resources stay live for the surrounding scope.
import { Context, Effect } from "effect"import { Greeter } from "./Greeter.ts"import { GreeterMap } from "./GreeterMap.ts"
const program = Effect.gen(function*() { // The acquired Context<Greeter>, kept live for the surrounding scope. const ctx = yield* GreeterMap.contextEffect("Jane")
// Read a service out of it directly... const greeter = Context.get(ctx, Greeter) console.log(yield* greeter.greet) // => "Hello, Jane!"
// ...or provide the whole context to an effect. const msg = yield* Greeter.pipe( Effect.flatMap((g) => g.greet), Effect.provideContext(ctx) ) console.log(msg) // => "Hello, Jane!"}).pipe(Effect.scoped, Effect.provide(GreeterMap.layer)).invalidate(key)
Section titled “.invalidate(key)”Finalizes the current cached entry for a key and removes it from the map; the
next get/contextEffect rebuilds it. Useful for forcing a reconnect or
discarding a stale resource. If the entry still has live references it is removed
from the map but only finalized once those references are released.
import { Effect } from "effect"import { GreeterMap } from "./GreeterMap.ts"
const refresh = Effect.gen(function*() { // Drop the cached "John" entry; the next get("John") builds a fresh one. yield* GreeterMap.invalidate("John")}).pipe(Effect.provide(GreeterMap.layer)).layer
Section titled “.layer”The default layer that constructs the LayerMap service, with any
dependencies already provided. Provide this once near the root so the accessor
layers (get, etc.) have a map to read from.
import { Effect } from "effect"import { GreeterMap } from "./GreeterMap.ts"
const main = Effect.void.pipe(Effect.provide(GreeterMap.layer))// => Layer<GreeterMap> (dependencies, if any, already satisfied).layerNoDeps
Section titled “.layerNoDeps”Same as .layer but without the configured dependencies provided — so its
requirement channel still lists those dependency services. Use it when you want
to wire the dependencies yourself (e.g. share a single dependency layer across
several services).
import { Layer } from "effect"import { ClientMap } from "./ClientMap.ts"
// Provide dependencies your own way instead of relying on options.dependencies.const wired = Layer.provide(ClientMap.layerNoDeps, [/* your dep layers */]).rcMap
Section titled “.rcMap”On the LayerMap value (from make / fromRecord), exposes the underlying
RcMap that stores the reference-counted contexts. Drop to
this layer to inspect keys or extend idle lifetimes with the RcMap combinators.
import { Effect, LayerMap, RcMap } from "effect"
const inspect = (map: LayerMap.LayerMap<string, never>) => Effect.gen(function*() { const keys = yield* RcMap.keys(map.rcMap) console.log([...keys]) // => the keys with live/cached entries })