Keyed Resources with 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 { Console, 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.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.