feat: add SDK foundation layer

Implement config resolution, typed errors, HTTP and WebSocket transport
helpers, and timeout handling for the SDK foundation. Add unit and local
integration tests covering the SDK foundation behaviour and align
package exports with the tsup output.
This commit is contained in:
Tasnim Kabir Sadik
2026-05-09 16:32:41 +06:00
parent db7fccbaed
commit 5b3f2741a3
11 changed files with 1127 additions and 9 deletions

View File

@ -0,0 +1,183 @@
import {
createServer,
type IncomingMessage,
type ServerResponse,
} from "node:http";
import type { AddressInfo } from "node:net";
import { afterEach, describe, expect, it } from "vitest";
import { WebSocketServer } from "ws";
import { HttpClient } from "../../src/_shared/http.js";
import { WsConnection } from "../../src/_shared/websocket.js";
import { ConflictError, TimeoutError } from "../../src/exceptions.js";
interface CapturedRequest {
method?: string;
url?: string;
headers: IncomingMessage["headers"];
body: string;
}
function readRequestBody(request: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let body = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
body += chunk;
});
request.on("end", () => resolve(body));
request.on("error", reject);
});
}
function listen(server: ReturnType<typeof createServer>): Promise<number> {
return new Promise((resolve) => {
server.listen(0, "127.0.0.1", () => {
const address = server.address() as AddressInfo;
resolve(address.port);
});
});
}
function closeHttpServer(
server: ReturnType<typeof createServer>,
): Promise<void> {
return new Promise((resolve, reject) => {
server.close((error) => {
if (error) reject(error);
else resolve();
});
});
}
function closeWebSocketServer(server: WebSocketServer): Promise<void> {
return new Promise((resolve, reject) => {
server.close((error) => {
if (error) reject(error);
else resolve();
});
});
}
describe("foundation integration", () => {
const cleanup: Array<() => Promise<void>> = [];
afterEach(async () => {
await Promise.all(cleanup.splice(0).map((close) => close()));
});
it("sends JSON requests through a real HTTP server", async () => {
let captured: CapturedRequest | undefined;
const server = createServer(async (request, response: ServerResponse) => {
captured = {
method: request.method,
url: request.url,
headers: request.headers,
body: await readRequestBody(request),
};
response.writeHead(200, { "Content-Type": "application/json" });
response.end(JSON.stringify({ ok: true }));
});
const port = await listen(server);
cleanup.push(() => closeHttpServer(server));
const client = new HttpClient({
baseUrl: `http://127.0.0.1:${port}`,
apiKey: "api-key",
token: "jwt-token",
hostToken: "host-token",
});
await expect(
client.post(
"/v1/foundation",
{ hello: "world" },
{ params: { page: 1 } },
),
).resolves.toEqual({ ok: true });
expect(captured).toMatchObject({
method: "POST",
url: "/v1/foundation?page=1",
body: JSON.stringify({ hello: "world" }),
});
expect(captured?.headers["content-type"]).toBe("application/json");
expect(captured?.headers.accept).toBe("application/json");
expect(captured?.headers.authorization).toBe("Bearer jwt-token");
expect(captured?.headers["x-api-key"]).toBe("api-key");
expect(captured?.headers["x-host-token"]).toBe("host-token");
});
it("maps real HTTP error responses to SDK errors", async () => {
const server = createServer((_request, response) => {
response.writeHead(409, { "Content-Type": "application/json" });
response.end(
JSON.stringify({
error: { code: "capsule_busy", message: "Capsule is busy" },
}),
);
});
const port = await listen(server);
cleanup.push(() => closeHttpServer(server));
const client = new HttpClient({ baseUrl: `http://127.0.0.1:${port}` });
await expect(client.get("/v1/error")).rejects.toMatchObject({
code: "capsule_busy",
message: "Capsule is busy",
statusCode: 409,
});
await expect(client.get("/v1/error")).rejects.toBeInstanceOf(ConflictError);
});
it("aborts real HTTP requests using timeoutMs", async () => {
const server = createServer((_request, response) => {
setTimeout(() => {
response.writeHead(200, { "Content-Type": "application/json" });
response.end(JSON.stringify({ ok: true }));
}, 200);
});
const port = await listen(server);
cleanup.push(() => closeHttpServer(server));
const client = new HttpClient({ baseUrl: `http://127.0.0.1:${port}` });
await expect(
client.get("/v1/slow", { timeoutMs: 10 }),
).rejects.toBeInstanceOf(TimeoutError);
});
it("exchanges JSON messages through a real WebSocket server", async () => {
const server = new WebSocketServer({ port: 0, host: "127.0.0.1" });
cleanup.push(() => closeWebSocketServer(server));
const receivedByServer = new Promise<unknown>((resolve) => {
server.on("connection", (socket, request) => {
expect(request.headers["x-api-key"]).toBe("api-key");
expect(request.headers["x-host-token"]).toBe("host-token");
socket.on("message", (raw) => resolve(JSON.parse(raw.toString())));
socket.send(JSON.stringify({ type: "ready" }));
});
});
await new Promise<void>((resolve) => server.once("listening", resolve));
const address = server.address() as AddressInfo;
const messages: unknown[] = [];
const connection = await WsConnection.connect({
baseUrl: `http://127.0.0.1:${address.port}`,
path: "/v1/ws",
apiKey: "api-key",
hostToken: "host-token",
onMessage: (message) => messages.push(message),
});
connection.send({ type: "start" });
await expect(receivedByServer).resolves.toEqual({ type: "start" });
expect(messages).toEqual([{ type: "ready" }]);
expect(connection.isClosed).toBe(false);
connection.close();
});
});