Showing the changes to apps/cloud/src/api.request-scope.node.test.ts (1 of 4 files). Show all
Unified · Split
| @@ -154,6 +154,60 @@ describe("HttpRouter.toWebHandler request scoping", () => { · 153 unmodified lines | |||
| 154 | }); | 154 | }); |
| 155 | }); | 155 | }); |
| 156 | 156 | ||
| 157 | // --------------------------------------------------------------------------- | ||
| 158 | // WHEN the per-request resource dies, relative to the response bytes. | ||
| 159 | // | ||
| 160 | // `requestScopedMiddleware` wraps the ROUTE HANDLER in `Effect.scoped`, and the | ||
| 161 | // response is written later still, by `HttpEffect.toHandled`'s callback. So the | ||
| 162 | // postgres socket is already closed by the time the client has anything. That | ||
| 163 | // is why cloud's HTTP plane leaves `ExecutorConfig.deferToolSync` unset (see | ||
| 164 | // `engine/execution-stack.ts`): there is no window on this plane that is both | ||
| 165 | // after the response and before the socket closes, so `waitUntil` would only | ||
| 166 | // ever hand a dead pool to the deferred batch. | ||
| 167 | // | ||
| 168 | // Pinned rather than assumed. If the request lifecycle is ever restructured so | ||
| 169 | // the scope outlives the written response, this test fails and the deferral | ||
| 170 | // becomes available — which is a thing to be told, not to rediscover. | ||
| 171 | // --------------------------------------------------------------------------- | ||
| 172 | |||
| 173 | describe("per-request resource lifetime vs the written response", () => { | ||
| 174 | it("releases the request-scoped resource before the response resolves", async () => { | ||
| 175 | const order: string[] = []; | ||
| 176 | const Tracked = Layer.effectDiscard( | ||
| 177 | Effect.acquireRelease( | ||
| 178 | Effect.sync(() => { | ||
| 179 | order.push("acquire"); | ||
| 180 | }), | ||
| 181 | () => | ||
| 182 | Effect.sync(() => { | ||
| 183 | order.push("release"); | ||
| 184 | }), | ||
| 185 | ), | ||
| 186 | ); | ||
| 187 | const Route = HttpRouter.add( | ||
| 188 | "GET", | ||
| 189 | "/", | ||
| 190 | Effect.sync(() => { | ||
| 191 | order.push("handler"); | ||
| 192 | return HttpServerResponse.jsonUnsafe({ ok: true }); | ||
| 193 | }), | ||
| 194 | ); | ||
| 195 | const handler = HttpRouter.toWebHandler( | ||
| 196 | Route.pipe( | ||
| 197 | Layer.provide(requestScopedMiddleware(Tracked).layer), | ||
| 198 | Layer.provideMerge(HttpServer.layerServices), | ||
| 199 | ), | ||
| 200 | { disableLogger: true }, | ||
| 201 | ).handler; | ||
| 202 | |||
| 203 | const response = await handler(new Request("http://test.local/")); | ||
| 204 | order.push("response"); | ||
| 205 | |||
| 206 | expect(response.status).toBe(200); | ||
| 207 | expect(order).toEqual(["acquire", "handler", "release", "response"]); | ||
| 208 | }); | ||
| 209 | }); | ||
| 210 | |||
| 157 | // --------------------------------------------------------------------------- | 211 | // --------------------------------------------------------------------------- |
| 158 | // Regression test against the prod handler factory. If anyone reverts | 212 | // Regression test against the prod handler factory. If anyone reverts |
| 159 | // `makeApiLive` back to wiring `RequestScopedServicesLive` via | 213 | // `makeApiLive` back to wiring `RequestScopedServicesLive` via |