├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── events ├── error.events.js ├── exit.events.js └── signal.events.js ├── example.js ├── index.d.ts ├── index.js ├── index.test-d.ts ├── package.json └── test ├── callbacks.js ├── close.test.js ├── closing-state.js ├── default-delay.js ├── no-resolve-custom-logger.js ├── no-resolve-without-delay.js ├── no-resolve-without-logger.js ├── no-resolve.js ├── normal-close.js ├── self-close.js ├── simple.js ├── uncaughtException.js ├── unhandledRejection.js └── uninstall.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | 6 | # This allows a subsequently queued workflow run to interrupt previous runs 7 | concurrency: 8 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | test: 13 | permissions: 14 | contents: read 15 | 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | node-version: [18.x, 20.x, 22.x] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | persist-credentials: false 26 | 27 | - name: Use Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | 32 | - name: Install 33 | run: | 34 | npm install 35 | 36 | - name: Run tests 37 | run: | 38 | npm run test 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Matteo Collina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # close-with-grace 2 | 3 | Exit your process, gracefully (if possible) - for Node.js 4 | 5 | ## Install 6 | 7 | ``` 8 | npm i close-with-grace 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```js 14 | const closeWithGrace = require('close-with-grace') 15 | 16 | // delay is the number of milliseconds for the graceful close to 17 | // finish. 18 | closeWithGrace({ delay: 500 }, async function ({ signal, err, manual }) { 19 | if (err) { 20 | console.error(err) 21 | } 22 | await closeYourServer() 23 | }) 24 | 25 | // default delay is 10000 26 | // to disable delay feature at all, pass falsy value to delay option. 27 | closeWithGrace({ delay: false }, () => await somethingUseful()) 28 | ``` 29 | 30 | ### Injecting custom logger 31 | 32 | ```js 33 | const closeWithGrace = require('close-with-grace') 34 | 35 | // delay is the number of milliseconds for the graceful close to 36 | // finish. 37 | closeWithGrace( 38 | { 39 | delay: 500, 40 | logger: { error: (m) => console.error(`[close-with-grace] ${m}`) } 41 | }, 42 | async function ({ signal, err, manual }) { 43 | if (err) { 44 | console.error(err) 45 | } 46 | await closeYourServer() 47 | }) 48 | 49 | // default logger is console 50 | // to disable logging at all, pass falsy value to logger option. 51 | closeWithGrace({ logger: false }, () => await somethingUseful()) 52 | ``` 53 | 54 | ### Example with Fastify 55 | 56 | ```js 57 | import fastify from 'fastify' 58 | import closeWithGrace from 'close-with-grace' 59 | 60 | const app = fastify() 61 | 62 | closeWithGrace(async function ({ signal, err, manual }) { 63 | if (err) { 64 | app.log.error({ err }, 'server closing with error') 65 | } else { 66 | app.log.info(`${signal} received, server closing`) 67 | } 68 | await app.close() 69 | }) 70 | 71 | await app.listen() 72 | ``` 73 | 74 | ## API 75 | 76 | ### `closeWithGrace([opts], fn({ err, signal, manual }))` 77 | 78 | `closeWithGrace` adds a global listeners to the events: 79 | 80 | * `process.once('SIGHUP')` 81 | * `process.once('SIGINT')` 82 | * `process.once('SIGQUIT')` 83 | * `process.once('SIGILL')` 84 | * `process.once('SIGTRAP')` 85 | * `process.once('SIGABRT')` 86 | * `process.once('SIGBUS')` 87 | * `process.once('SIGFPE')` 88 | * `process.once('SIGSEGV')` 89 | * `process.once('SIGUSR2')` 90 | * `process.once('SIGTERM')` 91 | * `process.once('uncaughtException')` 92 | * `process.once('unhandledRejection')` 93 | * `process.once('beforeExit')` 94 | 95 | In case one of them is emitted, it will call the given function. 96 | If it is emitted again, it will terminate the process abruptly. 97 | 98 | #### opts 99 | 100 | * `delay`: the numbers of milliseconds before abruptly close the 101 | process. Default: `10000`. 102 | - Pass `false`, `null` or `undefined` to disable this feature. 103 | 104 | * `logger`: instance of logger which will be used internally. Default: `console`. 105 | - Pass `false`, `null` or `undefined` to disable this feature. 106 | 107 | #### fn({ err, signal, manual } [, cb]) 108 | 109 | Execute the given function to perform a graceful close. 110 | The function can either return a `Promise` or call the callback. 111 | If this function does not error, the process will be closed with 112 | exit code `0`. 113 | If the function rejects with an `Error`, or call the callback with an 114 | `Error` as first argument, the process will be closed with exit code 115 | `1`. 116 | 117 | #### return values 118 | 119 | Calling `closeWithGrace()` will return an object as formed: 120 | 121 | * `close()`: close the process, the `manual` argument will be set to 122 | true. 123 | * `uninstall()`: remove all global listeners. 124 | 125 | ## License 126 | 127 | MIT 128 | -------------------------------------------------------------------------------- /events/error.events.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const errorEvents = [ 4 | 'uncaughtException', 5 | 'unhandledRejection' 6 | ] 7 | 8 | module.exports = { 9 | errorEvents 10 | } 11 | -------------------------------------------------------------------------------- /events/exit.events.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const exitEvents = [ 4 | 'beforeExit' 5 | ] 6 | 7 | module.exports = { 8 | exitEvents 9 | } 10 | -------------------------------------------------------------------------------- /events/signal.events.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const signalEvents = [ 4 | 'SIGHUP', 5 | 'SIGINT', 6 | 'SIGQUIT', 7 | 'SIGILL', 8 | 'SIGTRAP', 9 | 'SIGABRT', 10 | 'SIGBUS', 11 | 'SIGFPE', 12 | 'SIGSEGV', 13 | 'SIGUSR2', 14 | 'SIGTERM' 15 | ] 16 | 17 | module.exports = { 18 | signalEvents 19 | } 20 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { createServer } = require('http') 4 | const closeWithGrace = require('.') 5 | 6 | const server = createServer(function (req, res) { 7 | if (closeWithGrace.closing) { 8 | res.statusCode = 503 9 | res.setHeader('Connection', 'close') 10 | res.end('try again later') 11 | return 12 | } 13 | res.end('hello world') 14 | }) 15 | 16 | server.listen(3000) 17 | 18 | closeWithGrace(function (opts, cb) { 19 | console.log(opts, 'server closing') 20 | server.close(cb) 21 | }) 22 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace closeWithGrace { 2 | interface Logger { 3 | error(message?: any, ...optionalParams: any[]): void 4 | } 5 | 6 | interface Options { 7 | /** 8 | * The numbers of milliseconds before abruptly close the process 9 | * @default 10000 10 | */ 11 | delay?: number | undefined | null | false 12 | /** 13 | * Instance of logger which will be used internally 14 | * @default console 15 | */ 16 | logger?: Logger | undefined | null | false 17 | } 18 | 19 | type Signals = 'SIGHUP' | 'SIGINT' | 'SIGQUIT' | 'SIGILL' | 'SIGTRAP' | 'SIGABRT' | 'SIGBUS' | 'SIGFPE' | 'SIGSEGV' | 'SIGUSR2' | 'SIGTERM' 20 | interface CloseWithGraceCallback { 21 | ( 22 | options: { err?: Error, signal?: Signals, manual?: boolean }, 23 | cb: (error?: Error) => void 24 | ): void 25 | } 26 | interface CloseWithGraceAsyncCallback { 27 | (options: { 28 | err?: Error 29 | signal?: Signals 30 | manual?: boolean 31 | }): Promise 32 | } 33 | } 34 | declare interface CloseWithGraceReturn { 35 | /** 36 | * Close the process, the manual argument will be set to true. 37 | */ 38 | close: () => void 39 | /** 40 | * Remove all global listeners 41 | */ 42 | uninstall: () => void 43 | } 44 | 45 | /** 46 | @example 47 | import * as closeWithGrace from 'close-with-grace' 48 | 49 | // delay is the number of milliseconds for the graceful close to 50 | // finish. 51 | closeWithGrace({ delay: 500 }, async function ({ signal, err, manual }) { 52 | if (err) { 53 | console.error(err) 54 | } 55 | await closeYourServer() 56 | }) 57 | 58 | // default delay is 10000 59 | // to disable delay feature at all, pass falsy value to delay option. 60 | closeWithGrace({ delay: false }, () => await somethingUseful()) 61 | 62 | // default logger is console 63 | // to disable logging at all, pass falsy value to logger option. 64 | closeWithGrace({ logger: false }, () => await somethingUseful()) 65 | */ 66 | declare function closeWithGrace ( 67 | fn: closeWithGrace.CloseWithGraceAsyncCallback 68 | ): CloseWithGraceReturn 69 | declare function closeWithGrace ( 70 | fn: closeWithGrace.CloseWithGraceCallback 71 | ): CloseWithGraceReturn 72 | declare function closeWithGrace ( 73 | opts: closeWithGrace.Options, 74 | fn: closeWithGrace.CloseWithGraceAsyncCallback 75 | ): CloseWithGraceReturn 76 | declare function closeWithGrace ( 77 | opts: closeWithGrace.Options, 78 | fn: closeWithGrace.CloseWithGraceCallback 79 | ): CloseWithGraceReturn 80 | declare function closeWithGrace ( 81 | fn: closeWithGrace.CloseWithGraceAsyncCallback 82 | ): CloseWithGraceReturn 83 | declare function closeWithGrace ( 84 | fn: closeWithGrace.CloseWithGraceCallback 85 | ): CloseWithGraceReturn 86 | 87 | export = closeWithGrace 88 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { signalEvents } = require('./events/signal.events') 4 | const { errorEvents } = require('./events/error.events') 5 | const { exitEvents } = require('./events/exit.events') 6 | 7 | const { promisify } = require('node:util') 8 | const sleep = promisify(setTimeout) 9 | 10 | closeWithGrace.closing = false 11 | 12 | function closeWithGrace (opts, fn) { 13 | if (typeof opts === 'function') { 14 | fn = opts 15 | opts = {} 16 | } 17 | 18 | opts = { 19 | delay: 10000, 20 | logger: console, 21 | ...opts 22 | } 23 | 24 | const delay = typeof opts.delay === 'number' ? opts.delay : undefined 25 | const logger = 26 | typeof opts.logger === 'object' || typeof opts.logger === 'function' 27 | ? opts.logger 28 | : undefined 29 | 30 | signalEvents.forEach((event) => process.once(event, onSignal)) 31 | errorEvents.forEach((event) => process.once(event, onError)) 32 | exitEvents.forEach((event) => process.once(event, onNormalExit)) 33 | 34 | const sleeped = Symbol('sleeped') 35 | 36 | return { 37 | close: () => run({ manual: true }), 38 | uninstall: cleanup 39 | } 40 | 41 | function cleanup () { 42 | signalEvents.forEach((event) => process.removeListener(event, onSignal)) 43 | errorEvents.forEach((event) => process.removeListener(event, onError)) 44 | exitEvents.forEach((event) => process.removeListener(event, onNormalExit)) 45 | } 46 | 47 | function onSignal (signal) { 48 | run({ signal }) 49 | } 50 | 51 | function afterFirstSignal (signal) { 52 | if (logger) logger.error(`second ${signal}, exiting`) 53 | process.exit(1) 54 | } 55 | 56 | function onError (err) { 57 | run({ err }) 58 | } 59 | 60 | function afterFirstError (err) { 61 | if (logger) { 62 | logger.error('second error, exiting') 63 | logger.error(err) 64 | } 65 | process.exit(1) 66 | } 67 | 68 | function onNormalExit () { 69 | run({}) 70 | } 71 | 72 | function exec (out) { 73 | const res = fn(out, done) 74 | 75 | if (res && typeof res.then === 'function') { 76 | return res 77 | } 78 | 79 | let _resolve 80 | let _reject 81 | 82 | const p = new Promise(function (resolve, reject) { 83 | _resolve = resolve 84 | _reject = reject 85 | }) 86 | 87 | return p 88 | 89 | function done (err) { 90 | if (!_resolve) { 91 | return 92 | } 93 | 94 | if (err) { 95 | _reject(err) 96 | return 97 | } 98 | 99 | _resolve() 100 | } 101 | } 102 | 103 | async function run (out) { 104 | cleanup() 105 | 106 | signalEvents.forEach((event) => process.on(event, afterFirstSignal)) 107 | errorEvents.forEach((event) => process.on(event, afterFirstError)) 108 | 109 | closeWithGrace.closing = true 110 | 111 | try { 112 | const res = await Promise.race([ 113 | // We create the timer first as fn 114 | // might block the event loop 115 | ...(typeof delay === 'number' ? [sleep(delay, sleeped)] : []), 116 | exec(out) 117 | ]) 118 | 119 | if (res === sleeped) { 120 | if (logger) logger.error(`killed by timeout (${delay}ms)`) 121 | process.exit(1) 122 | } else if (out.err) { 123 | process.exit(1) 124 | } else { 125 | process.exit(0) 126 | } 127 | } catch (err) { 128 | if (logger) logger.error(err) 129 | process.exit(1) 130 | } 131 | } 132 | } 133 | 134 | module.exports = closeWithGrace 135 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType, expectAssignable, expectError } from "tsd" 2 | import closeWithGrace from "." 3 | import { 4 | Options, 5 | CloseWithGraceCallback, 6 | CloseWithGraceAsyncCallback, 7 | Signals, 8 | } from "." 9 | 10 | type CallbackOptions = { 11 | manual?: boolean 12 | err?: Error 13 | signal?: Signals 14 | } 15 | 16 | async function asyncManualCallback (options: Pick) { } 17 | async function asyncErrorCallback (options: Pick) { } 18 | async function asyncSignalCallback (options: Pick) { } 19 | async function asyncAllCallback (options: CallbackOptions) { } 20 | 21 | function ManualCallback ( 22 | options: Pick, 23 | cb: (error?: Error) => void 24 | ) { 25 | cb() 26 | return 27 | } 28 | function ErrorCallback ( 29 | options: Pick, 30 | cb: (error?: Error) => void 31 | ) { 32 | cb() 33 | return 34 | } 35 | function SignalCallback ( 36 | options: Pick, 37 | cb: (error?: Error) => void 38 | ) { 39 | cb() 40 | return 41 | } 42 | function AllCallback (options: CallbackOptions, cb: (error?: Error) => void) { 43 | cb() 44 | return 45 | } 46 | function WrongCallback (options: CallbackOptions, cb: (error?: Error) => void) { 47 | cb() 48 | return Promise.resolve() 49 | } 50 | 51 | expectAssignable(asyncManualCallback) 52 | expectAssignable(asyncErrorCallback) 53 | expectAssignable(asyncSignalCallback) 54 | expectAssignable(asyncAllCallback) 55 | expectError(WrongCallback) 56 | expectAssignable(ManualCallback) 57 | expectAssignable(ErrorCallback) 58 | expectAssignable(SignalCallback) 59 | expectAssignable(AllCallback) 60 | expectAssignable(WrongCallback) 61 | 62 | expectAssignable("SIGINT") 63 | expectAssignable("SIGTERM") 64 | 65 | expectAssignable({ delay: 10 }) 66 | expectAssignable({ delay: null }) 67 | expectAssignable({ delay: false }) 68 | expectAssignable({ delay: undefined }) 69 | expectAssignable({ logger: console }) 70 | expectAssignable({ logger: null }) 71 | expectAssignable({ logger: false }) 72 | expectAssignable({ logger: undefined }) 73 | expectAssignable({ logger: console, delay: 10 }) 74 | expectAssignable({ logger: null, delay: null }) 75 | expectAssignable({ logger: false, delay: false }) 76 | expectAssignable({ logger: undefined, delay: undefined }) 77 | expectAssignable({ logger: { error: () => {} } }) 78 | 79 | expectAssignable<{ 80 | close: () => void 81 | uninstall: () => void 82 | }>(closeWithGrace({ delay: 100 }, asyncAllCallback)) 83 | expectAssignable<{ 84 | close: () => void 85 | uninstall: () => void 86 | }>(closeWithGrace({ delay: 100 }, AllCallback)) 87 | 88 | closeWithGrace({ delay: 100 }, async function ({ err }) { 89 | expectType(err) 90 | }) 91 | 92 | closeWithGrace(async function ({ err }) { 93 | expectType(err) 94 | }) 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "close-with-grace", 3 | "version": "2.2.0", 4 | "description": "Exit your process, gracefully (if possible)", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "devDependencies": { 8 | "@fastify/pre-commit": "^2.0.2", 9 | "standard": "^17.0.0", 10 | "tap-dot": "^2.0.0", 11 | "tape": "^5.1.1", 12 | "tsd": "^0.28.0" 13 | }, 14 | "scripts": { 15 | "test": "standard && npm run test-only && tsd", 16 | "test-only": "tape test/*.test.js | tap-dot" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/mcollina/close-with-grace.git" 21 | }, 22 | "keywords": [ 23 | "graceful", 24 | "shutdown", 25 | "close", 26 | "exit" 27 | ], 28 | "author": "Matteo Collina ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/mcollina/close-with-grace/issues" 32 | }, 33 | "homepage": "https://github.com/mcollina/close-with-grace#readme" 34 | } 35 | -------------------------------------------------------------------------------- /test/callbacks.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const closeWithGrace = require('..') 4 | 5 | closeWithGrace({ delay: 500 }, function ({ signal, err }, cb) { 6 | if (signal) { 7 | console.log(signal) 8 | } 9 | setImmediate(() => { 10 | if (err) { 11 | console.log(err.message) 12 | } 13 | cb() 14 | }) 15 | }) 16 | 17 | // to keep the process open 18 | setInterval(() => {}, 1000) 19 | console.error(process.pid) 20 | -------------------------------------------------------------------------------- /test/close.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { signalEvents } = require('../events/signal.events') 4 | const { errorEvents } = require('../events/error.events') 5 | const test = require('tape') 6 | const { fork } = require('node:child_process') 7 | const { join } = require('node:path') 8 | const { once } = require('node:events') 9 | const { promisify } = require('node:util') 10 | const sleep = promisify(setTimeout) 11 | 12 | async function all (stream) { 13 | stream.setEncoding('utf8') 14 | let data = '' 15 | for await (const chunk of stream) { 16 | data += chunk 17 | } 18 | return data 19 | } 20 | 21 | test('close abruptly after a timeout', async (t) => { 22 | const child = fork(join(__dirname, 'no-resolve.js'), { 23 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 24 | }) 25 | 26 | // one line to kickstart the test 27 | await once(child.stderr, 'readable') 28 | t.pass('readable emitted') 29 | 30 | const out = all(child.stdout) 31 | out.catch(() => {}) 32 | 33 | child.kill('SIGTERM') 34 | const now = Date.now() 35 | 36 | const [code, signal] = await once(child, 'close') 37 | t.is(code, 1) 38 | t.is(signal, null) 39 | t.is(Date.now() - now >= 500, true) 40 | }) 41 | 42 | test('when closed by timeout with default logger should log error', async (t) => { 43 | const child = fork(join(__dirname, 'no-resolve.js'), { 44 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 45 | }) 46 | 47 | // one line to kickstart the test 48 | await once(child.stderr, 'readable') 49 | t.pass('readable emitted') 50 | 51 | const err = all(child.stderr) 52 | 53 | child.kill('SIGTERM') 54 | await once(child, 'close') 55 | 56 | t.match(await err, /killed by timeout/) 57 | }) 58 | 59 | test('when closed by timeout with custom logger should log error', async (t) => { 60 | const child = fork(join(__dirname, 'no-resolve-custom-logger.js'), { 61 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 62 | }) 63 | 64 | // one line to kickstart the test 65 | await once(child.stderr, 'readable') 66 | t.pass('readable emitted') 67 | 68 | const err = all(child.stderr) 69 | 70 | child.kill('SIGTERM') 71 | await once(child, 'close') 72 | 73 | t.match(await err, /\[custom logger\] killed by timeout/) 74 | }) 75 | 76 | test('when closed by timeout without logger should NOT log error', async (t) => { 77 | const child = fork(join(__dirname, 'no-resolve-without-logger.js'), { 78 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 79 | }) 80 | 81 | // one line to kickstart the test 82 | await once(child.stderr, 'readable') 83 | t.pass('readable emitted') 84 | 85 | const err = all(child.stderr) 86 | 87 | child.kill('SIGTERM') 88 | await once(child, 'close') 89 | 90 | t.doesNotMatch(await err, /killed by timeout/) 91 | }) 92 | 93 | for (const signal of signalEvents) { 94 | test(`close gracefully (${signal}) async/await`, async (t) => { 95 | const child = fork(join(__dirname, 'simple.js'), { 96 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 97 | }) 98 | 99 | // one line to kickstart the test 100 | await once(child.stderr, 'readable') 101 | t.pass('readable emitted') 102 | 103 | const out = all(child.stdout) 104 | out.catch(() => {}) 105 | 106 | child.kill(signal) 107 | 108 | const [code, signalOut] = await once(child, 'close') 109 | t.is(code, 0) 110 | t.is(signalOut, null) 111 | t.is(await out, signal + '\n') 112 | }) 113 | 114 | test(`close gracefully (${signal}) callbacks`, async (t) => { 115 | const child = fork(join(__dirname, 'callbacks.js'), { 116 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 117 | }) 118 | 119 | // one line to kickstart the test 120 | await once(child.stderr, 'readable') 121 | t.pass('readable emitted') 122 | 123 | const out = all(child.stdout) 124 | out.catch(() => {}) 125 | 126 | child.kill(signal) 127 | 128 | const [code, signalOut] = await once(child, 'close') 129 | t.is(code, 0) 130 | t.is(signalOut, null) 131 | t.is(await out, signal + '\n') 132 | }) 133 | 134 | test(`default delay, close gracefully (${signal})`, async (t) => { 135 | const child = fork(join(__dirname, 'default-delay.js'), { 136 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 137 | }) 138 | 139 | // one line to kickstart the test 140 | await once(child.stderr, 'readable') 141 | t.pass('readable emitted') 142 | 143 | const out = all(child.stdout) 144 | out.catch(() => {}) 145 | 146 | child.kill(signal) 147 | 148 | const [code, signalOut] = await once(child, 'close') 149 | t.is(code, 0) 150 | t.is(signalOut, null) 151 | t.is(await out, signal + '\n') 152 | }) 153 | 154 | test(`a secong signal (${signal}) close abruptly`, async (t) => { 155 | const child = fork(join(__dirname, 'no-resolve.js'), { 156 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 157 | }) 158 | 159 | // one line to kickstart the test 160 | await once(child.stderr, 'readable') 161 | child.stderr.read() 162 | t.pass('readable emitted') 163 | 164 | const now = Date.now() 165 | 166 | child.kill(signal) 167 | 168 | await once(child.stdout, 'readable') 169 | 170 | const out = all(child.stdout) 171 | out.catch(() => {}) 172 | 173 | const err = all(child.stderr) 174 | err.catch(() => {}) 175 | 176 | child.kill(signal) 177 | 178 | const [code, signalOut] = await once(child, 'close') 179 | t.is(code, 1) 180 | t.is(signalOut, null) 181 | t.is(await out, 'fn called\n') 182 | t.is(await err, `second ${signal}, exiting\n`) 183 | t.is(Date.now() - now < 500, true) 184 | }) 185 | 186 | test(`a secong signal (${signal}) calls custom logger`, async (t) => { 187 | const child = fork(join(__dirname, 'no-resolve-custom-logger.js'), { 188 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 189 | }) 190 | 191 | // one line to kickstart the test 192 | await once(child.stderr, 'readable') 193 | child.stderr.read() 194 | t.pass('readable emitted') 195 | 196 | child.kill(signal) 197 | 198 | await once(child.stdout, 'readable') 199 | 200 | const err = all(child.stderr) 201 | err.catch(() => {}) 202 | 203 | child.kill(signal) 204 | 205 | t.is(await err, `[custom logger] second ${signal}, exiting\n`) 206 | }) 207 | 208 | test(`a secong signal (${signal}) calls without logger`, async (t) => { 209 | const child = fork(join(__dirname, 'no-resolve-without-logger.js'), { 210 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 211 | }) 212 | 213 | // one line to kickstart the test 214 | await once(child.stderr, 'readable') 215 | child.stderr.read() 216 | t.pass('readable emitted') 217 | 218 | child.kill(signal) 219 | 220 | await once(child.stdout, 'readable') 221 | 222 | const out = all(child.stdout) 223 | out.catch(() => {}) 224 | 225 | const err = all(child.stderr) 226 | err.catch(() => {}) 227 | 228 | child.kill(signal) 229 | 230 | const [code, signalOut] = await once(child, 'close') 231 | t.is(code, 1) 232 | t.is(signalOut, null) 233 | t.is(await out, 'fn called\n') 234 | t.is(await err, '') 235 | }) 236 | 237 | test(`a secong signal (${signal}) calls without delay`, async (t) => { 238 | const child = fork(join(__dirname, 'no-resolve-without-delay.js'), { 239 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 240 | }) 241 | 242 | // one line to kickstart the test 243 | await once(child.stderr, 'readable') 244 | child.stderr.read() 245 | t.pass('readable emitted') 246 | 247 | const now = Date.now() 248 | 249 | child.kill(signal) 250 | 251 | await once(child.stdout, 'readable') 252 | 253 | const out = all(child.stdout) 254 | out.catch(() => {}) 255 | 256 | const err = all(child.stderr) 257 | err.catch(() => {}) 258 | 259 | await sleep(550) 260 | child.kill(signal) 261 | 262 | const [code, signalOut] = await once(child, 'close') 263 | t.is(code, 1) 264 | t.is(signalOut, null) 265 | t.is(await out, 'fn called\n') 266 | t.is(await err, `second ${signal}, exiting\n`) 267 | t.is(Date.now() - now > 550 && Date.now() - now < 1000, true) 268 | }) 269 | } 270 | 271 | for (const event of errorEvents) { 272 | test(`close gracefully (${event})`, async (t) => { 273 | const child = fork(join(__dirname, event + '.js'), { 274 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 275 | }) 276 | 277 | const out = all(child.stdout) 278 | 279 | const [code, signalOut] = await once(child, 'close') 280 | t.is(code, 1) 281 | t.is(signalOut, null) 282 | t.is(await out, 'kaboom\n') 283 | }) 284 | } 285 | 286 | test('self close', async (t) => { 287 | const child = fork(join(__dirname, 'self-close.js'), { 288 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 289 | }) 290 | 291 | const out = all(child.stdout) 292 | out.catch(() => {}) 293 | 294 | const [code] = await once(child, 'close') 295 | t.is(code, 0) 296 | t.is(await out, 'close called\n') 297 | }) 298 | 299 | test('normal close', async (t) => { 300 | const child = fork(join(__dirname, 'normal-close.js'), { 301 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 302 | }) 303 | 304 | const out = all(child.stdout) 305 | out.catch(() => {}) 306 | 307 | const [code] = await once(child, 'close') 308 | t.is(code, 0) 309 | t.is(await out, 'close called\n') 310 | }) 311 | 312 | test('uninstall', async (t) => { 313 | const child = fork(join(__dirname, 'uninstall.js'), { 314 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 315 | }) 316 | 317 | // one line to kickstart the test 318 | await once(child.stderr, 'readable') 319 | t.pass('readable emitted') 320 | 321 | const out = all(child.stdout) 322 | out.catch(() => {}) 323 | 324 | child.kill('SIGTERM') 325 | 326 | const [code, signal] = await once(child, 'close') 327 | t.is(code, null) 328 | t.is(signal, 'SIGTERM') 329 | t.is(await out, '') 330 | }) 331 | 332 | test('closing state', async (t) => { 333 | const child = fork(join(__dirname, 'closing-state.js'), { 334 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 335 | }) 336 | 337 | const [code] = await once(child, 'close') 338 | t.is(code, 0) 339 | }) 340 | -------------------------------------------------------------------------------- /test/closing-state.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const closeWithGrace = require('..') 4 | const assert = require('node:assert') 5 | 6 | assert.strictEqual(closeWithGrace.closing, false) 7 | const { close } = closeWithGrace(async function ({ manual }) { 8 | assert.strictEqual(closeWithGrace.closing, true) 9 | }) 10 | 11 | setTimeout(close, 500) 12 | -------------------------------------------------------------------------------- /test/default-delay.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { promisify } = require('node:util') 4 | const closeWithGrace = require('..') 5 | 6 | const immediate = promisify(setImmediate) 7 | 8 | closeWithGrace(async function ({ signal, err }) { 9 | if (signal) { 10 | console.log(signal) 11 | } 12 | await immediate() 13 | if (err) { 14 | console.log(err.message) 15 | } 16 | }) 17 | 18 | // to keep the process open 19 | setInterval(() => {}, 1000) 20 | console.error(process.pid) 21 | -------------------------------------------------------------------------------- /test/no-resolve-custom-logger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const closeWithGrace = require('..') 4 | 5 | const customLogger = { 6 | error (message) { 7 | console.error(`[custom logger] ${message}`) 8 | } 9 | } 10 | 11 | closeWithGrace({ delay: 500, logger: customLogger }, function ({ signal, err }) { 12 | console.log('fn called') 13 | // this promise never resolves, so the delay should kick in 14 | return new Promise(() => {}) 15 | }) 16 | 17 | // to keep the process open 18 | setInterval(() => {}, 1000) 19 | console.error(process.pid) 20 | -------------------------------------------------------------------------------- /test/no-resolve-without-delay.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const closeWithGrace = require('..') 4 | 5 | closeWithGrace({ delay: undefined }, function ({ signal, err }) { 6 | console.log('fn called') 7 | // this promise never resolves 8 | // delay has falsy value 9 | // process can't be closed 10 | return new Promise(() => {}) 11 | }) 12 | 13 | // to keep the process open 14 | setInterval(() => {}, 1000) 15 | console.error(process.pid) 16 | -------------------------------------------------------------------------------- /test/no-resolve-without-logger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const closeWithGrace = require('..') 4 | 5 | closeWithGrace({ delay: 500, logger: undefined }, function ({ signal, err }) { 6 | console.log('fn called') 7 | // this promise never resolves, so the delay should kick in 8 | return new Promise(() => {}) 9 | }) 10 | 11 | // to keep the process open 12 | setInterval(() => {}, 1000) 13 | console.error(process.pid) 14 | -------------------------------------------------------------------------------- /test/no-resolve.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const closeWithGrace = require('..') 4 | 5 | closeWithGrace({ delay: 500 }, function ({ signal, err }) { 6 | console.log('fn called') 7 | // this promise never resolves, so the delay should kick in 8 | return new Promise(() => {}) 9 | }) 10 | 11 | // to keep the process open 12 | setInterval(() => {}, 1000) 13 | console.error(process.pid) 14 | -------------------------------------------------------------------------------- /test/normal-close.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const closeWithGrace = require('..') 4 | 5 | closeWithGrace(async () => console.log('close called')) 6 | setTimeout(() => {}, 500) 7 | -------------------------------------------------------------------------------- /test/self-close.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const closeWithGrace = require('..') 4 | const assert = require('node:assert') 5 | 6 | const { close } = closeWithGrace(async function ({ manual }) { 7 | assert.strictEqual(manual, true) 8 | console.log('close called') 9 | }) 10 | 11 | setTimeout(close, 500) 12 | -------------------------------------------------------------------------------- /test/simple.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { promisify } = require('node:util') 4 | const closeWithGrace = require('..') 5 | 6 | const immediate = promisify(setImmediate) 7 | 8 | closeWithGrace({ delay: 500 }, async function ({ signal, err }) { 9 | if (signal) { 10 | console.log(signal) 11 | } 12 | await immediate() 13 | if (err) { 14 | console.log(err.message) 15 | } 16 | }) 17 | 18 | // to keep the process open 19 | setInterval(() => {}, 1000) 20 | console.error(process.pid) 21 | -------------------------------------------------------------------------------- /test/uncaughtException.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const closeWithGrace = require('..') 4 | 5 | closeWithGrace(async function ({ signal, err }) { 6 | console.log(err.message) 7 | }) 8 | 9 | setTimeout(() => { 10 | throw new Error('kaboom') 11 | }, 500) 12 | console.error(process.pid) 13 | -------------------------------------------------------------------------------- /test/unhandledRejection.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const closeWithGrace = require('..') 4 | 5 | closeWithGrace(async function ({ signal, err }) { 6 | console.log(err.message) 7 | }) 8 | 9 | setTimeout(async () => { 10 | throw new Error('kaboom') 11 | }, 500) 12 | console.error(process.pid) 13 | -------------------------------------------------------------------------------- /test/uninstall.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const closeWithGrace = require('..') 4 | 5 | const { uninstall } = closeWithGrace(async function ({ manual }) { 6 | console.log('not called') 7 | }) 8 | 9 | setInterval(() => {}, 500) 10 | uninstall() 11 | console.error(process.pid) 12 | --------------------------------------------------------------------------------