pax_global_header00006660000000000000000000000064143247321310014512gustar00rootroot0000000000000052 comment=f58f6206d059aa7dc84cefb5a01fda569fb758d8 exit-hook-3.1.2/000077500000000000000000000000001432473213100134245ustar00rootroot00000000000000exit-hook-3.1.2/.editorconfig000066400000000000000000000002571432473213100161050ustar00rootroot00000000000000root = true [*] indent_style = tab end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.yml] indent_style = space indent_size = 2 exit-hook-3.1.2/.gitattributes000066400000000000000000000000231432473213100163120ustar00rootroot00000000000000* text=auto eol=lf exit-hook-3.1.2/.github/000077500000000000000000000000001432473213100147645ustar00rootroot00000000000000exit-hook-3.1.2/.github/funding.yml000066400000000000000000000001631432473213100171410ustar00rootroot00000000000000github: sindresorhus open_collective: sindresorhus tidelift: npm/exit-hook custom: https://sindresorhus.com/donate exit-hook-3.1.2/.github/security.md000066400000000000000000000002631432473213100171560ustar00rootroot00000000000000# Security Policy To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. exit-hook-3.1.2/.github/workflows/000077500000000000000000000000001432473213100170215ustar00rootroot00000000000000exit-hook-3.1.2/.github/workflows/main.yml000066400000000000000000000006451432473213100204750ustar00rootroot00000000000000name: CI on: - push - pull_request jobs: test: name: Node.js ${{ matrix.node-version }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: node-version: - 18 - 16 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm install - run: npm test exit-hook-3.1.2/.gitignore000066400000000000000000000000271432473213100154130ustar00rootroot00000000000000node_modules yarn.lock exit-hook-3.1.2/.npmrc000066400000000000000000000000231432473213100145370ustar00rootroot00000000000000package-lock=false exit-hook-3.1.2/fixtures/000077500000000000000000000000001432473213100152755ustar00rootroot00000000000000exit-hook-3.1.2/fixtures/async.js000066400000000000000000000010741432473213100167520ustar00rootroot00000000000000import process from 'node:process'; import exitHook, {asyncExitHook, gracefulExit} from '../index.js'; exitHook(() => { console.log('foo'); }); exitHook(() => { console.log('bar'); }); const unsubscribe = exitHook(() => { console.log('baz'); }); unsubscribe(); asyncExitHook( async () => { await new Promise(resolve => { setTimeout(() => { resolve(); }, 100); }); console.log('quux'); }, { minimumWait: 200, }, ); if (process.env.EXIT_HOOK_SYNC === '1') { process.exit(0); // eslint-disable-line unicorn/no-process-exit } gracefulExit(); exit-hook-3.1.2/fixtures/empty.js000066400000000000000000000001621432473213100167700ustar00rootroot00000000000000import exitHook from '../index.js'; exitHook(() => { // https://github.com/sindresorhus/exit-hook/issues/23 }); exit-hook-3.1.2/fixtures/sync.js000066400000000000000000000004561432473213100166140ustar00rootroot00000000000000import process from 'node:process'; import exitHook from '../index.js'; exitHook(() => { console.log('foo'); }); exitHook(() => { console.log('bar'); }); const unsubscribe = exitHook(() => { console.log('baz'); }); unsubscribe(); process.exit(0); // eslint-disable-line unicorn/no-process-exit exit-hook-3.1.2/index.d.ts000066400000000000000000000042461432473213100153330ustar00rootroot00000000000000/** Run some code when the process exits. The `process.on('exit')` event doesn't catch all the ways a process can exit. This is useful for cleaning synchronously before exiting. @param onExit - The callback function to execute when the process exits. @returns A function that removes the hook when called. @example ``` import exitHook from 'exit-hook'; exitHook(() => { console.log('Exiting'); }); // You can add multiple hooks, even across files exitHook(() => { console.log('Exiting 2'); }); throw new Error('🦄'); //=> 'Exiting' //=> 'Exiting 2' // Removing an exit hook: const unsubscribe = exitHook(() => {}); unsubscribe(); ``` */ export default function exitHook(onExit: () => void): () => void; /** Run code asynchronously when the process exits. @see https://github.com/sindresorhus/exit-hook/blob/main/readme.md#asynchronous-exit-notes @param 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'); }, { minimumWait: 500 }); throw new Error('🦄'); //=> 'Exiting' // Removing an exit hook: const unsubscribe = asyncExitHook(() => {}, {}); unsubscribe(); ``` */ export function asyncExitHook(onExit: () => (void | Promise), 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'); }, 500); // Instead of `process.exit()` gracefulExit(); ``` */ export function gracefulExit(signal?: number): void; export interface Options { /** The amount of time in milliseconds that the `onExit` function is expected to take. */ minimumWait: number; } exit-hook-3.1.2/index.js000066400000000000000000000064351432473213100151010ustar00rootroot00000000000000import process from 'node:process'; const asyncCallbacks = new Set(); const callbacks = new Set(); let isCalled = false; let isRegistered = false; async function exit(shouldManuallyExit, isSynchronous, signal) { if (isCalled) { return; } isCalled = true; 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 done = (force = false) => { if (force === true || shouldManuallyExit === true) { process.exit(128 + signal); // eslint-disable-line unicorn/no-process-exit } }; for (const callback of callbacks) { callback(); } 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())); } // Force exit if we exceeded our wait value const asyncTimer = setTimeout(() => { done(true); }, forceAfter); await Promise.all(promises); clearTimeout(asyncTimer); done(); } function addHook(options) { const {onExit, minimumWait, isSynchronous} = options; const asyncCallbackConfig = [onExit, minimumWait]; if (isSynchronous) { callbacks.add(onExit); } else { asyncCallbacks.add(asyncCallbackConfig); } if (!isRegistered) { isRegistered = true; // 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)); // 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, true, -128); } }); } return () => { 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?.minimumWait !== 'number' || options.minimumWait <= 0) { throw new TypeError('minimumWait must be set to a positive numeric value'); } return addHook({ onExit, minimumWait: options.minimumWait, isSynchronous: false, }); } export function gracefulExit(signal = 0) { exit(true, false, -128 + signal); } exit-hook-3.1.2/index.test-d.ts000066400000000000000000000006711432473213100163060ustar00rootroot00000000000000import {expectType} from 'tsd'; import exitHook, {asyncExitHook} from './index.js'; const unsubscribe = exitHook(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function const asyncUnsubscribe = asyncExitHook(async () => {}, // eslint-disable-line @typescript-eslint/no-empty-function {minimumWait: 300}, ); expectType<() => void>(unsubscribe); unsubscribe(); expectType<() => void>(asyncUnsubscribe); asyncUnsubscribe(); exit-hook-3.1.2/license000066400000000000000000000021351432473213100147720ustar00rootroot00000000000000MIT License Copyright (c) Sindre Sorhus (https://sindresorhus.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. exit-hook-3.1.2/package.json000066400000000000000000000015261432473213100157160ustar00rootroot00000000000000{ "name": "exit-hook", "version": "3.1.2", "description": "Run some code when the process exits", "license": "MIT", "repository": "sindresorhus/exit-hook", "funding": "https://github.com/sponsors/sindresorhus", "author": { "name": "Sindre Sorhus", "email": "sindresorhus@gmail.com", "url": "https://sindresorhus.com" }, "type": "module", "exports": "./index.js", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "scripts": { "test": "xo && ava && tsd" }, "files": [ "index.js", "index.d.ts" ], "keywords": [ "exit", "quit", "process", "hook", "graceful", "handler", "shutdown", "sigterm", "sigint", "terminate", "kill", "stop", "event", "signal", "async", "asynchronous" ], "devDependencies": { "ava": "^3.15.0", "execa": "^5.1.1", "tsd": "^0.17.0", "xo": "^0.44.0" } } exit-hook-3.1.2/readme.md000066400000000000000000000064701432473213100152120ustar00rootroot00000000000000# exit-hook > Run some code when the process exits The `process.on('exit')` event doesn't catch all the ways a process can exit. This package is useful for cleaning up before exiting. ## Install ```sh npm install exit-hook ``` ## Usage ```js import exitHook from 'exit-hook'; exitHook(() => { console.log('Exiting'); }); // You can add multiple hooks, even across files exitHook(() => { console.log('Exiting 2'); }); throw new Error('🦄'); //=> 'Exiting' //=> 'Exiting 2' ``` Removing an exit hook: ```js import exitHook from 'exit-hook'; const unsubscribe = exitHook(() => {}); unsubscribe(); ``` ## API ### exitHook(onExit) Register a function to run during `process.exit`. Returns a function that removes the hook when called. #### onExit Type: `function(): void` The callback function to execute when the process exits. ### asyncExitHook(onExit, minimumWait) 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. ```js import {asyncExitHook} from 'exit-hook'; asyncExitHook(async () => { console.log('Exiting'); }, 300); throw new Error('🦄'); //=> 'Exiting' ``` Removing an asynchronous exit hook: ```js import {asyncExitHook} from 'exit-hook'; const unsubscribe = asyncExitHook(async () => { console.log('Exiting'); }, { minimumWait: 300 }); unsubscribe(); ``` #### onExit Type: `function(): void | Promise` The callback function to execute when the process exits via `gracefulExit`, and will be wrapped in `Promise.resolve`. #### options ##### minimumWait Type: `number` The amount of time in milliseconds that the `onExit` function is expected to take. ### 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 `minimumWait` time, but also be written to assume they may not complete their tasks before termination. exit-hook-3.1.2/test.js000066400000000000000000000040471432473213100147460ustar00rootroot00000000000000import process from 'node:process'; import test from 'ava'; import execa from 'execa'; import exitHook, {asyncExitHook} from './index.js'; test('main', async t => { const {stdout, stderr, exitCode} = await execa(process.execPath, ['./fixtures/sync.js']); t.is(stdout, 'foo\nbar'); t.is(stderr, ''); t.is(exitCode, 0); }); test('main-empty', async t => { const {stderr, exitCode} = await execa(process.execPath, ['./fixtures/empty.js']); t.is(stderr, ''); t.is(exitCode, 0); }); test('main-async', async t => { const {stdout, stderr, exitCode} = await execa(process.execPath, ['./fixtures/async.js']); t.is(stdout, 'foo\nbar\nquux'); t.is(stderr, ''); t.is(exitCode, 0); }); test('main-async-notice', async t => { const {stdout, stderr, exitCode} = await execa(process.execPath, ['./fixtures/async.js'], { env: { EXIT_HOOK_SYNC: '1', }, }); t.is(stdout, 'foo\nbar'); t.regex(stderr, /SYNCHRONOUS TERMINATION NOTICE/); t.is(exitCode, 0); }); test('listener count', t => { t.is(process.listenerCount('exit'), 0); const unsubscribe1 = exitHook(() => {}); const unsubscribe2 = exitHook(() => {}); t.is(process.listenerCount('exit'), 1); // Remove all listeners unsubscribe1(); unsubscribe2(); t.is(process.listenerCount('exit'), 1); // Re-add listener const unsubscribe3 = exitHook(() => {}); t.is(process.listenerCount('exit'), 1); // Remove again unsubscribe3(); t.is(process.listenerCount('exit'), 1); // Add async style listener const unsubscribe4 = asyncExitHook( async () => {}, { minimumWait: 100, }, ); t.is(process.listenerCount('exit'), 1); // Remove again unsubscribe4(); t.is(process.listenerCount('exit'), 1); }); test('type enforcing', t => { // Non-function passed to `exitHook`. t.throws(() => { exitHook(null); }, {instanceOf: TypeError}); // Non-function passed to `asyncExitHook`. t.throws(() => { asyncExitHook(null, { minimumWait: 100, }); }, { instanceOf: TypeError, }); // Non-numeric passed to `minimumWait` option. t.throws(() => { asyncExitHook(async () => true, {}); }); });