|
1 | 1 | import { NodeHttpServer } from "@effect/platform-node" |
2 | 2 | import { expect, expectTypeOf, it } from "@effect/vitest" |
3 | | -import { Console, Effect, Layer, Result } from "effect" |
4 | | -import { S } from "effect-app" |
| 3 | +import { Console, Effect, Layer, Ref, Result } from "effect" |
| 4 | +import { Context, S } from "effect-app" |
5 | 5 | import { NotLoggedInError } from "effect-app/client" |
6 | 6 | import { HttpRouter } from "effect-app/http" |
7 | 7 | import { DefaultGenericMiddlewares } from "effect-app/middleware" |
8 | 8 | import { MiddlewareMaker } from "effect-app/rpc" |
9 | 9 | import { middlewareGroup } from "effect-app/rpc/MiddlewareMaker" |
10 | 10 | import { FetchHttpClient } from "effect/unstable/http" |
11 | | -import { RpcClient, RpcGroup, RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc" |
| 11 | +import { Rpc, RpcClient, RpcGroup, RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc" |
12 | 12 | import { createServer } from "http" |
13 | 13 | import { DefaultGenericMiddlewaresLive } from "../src/api/routing.js" |
14 | 14 | import { AllowAnonymous, AllowAnonymousLive, RequestContextMap, RequireRoles, RequireRolesLive, Some, SomeElseMiddleware, SomeElseMiddlewareLive, SomeMiddleware, SomeMiddlewareLive, SomeService, Test, TestLive, UserProfile } from "./fixtures.js" |
@@ -136,3 +136,72 @@ it.live( |
136 | 136 | Effect.provide(RpcTestLayer) |
137 | 137 | ) |
138 | 138 | ) |
| 139 | + |
| 140 | +// Per-request service isolation test |
| 141 | + |
| 142 | +class PerRequestCounter extends Context.Service<PerRequestCounter>()( |
| 143 | + "PerRequestCounter", |
| 144 | + { make: Effect.sync(() => ({ a: 0 })) } |
| 145 | +) { |
| 146 | + static Default = Layer.effect(this, this.make) |
| 147 | +} |
| 148 | + |
| 149 | +class GlobalCounter extends Context.Service<GlobalCounter, { |
| 150 | + readonly ref: Ref.Ref<number> |
| 151 | +}>()("GlobalCounter") {} |
| 152 | + |
| 153 | +const CounterRpcs = RpcGroup.make( |
| 154 | + Rpc.make("incrementA", { |
| 155 | + success: S.Number |
| 156 | + }), |
| 157 | + Rpc.make("incrementB", { |
| 158 | + success: S.Number |
| 159 | + }) |
| 160 | +) |
| 161 | + |
| 162 | +const counterImpl = CounterRpcs |
| 163 | + .toLayer({ |
| 164 | + incrementA: Effect.fn(function*() { |
| 165 | + const counter = yield* PerRequestCounter |
| 166 | + counter.a++ |
| 167 | + const global = yield* GlobalCounter |
| 168 | + yield* Ref.update(global.ref, (n) => n + 1) |
| 169 | + return counter.a |
| 170 | + }, Effect.provide(PerRequestCounter.Default)), |
| 171 | + incrementB: Effect.fn(function*() { |
| 172 | + const counter = yield* PerRequestCounter |
| 173 | + counter.a++ |
| 174 | + const global = yield* GlobalCounter |
| 175 | + yield* Ref.update(global.ref, (n) => n + 1) |
| 176 | + return counter.a |
| 177 | + }, Effect.provide(PerRequestCounter.Default)) |
| 178 | + }) |
| 179 | + |
| 180 | +const GlobalCounterLive = Layer.effect( |
| 181 | + GlobalCounter, |
| 182 | + Ref.make(0).pipe(Effect.map((ref) => ({ ref }))) |
| 183 | +) |
| 184 | + |
| 185 | +const CounterTestLayer = counterImpl.pipe(Layer.provideMerge(GlobalCounterLive)) |
| 186 | + |
| 187 | +it.live( |
| 188 | + "per-request service isolation with shared global counter", |
| 189 | + Effect.fnUntraced( |
| 190 | + function*() { |
| 191 | + const client = yield* RpcTest.makeClient(CounterRpcs) |
| 192 | + const global = yield* GlobalCounter |
| 193 | + |
| 194 | + const r1 = yield* client.incrementA() |
| 195 | + const r2 = yield* client.incrementB() |
| 196 | + |
| 197 | + // per-request counter is fresh each time → both return 1 |
| 198 | + expect(r1).toBe(1) |
| 199 | + expect(r2).toBe(1) |
| 200 | + |
| 201 | + // global counter is shared across requests → accumulates to 2 |
| 202 | + const globalCount = yield* Ref.get(global.ref) |
| 203 | + expect(globalCount).toBe(2) |
| 204 | + }, |
| 205 | + Effect.provide(CounterTestLayer) |
| 206 | + ) |
| 207 | +) |
0 commit comments