593 lines
19 KiB
TypeScript
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);
|
|
}
|