Skip to content

Commit c9cc5e7

Browse files
authored
Keep seat reconciliation alive after the response (#2001)
* Keep seat reconciliation alive after the response * Await callback background work in the test fixture * Use Effect test helpers for background work fixtures
1 parent 3fd28a5 commit c9cc5e7

3 files changed

Lines changed: 20 additions & 6 deletions

File tree

‎apps/cloud/src/auth/workos-callback-state.node.test.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
// HTTP surface (see api.request-scope.node.test.ts).
1111
// ---------------------------------------------------------------------------
1212

13-
import { afterAll, describe, expect, it } from "@effect/vitest";
13+
import { afterAll, describe, expect, it, vi } from "@effect/vitest";
14+
import { waitUntil } from "cloudflare:workers";
1415
import { Effect, Layer } from "effect";
1516
import { HttpRouter, HttpServer } from "effect/unstable/http";
1617
import { HttpApiBuilder } from "effect/unstable/httpapi";
@@ -21,6 +22,7 @@ import { CloudAuthPublicApi } from "./api";
2122
import { UserStoreService } from "./context";
2223
import { WorkOSClient, type WorkOSClientService } from "./workos";
2324
import { encodeLoginState } from "./login-state";
25+
import { AutumnService } from "../extensions/billing/service";
2426

2527
// The route under test serves under the `/api` prefix in the composed app;
2628
// toWebHandler mounts the raw group, so paths here are relative to the group.
@@ -46,6 +48,9 @@ const stubWorkOS = Layer.succeed(
4648
if (prop === "listUserMemberships") {
4749
return () => Effect.succeed({ data: [] });
4850
}
51+
if (prop === "listOrgMembers") {
52+
return () => Effect.succeed({ data: [{ status: "active" }] });
53+
}
4954
return () => Effect.die(`unexpected WorkOSClient.${String(prop)} call`);
5055
},
5156
}),
@@ -88,11 +93,15 @@ const App = HttpApiBuilder.layer(PublicApi).pipe(
8893
Layer.provide(CloudAuthPublicHandlers),
8994
Layer.provide(stubWorkOS),
9095
Layer.provide(stubUsers),
96+
Layer.provide(AutumnService.Default),
9197
Layer.provide(HttpServer.layerServices),
9298
);
9399

94100
const app = HttpRouter.toWebHandler(App, { disableLogger: true });
95-
afterAll(() => app.dispose());
101+
afterAll(async () => {
102+
await Promise.all(vi.mocked(waitUntil).mock.calls.map(([work]) => work));
103+
await app.dispose();
104+
});
96105

97106
const run = (request: Request) => {
98107
// beta.59: the handler type expects a context argument; this layer stack

‎apps/cloud/src/extensions/billing/member-seats.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// ---------------------------------------------------------------------------
44

55
import { Effect } from "effect";
6+
import { waitUntil } from "cloudflare:workers";
67

78
import { WorkOSClient } from "../../auth/workos";
89
import { AutumnService } from "./service";
@@ -38,16 +39,16 @@ export const reportMemberSeats = (
3839
/**
3940
* Fork `reportMemberSeats` off the calling request, mirroring how execution
4041
* tracking is forked: billing must never stall or fail a user-facing
41-
* request. Only boot-scoped services are captured (WorkOS + Autumn — no
42-
* request-scoped resources), so the forked fiber cannot outlive anything it
43-
* depends on.
42+
* request. Cloudflare owns the promise through waitUntil, so the recount can
43+
* finish after the response. Only boot-scoped WorkOS and Autumn services are
44+
* captured.
4445
*/
4546
export const forkReportMemberSeats = (
4647
organizationId: string,
4748
): Effect.Effect<void, never, WorkOSClient | AutumnService> =>
4849
Effect.gen(function* () {
4950
const ctx = yield* Effect.context<WorkOSClient | AutumnService>();
5051
yield* Effect.sync(() => {
51-
Effect.runForkWith(ctx)(reportMemberSeats(organizationId));
52+
waitUntil(Effect.runPromiseWith(ctx)(reportMemberSeats(organizationId)));
5253
});
5354
});

‎apps/cloud/test-stubs/cloudflare-workers.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
// from `process.env` into this stub at import time. Without this bridge
88
// the test DbService would dial the default postgres port instead of the
99
// PGlite socket server started by `scripts/test-globalsetup.ts`.
10+
import { vi } from "@effect/vitest";
11+
12+
export const waitUntil = vi.fn<(work: Promise<unknown>) => void>();
13+
1014
export const env: Record<string, unknown> = {
1115
DATABASE_URL: process.env.DATABASE_URL,
1216
WORKOS_API_KEY: process.env.WORKOS_API_KEY,

0 commit comments

Comments
 (0)