/** * tRPC HTTP Adapter * * Handles tRPC requests over HTTP using node:http. * Routes /trpc/* requests to the tRPC router. */ import type { IncomingMessage, ServerResponse } from 'node:http'; import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import { appRouter, createContext } from '../trpc/index.js'; import type { EventBus } from '../events/index.js'; import type { AgentManager } from '../agent/types.js'; import type { TaskRepository } from '../db/repositories/task-repository.js'; import type { MessageRepository } from '../db/repositories/message-repository.js'; import type { InitiativeRepository } from '../db/repositories/initiative-repository.js'; import type { PhaseRepository } from '../db/repositories/phase-repository.js'; import type { PlanRepository } from '../db/repositories/plan-repository.js'; import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js'; import type { CoordinationManager } from '../coordination/types.js'; /** * Options for creating the tRPC request handler. */ export interface TrpcAdapterOptions { /** Event bus for inter-module communication */ eventBus: EventBus; /** When the server started */ serverStartedAt: Date; /** Number of managed processes */ processCount: number; /** Agent manager for agent lifecycle operations (optional until full wiring) */ agentManager?: AgentManager; /** Task repository for task CRUD operations */ taskRepository?: TaskRepository; /** Message repository for agent-user communication */ messageRepository?: MessageRepository; /** Initiative repository for initiative CRUD operations */ initiativeRepository?: InitiativeRepository; /** Phase repository for phase CRUD operations */ phaseRepository?: PhaseRepository; /** Plan repository for plan CRUD operations */ planRepository?: PlanRepository; /** Dispatch manager for task queue operations */ dispatchManager?: DispatchManager; /** Coordination manager for merge queue operations */ coordinationManager?: CoordinationManager; /** Phase dispatch manager for phase queue operations */ phaseDispatchManager?: PhaseDispatchManager; } /** * Creates a tRPC request handler for node:http. * * Converts IncomingMessage/ServerResponse to fetch Request/Response * and delegates to the tRPC fetch adapter. * * @param options - Adapter options with context values * @returns Request handler function */ export function createTrpcHandler(options: TrpcAdapterOptions) { return async (req: IncomingMessage, res: ServerResponse): Promise => { // Build full URL from request const protocol = 'http'; const host = req.headers.host ?? 'localhost'; const url = new URL(req.url ?? '/', `${protocol}://${host}`); // Read request body if present let body: string | undefined; if (req.method !== 'GET' && req.method !== 'HEAD') { body = await new Promise((resolve) => { let data = ''; req.on('data', (chunk: Buffer) => { data += chunk.toString(); }); req.on('end', () => { resolve(data); }); }); } // Convert headers to fetch Headers const headers = new Headers(); for (const [key, value] of Object.entries(req.headers)) { if (value) { if (Array.isArray(value)) { value.forEach((v) => headers.append(key, v)); } else { headers.set(key, value); } } } // Create fetch Request const fetchRequest = new Request(url.toString(), { method: req.method, headers, body: body ?? undefined, }); // Handle with tRPC fetch adapter const fetchResponse = await fetchRequestHandler({ endpoint: '/trpc', req: fetchRequest, router: appRouter, createContext: () => createContext({ eventBus: options.eventBus, serverStartedAt: options.serverStartedAt, processCount: options.processCount, agentManager: options.agentManager, taskRepository: options.taskRepository, messageRepository: options.messageRepository, initiativeRepository: options.initiativeRepository, phaseRepository: options.phaseRepository, planRepository: options.planRepository, dispatchManager: options.dispatchManager, coordinationManager: options.coordinationManager, phaseDispatchManager: options.phaseDispatchManager, }), }); // Send response res.statusCode = fetchResponse.status; // Set response headers BEFORE streaming body fetchResponse.headers.forEach((value, key) => { res.setHeader(key, value); }); // Stream body if it's a ReadableStream (SSE subscriptions), otherwise buffer if (fetchResponse.body) { const reader = fetchResponse.body.getReader(); const pump = async () => { while (true) { const { done, value } = await reader.read(); if (done) { res.end(); return; } res.write(value); } }; pump().catch(() => res.end()); } else { const responseBody = await fetchResponse.text(); res.end(responseBody); } }; }