Skip to content

Commit 586a93f

Browse files
author
John Doe
committed
fix: add process exit handler
1 parent 7d3681c commit 586a93f

File tree

2 files changed

+152
-0
lines changed

2 files changed

+152
-0
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import process from 'node:process';
2+
3+
/* eslint-disable @typescript-eslint/no-magic-numbers */
4+
const SIGNALS = [
5+
['SIGINT', 130],
6+
['SIGTERM', 143],
7+
['SIGQUIT', 131],
8+
] as const;
9+
/* eslint-enable @typescript-eslint/no-magic-numbers */
10+
11+
export type FatalKind = 'uncaughtException' | 'unhandledRejection';
12+
type ExitHandlerOptions =
13+
| {
14+
onClose?: () => void;
15+
onFatal: (err: unknown, kind?: FatalKind) => void;
16+
}
17+
| {
18+
onClose: () => void;
19+
onFatal?: never;
20+
};
21+
22+
export function installExitHandlers(options: ExitHandlerOptions): void {
23+
// Fatal errors
24+
process.on('uncaughtException', err => {
25+
options.onFatal?.(err, 'uncaughtException');
26+
});
27+
28+
process.on('unhandledRejection', reason => {
29+
options.onFatal?.(reason, 'unhandledRejection');
30+
});
31+
32+
// Graceful shutdown signals
33+
SIGNALS.forEach(([signal]) => {
34+
process.on(signal, () => {
35+
options.onClose?.();
36+
});
37+
});
38+
39+
// Normal exit
40+
process.on('exit', () => {
41+
options.onClose?.();
42+
});
43+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import process from 'node:process';
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3+
import { installExitHandlers } from './exit-process.js';
4+
5+
describe('exit-process tests', () => {
6+
const onFatal = vi.fn();
7+
const onClose = vi.fn();
8+
const processOnSpy = vi.spyOn(process, 'on');
9+
const processExitSpy = vi.spyOn(process, 'exit').mockImplementation(vi.fn());
10+
11+
beforeEach(() => {
12+
vi.clearAllMocks();
13+
});
14+
15+
afterEach(() => {
16+
[
17+
'uncaughtException',
18+
'unhandledRejection',
19+
'SIGINT',
20+
'SIGTERM',
21+
'SIGQUIT',
22+
'exit',
23+
].forEach(event => {
24+
process.removeAllListeners(event);
25+
});
26+
});
27+
28+
it('should install event listeners for all expected events', () => {
29+
expect(() => installExitHandlers({ onFatal, onClose })).not.toThrow();
30+
31+
expect(processOnSpy).toHaveBeenCalledWith(
32+
'uncaughtException',
33+
expect.any(Function),
34+
);
35+
expect(processOnSpy).toHaveBeenCalledWith(
36+
'unhandledRejection',
37+
expect.any(Function),
38+
);
39+
expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function));
40+
expect(processOnSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function));
41+
expect(processOnSpy).toHaveBeenCalledWith('SIGQUIT', expect.any(Function));
42+
expect(processOnSpy).toHaveBeenCalledWith('exit', expect.any(Function));
43+
});
44+
45+
it('should call onFatal with error and kind for uncaughtException', () => {
46+
expect(() => installExitHandlers({ onFatal })).not.toThrow();
47+
48+
const testError = new Error('Test uncaught exception');
49+
50+
(process as any).emit('uncaughtException', testError);
51+
52+
expect(onFatal).toHaveBeenCalledWith(testError, 'uncaughtException');
53+
expect(onFatal).toHaveBeenCalledTimes(1);
54+
expect(onClose).not.toHaveBeenCalled();
55+
});
56+
57+
it('should call onFatal with reason and kind for unhandledRejection', () => {
58+
expect(() => installExitHandlers({ onFatal })).not.toThrow();
59+
60+
const testReason = 'Test unhandled rejection';
61+
62+
(process as any).emit('unhandledRejection', testReason);
63+
64+
expect(onFatal).toHaveBeenCalledWith(testReason, 'unhandledRejection');
65+
expect(onFatal).toHaveBeenCalledTimes(1);
66+
expect(onClose).not.toHaveBeenCalled();
67+
});
68+
69+
it('should call onClose and exit with code 0 for SIGINT', () => {
70+
expect(() => installExitHandlers({ onClose })).not.toThrow();
71+
72+
(process as any).emit('SIGINT');
73+
74+
expect(onClose).toHaveBeenCalledTimes(1);
75+
expect(onClose).toHaveBeenCalledWith();
76+
expect(onFatal).not.toHaveBeenCalled();
77+
});
78+
79+
it('should call onClose and exit with code 0 for SIGTERM', () => {
80+
expect(() => installExitHandlers({ onClose })).not.toThrow();
81+
82+
(process as any).emit('SIGTERM');
83+
84+
expect(onClose).toHaveBeenCalledTimes(1);
85+
expect(onClose).toHaveBeenCalledWith();
86+
expect(onFatal).not.toHaveBeenCalled();
87+
});
88+
89+
it('should call onClose and exit with code 0 for SIGQUIT', () => {
90+
expect(() => installExitHandlers({ onClose })).not.toThrow();
91+
92+
(process as any).emit('SIGQUIT');
93+
94+
expect(onClose).toHaveBeenCalledTimes(1);
95+
expect(onClose).toHaveBeenCalledWith();
96+
expect(onFatal).not.toHaveBeenCalled();
97+
});
98+
99+
it('should call onClose for normal exit', () => {
100+
expect(() => installExitHandlers({ onClose })).not.toThrow();
101+
102+
(process as any).emit('exit');
103+
104+
expect(onClose).toHaveBeenCalledTimes(1);
105+
expect(onClose).toHaveBeenCalledWith();
106+
expect(onFatal).not.toHaveBeenCalled();
107+
expect(processExitSpy).not.toHaveBeenCalled();
108+
});
109+
});

0 commit comments

Comments
 (0)