Fix tRPC HTTP adapter to stream ReadableStream responses instead of buffering (required for SSE). Create subscriptions module that bridges EventBus domain events into tRPC async generator subscriptions using a queue-based pattern. Add three subscription procedures: onEvent (all events), onAgentUpdate (agent lifecycle), onTaskUpdate (task/phase).
150 lines
5.1 KiB
TypeScript
150 lines
5.1 KiB
TypeScript
/**
|
|
* 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<void> => {
|
|
// 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<string>((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);
|
|
}
|
|
};
|
|
}
|