| 1 | import { afterAll, assert, beforeAll, describe, expect, layer } from "@effect/vitest" |
| 2 | import { Context, Effect, Layer, Ref } from "effect" |
| 3 | |
| 4 | describe("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 | |
| 65 | describe.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 | |
| 122 | describe("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 | }) |