Rename src to source

This commit is contained in:
2026-07-27 10:20:57 +00:00
parent ff0aacc9b4
commit a4155b52a6
38 changed files with 35 additions and 35 deletions
+166
View File
@@ -0,0 +1,166 @@
import { Hono, type MiddlewareHandler } from 'hono';
import { serve } from '@hono/node-server';
import { cors } from 'hono/cors';
import { compress } from 'hono/compress';
import { bodyLimit } from 'hono/body-limit';
import type { Logger } from '../utils/logger.ts';
import type { Config } from './config.ts';
import type { TransportRouter, UpgradeTransportRouter, AppEnv } from './transport/transport-router.ts';
/**
* Owns the Hono server and installs transport adapters around shared middleware.
*
* Application routes are deliberately absent from this layer.
*/
export class ServerHost {
private readonly debug: Logger;
private server: ReturnType<typeof serve> | undefined;
private stopPromise: Promise<void> | undefined;
private readonly app: Hono<AppEnv>;
/**
* @param config - Server listen address, CORS, and related settings.
* @param debug - Root logger extended with a server-host namespace.
* @param transports - Protocol adapters registered onto the Hono app.
*/
constructor(
private readonly config: Config,
debug: Logger,
private readonly transports: TransportRouter[],
) {
this.debug = debug.extend('server-host');
this.app = new Hono<AppEnv>();
}
/** Configure middleware, register adapters, and begin listening. */
async start(): Promise<void> {
const { port, host, cors: corsConfig, maxRequestBodyBytes } = this.config.server;
this.debug(`Starting on http://${host}:${port}`);
const corsMiddleware = cors({
origin: corsConfig.origin ?? '*',
allowMethods: corsConfig.methods ?? ['POST', 'OPTIONS'],
allowHeaders: corsConfig.allowedHeaders ?? ['Content-Type', 'Accept'],
});
this.app.use('*', corsMiddleware);
// Reject oversized request bodies before any decoder or route reads them.
// Hono checks Content-Length when it can and counts streamed chunks when it
// cannot, covering both fixed-length and chunked HTTP requests.
this.app.use('*', ServerHost.limitBodySizeMiddleware(maxRequestBodyBytes, this.debug));
const compression = compress();
// Compression can buffer event streams, so never apply it to requested SSE.
this.app.use('*', async (c, next) => {
if (c.req.header('accept')?.includes('text/event-stream')) {
await next();
return;
}
return compression(c, next);
});
this.app.get('/health', (c) => c.json({ status: 'ok' }));
// Each transport adapter registers its own wire endpoints.
for (const transport of this.transports) {
await transport.register(this.app);
}
// The Node Hono server accepts one WebSocketServer instance per listener.
const upgradeTransports = this.transports.filter((transport): transport is UpgradeTransportRouter => 'websocketServer' in transport);
if (upgradeTransports.length > 1) {
throw new Error('ServerHost supports only one WebSocket upgrade server');
}
const [upgradeTransport] = upgradeTransports;
this.server = serve({
fetch: this.app.fetch,
port,
hostname: host,
...(upgradeTransport ? { websocket: { server: upgradeTransport.websocketServer } } : {}),
});
// Wait for the server to start listening or timeout after 10 seconds.
await Promise.race([
// Wait for the server to start listening.
new Promise((resolve) => {
this.server?.once('listening', resolve);
}),
// Timeout after 10 seconds.
new Promise((_resolve, reject) => {
setTimeout(reject, 10000);
}),
]);
this.debug(`Started on http://${host}:${port}`);
}
/** Stop accepting traffic and close all transport-owned connections. */
async stop(): Promise<void> {
// If we are already shutting down, return the existing promise
if (this.stopPromise) {
return this.stopPromise;
}
// If the server hasn't been started yet or already shut down, return early
const server = this.server;
if (!server) {
return;
}
// Create a promise that resolves when the server is closed
this.debug('stopping server');
const closeServer = new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
// Close the transports
const closeTransports = this.transports.map((transport) => Promise.resolve(transport.stop?.()));
// Create a promise that resolves when the server and transports are closed
this.stopPromise = Promise.all([closeServer, ...closeTransports]).then(() => {
this.stopPromise = undefined;
});
// Return the promise
return this.stopPromise;
}
/**
* Build the server-wide Hono request-body limit policy.
*
* Keeping this policy in ServerHost ensures it runs before every HTTP adapter
* without making application routes or connection streams aware of HTTP.
* Hono preserves an accepted streamed body for downstream middleware, allowing
* the Extended JSON decoder to read it normally after validation.
*
* @param maxRequestBodyBytes - Maximum encoded HTTP request body size in bytes.
* @param debug - Logger used to record rejected requests and configured limits.
* @returns Hono middleware which returns the shared public 413 error contract.
*/
static limitBodySizeMiddleware(maxRequestBodyBytes: number, debug: Logger): MiddlewareHandler<AppEnv> {
return bodyLimit({
// Hono interprets maxSize as encoded request bytes, not decoded JSON size.
maxSize: maxRequestBodyBytes,
onError: (context) => {
// Do not parse the rejected payload: Hono stopped consuming it at the cap.
debug('request body exceeded configured %d byte limit: %s %s', maxRequestBodyBytes, context.req.method, context.req.path);
// Limiting occurs before dispatch, so a normal HTTP 413 remains available.
return context.json(
{
statusCode: 413,
error: `Request body exceeds the ${maxRequestBodyBytes} byte limit`,
},
413,
);
},
});
}
}