veloze
Version:
A modern and fast express-like webserver for the web
141 lines (118 loc) • 3.17 kB
JavaScript
import * as http from 'node:http'
import { logger } from './logger.js'
/**
* @typedef {import('../types.js').Log} Log
* @typedef {import('http').Server} HttpServer
* @typedef {import('https').Server} HttpSecureServer
* @typedef {import('http2').Http2Server} Http2Server
* @typedef {import('http2').Http2SecureServer} Http2SecureServer
* @typedef {{closeAsync: () => Promise<void>}} CloseAsync
* @typedef {(HttpServer|HttpSecureServer|Http2Server|Http2SecureServer) & CloseAsync} Server
*/
const log = logger(':safeShutdown')
const EXIT_EVENTS = [
'uncaughtException',
'beforeExit',
'SIGINT',
'SIGTERM',
'SIGHUP',
'SIGBREAK'
]
/**
* gracefully shutdown http/ https server
* alternative to [stoppable](https://github.com/hunterloftis/stoppable).
*
* @param {Server} server the server instance
* @param {object} [options]
* @param {number} [options.gracefulTimeout=1000] (ms) graceful timeout for existing connections
*/
export function safeServerShutdown(server, options) {
const { gracefulTimeout = 1000 } = options || {}
let isShutdown = false
const serverClose = server.close.bind(server)
const sockets = new Set()
function connect(socket) {
if (isShutdown) {
destroy([socket])
return
}
sockets.add(socket)
socket.once('close', function () {
sockets.delete(socket)
})
}
function setHeaderConnectionClose(res) {
if (!res.headersSent) {
res.setHeader('connection', 'close')
}
}
server.on('connection', connect)
server.on('secureConnection', connect)
// @ts-expect-error
server.close = function close(callback) {
isShutdown = true
log.info('server is shutting down')
server.on('request', (req, res) => {
/* c8 ignore next */
setHeaderConnectionClose(res)
})
for (const socket of sockets) {
if (!(socket.server instanceof http.Server)) {
// HTTP CONNECT request socket
continue
}
const res = socket._httpMessage
if (res) {
setHeaderConnectionClose(res)
}
}
;(async () => {
if (sockets.size) {
await sleep(gracefulTimeout)
destroy(sockets)
}
serverClose((err) => {
err
? log.error(`server shutdown with failures ${err.message}`)
: log.info('server shutdown successful')
if (typeof callback === 'function') {
callback(err)
}
})
})()
return server
}
server.closeAsync = () =>
new Promise((resolve, reject) => {
server.close((err) => {
err ? reject(err) : resolve()
})
})
EXIT_EVENTS.forEach((ev) =>
process.on(ev, (err) => {
log.fatal(`process received "${ev}" signal, shutting down server...`)
if (err) {
log.fatal(err)
}
if (isShutdown) return
/* c8 ignore next */
server.close()
})
)
}
const sleep = (ms) =>
new Promise((resolve) =>
setTimeout(() => {
resolve(ms)
}, ms)
)
function destroy(sockets) {
for (const socket of sockets) {
socket.end()
}
setImmediate(() => {
for (const socket of sockets) {
socket.destroy()
}
})
}