exstack
Version:
A utility library designed to simplify and enhance express.js applications.
90 lines (89 loc) • 3.34 kB
JavaScript
//#region src/serve.ts
/**
* Start an Express app or HTTP/HTTPS server with graceful shutdown support
*
* @param app - Express application or HTTP/HTTPS server instance
* @param options - Server configuration options
* @returns HTTP/HTTPS server instance
*
* @example
* ```typescript
* // Basic usage with Express
* const app = express();
* serve(app, { port: 3000 });
*
* // With HTTPS
* const httpsServer = https.createServer({ cert, key }, app);
* serve(httpsServer, { port: 443 });
*
* // Custom graceful shutdown timeout (10 seconds)
* serve(app, { port: 3000, gracefulShutdown: 10 });
*
* // Disable graceful shutdown
* serve(app, { gracefulShutdown: false });
* ```
*
* @remarks
* Graceful shutdown behavior:
* - On SIGINT/SIGTERM: stops accepting new connections, waits for active requests
* - Shows countdown timer with remaining time
* - Press Ctrl+C again to force close immediately
* - After timeout: automatically force closes all connections
* - Long-running connections (SSE, WebSocket) will be force closed on timeout
*/
const serve = (app, options = {}) => {
const port = options.port ?? (Number.parseInt(process.env.PORT || "") || 3e3);
const hostname = options.host ?? process.env.HOST ?? "localhost";
const gracefulConfig = options.gracefulShutdown ?? true;
const silent = options.silent ?? false;
const connections = /* @__PURE__ */ new Set();
let closeCalled = false;
let isShuttingDown = false;
const server = app.listen(port, hostname, () => {
if (!silent) {
const url = `http://${hostname}:${port}/`;
console.log(`\x1b[32m➜ Listening on:\x1b[0m \x1b[36m${url}\x1b[0m`);
}
});
server.on("connection", (socket) => {
connections.add(socket);
socket.on("close", () => connections.delete(socket));
});
if (gracefulConfig !== false && !(typeof gracefulConfig === "number" && gracefulConfig <= 0) && !process.env.CI && !process.env.TEST) {
const gracefulTimeout = typeof gracefulConfig === "number" ? gracefulConfig : Number.parseInt(process.env.SERVER_SHUTDOWN_TIMEOUT || "") || 5;
const closeServer = () => new Promise((resolve, reject) => {
if (closeCalled) return resolve();
closeCalled = true;
server.close((err) => err ? reject(err) : resolve());
});
const forceClose = async () => {
process.stderr.write("\x1B[31m\x1B[2K\rForcibly closing connections...\n\x1B[0m");
for (const socket of connections) socket.destroy();
await closeServer();
process.stderr.write("\x1B[33mServer closed.\n\x1B[0m");
process.exit(0);
};
const shutdown = async () => {
if (isShuttingDown) {
await forceClose();
return;
}
isShuttingDown = true;
const closePromise = closeServer();
for (let remaining = gracefulTimeout; remaining > 0; remaining--) {
process.stderr.write(`\x1b[90m\rStopping server gracefully (${remaining}s)... Press \x1b[1mCtrl+C\x1b[0m\x1b[90m again to force close.\x1b[0m`);
if (await Promise.race([closePromise.then(() => true), new Promise((r) => setTimeout(() => r(false), 1e3))])) {
process.stderr.write("\x1B[2K\r\x1B[32mServer closed successfully.\n\x1B[0m");
process.exit(0);
}
}
process.stderr.write("\x1B[2K\rGraceful shutdown timed out.\n");
await forceClose();
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
}
return server;
};
//#endregion
export { serve };