Skip to content

Commit 2836c48

Browse files
committed
add LayerMap module
A `LayerMap` allows you to create a map of Layer's that can be used to dynamically access resources based on a key.
1 parent 46ed7b6 commit 2836c48

File tree

6 files changed

+254
-2
lines changed

6 files changed

+254
-2
lines changed

.changeset/calm-spoons-own.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"effect": minor
3+
---
4+
5+
expose the Layer.MemoMap via Layer.CurrentMemoMap to the layers being built

.changeset/tricky-apricots-flow.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
"effect": minor
3+
---
4+
5+
add LayerMap module
6+
7+
A `LayerMap` allows you to create a map of Layer's that can be used to
8+
dynamically access resources based on a key.
9+
10+
Here is an example of how you can use a `LayerMap` to create a service that
11+
provides access to multiple OpenAI completions services.
12+
13+
```ts
14+
import { Completions } from "@effect/ai"
15+
import { OpenAiClient, OpenAiCompletions } from "@effect/ai-openai"
16+
import { FetchHttpClient } from "@effect/platform"
17+
import { NodeRuntime } from "@effect/platform-node"
18+
import { Config, Effect, Layer, LayerMap } from "effect"
19+
20+
// create the openai client layer
21+
const OpenAiLayer = OpenAiClient.layerConfig({
22+
apiKey: Config.redacted("OPENAI_API_KEY")
23+
}).pipe(Layer.provide(FetchHttpClient.layer))
24+
25+
// create a service that wraps a LayerMap
26+
class AiClients extends Effect.Service<AiClients>()("AiClients", {
27+
scoped: LayerMap.fromRecord(Completions.Completions, {
28+
gpt4o: OpenAiCompletions.layer({ model: "gpt-4o" }),
29+
gpt3Turbo: OpenAiCompletions.layer({ model: "gpt-3.5-turbo" })
30+
}),
31+
dependencies: [OpenAiLayer]
32+
}) {}
33+
34+
// You could also use the `make` constructor to create a LayerMap from a lookup function
35+
LayerMap.make(Completions.Completions, (model: string) =>
36+
OpenAiCompletions.layer({ model })
37+
)
38+
39+
// usage
40+
Effect.gen(function* () {
41+
// get the AiClients service
42+
const clients = yield* AiClients
43+
// retrieve the gpt4o client
44+
const client = yield* clients.get("gpt4o")
45+
// create a completion
46+
const response = yield* client.create("Hello, world!")
47+
console.log(response.text)
48+
}).pipe(
49+
// the Scope ensures that the clients are cleaned up when no longer needed
50+
Effect.scoped,
51+
Effect.provide(AiClients.Default),
52+
NodeRuntime.runMain
53+
)
54+
```

packages/effect/src/Layer.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,20 @@ export interface MemoMap {
133133
) => Effect.Effect<Context.Context<ROut>, E, RIn>
134134
}
135135

136+
/**
137+
* @since 3.13.0
138+
* @category models
139+
*/
140+
export interface CurrentMemoMap {
141+
readonly _: unique symbol
142+
}
143+
144+
/**
145+
* @since 3.13.0
146+
* @category models
147+
*/
148+
export const CurrentMemoMap: Context.Reference<CurrentMemoMap, MemoMap> = internal.CurrentMemoMap
149+
136150
/**
137151
* Returns `true` if the specified value is a `Layer`, `false` otherwise.
138152
*

packages/effect/src/LayerMap.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* @since 3.13.0
3+
*/
4+
import * as Context from "./Context.js"
5+
import type { DurationInput } from "./Duration.js"
6+
import * as Effect from "./Effect.js"
7+
import { identity } from "./Function.js"
8+
import * as Layer from "./Layer.js"
9+
import * as RcMap from "./RcMap.js"
10+
import type * as Scope from "./Scope.js"
11+
12+
/**
13+
* @since 3.13.0
14+
* @category Symbols
15+
*/
16+
export const TypeId: unique symbol = Symbol.for("effect/LayerMap")
17+
18+
/**
19+
* @since 3.13.0
20+
* @category Symbols
21+
*/
22+
export type TypeId = typeof TypeId
23+
24+
/**
25+
* @since 3.13.0
26+
* @category Models
27+
*/
28+
export interface LayerMap<in K, out I, out S, out E = never> {
29+
readonly [TypeId]: TypeId
30+
31+
/**
32+
* The internal RcMap that stores the resources.
33+
*/
34+
readonly rcMap: RcMap.RcMap<K, S, E>
35+
36+
/**
37+
* Retrieves an instance of the resource associated with the key.
38+
*/
39+
get(key: K): Effect.Effect<S, E, Scope.Scope>
40+
41+
/**
42+
* Provides an instance of the resource associated with the key
43+
* to the given effect.
44+
*/
45+
provide(key: K): <A, EX, R>(effect: Effect.Effect<A, EX, R>) => Effect.Effect<A, EX | E, Exclude<R, I> | Scope.Scope>
46+
47+
/**
48+
* Invalidates the resource associated with the key.
49+
*/
50+
invalidate(key: K): Effect.Effect<void>
51+
}
52+
53+
/**
54+
* @since 3.13.0
55+
* @category Constructors
56+
*
57+
* A `LayerMap` allows you to create a map of Layer's that can be used to
58+
* dynamically access resources based on a key.
59+
*
60+
* ```ts
61+
* import { Completions } from "@effect/ai"
62+
* import { OpenAiClient, OpenAiCompletions } from "@effect/ai-openai"
63+
* import { FetchHttpClient } from "@effect/platform"
64+
* import { NodeRuntime } from "@effect/platform-node"
65+
* import { Config, Effect, Layer, LayerMap } from "effect"
66+
*
67+
* // create the openai client layer
68+
* const OpenAiLayer = OpenAiClient.layerConfig({
69+
* apiKey: Config.redacted("OPENAI_API_KEY")
70+
* }).pipe(Layer.provide(FetchHttpClient.layer))
71+
*
72+
* // create a service that wraps a LayerMap
73+
* class AiClients extends Effect.Service<AiClients>()("AiClients", {
74+
* scoped: LayerMap.fromRecord(Completions.Completions, {
75+
* gpt4o: OpenAiCompletions.layer({ model: "gpt-4o" }),
76+
* gpt3Turbo: OpenAiCompletions.layer({ model: "gpt-3.5-turbo" })
77+
* }),
78+
* dependencies: [OpenAiLayer]
79+
* }) {}
80+
*
81+
* // You could also use the `make` constructor to create a LayerMap from a lookup function
82+
* LayerMap.make(
83+
* Completions.Completions,
84+
* (model: string) => OpenAiCompletions.layer({ model })
85+
* )
86+
*
87+
* // usage
88+
* Effect.gen(function*() {
89+
* // get the AiClients service
90+
* const clients = yield* AiClients
91+
* // retrieve the gpt4o client
92+
* const client = yield* clients.get("gpt4o")
93+
* // create a completion
94+
* const response = yield* client.create("Hello, world!")
95+
* console.log(response.text)
96+
* }).pipe(
97+
* Effect.scoped,
98+
* Effect.provide(AiClients.Default),
99+
* NodeRuntime.runMain
100+
* )
101+
* ```
102+
*/
103+
export const make: <I, S, K, L extends Layer.Layer<I, any, any>>(
104+
tag: Context.Tag<I, S>,
105+
lookup: (key: K) => L,
106+
options?: {
107+
readonly idleTimeToLive?: DurationInput | undefined
108+
} | undefined
109+
) => Effect.Effect<
110+
LayerMap<K, I, S, L extends Layer.Layer<infer _A, infer _E, infer _R> ? _E : never>,
111+
never,
112+
Scope.Scope | (L extends Layer.Layer<infer _A, infer _E, infer _R> ? _R : never)
113+
> = Effect.fnUntraced(function*<I, S, K, L extends Layer.Layer<I, any, any>>(
114+
tag: Context.Tag<I, S>,
115+
lookup: (key: K) => L,
116+
options?: {
117+
readonly idleTimeToLive?: DurationInput | undefined
118+
}
119+
) {
120+
const context = yield* Effect.context<never>()
121+
122+
// If we are inside another layer build, use the current memo map,
123+
// otherwise create a new one.
124+
const memoMap = context.unsafeMap.has(Layer.CurrentMemoMap.key)
125+
? Context.get(context, Layer.CurrentMemoMap)
126+
: yield* Layer.makeMemoMap
127+
128+
const rcMap = yield* RcMap.make({
129+
lookup: (key: K) =>
130+
Effect.scopeWith((scope) =>
131+
Layer.buildWithMemoMap(lookup(key), memoMap, scope) as Effect.Effect<
132+
Context.Context<I>
133+
>
134+
).pipe(
135+
Effect.map((context) => Context.get(context, tag as any) as S)
136+
),
137+
idleTimeToLive: options?.idleTimeToLive
138+
})
139+
140+
const get = (key: K): Effect.Effect<S, never, Scope.Scope> => RcMap.get(rcMap, key)
141+
142+
return identity<LayerMap<K, I, S, any>>({
143+
[TypeId]: TypeId,
144+
rcMap,
145+
get,
146+
provide: (key) => Effect.provideServiceEffect(tag, get(key)),
147+
invalidate: (key) => RcMap.invalidate(rcMap, key)
148+
})
149+
})
150+
151+
/**
152+
* @since 3.13.0
153+
* @category Constructors
154+
*/
155+
export const fromRecord = <I, S, const Layers extends Record<string, Layer.Layer<I, any, any>>>(
156+
tag: Context.Tag<I, S>,
157+
layers: Layers
158+
): Effect.Effect<
159+
LayerMap<keyof Layers, I, S, Layers[keyof Layers] extends Layer.Layer<infer _A, infer _E, infer _R> ? _E : never>,
160+
never,
161+
Scope.Scope | (Layers[keyof Layers] extends Layer.Layer<infer _A, infer _E, infer _R> ? _R : never)
162+
> => make(tag, (key: keyof Layers) => layers[key])

packages/effect/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,11 @@ export * as KeyedPool from "./KeyedPool.js"
406406
*/
407407
export * as Layer from "./Layer.js"
408408

409+
/**
410+
* @since 3.13.0
411+
*/
412+
export * as LayerMap from "./LayerMap.js"
413+
409414
/**
410415
* A data type for immutable linked lists representing ordered collections of elements of type `A`.
411416
*

packages/effect/src/internal/layer.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ export const MemoMapTypeId: Layer.MemoMapTypeId = Symbol.for(
6666
MemoMapTypeIdKey
6767
) as Layer.MemoMapTypeId
6868

69+
/** @internal */
70+
export const CurrentMemoMap = Context.Reference<Layer.CurrentMemoMap>()("effect/Layer/CurrentMemoMap", {
71+
defaultValue: () => unsafeMakeMemoMap()
72+
})
73+
6974
/** @internal */
7075
export type Primitive =
7176
| ExtendScope
@@ -336,7 +341,7 @@ export const buildWithScope = dual<
336341
>(2, (self, scope) =>
337342
core.flatMap(
338343
makeMemoMap,
339-
(memoMap) => core.flatMap(makeBuilder(self, scope), (run) => run(memoMap))
344+
(memoMap) => buildWithMemoMap(self, memoMap, scope)
340345
))
341346

342347
/** @internal */
@@ -350,7 +355,14 @@ export const buildWithMemoMap = dual<
350355
memoMap: Layer.MemoMap,
351356
scope: Scope.Scope
352357
) => Effect.Effect<Context.Context<ROut>, E, RIn>
353-
>(3, (self, memoMap, scope) => core.flatMap(makeBuilder(self, scope), (run) => run(memoMap)))
358+
>(
359+
3,
360+
(self, memoMap, scope) =>
361+
core.flatMap(
362+
makeBuilder(self, scope),
363+
(run) => effect.provideService(run(memoMap), CurrentMemoMap, memoMap)
364+
)
365+
)
354366

355367
const makeBuilder = <RIn, E, ROut>(
356368
self: Layer.Layer<ROut, E, RIn>,

0 commit comments

Comments
 (0)