-
-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Sindre Sorhus <[email protected]>
- Loading branch information
1 parent
234efba
commit 8e4879a
Showing
6 changed files
with
307 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
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, | ||
}, | ||
); | ||
|
||
gracefulExit(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,46 +1,132 @@ | ||
import process from 'node:process'; | ||
|
||
const asyncCallbacks = new Set(); | ||
const callbacks = new Set(); | ||
|
||
let isCalled = false; | ||
let isRegistered = false; | ||
|
||
function exit(shouldManuallyExit, signal) { | ||
async function exit(shouldManuallyExit, isSynchronous, signal) { | ||
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(' ')); | ||
} | ||
|
||
if (isCalled) { | ||
return; | ||
} | ||
|
||
isCalled = true; | ||
|
||
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 (shouldManuallyExit === 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())); | ||
} | ||
|
||
// Force exit if we exceeded our wait value | ||
const asyncTimer = setTimeout(() => { | ||
done(true); | ||
}, forceAfter); | ||
|
||
await Promise.all(promises); | ||
clearTimeout(asyncTimer); | ||
done(); | ||
} | ||
|
||
export default function exitHook(onExit) { | ||
callbacks.add(onExit); | ||
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; | ||
|
||
process.once('exit', exit); | ||
process.once('SIGINT', exit.bind(undefined, true, 2)); | ||
process.once('SIGTERM', exit.bind(undefined, true, 15)); | ||
// Exit cases that support asynchronous handling | ||
process.once('beforeExit', exit.bind(undefined, true, false, 0)); | ||
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. | ||
// 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); | ||
} | ||
}); | ||
} | ||
|
||
return () => { | ||
callbacks.delete(onExit); | ||
if (isSynchronous) { | ||
callbacks.delete(onExit); | ||
} else { | ||
asyncCallbacks.delete(asyncCallbackConfig); | ||
} | ||
}; | ||
} | ||
|
||
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); | ||
} | ||
|
||
export default exitHook; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,14 @@ | ||
import {expectType} from 'tsd'; | ||
import exitHook from './index.js'; | ||
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.