@effect-app/infra 4.0.0-beta.239 → 4.0.0-beta.240

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @effect-app/infra
2
2
 
3
+ ## 4.0.0-beta.240
4
+
5
+ ### Patch Changes
6
+
7
+ - c5e348f: Fix `provideOnRequestScope` leaking a single `ContextMap` across concurrent requests.
8
+
9
+ `Layer.buildWithScope(layer, requestScope)` resolves its `MemoMap` from the
10
+ ambient fiber context, which lives on the HTTP server fiber and is therefore
11
+ shared by every request that server handles. With the resulting memoization,
12
+ the first request to land on a freshly-started server built
13
+ `ContextMapContainer.layer` once; every subsequent overlapping request received
14
+ the same `ContextMap` instance — etags written by one request were observed
15
+ (or overwritten) by another, and the finalizer was anchored to the first
16
+ request's scope.
17
+
18
+ `provideOnRequestScope` now allocates a fresh `MemoMap` per call via
19
+ `Layer.makeMemoMap` and builds with `Layer.buildWithMemoMap(layer, memoMap,
20
+ requestScope)`. Each request gets its own `ContextMap`, the request-scope
21
+ binding from the earlier SSE fix is preserved, and the finalizer still only
22
+ fires once the response body has fully drained.
23
+
24
+ Adds regression coverage in `rpc-context-map-streaming.test.ts` for three
25
+ properties: mid-stream survival of ContextMap state, a fresh map on each
26
+ succeeding request, and isolation between overlapping concurrent requests.
27
+
28
+ - effect-app@4.0.0-beta.240
29
+
3
30
  ## 4.0.0-beta.239
4
31
 
5
32
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"setupRequest.d.ts","sourceRoot":"","sources":["../../src/api/setupRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAA;AAC3C,OAAO,KAAK,KAAK,MAAM,kBAAkB,CAAA;AAEzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,EAAa,cAAc,EAAkB,MAAM,sBAAsB,CAAA;AAYhF,eAAO,MAAM,iBAAiB,6CAe3B,CAAA;AAEH,eAAO,MAAM,KAAK;;;gBAGhB,CAAA;AAmBF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAA;CACnC;AAMD,eAAO,MAAM,qBAAqB,GAC/B,IAAI,EAAE,EAAE,EAAE,GAAG,SAAS,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,oFAKxF,CAAA;AAEN,eAAO,MAAM,8BAA8B,4BACZ,MAAM,CAAC,WAAW,GAAG,mBAAmB,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,uEAM3G,CAAA;AAMP,eAAO,MAAM,uCAAuC,4BACrB,MAAM,CAAC,WAAW,GAAG,mBAAmB,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,sGAK7G,CAAA;AAGL,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EACzC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC5B,cAAc,EAAE,cAAc,EAC9B,OAAO,CAAC,EAAE,mBAAmB,sEAa9B;AAED,wBAAgB,iCAAiC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EACvD,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC5B,cAAc,EAAE,cAAc,EAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,CAAC,WAAW,GAAG,mBAAmB,sEAanD"}
1
+ {"version":3,"file":"setupRequest.d.ts","sourceRoot":"","sources":["../../src/api/setupRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAA;AAC3C,OAAO,KAAK,KAAK,MAAM,kBAAkB,CAAA;AAEzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,EAAa,cAAc,EAAkB,MAAM,sBAAsB,CAAA;AAYhF,eAAO,MAAM,iBAAiB,6CAe3B,CAAA;AAEH,eAAO,MAAM,KAAK;;;gBAGhB,CAAA;AAmBF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAA;CACnC;AAMD,eAAO,MAAM,qBAAqB,GAC/B,IAAI,EAAE,EAAE,EAAE,GAAG,SAAS,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,oFASxF,CAAA;AAEN,eAAO,MAAM,8BAA8B,4BACZ,MAAM,CAAC,WAAW,GAAG,mBAAmB,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,uEAM3G,CAAA;AAMP,eAAO,MAAM,uCAAuC,4BACrB,MAAM,CAAC,WAAW,GAAG,mBAAmB,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,sGAK7G,CAAA;AAGL,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EACzC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC5B,cAAc,EAAE,cAAc,EAC9B,OAAO,CAAC,EAAE,mBAAmB,sEAa9B;AAED,wBAAgB,iCAAiC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EACvD,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC5B,cAAc,EAAE,cAAc,EAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,CAAC,WAAW,GAAG,mBAAmB,sEAanD"}
@@ -42,7 +42,11 @@ Effect.withLogSpan(name)));
42
42
  // Effect returns, so a sub-scope would close too early and run finalizers mid-stream.
43
43
  export const provideOnRequestScope = (layer) => (self) => Effect.gen(function* () {
44
44
  const requestScope = yield* Effect.scope;
45
- const ctx = yield* Layer.buildWithScope(layer, requestScope);
45
+ // Fresh MemoMap per request: `Layer.buildWithScope` would otherwise reuse
46
+ // the ambient MemoMap living on the HTTP server fiber, sharing the built
47
+ // value (e.g. ContextMap) across every request handled by that server.
48
+ const memoMap = yield* Layer.makeMemoMap;
49
+ const ctx = yield* Layer.buildWithMemoMap(layer, memoMap, requestScope);
46
50
  return yield* Effect.provide(self, ctx);
47
51
  });
48
52
  export const setupRequestContextFromCurrent = (name = "request", options) => (self) => self
@@ -63,4 +67,4 @@ export function setupRequestContextWithCustomSpan(self, requestContext, name, op
63
67
  return self
64
68
  .pipe(options?.withTransaction === true ? withSqlTransaction : (_) => _, withRequestSpan(name, options), Effect.provide(layer, { local: true }));
65
69
  }
66
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2V0dXBSZXF1ZXN0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FwaS9zZXR1cFJlcXVlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE1BQU0sTUFBTSxtQkFBbUIsQ0FBQTtBQUMzQyxPQUFPLEtBQUssS0FBSyxNQUFNLGtCQUFrQixDQUFBO0FBQ3pDLE9BQU8sS0FBSyxNQUFNLE1BQU0sbUJBQW1CLENBQUE7QUFDM0MsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sbUJBQW1CLENBQUE7QUFDckQsT0FBTyxLQUFLLE1BQU0sTUFBTSxlQUFlLENBQUE7QUFDdkMsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLHFCQUFxQixDQUFBO0FBQy9DLE9BQU8sRUFBRSxTQUFTLEVBQUUsY0FBYyxFQUFFLGNBQWMsRUFBRSxNQUFNLHNCQUFzQixDQUFBO0FBQ2hGLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLGlDQUFpQyxDQUFBO0FBQ3JFLE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQTtBQUU1QyxNQUFNLGtCQUFrQixHQUFHLENBQVUsSUFBNEIsRUFBMEIsRUFBRSxDQUMzRixNQUFNLENBQUMsYUFBYSxDQUFDLFNBQVMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxJQUFJLENBQzVDLE1BQU0sQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQztJQUMxQixNQUFNLEVBQUUsR0FBRyxFQUFFLENBQUMsSUFBSTtJQUNsQixNQUFNLEVBQUUsQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLEdBQUcsQ0FBQyxlQUFlLENBQUMsSUFBSSxDQUFDLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUM7Q0FDOUQsQ0FBQyxDQUFDLENBQ0osQ0FBQTtBQUVILE1BQU0sQ0FBQyxNQUFNLGlCQUFpQixHQUFHLE1BQU07S0FDcEMsR0FBRyxDQUFDO0lBQ0gsSUFBSSxFQUFFLE1BQU0sQ0FB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
70
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2V0dXBSZXF1ZXN0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FwaS9zZXR1cFJlcXVlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE1BQU0sTUFBTSxtQkFBbUIsQ0FBQTtBQUMzQyxPQUFPLEtBQUssS0FBSyxNQUFNLGtCQUFrQixDQUFBO0FBQ3pDLE9BQU8sS0FBSyxNQUFNLE1BQU0sbUJBQW1CLENBQUE7QUFDM0MsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sbUJBQW1CLENBQUE7QUFDckQsT0FBTyxLQUFLLE1BQU0sTUFBTSxlQUFlLENBQUE7QUFDdkMsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLHFCQUFxQixDQUFBO0FBQy9DLE9BQU8sRUFBRSxTQUFTLEVBQUUsY0FBYyxFQUFFLGNBQWMsRUFBRSxNQUFNLHNCQUFzQixDQUFBO0FBQ2hGLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLGlDQUFpQyxDQUFBO0FBQ3JFLE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQTtBQUU1QyxNQUFNLGtCQUFrQixHQUFHLENBQVUsSUFBNEIsRUFBMEIsRUFBRSxDQUMzRixNQUFNLENBQUMsYUFBYSxDQUFDLFNBQVMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxJQUFJLENBQzVDLE1BQU0sQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQztJQUMxQixNQUFNLEVBQUUsR0FBRyxFQUFFLENBQUMsSUFBSTtJQUNsQixNQUFNLEVBQUUsQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLEdBQUcsQ0FBQyxlQUFlLENBQUMsSUFBSSxDQUFDLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUM7Q0FDOUQsQ0FBQyxDQUFDLENBQ0osQ0FBQTtBQUVILE1BQU0sQ0FBQyxNQUFNLGlCQUFpQixHQUFHLE1BQU07S0FDcEMsR0FBRyxDQUFDO0lBQ0gsSUFBSSxFQUFFLE1BQU0sQ0FB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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-app/infra",
3
- "version": "4.0.0-beta.239",
3
+ "version": "4.0.0-beta.240",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -13,7 +13,7 @@
13
13
  "proper-lockfile": "^4.1.2",
14
14
  "pure-rand": "8.4.0",
15
15
  "query-string": "^9.3.1",
16
- "effect-app": "4.0.0-beta.239"
16
+ "effect-app": "4.0.0-beta.240"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@azure/cosmos": "^4.9.3",
@@ -67,7 +67,11 @@ export const provideOnRequestScope =
67
67
  <ROut, E2, RIn>(layer: Layer.Layer<ROut, E2, RIn>) => <A, E, R>(self: Effect.Effect<A, E, R>) =>
68
68
  Effect.gen(function*() {
69
69
  const requestScope = yield* Effect.scope
70
- const ctx = yield* Layer.buildWithScope(layer, requestScope)
70
+ // Fresh MemoMap per request: `Layer.buildWithScope` would otherwise reuse
71
+ // the ambient MemoMap living on the HTTP server fiber, sharing the built
72
+ // value (e.g. ContextMap) across every request handled by that server.
73
+ const memoMap = yield* Layer.makeMemoMap
74
+ const ctx = yield* Layer.buildWithMemoMap(layer, memoMap, requestScope)
71
75
  return yield* Effect.provide(self, ctx)
72
76
  })
73
77
 
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc-context-map-streaming.test.d.ts","sourceRoot":"","sources":["../rpc-context-map-streaming.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,262 @@
1
+ /**
2
+ * E2E test for commit bb3f51d03 — `fix(infra): bind ContextMap to request scope
3
+ * for SSE streams`.
4
+ *
5
+ * Background
6
+ * ----------
7
+ * The RpcServer returns `HttpServerResponse.stream(...)` for streaming RPC
8
+ * resources. The body of that response keeps producing chunks AFTER the
9
+ * outer Effect that built the response has returned. `RequestContextMiddleware`
10
+ * provisions `ContextMapContainer` for the request. The acquireRelease
11
+ * inside `ContextMapContainer.layer` calls `clear()` on finalize, wiping
12
+ * the etag map and the per-request resolver/store cache.
13
+ *
14
+ * If that layer is built against a sub-scope of the outer Effect (the
15
+ * pre-fix behaviour: `Effect.provide(layer)`), the finalizer fires as soon
16
+ * as the middleware Effect returns the HttpServerResponse — i.e. between
17
+ * "handler done" and "first chunk written" — wiping ContextMap state that
18
+ * later chunks still need. In production this surfaces as spurious
19
+ * OptimisticConcurrencyException on writes that follow a streaming read.
20
+ *
21
+ * The fix binds the layer to the ambient request scope via
22
+ * `provideOnRequestScope`, so `clear()` only runs once the response body
23
+ * has fully drained.
24
+ *
25
+ * Reproduction strategy
26
+ * ---------------------
27
+ * - Mirror the production wiring: apply `RequestContextMiddleware` to the
28
+ * RPC router (see `boilerplate/api/src/router.ts`).
29
+ * - The stream handler sets an etag on the ContextMap BEFORE returning the
30
+ * Stream value.
31
+ * - The Stream emits three values 100ms apart; each emission reads back the
32
+ * etag via `getContextMap` and yields 1 if the value is still present,
33
+ * 0 otherwise.
34
+ * - Expectation: [1, 1, 1]. If the layer's `clear()` runs mid-stream the
35
+ * later chunks observe an empty map and the assertion fails (typically
36
+ * with [1, 0, 0] or [0, 0, 0]).
37
+ */
38
+ import { NodeHttpServer } from "@effect/platform-node"
39
+ import { expect, it } from "@effect/vitest"
40
+ import { ApiClientFactory, makeRpcClient } from "effect-app/client"
41
+ import { HttpRouter, HttpServer } from "effect-app/http"
42
+ import { DefaultGenericMiddlewares } from "effect-app/middleware"
43
+ import { MiddlewareMaker } from "effect-app/rpc"
44
+ import * as S from "effect-app/Schema"
45
+ import * as Effect from "effect/Effect"
46
+ import * as Layer from "effect/Layer"
47
+ import * as Option from "effect/Option"
48
+ import * as Stream from "effect/Stream"
49
+ import { FetchHttpClient } from "effect/unstable/http"
50
+ import { RpcSerialization } from "effect/unstable/rpc"
51
+ import { createServer } from "http"
52
+ import { RequestContextMiddleware } from "../src/api/internal/RequestContextMiddleware.js"
53
+ import { makeRouter } from "../src/api/routing.js"
54
+ import { DefaultGenericMiddlewaresLive } from "../src/api/routing/middleware.js"
55
+ import { getContextMap } from "../src/Store/ContextMapContainer.js"
56
+ import { AllowAnonymous, AllowAnonymousLive, RequestContextMap, RequireRoles, RequireRolesLive, SomeElseMiddleware, SomeElseMiddlewareLive, SomeService, Test, TestLive } from "./fixtures.js"
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Middleware — mirrors AppMiddleware shape used by the other rpc e2e tests.
60
+ // ---------------------------------------------------------------------------
61
+
62
+ class AppMiddleware extends MiddlewareMaker
63
+ .Tag<AppMiddleware>()("AppMiddleware", RequestContextMap)
64
+ .middleware(RequireRoles, Test)
65
+ .middleware(AllowAnonymous)
66
+ .middleware(SomeElseMiddleware)
67
+ .middleware(...DefaultGenericMiddlewares)
68
+ {
69
+ static Default = this.layer.pipe(
70
+ Layer.provide(
71
+ [
72
+ RequireRolesLive.pipe(Layer.provide(SomeService.Default)),
73
+ AllowAnonymousLive,
74
+ TestLive,
75
+ SomeElseMiddlewareLive,
76
+ DefaultGenericMiddlewaresLive
77
+ ] as const
78
+ )
79
+ )
80
+ }
81
+
82
+ const { Router, matchAll } = makeRouter(AppMiddleware.Default)
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Resource — single streaming command that exercises ContextMap mid-stream.
86
+ // ---------------------------------------------------------------------------
87
+
88
+ const { TaggedRequestFor } = makeRpcClient(AppMiddleware)
89
+ const Req = TaggedRequestFor("CtxMap")
90
+
91
+ class StreamEtag extends Req.Command<StreamEtag>()("StreamEtag", {}, {
92
+ stream: true,
93
+ allowAnonymous: true,
94
+ success: S.Number
95
+ }) {}
96
+
97
+ // Per-request isolation probes: each handler writes a caller-supplied value to
98
+ // the SHARED key, then emits 3 chunks each re-reading the SHARED key. If two
99
+ // concurrent requests share a ContextMap, the second writer overwrites the
100
+ // first and the first request observes the wrong value mid-stream.
101
+ class StreamWithEtag extends Req.Command<StreamWithEtag>()("StreamWithEtag", {
102
+ value: S.String
103
+ }, {
104
+ stream: true,
105
+ allowAnonymous: true,
106
+ success: S.String
107
+ }) {}
108
+
109
+ class ReadEtagOnce extends Req.Query<ReadEtagOnce>()("ReadEtagOnce", {}, {
110
+ allowAnonymous: true,
111
+ success: S.String
112
+ }) {}
113
+
114
+ const Rsc = { StreamEtag, StreamWithEtag, ReadEtagOnce }
115
+
116
+ // Distinct constants so an assertion failure points squarely at "the etag
117
+ // the handler wrote was no longer there when later chunks ran".
118
+ const ETAG_ID = "ctxmap-test-id"
119
+ const ETAG_VALUE = "v1"
120
+ const SHARED_KEY = "ctxmap-shared-key"
121
+ const MISSING = "<missing>"
122
+
123
+ const router = Router(Rsc)({
124
+ *effect(match) {
125
+ return match({
126
+ StreamEtag: () =>
127
+ Effect
128
+ .gen(function*() {
129
+ // 1) Acquire the request-scoped ContextMap. Fails (dies) if the
130
+ // container is still the default "root" — which would mean
131
+ // RequestContextMiddleware did not run for this request.
132
+ const ctxMap = yield* getContextMap.pipe(Effect.orDie)
133
+ // 2) Seed an etag BEFORE handing back the Stream. This write is
134
+ // what the per-chunk readers below verify.
135
+ ctxMap.set(ETAG_ID, ETAG_VALUE)
136
+ // 3) Emit three values 100ms apart so chunks are produced AFTER
137
+ // the outer Effect that built the response has returned. Each
138
+ // emission re-reads the etag from the request-scoped ContextMap.
139
+ return Stream.fromIterable([0, 1, 2]).pipe(
140
+ Stream.mapEffect(() =>
141
+ Effect.sleep("100 millis").pipe(
142
+ Effect.flatMap(() => getContextMap.pipe(Effect.orDie)),
143
+ Effect.map((m) => m.get(ETAG_ID) === ETAG_VALUE ? 1 : 0)
144
+ )
145
+ )
146
+ )
147
+ })
148
+ .pipe(Stream.unwrap),
149
+ StreamWithEtag: ({ value }: { readonly value: string }) =>
150
+ Effect
151
+ .gen(function*() {
152
+ const ctxMap = yield* getContextMap.pipe(Effect.orDie)
153
+ ctxMap.set(SHARED_KEY, value)
154
+ return Stream.fromIterable([0, 1, 2]).pipe(
155
+ Stream.mapEffect(() =>
156
+ Effect.sleep("100 millis").pipe(
157
+ Effect.flatMap(() => getContextMap.pipe(Effect.orDie)),
158
+ Effect.map((m) => m.get(SHARED_KEY) ?? MISSING)
159
+ )
160
+ )
161
+ )
162
+ })
163
+ .pipe(Stream.unwrap),
164
+ ReadEtagOnce: () => getContextMap.pipe(Effect.orDie, Effect.map((m) => m.get(SHARED_KEY) ?? MISSING))
165
+ })
166
+ }
167
+ })
168
+
169
+ const RpcRouterLayer = matchAll({ router })
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // HTTP wiring — fresh server on a loopback port per `it.live`. The critical
173
+ // difference vs. rpc-stream-fullstack: we apply `RequestContextMiddleware`
174
+ // here, exactly as the production boilerplate does, so the fix code path
175
+ // is what runs.
176
+ // ---------------------------------------------------------------------------
177
+
178
+ const NodeServerLayer = NodeHttpServer.layer(() => createServer(), { port: 0 })
179
+
180
+ const RequestContextMiddlewareLayer = HttpRouter.middleware(RequestContextMiddleware()).layer
181
+
182
+ const ServerLayer = HttpRouter
183
+ .serve(
184
+ RpcRouterLayer.pipe(Layer.provide(RequestContextMiddlewareLayer))
185
+ )
186
+ .pipe(
187
+ Layer.provide(NodeServerLayer),
188
+ Layer.provide(RpcSerialization.layerNdjson)
189
+ )
190
+
191
+ const ClientLayer = Layer
192
+ .unwrap(
193
+ Effect.gen(function*() {
194
+ const server = yield* HttpServer.HttpServer
195
+ const addr = server.address
196
+ if (addr._tag !== "TcpAddress") return yield* Effect.die(new Error("expected TcpAddress"))
197
+ const host = addr.hostname === "0.0.0.0" ? "127.0.0.1" : addr.hostname
198
+ const url = `http://${host}:${addr.port}`
199
+ return ApiClientFactory
200
+ .layer({ url, headers: Option.none() })
201
+ .pipe(Layer.provide(FetchHttpClient.layer))
202
+ })
203
+ )
204
+ .pipe(Layer.provide(NodeServerLayer))
205
+
206
+ const TestLayer = Layer.mergeAll(ServerLayer, ClientLayer)
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Test
210
+ // ---------------------------------------------------------------------------
211
+
212
+ it.live(
213
+ "ContextMap survives mid-stream: etag set in handler is readable by every chunk",
214
+ Effect.fnUntraced(function*() {
215
+ const client = yield* ApiClientFactory.makeFor(Layer.empty)(Rsc)
216
+ const values = yield* Stream.runCollect(client.StreamEtag.handler())
217
+ // All three chunks emit 1 → the etag was still readable when each chunk
218
+ // executed. If the layer-bound ContextMap's `clear()` finalizer fired
219
+ // mid-stream (the pre-fix behaviour), later chunks would emit 0.
220
+ expect(values).toStrictEqual([1, 1, 1])
221
+ }, Effect.provide(TestLayer)),
222
+ { timeout: 10_000 }
223
+ )
224
+
225
+ it.live(
226
+ "succeeding requests get a fresh ContextMap: request N+1 cannot see request N's writes",
227
+ Effect.fnUntraced(function*() {
228
+ const client = yield* ApiClientFactory.makeFor(Layer.empty)(Rsc)
229
+ // 1st request writes SHARED_KEY = "first" and drains its stream so its
230
+ // request scope (and ContextMap) is closed before request 2 starts.
231
+ const first = yield* Stream.runCollect(client.StreamWithEtag.handler({ value: "first" }))
232
+ expect(first).toStrictEqual(["first", "first", "first"])
233
+ // 2nd request must NOT observe the previous request's value at any point.
234
+ const peek = yield* client.ReadEtagOnce.handler()
235
+ expect(peek).toBe(MISSING)
236
+ // 3rd request writes a different value and drains; must not be polluted by request 1.
237
+ const third = yield* Stream.runCollect(client.StreamWithEtag.handler({ value: "third" }))
238
+ expect(third).toStrictEqual(["third", "third", "third"])
239
+ }, Effect.provide(TestLayer)),
240
+ { timeout: 10_000 }
241
+ )
242
+
243
+ it.live(
244
+ "overlapping requests get isolated ContextMaps: concurrent streams see only their own writes",
245
+ Effect.fnUntraced(function*() {
246
+ const client = yield* ApiClientFactory.makeFor(Layer.empty)(Rsc)
247
+ // Two streams in flight at the same time, each writing the SAME key with a
248
+ // different value. With per-request maps each stream reads back only its
249
+ // own value across all chunks. With a shared map the later writer's value
250
+ // would leak into the earlier stream's later chunks.
251
+ const [a, b] = yield* Effect.all(
252
+ [
253
+ Stream.runCollect(client.StreamWithEtag.handler({ value: "alpha" })),
254
+ Stream.runCollect(client.StreamWithEtag.handler({ value: "beta" }))
255
+ ],
256
+ { concurrency: "unbounded" }
257
+ )
258
+ expect(a).toStrictEqual(["alpha", "alpha", "alpha"])
259
+ expect(b).toStrictEqual(["beta", "beta", "beta"])
260
+ }, Effect.provide(TestLayer)),
261
+ { timeout: 10_000 }
262
+ )