import { describe, expect, it, onTestFinished } from "@effect/vitest";import { Context, Effect, Layer } from "effect";import { HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http"; import { RequestBackgroundTasks, requestScopedMiddleware } from "./request-scoped"; class Resource extends Context.Service<Resource, { readonly id: number; closed: boolean }>()( "test/RequestResource",) {} const fixture = ( options: { background?: boolean; failTask?: boolean; failRequest?: boolean; blockRequest?: boolean; } = {},) => { const resources: Resource["Service"][] = []; const gates: ReturnType<typeof Promise.withResolvers<void>>[] = []; const keptAlive: Promise<void>[] = []; const writes: number[] = []; const entered = Promise.withResolvers<void>(); const resource = Layer.effect(Resource)( Effect.acquireRelease( Effect.sync(() => { const acquired = { id: resources.length, closed: false }; resources.push(acquired); gates.push(Promise.withResolvers<void>()); return acquired; }), (acquired) => Effect.sync(() => { acquired.closed = true; }), ), ); const routes = HttpRouter.add( "GET", "/", Effect.gen(function* () { const acquired = yield* Resource; if (options.background !== false) { const tasks = yield* RequestBackgroundTasks; const gate = gates[acquired.id]; if (!gate) return yield* Effect.die("Missing request gate"); const task = Effect.runPromise( Effect.gen(function* () { yield* Effect.promise(() => gate.promise); if (options.failTask) return yield* Effect.fail("Background failure"); if (acquired.closed) return yield* Effect.fail("Resource closed before background write"); writes.push(acquired.id); }), ); keptAlive.push(tasks.retain(task)); } entered.resolve(); if (options.blockRequest) return yield* Effect.never; if (options.failRequest) return yield* Effect.die("Request failure"); return HttpServerResponse.empty(); }), ); const app = HttpRouter.toWebHandler( routes.pipe( Layer.provide(requestScopedMiddleware(resource).layer), Layer.provideMerge(HttpServer.layerServices), ), { disableLogger: true }, ); onTestFinished(async () => { for (const gate of gates) gate.resolve(); await Promise.all(keptAlive); await app.dispose(); }); return { resources, gates, keptAlive, writes, entered, request: (signal?: AbortSignal) => app.handler(new Request("http://test.local/", { signal }), Context.empty()), };}; describe("request background resource ownership", () => { it("returns the response before background work, and releases after its write", async () => { const test = fixture(); expect((await test.request()).status).toBe(204); expect(test.resources.map((item) => item.closed)).toEqual([false]); expect(test.writes).toEqual([]); test.gates[0]?.resolve(); await Promise.all(test.keptAlive); expect(test.writes).toEqual([0]); expect(test.resources.map((item) => item.closed)).toEqual([true]); }); it("keeps concurrent requests isolated and releases each independently", async () => { const test = fixture(); const responses = await Promise.all([test.request(), test.request()]); expect(responses.map((response) => response.status)).toEqual([204, 204]); test.gates[0]?.resolve(); await test.keptAlive[0]; expect(test.resources.map((item) => item.closed)).toEqual([true, false]); test.gates[1]?.resolve(); await test.keptAlive[1]; expect(test.writes).toEqual([0, 1]); expect(test.resources.map((item) => item.closed)).toEqual([true, true]); }); for (const failure of ["task", "request"] as const) { it(`releases resources after a ${failure} failure`, async () => { const test = fixture({ failTask: failure === "task", failRequest: failure === "request" }); expect((await test.request()).status).toBe(failure === "request" ? 500 : 204); expect(test.resources.map((item) => item.closed)).toEqual([false]); test.gates[0]?.resolve(); await Promise.all(test.keptAlive); expect(test.writes).toEqual(failure === "task" ? [] : [0]); expect(test.resources.map((item) => item.closed)).toEqual([true]); }); } it("closes before responding when no background work was registered", async () => { const test = fixture({ background: false }); expect((await test.request()).status).toBe(204); expect(test.resources.map((item) => item.closed)).toEqual([true]); expect(test.keptAlive).toEqual([]); }); it("finishes retained work and cleanup after the client cancels the request", async () => { const test = fixture({ blockRequest: true }); const controller = new AbortController(); const response = test.request(controller.signal); await test.entered.promise; controller.abort(); expect((await response).status).toBe(499); expect(test.resources.map((item) => item.closed)).toEqual([false]); test.gates[0]?.resolve(); await Promise.all(test.keptAlive); expect(test.writes).toEqual([0]); expect(test.resources.map((item) => item.closed)).toEqual([true]); });});
import { describe, expect, it, onTestFinished } from "@effect/vitest";
import { Context, Effect, Layer } from "effect";
import { HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http";
import { RequestBackgroundTasks, requestScopedMiddleware } from "./request-scoped";
class Resource extends Context.Service<Resource, { readonly id: number; closed: boolean }>()(
"test/RequestResource",
) {}
const fixture = (
options: {
background?: boolean;
failTask?: boolean;
failRequest?: boolean;
blockRequest?: boolean;
} = {},
) => {
const resources: Resource["Service"][] = [];
const gates: ReturnType<typeof Promise.withResolvers<void>>[] = [];
const keptAlive: Promise<void>[] = [];
const writes: number[] = [];
const entered = Promise.withResolvers<void>();
const resource = Layer.effect(Resource)(
Effect.acquireRelease(
Effect.sync(() => {
const acquired = { id: resources.length, closed: false };
resources.push(acquired);
gates.push(Promise.withResolvers<void>());
return acquired;
}),
(acquired) =>
Effect.sync(() => {
acquired.closed = true;
}),
),
);
const routes = HttpRouter.add(
"GET",
"/",
Effect.gen(function* () {
const acquired = yield* Resource;
if (options.background !== false) {
const tasks = yield* RequestBackgroundTasks;
const gate = gates[acquired.id];
if (!gate) return yield* Effect.die("Missing request gate");
const task = Effect.runPromise(
Effect.gen(function* () {
yield* Effect.promise(() => gate.promise);
if (options.failTask) return yield* Effect.fail("Background failure");
if (acquired.closed)
return yield* Effect.fail("Resource closed before background write");
writes.push(acquired.id);
}),
);
keptAlive.push(tasks.retain(task));
}
entered.resolve();
if (options.blockRequest) return yield* Effect.never;
if (options.failRequest) return yield* Effect.die("Request failure");
return HttpServerResponse.empty();
}),
);
const app = HttpRouter.toWebHandler(
routes.pipe(
Layer.provide(requestScopedMiddleware(resource).layer),
Layer.provideMerge(HttpServer.layerServices),
),
{ disableLogger: true },
);
onTestFinished(async () => {
for (const gate of gates) gate.resolve();
await Promise.all(keptAlive);
await app.dispose();
});
return {
resources,
gates,
keptAlive,
writes,
entered,
request: (signal?: AbortSignal) =>
app.handler(new Request("http://test.local/", { signal }), Context.empty()),
};
};
describe("request background resource ownership", () => {
it("returns the response before background work, and releases after its write", async () => {
const test = fixture();
expect((await test.request()).status).toBe(204);
expect(test.resources.map((item) => item.closed)).toEqual([false]);
expect(test.writes).toEqual([]);
test.gates[0]?.resolve();
await Promise.all(test.keptAlive);
expect(test.writes).toEqual([0]);
expect(test.resources.map((item) => item.closed)).toEqual([true]);
});
it("keeps concurrent requests isolated and releases each independently", async () => {
const test = fixture();
const responses = await Promise.all([test.request(), test.request()]);
expect(responses.map((response) => response.status)).toEqual([204, 204]);
test.gates[0]?.resolve();
await test.keptAlive[0];
expect(test.resources.map((item) => item.closed)).toEqual([true, false]);
test.gates[1]?.resolve();
await test.keptAlive[1];
expect(test.writes).toEqual([0, 1]);
expect(test.resources.map((item) => item.closed)).toEqual([true, true]);
});
for (const failure of ["task", "request"] as const) {
it(`releases resources after a ${failure} failure`, async () => {
const test = fixture({ failTask: failure === "task", failRequest: failure === "request" });
expect((await test.request()).status).toBe(failure === "request" ? 500 : 204);
expect(test.resources.map((item) => item.closed)).toEqual([false]);
test.gates[0]?.resolve();
await Promise.all(test.keptAlive);
expect(test.writes).toEqual(failure === "task" ? [] : [0]);
expect(test.resources.map((item) => item.closed)).toEqual([true]);
});
}
it("closes before responding when no background work was registered", async () => {
const test = fixture({ background: false });
expect((await test.request()).status).toBe(204);
expect(test.resources.map((item) => item.closed)).toEqual([true]);
expect(test.keptAlive).toEqual([]);
});
it("finishes retained work and cleanup after the client cancels the request", async () => {
const test = fixture({ blockRequest: true });
const controller = new AbortController();
const response = test.request(controller.signal);
await test.entered.promise;
controller.abort();
expect((await response).status).toBe(499);
expect(test.resources.map((item) => item.closed)).toEqual([false]);
test.gates[0]?.resolve();
await Promise.all(test.keptAlive);
expect(test.writes).toEqual([0]);
expect(test.resources.map((item) => item.closed)).toEqual([true]);
});
});