Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions apps/cloud/src/auth/workos-callback-state.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
// HTTP surface (see api.request-scope.node.test.ts).
// ---------------------------------------------------------------------------

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

// The route under test serves under the `/api` prefix in the composed app;
// toWebHandler mounts the raw group, so paths here are relative to the group.
Expand All @@ -46,6 +48,9 @@ const stubWorkOS = Layer.succeed(
if (prop === "listUserMemberships") {
return () => Effect.succeed({ data: [] });
}
if (prop === "listOrgMembers") {
return () => Effect.succeed({ data: [{ status: "active" }] });
}
return () => Effect.die(`unexpected WorkOSClient.${String(prop)} call`);
},
}),
Expand Down Expand Up @@ -88,11 +93,15 @@ const App = HttpApiBuilder.layer(PublicApi).pipe(
Layer.provide(CloudAuthPublicHandlers),
Layer.provide(stubWorkOS),
Layer.provide(stubUsers),
Layer.provide(AutumnService.Default),
Layer.provide(HttpServer.layerServices),
);

const app = HttpRouter.toWebHandler(App, { disableLogger: true });
afterAll(() => app.dispose());
afterAll(async () => {
await Promise.all(vi.mocked(waitUntil).mock.calls.map(([work]) => work));
await app.dispose();
});

const run = (request: Request) => {
// beta.59: the handler type expects a context argument; this layer stack
Expand Down
9 changes: 5 additions & 4 deletions apps/cloud/src/extensions/billing/member-seats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// ---------------------------------------------------------------------------

import { Effect } from "effect";
import { waitUntil } from "cloudflare:workers";

import { WorkOSClient } from "../../auth/workos";
import { AutumnService } from "./service";
Expand Down Expand Up @@ -38,16 +39,16 @@ export const reportMemberSeats = (
/**
* Fork `reportMemberSeats` off the calling request, mirroring how execution
* tracking is forked: billing must never stall or fail a user-facing
* request. Only boot-scoped services are captured (WorkOS + Autumn — no
* request-scoped resources), so the forked fiber cannot outlive anything it
* depends on.
* request. Cloudflare owns the promise through waitUntil, so the recount can
* finish after the response. Only boot-scoped WorkOS and Autumn services are
* captured.
*/
export const forkReportMemberSeats = (
organizationId: string,
): Effect.Effect<void, never, WorkOSClient | AutumnService> =>
Effect.gen(function* () {
const ctx = yield* Effect.context<WorkOSClient | AutumnService>();
yield* Effect.sync(() => {
Effect.runForkWith(ctx)(reportMemberSeats(organizationId));
waitUntil(Effect.runPromiseWith(ctx)(reportMemberSeats(organizationId)));
});
});
4 changes: 4 additions & 0 deletions apps/cloud/test-stubs/cloudflare-workers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
// from `process.env` into this stub at import time. Without this bridge
// the test DbService would dial the default postgres port instead of the
// PGlite socket server started by `scripts/test-globalsetup.ts`.
import { vi } from "@effect/vitest";

export const waitUntil = vi.fn<(work: Promise<unknown>) => void>();

export const env: Record<string, unknown> = {
DATABASE_URL: process.env.DATABASE_URL,
WORKOS_API_KEY: process.env.WORKOS_API_KEY,
Expand Down
Loading