1/**
2 * @title Dynamic resources with LayerMap
3 *
4 * Use `LayerMap.Service` to dynamically build and manage resources that are
5 * keyed by some identifier, such as a tenant ID.
6 */
7import { Context, Effect, Layer, LayerMap, Schema } from "effect"
8
9class DatabaseQueryError extends Schema.TaggedError<DatabaseQueryError>()("DatabaseQueryError", {
10 tenantId: Schema.String,
11 cause: Schema.Defect()
12}) {}
13
14type UserRecord = {
15 readonly id: number
16 readonly email: string
17}
18
19let nextConnectionId = 0
20
21export class DatabasePool extends Context.Service<DatabasePool, {
22 readonly tenantId: string
23 readonly connectionId: number
24 readonly query: (sql: string) => Effect.Effect<ReadonlyArray<UserRecord>, DatabaseQueryError>
25}>()("app/DatabasePool") {
26 // A layer factory that builds one pool per tenant.
27 static readonly layer = (tenantId: string) =>
28 Layer.effect(
29 DatabasePool,
30 Effect.acquireRelease(
31 Effect.sync(() => {
32 const connectionId = ++nextConnectionId
33
34 return DatabasePool.of({
35 tenantId,
36 connectionId,
37 query: Effect.fn("DatabasePool.query")((_sql: string) =>
38 Effect.succeed([
39 { id: 1, email: `admin@${tenantId}.example.com` },
40 { id: 2, email: `ops@${tenantId}.example.com` }
41 ])
42 )
43 })
44 }),
45 (pool) => Effect.logInfo(`Closing tenant pool ${pool.tenantId}#${pool.connectionId}`)
46 )
47 )
48}
49
50// extend `LayerMap.Service` to create a `LayerMap` service
51export class PoolMap extends LayerMap.Service<PoolMap>()("app/PoolMap", {
52 // `lookup` tells LayerMap how to build a layer for each tenant key.
53 lookup: (tenantId: string) => DatabasePool.layer(tenantId),
54
55 // You can also use the layers option for a static set of layers
56 // layers: {
57 // acme: DatabasePool.layer("acme"),
58 // globex: DatabasePool.layer("globex")
59 // },
60
61 // If a pool is not used for this duration, it is released automatically.
62 idleTimeToLive: "1 minute"
63}) {}
64
65const queryUsersForCurrentTenant = Effect.gen(function*() {
66 // Run a query agnostic of the tenant. The correct pool will be provided by
67 // the LayerMap.
68 const pool = yield* DatabasePool
69 return yield* pool.query("SELECT id, email FROM users ORDER BY id")
70})
71
72export const program = Effect.gen(function*() {
73 yield* queryUsersForCurrentTenant.pipe(
74 // Use `PoolMap.get` to access the pool for a specific tenant. The first
75 // time this is called for a tenant, the pool will be built using the
76 // `lookup` function defined in `PoolMap`. Subsequent calls will reuse the
77 // cached pool until it is idle for too long or invalidated.
78 Effect.provide(PoolMap.get("acme"))
79 )
80
81 // `PoolMap.invalidate` forces a key to rebuild on the next access.
82 yield* PoolMap.invalidate("acme")
83}).pipe(
84 // Provide the `PoolMap` layer to the entire program.
85 Effect.provide(PoolMap.layer)
86)