1import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils"
2import * as Context from "effect/Context"
3import * as Equal from "effect/Equal"
4import * as Option from "effect/Option"
5import * as Redactable from "effect/Redactable"
6import { describe, it } from "vitest"
7
8describe("Context", () => {
9 const A = Context.Service<number>("ContextTest/A")
10 const B = Context.Service<number>("ContextTest/B")
11 const C = Context.Service<number>("ContextTest/C")
12
13 it("keeps the source immutable across additions", () => {
14 const source = Context.make(A, 1)
15 const result = Context.add(source, B, 2)
16
17 deepStrictEqual([...source.mapUnsafe], [[A.key, 1]])
18 deepStrictEqual([...result.mapUnsafe], [[A.key, 1], [B.key, 2]])
19 })
20
21 it("removes a service with addOrOmit", () => {
22 const context = Context.make(A, 1).pipe(Context.addOrOmit(A, Option.none()))
23
24 assertTrue(Context.getOption(context, A)._tag === "None")
25 })
26
27 it("preserves Map insertion order for replacements and appends", () => {
28 const Ref = Context.Reference<number>("ContextTest/OrderRef", { defaultValue: () => 0 })
29 const context = Context.empty().pipe(
30 Context.add(A, 1),
31 Context.add(Ref, 2),
32 Context.add(B, 3),
33 Context.add(A, 4),
34 Context.add(C, 5)
35 )
36
37 deepStrictEqual([...context.mapUnsafe], [
38 [A.key, 4],
39 [Ref.key, 2],
40 [B.key, 3],
41 [C.key, 5]
42 ])
43 })
44
45 it("invalidates the fiber cache only for opted-in keys", () => {
46 const Cached = Context.Service<number>("ContextTest/Cached", { fiberCached: true })
47 class CachedClass extends Context.Service<CachedClass, number>()("ContextTest/CachedClass", {
48 fiberCached: true
49 }) {}
50 const Ref = Context.Reference<number>("ContextTest/Ref", { defaultValue: () => 0 })
51 const CachedRef = Context.Reference<number>("ContextTest/CachedRef", {
52 fiberCached: true,
53 defaultValue: () => 0
54 })
55
56 const source = Context.make(A, 1)
57 assertTrue(Context.hasSameCache(source, Context.add(source, B, 1)))
58 assertTrue(Context.hasSameCache(source, Context.add(source, Ref, 1)))
59 assertFalse(Context.hasSameCache(source, Context.add(source, CachedRef, 1)))
60 const context = source.pipe(
61 Context.add(Cached, 2),
62 Context.add(CachedClass, 3)
63 )
64
65 strictEqual(Context.get(context, Cached), 2)
66 strictEqual(Context.get(context, CachedClass), 3)
67 assertTrue(Context.getOption(source, Cached)._tag === "None")
68
69 const replaced = Context.add(context, Cached, 4)
70 strictEqual(Context.get(replaced, Cached), 4)
71 strictEqual(Context.get(context, Cached), 2)
72
73 deepStrictEqual([...replaced.mapUnsafe], [
74 [A.key, 1],
75 [Cached.key, 4],
76 [CachedClass.key, 3]
77 ])
78 deepStrictEqual([...Context.omit(Cached)(replaced).mapUnsafe], [
79 [A.key, 1],
80 [CachedClass.key, 3]
81 ])
82 })
83
84 it("supports the Redactable fallback context", () => {
85 const Cached = Context.Service<number>("ContextTest/RedactableCached", { fiberCached: true })
86 const context = Redactable.getRedacted({
87 [Redactable.symbolRedactable](context: Context.Context<never>) {
88 return context
89 }
90 }) as Context.Context<never>
91
92 strictEqual(context.mapUnsafe.size, 0)
93 assertTrue(Context.getOption(context, Cached)._tag === "None")
94
95 const added = Context.add(context, Cached, 1)
96 strictEqual(Context.get(added, Cached), 1)
97 strictEqual(context.mapUnsafe.size, 0)
98 })
99
100 it("distinguishes an undefined service from an absent service", () => {
101 const Undefined = Context.Service<undefined>("ContextTest/Undefined", { fiberCached: true })
102 const Missing = Context.Service<undefined>("ContextTest/Missing")
103 const Ref = Context.Reference<string | undefined>("ContextTest/UndefinedRef", {
104 defaultValue: () => "default",
105 fiberCached: true
106 })
107 const context = Context.make(Undefined, undefined).pipe(Context.add(Ref, undefined))
108
109 assertTrue(Context.getOption(context, Undefined)._tag === "Some")
110 assertTrue(Context.getOption(context, Missing)._tag === "None")
111 strictEqual(Context.getUnsafe(context, Undefined), undefined)
112 strictEqual(Context.getOrElse(context, Undefined, () => 1), undefined)
113 deepStrictEqual(Context.getOption(context, Ref), Option.some(undefined))
114 strictEqual(Context.get(context, Ref), undefined)
115 })
116
117 it("bounds deep overlay chains without changing values or order", () => {
118 const keys = Array.from({ length: 20 }, (_, i) => Context.Service<number>(`ContextTest/Deep${i}`))
119 let context = Context.empty()
120 for (let i = 0; i < keys.length; i++) {
121 context = Context.add(context, keys[i], i)
122 }
123
124 strictEqual(context.mapUnsafe.size, 20)
125 deepStrictEqual([...context.mapUnsafe.keys()], keys.map((key) => key.key))
126 for (let i = 0; i < keys.length; i++) {
127 strictEqual(Context.getUnsafe(context, keys[i]), i)
128 }
129 // Rebasing on ordinary keys must not invalidate fiber caches
130 assertTrue(Context.hasSameCache(Context.empty(), context))
131 })
132
133 it("flattens after repeated base fall-throughs", () => {
134 const context = Context.make(A, 1).pipe(Context.add(B, 2))
135 const impl = context as any
136
137 for (let i = 0; i < 7; i++) {
138 strictEqual(Context.getUnsafe(context, A), 1)
139 }
140 strictEqual(impl._flat, undefined)
141
142 strictEqual(Context.getUnsafe(context, A), 1)
143 assertTrue(impl._flat instanceof Map)
144 strictEqual(impl.overlay, undefined)
145 strictEqual(impl.depth, 0)
146
147 const added = Context.add(context, C, 3) as any
148 strictEqual(added._flat, undefined)
149 strictEqual(added.baseHits, 0)
150 })
151
152 it("defers flattening a large base until enough base hits", () => {
153 const keys = Array.from({ length: 32 }, (_, i) => Context.Service<number>(`ContextTest/Large${i}`))
154 const base = Context.makeUnsafe(new Map(keys.map((key, i) => [key.key, i])))
155 const context = Context.add(base, A, -1)
156 const impl = context as any
157
158 for (let i = 0; i < 8; i++) {
159 strictEqual(Context.getUnsafe(context, keys[0]), 0)
160 }
161 strictEqual(impl._flat, undefined)
162 strictEqual(impl.base, base.mapUnsafe)
163 strictEqual(Context.getUnsafe(context, A), -1)
164 assertTrue(Option.isNone(Context.getOption(context, B)))
165 strictEqual(impl.baseHits, 8)
166
167 for (let i = 8; i < 31; i++) {
168 strictEqual(Context.getUnsafe(context, keys[i]), i)
169 }
170 strictEqual(impl._flat, undefined)
171 strictEqual(impl.base, base.mapUnsafe)
172
173 strictEqual(Context.getUnsafe(context, keys[31]), 31)
174 assertTrue(impl._flat instanceof Map)
175 strictEqual(impl.overlay, undefined)
176 strictEqual(impl.baseHits, 32)
177 strictEqual(Context.getUnsafe(context, A), -1)
178 strictEqual(Context.getUnsafe(context, keys[0]), 0)
179 })
180
181 it("supports the ReadonlyMap surface through mapUnsafe", () => {
182 const context = Context.make(A, 1).pipe(Context.add(B, 2))
183 const visited: Array<[string, number]> = []
184 context.mapUnsafe.forEach((value, key) => visited.push([key, value]))
185
186 strictEqual(context.mapUnsafe.size, 2)
187 assertTrue(context.mapUnsafe.has(A.key))
188 strictEqual(context.mapUnsafe.get(B.key), 2)
189 deepStrictEqual([...context.mapUnsafe.keys()], [A.key, B.key])
190 deepStrictEqual([...context.mapUnsafe.values()], [1, 2])
191 deepStrictEqual([...context.mapUnsafe.entries()], [[A.key, 1], [B.key, 2]])
192 deepStrictEqual(visited, [[A.key, 1], [B.key, 2]])
193 })
194
195 it("supports equality and JSON materialization", () => {
196 const left = Context.make(A, 1).pipe(Context.add(B, 2))
197 const right = Context.makeUnsafe(new Map([[A.key, 1], [B.key, 2]]))
198
199 assertTrue(Equal.equals(left, right))
200 assertFalse(Equal.equals(left, Context.make(A, 2)))
201 deepStrictEqual(left.toJSON(), {
202 _id: "Context",
203 services: [{ key: A.key, value: 1 }, { key: B.key, value: 2 }]
204 })
205 })
206
207 it("merges with right bias and preserves empty operand identity", () => {
208 const left = Context.make(A, 1).pipe(Context.add(B, 2))
209 const right = Context.make(A, 3).pipe(Context.add(C, 4))
210 const merged = Context.merge(left, right)
211
212 strictEqual(Context.merge(Context.empty(), left), left)
213 strictEqual(Context.merge(left, Context.empty()), left)
214 deepStrictEqual([...merged.mapUnsafe], [[A.key, 3], [B.key, 2], [C.key, 4]])
215 })
216
217 it("pick and omit retain source ordering", () => {
218 const source = Context.make(A, 1).pipe(Context.add(B, 2), Context.add(C, 3))
219
220 deepStrictEqual([...Context.pick(C, A)(source).mapUnsafe], [[A.key, 1], [C.key, 3]])
221 deepStrictEqual([...Context.omit(B)(source).mapUnsafe], [[A.key, 1], [C.key, 3]])
222 })
223
224 it("resolves reference defaults lazily and caches them", () => {
225 let calls = 0
226 const Ref = Context.Reference<object>("ContextTest/LazyRef", {
227 defaultValue: () => {
228 calls++
229 return {}
230 }
231 })
232 const context = Context.empty()
233
234 strictEqual(calls, 0)
235 const first = Context.get(context, Ref)
236 strictEqual(calls, 1)
237 strictEqual(Context.get(context, Ref), first)
238 strictEqual(calls, 1)
239 })
240
241 it("retains the makeUnsafe input map", () => {
242 const map = new Map([[A.key, 1]])
243 const context = Context.makeUnsafe(map)
244
245 strictEqual(context.mapUnsafe, map)
246 })
247})