Comparing version 2.2.1 to 4.0.0
/** | ||
@callback onExit | ||
@param {number} signal - The exit code. | ||
*/ | ||
/** | ||
Run some code when the process exits. | ||
@@ -6,5 +11,5 @@ | ||
This package is useful for cleaning up before exiting. | ||
This is useful for cleaning synchronously before exiting. | ||
@param callback - The callback to execute when the process exits. | ||
@param {onExit} onExit - The callback function to execute when the process exits. | ||
@returns A function that removes the hook when called. | ||
@@ -14,6 +19,6 @@ | ||
``` | ||
import exitHook = require('exit-hook'); | ||
import exitHook from 'exit-hook'; | ||
exitHook(() => { | ||
console.log('Exiting'); | ||
exitHook(signal => { | ||
console.log(`Exiting with signal: ${signal}`); | ||
}); | ||
@@ -37,4 +42,62 @@ | ||
*/ | ||
declare function exitHook(callback: () => void): () => void; | ||
export default function exitHook(onExit: (signal: number) => void): () => void; | ||
export = exitHook; | ||
/** | ||
Run code asynchronously when the process exits. | ||
@see https://github.com/sindresorhus/exit-hook/blob/main/readme.md#asynchronous-exit-notes | ||
@param {onExit} onExit - The callback function to execute when the process exits via `gracefulExit`, and will be wrapped in `Promise.resolve`. | ||
@returns A function that removes the hook when called. | ||
@example | ||
``` | ||
import {asyncExitHook} from 'exit-hook'; | ||
asyncExitHook(() => { | ||
console.log('Exiting'); | ||
}, { | ||
wait: 500 | ||
}); | ||
throw new Error('🦄'); | ||
//=> 'Exiting' | ||
// Removing an exit hook: | ||
const unsubscribe = asyncExitHook(() => {}, {wait: 500}); | ||
unsubscribe(); | ||
``` | ||
*/ | ||
export function asyncExitHook(onExit: (signal: number) => (void | Promise<void>), options: Options): () => void; | ||
/** | ||
Exit the process and make a best-effort to complete all asynchronous hooks. | ||
If you are using `asyncExitHook`, consider using `gracefulExit()` instead of `process.exit()` to ensure all asynchronous tasks are given an opportunity to run. | ||
@param signal - The exit code to use. Same as the argument to `process.exit()`. | ||
@see https://github.com/sindresorhus/exit-hook/blob/main/readme.md#asynchronous-exit-notes | ||
@example | ||
``` | ||
import {asyncExitHook, gracefulExit} from 'exit-hook'; | ||
asyncExitHook(() => { | ||
console.log('Exiting'); | ||
}, { | ||
wait: 500 | ||
}); | ||
// Instead of `process.exit()` | ||
gracefulExit(); | ||
``` | ||
*/ | ||
export function gracefulExit(signal?: number): void; | ||
export type Options = { | ||
/** | ||
The amount of time in milliseconds that the `onExit` function is expected to take. When multiple async handlers are registered, the longest `wait` time will be used. | ||
*/ | ||
readonly wait: number; | ||
}; |
118
index.js
@@ -1,8 +0,10 @@ | ||
'use strict'; | ||
import process from 'node:process'; | ||
const asyncCallbacks = new Set(); | ||
const callbacks = new Set(); | ||
let isCalled = false; | ||
let isRegistered = false; | ||
function exit(exit, signal) { | ||
async function exit(shouldManuallyExit, isSynchronous, signal) { | ||
if (isCalled) { | ||
@@ -14,27 +16,76 @@ return; | ||
if (asyncCallbacks.size > 0 && isSynchronous) { | ||
console.error([ | ||
'SYNCHRONOUS TERMINATION NOTICE:', | ||
'When explicitly exiting the process via process.exit or via a parent process,', | ||
'asynchronous tasks in your exitHooks will not run. Either remove these tasks,', | ||
'use gracefulExit() instead of process.exit(), or ensure your parent process', | ||
'sends a SIGINT to the process running this code.', | ||
].join(' ')); | ||
} | ||
const exitCode = 128 + signal; | ||
const done = (force = false) => { | ||
if (force === true || shouldManuallyExit === true) { | ||
process.exit(exitCode); // eslint-disable-line unicorn/no-process-exit | ||
} | ||
}; | ||
for (const callback of callbacks) { | ||
callback(); | ||
callback(exitCode); | ||
} | ||
if (exit === true) { | ||
process.exit(128 + signal); // eslint-disable-line unicorn/no-process-exit | ||
if (isSynchronous) { | ||
done(); | ||
return; | ||
} | ||
const promises = []; | ||
let forceAfter = 0; | ||
for (const [callback, wait] of asyncCallbacks) { | ||
forceAfter = Math.max(forceAfter, wait); | ||
promises.push(Promise.resolve(callback(exitCode))); | ||
} | ||
// Force exit if we exceeded our wait value | ||
const asyncTimer = setTimeout(() => { | ||
done(true); | ||
}, forceAfter); | ||
await Promise.all(promises); | ||
clearTimeout(asyncTimer); | ||
done(); | ||
} | ||
module.exports = callback => { | ||
callbacks.add(callback); | ||
function addHook(options) { | ||
const {onExit, wait, isSynchronous} = options; | ||
const asyncCallbackConfig = [onExit, wait]; | ||
if (isSynchronous) { | ||
callbacks.add(onExit); | ||
} else { | ||
asyncCallbacks.add(asyncCallbackConfig); | ||
} | ||
if (!isRegistered) { | ||
isRegistered = true; | ||
process.once('exit', exit); | ||
process.once('SIGINT', exit.bind(null, true, 2)); | ||
process.once('SIGTERM', exit.bind(null, true, 15)); | ||
// Exit cases that support asynchronous handling | ||
process.once('beforeExit', exit.bind(undefined, true, false, -128)); | ||
process.once('SIGINT', exit.bind(undefined, true, false, 2)); | ||
process.once('SIGTERM', exit.bind(undefined, true, false, 15)); | ||
// PM2 Cluster shutdown message. Caught to support async handlers with pm2, needed because | ||
// explicitly calling process.exit() doesn't trigger the beforeExit event, and the exit | ||
// event cannot support async handlers, since the event loop is never called after it. | ||
// Explicit exit events. Calling will force an immediate exit and run all | ||
// synchronous hooks. Explicit exits must not extend the node process | ||
// artificially. Will log errors if asynchronous calls exist. | ||
process.once('exit', exit.bind(undefined, false, true, 0)); | ||
// PM2 Cluster shutdown message. Caught to support async handlers with pm2, | ||
// needed because explicitly calling process.exit() doesn't trigger the | ||
// beforeExit event, and the exit event cannot support async handlers, | ||
// since the event loop is never called after it. | ||
process.on('message', message => { | ||
if (message === 'shutdown') { | ||
exit(true, -128); | ||
exit(true, true, -128); | ||
} | ||
@@ -45,4 +96,39 @@ }); | ||
return () => { | ||
callbacks.delete(callback); | ||
if (isSynchronous) { | ||
callbacks.delete(onExit); | ||
} else { | ||
asyncCallbacks.delete(asyncCallbackConfig); | ||
} | ||
}; | ||
}; | ||
} | ||
export default function exitHook(onExit) { | ||
if (typeof onExit !== 'function') { | ||
throw new TypeError('onExit must be a function'); | ||
} | ||
return addHook({ | ||
onExit, | ||
isSynchronous: true, | ||
}); | ||
} | ||
export function asyncExitHook(onExit, options = {}) { | ||
if (typeof onExit !== 'function') { | ||
throw new TypeError('onExit must be a function'); | ||
} | ||
if (!(typeof options.wait === 'number' && options.wait > 0)) { | ||
throw new TypeError('wait must be set to a positive numeric value'); | ||
} | ||
return addHook({ | ||
onExit, | ||
wait: options.wait, | ||
isSynchronous: false, | ||
}); | ||
} | ||
export function gracefulExit(signal = 0) { | ||
exit(true, false, -128 + signal); | ||
} |
{ | ||
"name": "exit-hook", | ||
"version": "2.2.1", | ||
"version": "4.0.0", | ||
"description": "Run some code when the process exits", | ||
@@ -13,4 +13,9 @@ "license": "MIT", | ||
}, | ||
"type": "module", | ||
"exports": { | ||
"types": "./index.d.ts", | ||
"default": "./index.js" | ||
}, | ||
"engines": { | ||
"node": ">=6" | ||
"node": ">=18" | ||
}, | ||
@@ -38,10 +43,12 @@ "scripts": { | ||
"event", | ||
"signal" | ||
"signal", | ||
"async", | ||
"asynchronous" | ||
], | ||
"devDependencies": { | ||
"ava": "^1.4.1", | ||
"execa": "^1.0.0", | ||
"tsd": "^0.7.2", | ||
"xo": "^0.24.0" | ||
"ava": "^5.3.1", | ||
"execa": "^8.0.1", | ||
"tsd": "^0.28.1", | ||
"xo": "^0.56.0" | ||
} | ||
} |
111
readme.md
@@ -11,5 +11,5 @@ # exit-hook | ||
```sh | ||
npm install exit-hook | ||
``` | ||
$ npm install exit-hook | ||
``` | ||
@@ -19,6 +19,6 @@ ## Usage | ||
```js | ||
const exitHook = require('exit-hook'); | ||
import exitHook from 'exit-hook'; | ||
exitHook(() => { | ||
console.log('Exiting'); | ||
exitHook(signal => { | ||
console.log(`Exiting with signal: ${signal}`); | ||
}); | ||
@@ -40,3 +40,3 @@ | ||
```js | ||
const exitHook = require('exit-hook'); | ||
import exitHook from 'exit-hook'; | ||
@@ -50,22 +50,93 @@ const unsubscribe = exitHook(() => {}); | ||
### exitHook(callback) | ||
### exitHook(onExit) | ||
Register a function to run during `process.exit`. | ||
Returns a function that removes the hook when called. | ||
#### callback | ||
#### onExit | ||
Type: `Function` | ||
Type: `(signal: number) => void` | ||
The callback to execute when the process exits. | ||
The callback function to execute when the process exits. | ||
--- | ||
### asyncExitHook(onExit, options) | ||
<div align="center"> | ||
<b> | ||
<a href="https://tidelift.com/subscription/pkg/npm-exit-hook?utm_source=npm-exit-hook&utm_medium=referral&utm_campaign=readme">Get professional support for this package with a Tidelift subscription</a> | ||
</b> | ||
<br> | ||
<sub> | ||
Tidelift helps make open source sustainable for maintainers while giving companies<br>assurances about security, maintenance, and licensing for their dependencies. | ||
</sub> | ||
</div> | ||
Register a function to run during `gracefulExit`. | ||
Returns a function that removes the hook when called. | ||
Please see [Async Notes](#asynchronous-exit-notes) for considerations when using the asynchronous API. | ||
#### onExit | ||
Type: `(signal: number) => (void | Promise<void>)` | ||
The callback function to execute when the process exits via `gracefulExit`, and will be wrapped in `Promise.resolve`. | ||
#### options | ||
Type: `object` | ||
##### wait | ||
Type: `number` | ||
The amount of time in milliseconds that the `onExit` function is expected to take. When multiple async handlers are registered, the longest `wait` time will be used. | ||
```js | ||
import {asyncExitHook} from 'exit-hook'; | ||
asyncExitHook(async () => { | ||
console.log('Exiting'); | ||
}, { | ||
wait: 300 | ||
}); | ||
throw new Error('🦄'); | ||
//=> 'Exiting' | ||
``` | ||
Removing an asynchronous exit hook: | ||
```js | ||
import {asyncExitHook} from 'exit-hook'; | ||
const unsubscribe = asyncExitHook(async () => { | ||
console.log('Exiting'); | ||
}, { | ||
wait: 300 | ||
}); | ||
unsubscribe(); | ||
``` | ||
### gracefulExit(signal?: number): void | ||
Exit the process and make a best-effort to complete all asynchronous hooks. | ||
If you are using `asyncExitHook`, consider using `gracefulExit()` instead of `process.exit()` to ensure all asynchronous tasks are given an opportunity to run. | ||
```js | ||
import {gracefulExit} from 'exit-hook'; | ||
gracefulExit(); | ||
``` | ||
#### signal | ||
Type: `number`\ | ||
Default: `0` | ||
The exit code to use. Same as the argument to `process.exit()`. | ||
## Asynchronous Exit Notes | ||
**tl;dr** If you have 100% control over how your process terminates, then you can swap `exitHook` and `process.exit` for `asyncExitHook` and `gracefulExit` respectively. Otherwise, keep reading to understand important tradeoffs if you're using `asyncExitHook`. | ||
Node.js does not offer an asynchronous shutdown API by default [#1](https://github.com/nodejs/node/discussions/29480#discussioncomment-99213) [#2](https://github.com/nodejs/node/discussions/29480#discussioncomment-99217), so `asyncExitHook` and `gracefulExit` will make a "best effort" attempt to shut down the process and run your asynchronous tasks. | ||
If you have asynchronous hooks registered and your Node.js process is terminated in a synchronous manner, a `SYNCHRONOUS TERMINATION NOTICE` error will be logged to the console. To avoid this, ensure you're only exiting via `gracefulExit` or that an upstream process manager is sending a `SIGINT` or `SIGTERM` signal to Node.js. | ||
Asynchronous hooks should make a "best effort" to perform their tasks within the `wait` time, but also be written to assume they may not complete their tasks before termination. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
11308
181
139
Yes