1import { afterAll, assert, beforeAll, describe, expect, layer } from "@effect/vitest"
2import { Context, Effect, Layer, Ref } from "effect"
3
4describe("nested sibling layers", { concurrent: false }, () => {
5 let nextChildId = 0
6 let firstChildId = -1
7 let secondChildId = -1
8 const releasedChildIds: Array<number> = []
9
10 class Parent extends Context.Service<Parent, "parent">()("Parent") {
11 static readonly layer = Layer.succeed(Parent)("parent")
12 }
13
14 class Child extends Context.Service<Child, { readonly id: number }>()("Child") {
15 static readonly layer = Layer.effect(Child)(
16 Effect.flatMap(Parent, () => {
17 const id = ++nextChildId
18 return Effect.acquireRelease(
19 Effect.succeed({ id }),
20 () =>
21 Effect.sync(() => {
22 releasedChildIds.push(id)
23 })
24 )
25 })
26 )
27 }
28
29 layer(Parent.layer)("parent", (it) => {
30 it.layer(Child.layer)("first sibling", (it) => {
31 it.effect("allocates child", () =>
32 Effect.gen(function*() {
33 const child = yield* Child
34 firstChildId = child.id
35
36 assert.strictEqual(child.id, 1)
37 assert.deepStrictEqual(releasedChildIds, [])
38 }))
39 })
40
41 it.layer(Child.layer)("second sibling", (it) => {
42 beforeAll(() => {
43 expect(releasedChildIds).toEqual([firstChildId])
44 })
45
46 it.effect("allocates a fresh child", () =>
47 Effect.gen(function*() {
48 const child = yield* Child
49 secondChildId = child.id
50
51 assert.strictEqual(child.id, 2)
52 assert.isTrue(child.id !== firstChildId)
53 assert.deepStrictEqual(releasedChildIds, [firstChildId])
54 }))
55 })
56
57 afterAll(() => {
58 expect(firstChildId).toEqual(1)
59 expect(secondChildId).toEqual(2)
60 expect(releasedChildIds).toEqual([1, 2])
61 })
62 })
63})
64
65describe.concurrent("nested sibling layers in concurrent suites", () => {
66 let nextSharedId = 0
67 let firstSharedId: number | undefined
68 let secondSharedId: number | undefined
69 const releasedSharedIds: Array<number> = []
70
71 class Parent extends Context.Service<Parent, "parent">()("ConcurrentParent") {
72 static readonly layer = Layer.succeed(Parent)("parent")
73 }
74
75 class SharedChild extends Context.Service<SharedChild, { readonly id: number }>()("SharedChild") {
76 static readonly layer = Layer.effect(SharedChild)(
77 Effect.flatMap(Parent, () =>
78 Effect.gen(function*() {
79 yield* Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, 50)))
80
81 const id = ++nextSharedId
82 return yield* Effect.acquireRelease(
83 Effect.succeed({ id }),
84 () =>
85 Effect.sync(() => {
86 releasedSharedIds.push(id)
87 })
88 )
89 }))
90 )
91 }
92
93 layer(Parent.layer)("parent", (it) => {
94 describe.concurrent("concurrent siblings", () => {
95 it.layer(SharedChild.layer)("first sibling", (it) => {
96 it.effect("captures shared child", () =>
97 Effect.gen(function*() {
98 const child = yield* SharedChild
99 firstSharedId = child.id
100 assert.isTrue(child.id === 1 || child.id === 2)
101 }))
102 })
103
104 it.layer(SharedChild.layer)("second sibling", (it) => {
105 it.effect("allocates an isolated child", () =>
106 Effect.gen(function*() {
107 const child = yield* SharedChild
108 secondSharedId = child.id
109 assert.isTrue(child.id === 1 || child.id === 2)
110 }))
111 })
112 })
113
114 afterAll(() => {
115 expect(firstSharedId).not.toEqual(secondSharedId)
116 expect(nextSharedId).toEqual(2)
117 expect([...releasedSharedIds].sort((a, b) => a - b)).toEqual([1, 2])
118 })
119 })
120})
121
122describe("nested sibling isolation with provided state graph", { concurrent: false }, () => {
123 interface StateShape {
124 readonly id: number
125 readonly todos: Ref.Ref<Array<string>>
126 readonly migrated: Ref.Ref<boolean>
127 }
128
129 class Parent extends Context.Service<Parent, "parent">()("ProvidedParent") {
130 static readonly layer = Layer.succeed(Parent)("parent")
131 }
132
133 class State extends Context.Service<State, StateShape>()("ProvidedState") {}
134
135 class TodoService extends Context.Service<TodoService, {
136 readonly stateId: Effect.Effect<number>
137 readonly migrated: Effect.Effect<boolean>
138 readonly add: (title: string) => Effect.Effect<void>
139 readonly list: Effect.Effect<Array<string>>
140 }>()("ProvidedTodoService") {}
141
142 let nextId = 0
143 let firstStateId = -1
144 let secondStateId = -1
145
146 const baseLayer = Layer.effect(State)(
147 Effect.gen(function*() {
148 const id = ++nextId
149 const todos = yield* Ref.make<Array<string>>([])
150 const migrated = yield* Ref.make(false)
151 return { id, todos, migrated }
152 })
153 )
154
155 const migrationLayer = Layer.effectDiscard(
156 Effect.gen(function*() {
157 const state = yield* State
158 yield* Ref.set(state.migrated, true)
159 })
160 )
161
162 const migratedLayer = Layer.merge(
163 baseLayer,
164 migrationLayer.pipe(Layer.provide(baseLayer))
165 )
166
167 const inMemoryLayer = Layer.effect(TodoService)(
168 Effect.gen(function*() {
169 const state = yield* State
170 return {
171 stateId: Effect.succeed(state.id),
172 migrated: Ref.get(state.migrated),
173 add: (title: string) => Ref.update(state.todos, (todos) => [...todos, title]),
174 list: Ref.get(state.todos)
175 } as const
176 })
177 ).pipe(Layer.provide(migratedLayer))
178
179 layer(Parent.layer)("parent", (it) => {
180 it.layer(inMemoryLayer)("first sibling", (it) => {
181 it.effect("mutates isolated provided state", () =>
182 Effect.gen(function*() {
183 const service = yield* TodoService
184 firstStateId = yield* service.stateId
185
186 assert.isTrue(yield* service.migrated)
187 yield* service.add("write tests")
188 assert.deepStrictEqual(yield* service.list, ["write tests"])
189 }))
190 })
191
192 it.layer(inMemoryLayer)("second sibling", (it) => {
193 it.effect("starts fresh with a new provided state", () =>
194 Effect.gen(function*() {
195 const service = yield* TodoService
196 secondStateId = yield* service.stateId
197
198 assert.isTrue(yield* service.migrated)
199 assert.deepStrictEqual(yield* service.list, [])
200
201 yield* service.add("ship feature")
202 assert.deepStrictEqual(yield* service.list, ["ship feature"])
203 }))
204 })
205
206 afterAll(() => {
207 expect(firstStateId).toEqual(1)
208 expect(secondStateId).toEqual(2)
209 expect(nextId).toEqual(2)
210 })
211 })
212})