Skip to content

Commit 6ec0450

Browse files
committed
refactor: impl unsub
1 parent 170bab2 commit 6ec0450

File tree

3 files changed

+76
-46
lines changed

3 files changed

+76
-46
lines changed

packages/utils/src/lib/exit-process.ts

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,19 @@ export type ExitHandlerOptions = {
3838
fatalExitCode?: number;
3939
};
4040

41-
export function installExitHandlers(options: ExitHandlerOptions = {}): void {
41+
/**
42+
*
43+
* @param options - Options for the exit handler
44+
* @param options.onExit - Callback to be called when the process exits
45+
* @param options.onError - Callback to be called when an error occurs
46+
* @param options.exitOnFatal - Whether to exit the process on fatal errors
47+
* @param options.exitOnSignal - Whether to exit the process on signals
48+
* @param options.fatalExitCode - The exit code to use for fatal errors
49+
* @returns A function to unsubscribe from the exit handlers
50+
*/
51+
export function subscribeProcessExit(
52+
options: ExitHandlerOptions = {},
53+
): () => void {
4254
// eslint-disable-next-line functional/no-let
4355
let closedReason: CloseReason | undefined;
4456
const {
@@ -57,40 +69,57 @@ export function installExitHandlers(options: ExitHandlerOptions = {}): void {
5769
onExit?.(code, reason);
5870
};
5971

60-
process.on('uncaughtException', err => {
72+
const uncaughtExceptionHandler = (err: unknown) => {
6173
onError?.(err, 'uncaughtException');
6274
if (exitOnFatal) {
6375
close(fatalExitCode, {
6476
kind: 'fatal',
6577
fatal: 'uncaughtException',
6678
});
6779
}
68-
});
80+
};
6981

70-
process.on('unhandledRejection', reason => {
82+
const unhandledRejectionHandler = (reason: unknown) => {
7183
onError?.(reason, 'unhandledRejection');
7284
if (exitOnFatal) {
7385
close(fatalExitCode, {
7486
kind: 'fatal',
7587
fatal: 'unhandledRejection',
7688
});
7789
}
78-
});
90+
};
7991

80-
(['SIGINT', 'SIGTERM', 'SIGQUIT'] as const).forEach(signal => {
81-
process.on(signal, () => {
82-
close(SIGNAL_EXIT_CODES()[signal], { kind: 'signal', signal });
83-
if (exitOnSignal) {
84-
// eslint-disable-next-line n/no-process-exit
85-
process.exit(SIGNAL_EXIT_CODES()[signal]);
86-
}
87-
});
88-
});
92+
const signalHandlers = (['SIGINT', 'SIGTERM', 'SIGQUIT'] as const).map(
93+
signal => {
94+
const handler = () => {
95+
close(SIGNAL_EXIT_CODES()[signal], { kind: 'signal', signal });
96+
if (exitOnSignal) {
97+
// eslint-disable-next-line n/no-process-exit
98+
process.exit(SIGNAL_EXIT_CODES()[signal]);
99+
}
100+
};
101+
process.on(signal, handler);
102+
return { signal, handler };
103+
},
104+
);
89105

90-
process.on('exit', code => {
106+
const exitHandler = (code: number) => {
91107
if (closedReason) {
92108
return;
93109
}
94110
close(code, { kind: 'exit' });
95-
});
111+
};
112+
113+
process.on('uncaughtException', uncaughtExceptionHandler);
114+
process.on('unhandledRejection', unhandledRejectionHandler);
115+
process.on('exit', exitHandler);
116+
117+
return () => {
118+
process.removeListener('uncaughtException', uncaughtExceptionHandler);
119+
process.removeListener('unhandledRejection', unhandledRejectionHandler);
120+
process.removeListener('exit', exitHandler);
121+
signalHandlers.forEach(({ signal, handler }) => {
122+
process.removeListener(signal, handler);
123+
});
124+
};
96125
}

packages/utils/src/lib/profiler/profiler.ts

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import process from 'node:process';
22
import { isEnvVarEnabled } from '../env.js';
3-
import { installExitHandlers } from '../exit-process';
3+
import { subscribeProcessExit } from '../exit-process';
44
import type { TraceEvent } from '../trace-file.type';
55
import {
66
type ActionTrackConfigs,
@@ -16,7 +16,6 @@ import type {
1616
ActionTrackEntryPayload,
1717
DevToolsColor,
1818
EntryMeta,
19-
UserTimingDetail,
2019
} from '../user-timing-extensibility-api.type.js';
2120
import { PROFILER_ENABLED_ENV_VAR } from './constants.js';
2221

@@ -236,6 +235,7 @@ type WalSink = {
236235
append(event: TraceEvent): void;
237236
open(): void;
238237
close(): void;
238+
isClosed(): boolean;
239239
};
240240

241241
export type NodeJsProfilerOptions<T extends ActionTrackConfigs> =
@@ -247,6 +247,7 @@ export type NodeJsProfilerOptions<T extends ActionTrackConfigs> =
247247
};
248248

249249
export class NodeJsProfiler<T extends ActionTrackConfigs> extends Profiler<T> {
250+
#exitHandlerSubscribscription: null | (() => void) = null;
250251
protected sink: WalSink | null = null;
251252

252253
constructor(options: NodeJsProfilerOptions<T>) {
@@ -258,8 +259,9 @@ export class NodeJsProfiler<T extends ActionTrackConfigs> extends Profiler<T> {
258259
},
259260
open: () => void 0,
260261
close: () => void 0,
262+
isClosed: () => false,
261263
};
262-
this.installExitHandlers();
264+
this.#exitHandlerSubscribscription = this.subscribeProcessExit();
263265
}
264266

265267
/**
@@ -270,27 +272,36 @@ export class NodeJsProfiler<T extends ActionTrackConfigs> extends Profiler<T> {
270272
*
271273
* @protected
272274
*/
273-
protected installExitHandlers(): void {
274-
installExitHandlers({
275+
protected subscribeProcessExit(): () => void {
276+
return subscribeProcessExit({
275277
onError: (err, kind) => {
276-
if (!this.isEnabled()) {
278+
if (!this.isRunning()) {
277279
return;
278280
}
279281
this.marker('Fatal Error', {
280282
...errorToMarkerPayload(err),
281283
tooltipText: `${kind} caused fatal error`,
282284
});
283-
this.shutdown();
285+
this.close();
284286
},
285-
onExit: () => {
286-
if (!this.isEnabled()) {
287+
onExit: (code, reason) => {
288+
if (!this.isRunning()) {
287289
return;
288290
}
289-
this.shutdown();
291+
this.marker('Process Exit', {
292+
...(code !== 0 ? { color: 'warning' } : {}),
293+
properties: [['reason', JSON.stringify(reason)]],
294+
tooltipText: `Process exited with code ${code}`,
295+
});
296+
this.close();
290297
},
291298
});
292299
}
293300

301+
isRunning(): boolean {
302+
return this.isEnabled() && !this.sink?.isClosed();
303+
}
304+
294305
override setEnabled(enabled: boolean): void {
295306
super.setEnabled(enabled);
296307
enabled ? this.sink?.open() : this.sink?.close();
@@ -304,7 +315,10 @@ export class NodeJsProfiler<T extends ActionTrackConfigs> extends Profiler<T> {
304315
* data is flushed and the WAL sink is properly closed.
305316
*/
306317
close(): void {
307-
this.shutdown();
318+
if (!this.isEnabled()) return;
319+
this.flush();
320+
this.setEnabled(false);
321+
this.#exitHandlerSubscribscription?.();
308322
}
309323

310324
/**
@@ -313,18 +327,4 @@ export class NodeJsProfiler<T extends ActionTrackConfigs> extends Profiler<T> {
313327
flush(): void {
314328
// @TODO implement WAL flush, currently all entries are buffered in memory
315329
}
316-
317-
/**
318-
* Performs internal cleanup of profiling resources.
319-
*
320-
* Flushes any remaining buffered data and closes the WAL sink.
321-
* This method is called automatically on process exit or error.
322-
*
323-
* @protected
324-
*/
325-
protected shutdown(): void {
326-
if (!this.isEnabled()) return;
327-
this.flush();
328-
this.setEnabled(false);
329-
}
330330
}

packages/utils/src/lib/profiler/profiler.unit.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { performance } from 'node:perf_hooks';
22
import { beforeEach, describe, expect, it, vi } from 'vitest';
3-
import { installExitHandlers } from '../exit-process.js';
3+
import { subscribeProcessExit } from '../exit-process.js';
44
import type { ActionTrackEntryPayload } from '../user-timing-extensibility-api.type.js';
55
import { NodeJsProfiler, Profiler, type ProfilerOptions } from './profiler.js';
66

7-
// Spy on installExitHandlers to capture handlers
7+
// Spy on subscribeProcessExit to capture handlers
88
vi.mock('../exit-process.js');
99

1010
describe('Profiler', () => {
@@ -429,7 +429,7 @@ describe('Profiler', () => {
429429
});
430430
});
431431
describe('NodeJsProfiler', () => {
432-
const mockInstallExitHandlers = vi.mocked(installExitHandlers);
432+
const mockSubscribeProcessExit = vi.mocked(subscribeProcessExit);
433433

434434
let capturedOnError:
435435
| ((
@@ -456,9 +456,10 @@ describe('NodeJsProfiler', () => {
456456
capturedOnError = undefined;
457457
capturedOnExit = undefined;
458458

459-
mockInstallExitHandlers.mockImplementation(options => {
459+
mockSubscribeProcessExit.mockImplementation(options => {
460460
capturedOnError = options?.onError;
461461
capturedOnExit = options?.onExit;
462+
return vi.fn();
462463
});
463464

464465
performance.clearMarks();
@@ -470,7 +471,7 @@ describe('NodeJsProfiler', () => {
470471
it('installs exit handlers on construction', () => {
471472
expect(() => createProfiler()).not.toThrow();
472473

473-
expect(mockInstallExitHandlers).toHaveBeenCalledWith({
474+
expect(mockSubscribeProcessExit).toHaveBeenCalledWith({
474475
onError: expect.any(Function),
475476
onExit: expect.any(Function),
476477
});

0 commit comments

Comments
 (0)