Initial Commit
This commit is contained in:
592
e2e/foundation-stack-engines/src/server.ts
Normal file
592
e2e/foundation-stack-engines/src/server.ts
Normal file
@@ -0,0 +1,592 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
Reference in New Issue
Block a user