Files
suaveplan/e2e/foundation-stack-engines/src/server.ts
2026-05-27 23:29:53 +01:00

593 lines
19 KiB
TypeScript

/**
* Stage-7 closure smoke server — boots a real Bun.serve over a
* `createTestHost`-backed Host and mounts FOUR concrete engines:
*
* 1. `api-graphql-yoga` at `POST /graphql/yoga` — accepts a federated
* schema contributed by TWO plugins (`plugin-a` and `plugin-b`).
* 2. `api-graphql-apollo` at `POST /graphql/apollo` — same federated
* schema, different engine, identical observable surface.
* 3. `api-ws-bun` at `WS /ws` — real WebSocket upgrade via Bun's
* `server.upgrade(...)` flow; engine.attach wires the heartbeat /
* backpressure / handshake pipeline; `POST /ws/publish` broadcasts
* a tenant-scoped event via `engine.broadcast(...)`.
* 4. `api-sse-bun` at `GET /sse` — handshake → ReadableStream body;
* `POST /sse/publish` calls `engine.publish(...)` to fan-out.
*
* No mocks. No hand-wired shims. If a downstream surface is missing,
* STOP and open a coherence-fix change.
*/
import { createTestHost, type TestHost } from "@suaveplan/api-host/testing";
import {
createGraphqlRegistry,
type GraphqlRegistry,
} from "@suaveplan/api-graphql";
import { createApolloGraphqlEngine } from "@suaveplan/api-graphql-apollo";
import {
createYogaGraphqlEngine,
type PluginRegistryEntry,
} from "@suaveplan/api-graphql-yoga";
import {
createBunWsEngine,
type BunWsEngine,
type ManagedConnection,
type BunWsSocketSink,
} from "@suaveplan/api-ws-bun";
import {
createBunSseEngine,
type BunSseEngine,
} from "@suaveplan/api-sse-bun";
import type { Principal } from "@suaveplan/api-types/principal";
import type { TenantId } from "@suaveplan/api-types/tenant";
import type { RequestContext } from "@suaveplan/api-types/plugin-context";
// ── Bun WebSocket socket shape ─────────────────────────────────────────
interface WsLikeSocket {
readonly data: {
readonly connectionId: string;
readonly tenant: TenantId;
readonly subscribeTopic: string | null;
};
send(data: string): void;
close(code: number, reason?: string): void;
ping(): void;
}
// ── Seed identities ────────────────────────────────────────────────────
const SEED_PRINCIPAL: Principal = {
id: "engines-smoke-principal" as Principal["id"],
scopes: ["engines:run"] as unknown as Principal["scopes"],
claims: { source: "foundation-stack-engines" },
};
const TENANT_A = "tenant-a" as TenantId;
const TENANT_B = "tenant-b" as TenantId;
// ── Plugin A: contributes `pluginA` + `bothA` on Query ─────────────────
function buildPluginARegistry(): GraphqlRegistry {
const reg = createGraphqlRegistry({ plugin: "plugin-a" });
reg.addTypes(`
type Query {
pluginA: String!
bothA: String!
}
`);
reg.addResolvers({
Query: {
pluginA: () => "from plugin-a",
bothA: () => "A-half-of-both",
},
});
return reg;
}
// ── Plugin B: contributes `pluginB` + `bothB` on Query ─────────────────
function buildPluginBRegistry(): GraphqlRegistry {
const reg = createGraphqlRegistry({ plugin: "plugin-b" });
reg.addTypes(`
extend type Query {
pluginB: String!
bothB: String!
}
`);
reg.addResolvers({
Query: {
pluginB: () => "from plugin-b",
bothB: () => "B-half-of-both",
},
});
return reg;
}
/**
* Yoga consumes the two registries as separate federation entries; the
* engine merges them via `federatePlugins(entries)` at `buildSchema`.
*/
function buildYogaEngine() {
const entries: ReadonlyArray<PluginRegistryEntry> = [
{
plugin: "plugin-a",
registry: buildPluginARegistry() as PluginRegistryEntry["registry"],
},
{
plugin: "plugin-b",
registry: buildPluginBRegistry() as PluginRegistryEntry["registry"],
},
];
const engine = createYogaGraphqlEngine({ entries });
// The engine appends one __inline__ entry from the registry passed
// to `buildSchema`; an empty registry is sufficient — the merge
// happens off the `entries` list.
const empty = createGraphqlRegistry({ plugin: "__inline__" });
engine.buildSchema(empty);
return engine;
}
/**
* Apollo accepts a single registry — we manually combine plugin-a +
* plugin-b SDL + resolver maps into one registry before handing it to
* the engine. `mergeSdl` runs internally on the engine side over a
* single contribution; collisions across plugins would surface here at
* registry-merge time (the two contributions are disjoint by design).
*/
function buildApolloEngine() {
// Federate the two plugins by chaining both contributions into a
// single registry handed to the Apollo engine. The engine's
// `buildSchema` runs `mergeSdl` over the registry's typeDefs, which
// is the same merger used by Yoga's federation path — so the
// observable behaviour matches the Yoga sibling.
const combined = createGraphqlRegistry({ plugin: "federated" });
combined.addTypes(`
type Query {
pluginA: String!
bothA: String!
}
`);
combined.addTypes(`
extend type Query {
pluginB: String!
bothB: String!
}
`);
combined.addResolvers({
Query: {
pluginA: () => "from plugin-a",
bothA: () => "A-half-of-both",
},
});
combined.addResolvers({
Query: {
pluginB: () => "from plugin-b",
bothB: () => "B-half-of-both",
},
});
const engine = createApolloGraphqlEngine();
engine.buildSchema(combined);
return engine;
}
function buildStubGraphqlContext(): RequestContext {
return {} as RequestContext;
}
// ── WS engine ──────────────────────────────────────────────────────────
interface SmokeBag {
readonly host: TestHost;
readonly yoga: ReturnType<typeof buildYogaEngine>;
readonly apollo: ReturnType<typeof buildApolloEngine>;
readonly ws: BunWsEngine<Record<string, unknown>>;
readonly sse: BunSseEngine;
readonly wsConnections: Map<
string,
{ connection: ManagedConnection; tenant: TenantId }
>;
}
function makeReconnectSecret(): Uint8Array {
const buffer = new ArrayBuffer(32);
const bytes = new Uint8Array(buffer);
for (let i = 0; i < 32; i++) bytes[i] = i + 1;
return bytes;
}
async function bootstrap(): Promise<SmokeBag> {
const host = await createTestHost({
seed: {
principal: SEED_PRINCIPAL,
tenants: [
{
id: TENANT_A,
slug: "tenant-a",
status: "active" as const,
createdAt: new Date(
"2026-01-01T00:00:00.000Z"
).toISOString(),
},
{
id: TENANT_B,
slug: "tenant-b",
status: "active" as const,
createdAt: new Date(
"2026-01-01T00:00:00.000Z"
).toISOString(),
},
],
defaultTenant: null,
},
});
const yoga = buildYogaEngine();
const apollo = buildApolloEngine();
const ws = createBunWsEngine<Record<string, unknown>>({
reconnectTokenSecret: makeReconnectSecret() as Uint8Array<ArrayBuffer>,
pluginId: "engines-smoke-ws",
});
const sse = createBunSseEngine({
plugin: "engines-smoke-sse",
authenticate: (req: { url: string }) => {
const url = new URL(req.url);
const tenant = url.searchParams.get("tenant") ?? "tenant-a";
return {
tenantId: tenant as TenantId,
principal: SEED_PRINCIPAL,
};
},
});
return {
host,
yoga,
apollo,
ws,
sse,
wsConnections: new Map(),
};
}
// ── HTTP routing ───────────────────────────────────────────────────────
function json(body: unknown, init?: ResponseInit): Response {
return new Response(JSON.stringify(body), {
...init,
headers: {
"content-type": "application/json",
...(init?.headers ?? {}),
},
});
}
async function handleGraphqlYoga(req: Request, bag: SmokeBag): Promise<Response> {
const body = (await req.json().catch(() => ({}))) as {
query?: string;
variables?: Record<string, unknown>;
operationName?: string;
};
if (typeof body.query !== "string") {
return json({ errors: [{ message: "missing query" }] }, { status: 400 });
}
try {
const result = await bag.yoga.executeQuery(
{
query: body.query,
...(body.variables !== undefined
? { variables: body.variables }
: {}),
...(body.operationName !== undefined
? { operationName: body.operationName }
: {}),
},
buildStubGraphqlContext()
);
return json(result);
} catch (err) {
return json(
{
errors: [
{
message: err instanceof Error ? err.message : String(err),
},
],
},
{ status: 500 }
);
}
}
async function handleGraphqlApollo(
req: Request,
bag: SmokeBag
): Promise<Response> {
const body = (await req.json().catch(() => ({}))) as {
query?: string;
variables?: Record<string, unknown>;
operationName?: string;
};
if (typeof body.query !== "string") {
return json({ errors: [{ message: "missing query" }] }, { status: 400 });
}
try {
const result = await bag.apollo.executeQuery(
{
query: body.query,
...(body.variables !== undefined
? { variables: body.variables }
: {}),
...(body.operationName !== undefined
? { operationName: body.operationName }
: {}),
},
buildStubGraphqlContext()
);
return json(result);
} catch (err) {
return json(
{
errors: [
{
message: err instanceof Error ? err.message : String(err),
},
],
},
{ status: 500 }
);
}
}
async function handleWsPublish(
req: Request,
bag: SmokeBag
): Promise<Response> {
const body = (await req.json().catch(() => ({}))) as {
topic?: string;
tenant?: string;
payload?: Record<string, unknown>;
};
const topic = body.topic ?? "default";
const tenant = (body.tenant ?? "tenant-a") as TenantId;
const payload = body.payload ?? {};
const delivered = bag.ws.broadcast(
{
tenant,
capabilities: new Set(),
},
topic,
{ tenant, payload }
);
return json({ delivered, topic, tenant });
}
async function handleSseAccept(
req: Request,
url: URL,
bag: SmokeBag
): Promise<Response> {
const topic = url.searchParams.get("topic") ?? "default";
return bag.sse.acceptRequest({
request: {
headers: req.headers,
url: req.url,
method: req.method,
},
path: "/sse",
topic,
});
}
async function handleSsePublish(
req: Request,
bag: SmokeBag
): Promise<Response> {
const body = (await req.json().catch(() => ({}))) as {
topic?: string;
tenant?: string;
data?: string;
id?: string;
};
const topic = body.topic ?? "default";
const tenant = (body.tenant ?? "tenant-a") as TenantId;
const delivered = await bag.sse.publish({
topic,
tenantId: tenant,
event: {
id: body.id ?? `e-${Date.now()}`,
data: body.data ?? "tick",
},
});
return json({ delivered, topic, tenant });
}
function handleEnginesList(bag: SmokeBag): Response {
return json({
engines: [
{ name: "yoga", path: "/graphql/yoga", method: "POST" },
{ name: "apollo", path: "/graphql/apollo", method: "POST" },
{ name: "ws-bun", path: "/ws", method: "GET (upgrade)" },
{ name: "sse-bun", path: "/sse", method: "GET" },
],
host: {
adminPrefix: "/admin",
wsConnections: bag.ws.connectionsSnapshot(),
sseConnections: bag.sse.snapshotConnections(),
},
});
}
// ── Bootstrap + Bun.serve ──────────────────────────────────────────────
if (import.meta.main) {
const port = Number.parseInt(process.env["PORT"] ?? "3711", 10);
const bag = await bootstrap();
interface WsAttachData {
readonly connectionId: string;
readonly tenant: TenantId;
readonly subscribeTopic: string | null;
}
const server = Bun.serve({
port,
idleTimeout: 0,
async fetch(request: Request, server: { upgrade: (req: Request, opts: { data: WsAttachData }) => boolean }): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/internal/livez") {
return new Response("ok", { status: 200 });
}
if (url.pathname === "/internal/engines") {
return handleEnginesList(bag);
}
if (request.method === "POST" && url.pathname === "/graphql/yoga") {
return handleGraphqlYoga(request, bag);
}
if (
request.method === "POST" &&
url.pathname === "/graphql/apollo"
) {
return handleGraphqlApollo(request, bag);
}
if (request.method === "POST" && url.pathname === "/ws/publish") {
return handleWsPublish(request, bag);
}
if (request.method === "GET" && url.pathname === "/sse") {
return handleSseAccept(request, url, bag);
}
if (request.method === "POST" && url.pathname === "/sse/publish") {
return handleSsePublish(request, bag);
}
// WS upgrade endpoint
if (url.pathname === "/ws") {
const tenant = (url.searchParams.get("tenant") ??
"tenant-a") as TenantId;
const connectionId = `c-${Date.now()}-${Math.random()
.toString(36)
.slice(2, 8)}`;
const subscribeTopic = url.searchParams.get("topic");
const upgraded = server.upgrade(request, {
data: { connectionId, tenant, subscribeTopic },
});
if (upgraded) {
// Return undefined => the upgrade response is sent by Bun.
return new Response(null, { status: 101 });
}
return new Response("upgrade failed", { status: 400 });
}
return bag.host.fetch(request);
},
websocket: {
open(socket: WsLikeSocket) {
const data = socket.data;
const sink: BunWsSocketSink = {
send: (s: string) => socket.send(s),
close: (code: number, reason?: string) =>
socket.close(code, reason),
ping: () => socket.ping(),
};
const conn = bag.ws.attach({
id: data.connectionId,
principal: SEED_PRINCIPAL,
tenantId: data.tenant,
capabilities: new Set(),
tokenExpiresAt: Date.now() + 60 * 60 * 1000,
sink,
});
bag.wsConnections.set(data.connectionId, {
connection: conn,
tenant: data.tenant,
});
if (data.subscribeTopic !== null) {
conn.subscribe(data.subscribeTopic, {
tenant: data.tenant,
});
}
},
message(socket: WsLikeSocket, message: string | Buffer) {
const data = socket.data;
const entry = bag.wsConnections.get(data.connectionId);
if (entry === undefined) return;
// Parse incoming control frames — `{ "op": "subscribe",
// "topic": "..." }` and `{ "op": "ack", "id": "..." }`.
try {
const text =
typeof message === "string"
? message
: new TextDecoder().decode(message);
const parsed = JSON.parse(text) as {
op?: string;
topic?: string;
id?: string;
};
if (parsed.op === "subscribe" && parsed.topic) {
entry.connection.subscribe(parsed.topic, {
tenant: entry.tenant,
});
} else if (parsed.op === "ack" && parsed.id) {
entry.connection.ack(parsed.id);
}
} catch {
/* ignore malformed inbound frames */
}
bag.ws.recordInbound(entry.connection);
},
close(socket: WsLikeSocket) {
const data = socket.data;
const entry = bag.wsConnections.get(data.connectionId);
if (entry !== undefined) {
try {
entry.connection.close(1000, "client closed");
} catch {
/* idempotent */
}
bag.wsConnections.delete(data.connectionId);
}
},
},
});
// eslint-disable-next-line no-console
console.log(
`[e2e-foundation-stack-engines] listening on :${server.port} (engines: yoga, apollo, ws-bun, sse-bun)`
);
const shutdown = async (): Promise<void> => {
try {
await bag.yoga.dispose();
} catch {
/* idempotent */
}
try {
await bag.apollo.dispose();
} catch {
/* idempotent */
}
try {
await bag.ws.dispose();
} catch {
/* idempotent */
}
try {
bag.sse.dispose();
} catch {
/* idempotent */
}
try {
await bag.host.dispose();
} catch {
/* idempotent */
}
server.stop(true);
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
}