Skip to content

Commit

Permalink
Avoid memory leaks via beforeExit listeners
Browse files Browse the repository at this point in the history
  • Loading branch information
lukastaegert committed May 21, 2023
1 parent ed1e93f commit 202a64d
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 23 deletions.
50 changes: 28 additions & 22 deletions src/utils/hookActions.ts
@@ -1,4 +1,3 @@
import { EventEmitter } from 'node:events';
import process from 'node:process';
import type { HookAction, PluginDriver } from './PluginDriver';

Expand All @@ -25,33 +24,40 @@ function formatAction([pluginName, hookName, parameters]: HookAction): string {
return action;
}

// We do not directly listen on process to avoid max listeners warnings for
// complicated build processes
const beforeExitEvent = 'beforeExit';
// eslint-disable-next-line unicorn/prefer-event-target
const beforeExitEmitter = new EventEmitter();
beforeExitEmitter.setMaxListeners(0);
process.on(beforeExitEvent, () => beforeExitEmitter.emit(beforeExitEvent));
let handleBeforeExit: null | (() => void) = null;
const rejectByPluginDriver = new Map<PluginDriver, (reason: Error) => void>();

export async function catchUnfinishedHookActions<T>(
pluginDriver: PluginDriver,
callback: () => Promise<T>
): Promise<T> {
let handleEmptyEventLoop: () => void;
const emptyEventLoopPromise = new Promise<T>((_, reject) => {
handleEmptyEventLoop = () => {
const unfulfilledActions = pluginDriver.getUnfulfilledHookActions();
reject(
new Error(
`Unexpected early exit. This happens when Promises returned by plugins cannot resolve. Unfinished hook action(s) on exit:\n` +
[...unfulfilledActions].map(formatAction).join('\n')
)
);
};
beforeExitEmitter.once(beforeExitEvent, handleEmptyEventLoop);
rejectByPluginDriver.set(pluginDriver, reject);
if (!handleBeforeExit) {
// We only ever create a single event listener to avoid max listener and
// other issues
handleBeforeExit = () => {
for (const [pluginDriver, reject] of rejectByPluginDriver) {
const unfulfilledActions = pluginDriver.getUnfulfilledHookActions();
reject(
new Error(
`Unexpected early exit. This happens when Promises returned by plugins cannot resolve. Unfinished hook action(s) on exit:\n` +
[...unfulfilledActions].map(formatAction).join('\n')
)
);
}
};
process.once('beforeExit', handleBeforeExit);
}
});

const result = await Promise.race([callback(), emptyEventLoopPromise]);
beforeExitEmitter.off(beforeExitEvent, handleEmptyEventLoop!);
return result;
try {
return await Promise.race([callback(), emptyEventLoopPromise]);
} finally {
rejectByPluginDriver.delete(pluginDriver);
if (rejectByPluginDriver.size === 0) {
process.off('beforeExit', handleBeforeExit!);
handleBeforeExit = null;
}
}
}
2 changes: 1 addition & 1 deletion test/watch/index.js
Expand Up @@ -10,7 +10,7 @@ const { copy } = require('fs-extra');
const rollup = require('../../dist/rollup');
const { atomicWriteFileSync, wait } = require('../utils');

describe.only('rollup.watch', () => {
describe('rollup.watch', () => {
let watcher;

beforeEach(() => {
Expand Down

0 comments on commit 202a64d

Please sign in to comment.