fix(stats): harden server lifecycle and verify compiled runtime (#261)

This commit is contained in:
2026-09-20 23:19:03 -07:00
committed by GitHub
parent f9c4e892dc
commit 383aed8bad
37 changed files with 1381 additions and 323 deletions
+67 -19
View File
@@ -50,8 +50,26 @@ async function writeFetchResponse(res: ServerResponse, response: Response): Prom
res.end(Buffer.from(await response.arrayBuffer()));
}
function startNodeHttpServer(app: Hono, config: StatsServerConfig): { close: () => void } {
const server = http.createServer((req, res) => {
export interface StatsServer {
close: () => Promise<void>;
}
const SHUTDOWN_GRACE_MS = 1_000;
type BunServe = (options: {
fetch: (typeof Hono.prototype)['fetch'];
port: number;
hostname: string;
}) => {
stop: () => Promise<void> | void;
};
export function startNodeHttpServer(
app: Hono,
config: StatsServerConfig,
createServer: (listener: http.RequestListener) => http.Server = http.createServer,
): Promise<StatsServer> {
const server = createServer((req, res) => {
void (async () => {
try {
await writeFetchResponse(res, await app.fetch(toFetchRequest(req)));
@@ -61,12 +79,33 @@ function startNodeHttpServer(app: Hono, config: StatsServerConfig): { close: ()
}
})();
});
server.listen(config.port, '127.0.0.1');
return {
close: () => {
server.close();
},
};
return new Promise((resolve, reject) => {
const handleStartupError = (error: Error): void => {
server.removeListener('listening', handleListening);
reject(error);
};
const handleListening = (): void => {
server.removeListener('error', handleStartupError);
let closePromise: Promise<void> | null = null;
resolve({
close: () => {
closePromise ??= new Promise<void>((closeResolve, closeReject) => {
const forceClose = setTimeout(() => server.closeAllConnections(), SHUTDOWN_GRACE_MS);
server.close((error) => {
clearTimeout(forceClose);
if (error) closeReject(error);
else closeResolve();
});
});
return closePromise;
},
});
};
server.once('error', handleStartupError);
server.once('listening', handleListening);
server.listen(config.port, '127.0.0.1');
});
}
export interface StatsServerConfig {
@@ -125,7 +164,10 @@ export function createStatsApp(
return app;
}
export function startStatsServer(config: StatsServerConfig): { close: () => void } {
export async function startStatsServerWithRuntime(
config: StatsServerConfig,
runtime: { bunServe: BunServe | null },
): Promise<StatsServer> {
const app = createStatsApp(config.tracker, {
staticDir: config.staticDir,
knownWordCachePath: config.knownWordCachePath,
@@ -144,20 +186,26 @@ export function startStatsServer(config: StatsServerConfig): { close: () => void
resolveSentenceSearchHeadwords: config.resolveSentenceSearchHeadwords,
});
const bunRuntime = globalThis as typeof globalThis & {
Bun?: {
serve?: (options: { fetch: (typeof app)['fetch']; port: number; hostname: string }) => {
stop: () => void;
};
};
};
if (bunRuntime.Bun?.serve) {
const server = bunRuntime.Bun.serve({
if (runtime.bunServe) {
const server = runtime.bunServe({
fetch: app.fetch,
port: config.port,
hostname: '127.0.0.1',
});
return { close: () => server.stop() };
let closePromise: Promise<void> | null = null;
return Promise.resolve({
close: () => {
closePromise ??= Promise.resolve().then(() => server.stop());
return closePromise;
},
});
}
return startNodeHttpServer(app, config);
}
export function startStatsServer(config: StatsServerConfig): Promise<StatsServer> {
const bunRuntime = globalThis as typeof globalThis & {
Bun?: { serve?: BunServe };
};
return startStatsServerWithRuntime(config, { bunServe: bunRuntime.Bun?.serve ?? null });
}