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 | 212 | // Regression test against the prod handler factory. If anyone reverts |
| 159 | 213 | // `makeApiLive` back to wiring `RequestScopedServicesLive` via |