1import { assert, describe, it } from "@effect/vitest"
2import { Duration, Effect, Exit, Layer, LayerMap, Option, Scope } from "effect"
3import { TestClock } from "effect/testing"
4
5const makeLayer = (key: string, acquired: Array<string>, released: Array<string>): Layer.Layer<any> =>
6 Layer.effectDiscard(
7 Effect.acquireRelease(
8 Effect.sync(() => {
9 acquired.push(key)
10 }),
11 () =>
12 Effect.sync(() => {
13 released.push(key)
14 })
15 )
16 ) as Layer.Layer<any>
17
18describe("LayerMap", () => {
19 it.effect("contextEffectOption does not build missing entries and returns existing entries", () =>
20 Effect.gen(function*() {
21 const acquired: Array<string> = []
22 const layerMap = yield* LayerMap.make(
23 (key: string) => Layer.effectDiscard(Effect.sync(() => acquired.push(key))) as Layer.Layer<any>
24 )
25
26 assert.deepStrictEqual(yield* layerMap.contextEffectOption("key"), Option.none())
27 assert.deepStrictEqual(acquired, [])
28
29 const ownerScope = yield* Scope.make()
30 const borrowerScope = yield* Scope.make()
31 yield* layerMap.contextEffect("key").pipe(Scope.provide(ownerScope))
32 const cached = yield* layerMap.contextEffectOption("key").pipe(Scope.provide(borrowerScope))
33 assert.isTrue(Option.isSome(cached))
34 assert.deepStrictEqual(acquired, ["key"])
35
36 yield* Scope.close(ownerScope, Exit.void)
37 yield* Scope.close(borrowerScope, Exit.void)
38 }))
39
40 it.effect("Service contextEffectOption does not build missing entries and returns existing entries", () =>
41 Effect.gen(function*() {
42 const acquired: Array<string> = []
43 class TestMap extends LayerMap.Service<TestMap>()("LayerMapTest/ContextEffectOption", {
44 lookup: (key: string) => Layer.effectDiscard(Effect.sync(() => acquired.push(key))) as Layer.Layer<any>
45 }) {}
46
47 yield* Effect.gen(function*() {
48 assert.deepStrictEqual(yield* TestMap.contextEffectOption("key"), Option.none())
49 assert.deepStrictEqual(acquired, [])
50
51 const ownerScope = yield* Scope.make()
52 const borrowerScope = yield* Scope.make()
53 yield* TestMap.contextEffect("key").pipe(Scope.provide(ownerScope))
54 const cached = yield* TestMap.contextEffectOption("key").pipe(Scope.provide(borrowerScope))
55 assert.isTrue(Option.isSome(cached))
56 assert.deepStrictEqual(acquired, ["key"])
57
58 yield* Scope.close(ownerScope, Exit.void)
59 yield* Scope.close(borrowerScope, Exit.void)
60 }).pipe(Effect.provide(TestMap.layer))
61 }))
62
63 it.effect("make does not preload with the default idle TTL", () =>
64 Effect.gen(function*() {
65 const acquired: Array<string> = []
66 const released: Array<string> = []
67 yield* LayerMap.make(
68 (key: string) => makeLayer(key, acquired, released),
69 { preloadKeys: ["a", "b"] }
70 )
71 assert.deepStrictEqual(acquired, [])
72 assert.deepStrictEqual(released, [])
73 }))
74
75 it.effect("make preloads only keys with non-zero idle TTL", () =>
76 Effect.gen(function*() {
77 const acquired: Array<string> = []
78 const released: Array<string> = []
79 const layerMap = yield* LayerMap.make(
80 (key: string) => makeLayer(key, acquired, released),
81 {
82 preloadKeys: ["zero", "finite", "infinite"],
83 idleTimeToLive: (key: string) => key === "zero" ? 0 : key === "finite" ? 1000 : Duration.infinity
84 }
85 )
86 assert.deepStrictEqual(acquired, ["finite", "infinite"])
87 assert.deepStrictEqual(released, [])
88 yield* Effect.scoped(layerMap.contextEffect("finite"))
89 assert.deepStrictEqual(acquired, ["finite", "infinite"])
90 yield* TestClock.adjust(999)
91 assert.deepStrictEqual(released, [])
92 yield* TestClock.adjust(1)
93 assert.deepStrictEqual(released, ["finite"])
94 yield* Effect.scoped(layerMap.contextEffect("infinite"))
95 assert.deepStrictEqual(acquired, ["finite", "infinite"])
96 assert.deepStrictEqual(released, ["finite"])
97 }))
98
99 it.effect("make fails construction when a non-zero TTL preload fails", () =>
100 Effect.gen(function*() {
101 const exit = yield* Effect.exit(LayerMap.make(
102 (key: string) => Layer.effectDiscard(Effect.fail(key)) as Layer.Layer<any, string>,
103 { preloadKeys: ["broken"], idleTimeToLive: 1000 }
104 ))
105 assert.isTrue(Exit.isFailure(exit))
106 }))
107
108 it.effect("make supports dynamic idleTimeToLive", () =>
109 Effect.gen(function*() {
110 const acquired: Array<string> = []
111 const released: Array<string> = []
112 const layerMap = yield* LayerMap.make(
113 (key: string) => makeLayer(key, acquired, released),
114 { idleTimeToLive: (key: string) => key.startsWith("short:") ? 500 : 2000 }
115 )
116
117 yield* Effect.scoped(layerMap.contextEffect("short:a"))
118 yield* Effect.scoped(layerMap.contextEffect("long:b"))
119 assert.deepStrictEqual(acquired, ["short:a", "long:b"])
120 assert.deepStrictEqual(released, [])
121
122 yield* TestClock.adjust(500)
123 assert.deepStrictEqual(released, ["short:a"])
124
125 yield* TestClock.adjust(1500)
126 assert.deepStrictEqual(released, ["short:a", "long:b"])
127 }))
128
129 it.effect("fromRecord supports dynamic idleTimeToLive", () =>
130 Effect.gen(function*() {
131 const acquired: Array<string> = []
132 const released: Array<string> = []
133 const layers = {
134 short: makeLayer("short", acquired, released),
135 long: makeLayer("long", acquired, released)
136 } as const
137 const layerMap = yield* LayerMap.fromRecord(
138 layers,
139 {
140 idleTimeToLive: (key) => {
141 const key_: "short" | "long" = key
142 return key_ === "short" ? 500 : 2000
143 }
144 }
145 )
146
147 yield* Effect.scoped(layerMap.contextEffect("short"))
148 yield* Effect.scoped(layerMap.contextEffect("long"))
149 assert.deepStrictEqual(acquired, ["short", "long"])
150 assert.deepStrictEqual(released, [])
151
152 yield* TestClock.adjust(500)
153 assert.deepStrictEqual(released, ["short"])
154
155 yield* TestClock.adjust(1500)
156 assert.deepStrictEqual(released, ["short", "long"])
157 }))
158
159 it.effect("Service supports dynamic idleTimeToLive", () =>
160 Effect.gen(function*() {
161 const acquired: Array<string> = []
162 const released: Array<string> = []
163
164 class LookupMap extends LayerMap.Service<LookupMap>()("LayerMapTest/LookupMap", {
165 lookup: (key: string) => makeLayer(key, acquired, released),
166 idleTimeToLive: (key: string) => key.startsWith("short:") ? 500 : 2000
167 }) {}
168
169 class RecordMap extends LayerMap.Service<RecordMap>()("LayerMapTest/RecordMap", {
170 layers: {
171 short: makeLayer("short", acquired, released),
172 long: makeLayer("long", acquired, released)
173 },
174 idleTimeToLive: (key) => {
175 const key_: "short" | "long" = key
176 return key_ === "short" ? 500 : 2000
177 }
178 }) {}
179
180 yield* Effect.scoped(LookupMap.contextEffect("short:a").pipe(Effect.provide(LookupMap.layer)))
181 yield* Effect.scoped(RecordMap.contextEffect("short").pipe(Effect.provide(RecordMap.layer)))
182 assert.deepStrictEqual(acquired, ["short:a", "short"])
183
184 yield* TestClock.adjust(500)
185 assert.deepStrictEqual(released, ["short:a", "short"])
186 }))
187})