import http from "node:http"; const debug = (...args) => { }; function GracefulShutdown(server, opts) { opts = opts || {}; const options = Object.assign( { signals: "SIGINT SIGTERM", timeout: 3e4, development: false, forceExit: true, onShutdown: (signal) => Promise.resolve(signal), preShutdown: (signal) => Promise.resolve(signal) }, opts ); let isShuttingDown = false; const connections = {}; let connectionCounter = 0; const secureConnections = {}; let secureConnectionCounter = 0; let failed = false; let finalRun = false; function onceFactory() { let called = false; return (emitter, events, callback) => { function call() { if (!called) { called = true; return Reflect.apply(callback, this, arguments); } } for (const e of events) { emitter.on(e, call); } }; } const signals = options.signals.split(" ").map((s) => s.trim()).filter((s) => s.length > 0); const once = onceFactory(); once(process, signals, (signal) => { debug("received shut down signal", signal); shutdown(signal).then(() => { if (options.forceExit) { process.exit(failed ? 1 : 0); } }).catch((error) => { debug("server shut down error occurred", error); process.exit(1); }); }); function isFunction(functionToCheck) { const getType = Object.prototype.toString.call(functionToCheck); return /^\[object\s([A-Za-z]+)?Function]$/.test(getType); } function destroy(socket, force = false) { if (socket._isIdle && isShuttingDown || force) { socket.destroy(); if (socket.server instanceof http.Server) { delete connections[socket._connectionId]; } else { delete secureConnections[socket._connectionId]; } } } function destroyAllConnections(force = false) { debug("Destroy Connections : " + (force ? "forced close" : "close")); let counter = 0; let secureCounter = 0; for (const key of Object.keys(connections)) { const socket = connections[key]; const serverResponse = socket._httpMessage; if (serverResponse && !force) { if (!serverResponse.headersSent) { serverResponse.setHeader("connection", "close"); } } else { counter++; destroy(socket); } } debug("Connections destroyed : " + counter); debug("Connection Counter : " + connectionCounter); for (const key of Object.keys(secureConnections)) { const socket = secureConnections[key]; const serverResponse = socket._httpMessage; if (serverResponse && !force) { if (!serverResponse.headersSent) { serverResponse.setHeader("connection", "close"); } } else { secureCounter++; destroy(socket); } } debug("Secure Connections destroyed : " + secureCounter); debug("Secure Connection Counter : " + secureConnectionCounter); } server.on("request", (req, res) => { req.socket._isIdle = false; if (isShuttingDown && !res.headersSent) { res.setHeader("connection", "close"); } res.on("finish", () => { req.socket._isIdle = true; destroy(req.socket); }); }); server.on("connection", (socket) => { if (isShuttingDown) { socket.destroy(); } else { const id = connectionCounter++; socket._isIdle = true; socket._connectionId = id; connections[id] = socket; socket.once("close", () => { delete connections[socket._connectionId]; }); } }); server.on("secureConnection", (socket) => { if (isShuttingDown) { socket.destroy(); } else { const id = secureConnectionCounter++; socket._isIdle = true; socket._connectionId = id; secureConnections[id] = socket; socket.once("close", () => { delete secureConnections[socket._connectionId]; }); } }); process.on("close", () => { debug("closed"); }); function shutdown(sig) { function cleanupHttp() { destroyAllConnections(); debug("Close http server"); return new Promise((resolve, reject) => { server.close((err) => { if (err) { return reject(err); } return resolve(true); }); }); } debug("shutdown signal - " + sig); if (options.development) { debug("DEV-Mode - immediate forceful shutdown"); return process.exit(0); } function finalHandler() { if (!finalRun) { finalRun = true; if (options.finally && isFunction(options.finally)) { debug("executing finally()"); options.finally(); } } return Promise.resolve(); } function waitForReadyToShutDown(totalNumInterval) { debug(`waitForReadyToShutDown... ${totalNumInterval}`); if (totalNumInterval === 0) { debug( `Could not close connections in time (${options.timeout}ms), will forcefully shut down` ); return Promise.resolve(true); } const allConnectionsClosed = Object.keys(connections).length === 0 && Object.keys(secureConnections).length === 0; if (allConnectionsClosed) { debug("All connections closed. Continue to shutting down"); return Promise.resolve(false); } debug("Schedule the next waitForReadyToShutdown"); return new Promise((resolve) => { setTimeout(() => { resolve(waitForReadyToShutDown(totalNumInterval - 1)); }, 250); }); } if (isShuttingDown) { return Promise.resolve(); } debug("shutting down"); return options.preShutdown(sig).then(() => { isShuttingDown = true; cleanupHttp(); }).then(() => { const pollIterations = options.timeout ? Math.round(options.timeout / 250) : 0; return waitForReadyToShutDown(pollIterations); }).then((force) => { debug("Do onShutdown now"); if (force) { destroyAllConnections(force); } return options.onShutdown(sig); }).then(finalHandler).catch((error) => { const errString = typeof error === "string" ? error : JSON.stringify(error); debug(errString); failed = true; throw errString; }); } function shutdownManual() { return shutdown("manual"); } return shutdownManual; } export default GracefulShutdown;