fix: harden identity HTTP transport
CI / verify (pull_request) Successful in 25s
CI / infrastructure-smoke (pull_request) Successful in 20s
CI / migration-smoke (pull_request) Successful in 25s

This commit is contained in:
ekklesia
2026-07-21 23:50:41 -05:00
parent c91f0c8c6d
commit d9b73fd81a
15 changed files with 967 additions and 72 deletions
+44 -22
View File
@@ -12,6 +12,7 @@ import { createApiApp } from "./app.js";
import { type ApiRuntimeConfig, createApiRuntimeConfig } from "./config.js";
const connectionTimeoutMs = 10_000;
const operationTimeoutMs = 10_000;
export interface RunningApi {
readonly app: Hono;
@@ -65,6 +66,7 @@ export async function startApi(
): Promise<RunningApi> {
const databaseHandle = createIdentityDrizzleDatabase(config.databaseUrl, {
connectionTimeoutMs,
operationTimeoutMs,
});
let runtime: ReturnType<typeof createIdentityRuntime>;
@@ -89,21 +91,53 @@ export async function startApi(
throw error;
}
let shuttingDown = false;
const shutdown = async (): Promise<void> => {
if (shuttingDown) {
return;
// Memoizes one shared promise rather than a boolean flag: a boolean only prevents a *second*
// cleanup from starting, but a caller that arrives while the first cleanup is still in flight
// would previously get an already-resolved `undefined` immediately, before the listener had
// actually drained or the pool had actually closed. Storing the promise itself means every
// caller — concurrent or sequential — awaits the exact same drain-then-close operation and
// observes it complete (or fail) exactly once.
let shutdownPromise: Promise<void> | undefined;
const shutdown = (): Promise<void> => {
if (!shutdownPromise) {
shutdownPromise = (async () => {
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
await safeCloseDatabase(databaseHandle);
})();
}
shuttingDown = true;
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
await safeCloseDatabase(databaseHandle);
return shutdownPromise;
};
return Object.freeze({ app, server, shutdown });
}
/**
* Wires `SIGINT`/`SIGTERM` to `running.shutdown()` and only then exits — extracted from `main()`
* so it can be exercised directly in tests with a fake `running`/`exit`, without sending a real
* process signal or invoking the real `process.exit` (which would terminate the test runner
* itself). Production composition (`main()`, below) calls this with real `process.exit`.
*/
export function installShutdownSignalHandlers(
running: Pick<RunningApi, "shutdown">,
exit: (code: number) => void = (code) => process.exit(code),
): void {
let signalReceived = false;
const handleSignal = (): void => {
if (signalReceived) {
return;
}
signalReceived = true;
running
.shutdown()
.then(() => exit(0))
.catch(() => exit(1));
};
process.on("SIGINT", handleSignal);
process.on("SIGTERM", handleSignal);
}
function readConfigFromEnvironment(): {
readonly input: Parameters<typeof createApiRuntimeConfig>[0];
readonly environment: "development" | "production";
@@ -138,19 +172,7 @@ export async function main(): Promise<void> {
const boundPort = address && typeof address === "object" ? address.port : config.port;
console.log(`Ekklesia API listening on ${config.host}:${boundPort}`);
let signalReceived = false;
const handleSignal = (): void => {
if (signalReceived) {
return;
}
signalReceived = true;
running
.shutdown()
.then(() => process.exit(0))
.catch(() => process.exit(1));
};
process.on("SIGINT", handleSignal);
process.on("SIGTERM", handleSignal);
installShutdownSignalHandlers(running);
}
function isDirectExecution(): boolean {