Centralize dashboard cleanup registration to prevent repeated module imports from accumulating beforeExit listeners. - Add a Symbol.for-backed dashboard process lifecycle registry with one shared beforeExit listener. - Register existing cleanup intervals through the shared lifecycle helper across dashboard modules. - Cover repeated module evaluation and multi-cleanup dispatch with Vitest regression tests. Files changed: .../src/__tests__/process-lifecycle.test.ts | 76 ++++++++++++++++++++++ packages/dashboard/src/agent-generation.ts | 3 +- packages/dashboard/src/ai-refine.ts | 3 +- .../dashboard/src/milestone-slice-interview.ts | 3 +- packages/dashboard/src/mission-interview.ts | 3 +- packages/dashboard/src/planning.ts | 3 +- packages/dashboard/src/process-lifecycle.ts | 63 ++++++++++++++++++ packages/dashboard/src/server.ts | 3 +- packages/dashboard/src/subtask-breakdown.ts | 3 +- 9 files changed, 153 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-6488 Fusion-Task-Lineage: 3ec47f17-ae38-4ee3-a3fa-3132d23a2b12
64 lines
2.1 KiB
TypeScript
64 lines
2.1 KiB
TypeScript
type BeforeExitCleanup = () => void;
|
|
|
|
type BeforeExitRegistry = {
|
|
cleanups: Set<BeforeExitCleanup>;
|
|
listener?: () => void;
|
|
};
|
|
|
|
const BEFORE_EXIT_REGISTRY_SYMBOL = Symbol.for("fusion.dashboard.beforeExit");
|
|
|
|
function getBeforeExitRegistry(): BeforeExitRegistry {
|
|
const globalWithRegistry = globalThis as typeof globalThis & {
|
|
[BEFORE_EXIT_REGISTRY_SYMBOL]?: BeforeExitRegistry;
|
|
};
|
|
|
|
globalWithRegistry[BEFORE_EXIT_REGISTRY_SYMBOL] ??= {
|
|
cleanups: new Set<BeforeExitCleanup>(),
|
|
};
|
|
|
|
return globalWithRegistry[BEFORE_EXIT_REGISTRY_SYMBOL];
|
|
}
|
|
|
|
function runBeforeExitCleanups(registry: BeforeExitRegistry): void {
|
|
for (const cleanup of Array.from(registry.cleanups)) {
|
|
cleanup();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* FNXC:ProcessLifecycle 2026-06-15-08:09:
|
|
* Dashboard modules create unref'd cleanup intervals at import time, and Vitest can re-evaluate those modules while the process singleton survives.
|
|
* Register cleanup callbacks behind one Symbol.for-backed beforeExit listener so repeated imports do not accumulate EventEmitter listeners or hide the leak with setMaxListeners appeasement.
|
|
*/
|
|
export function registerBeforeExitCleanup(cleanup: BeforeExitCleanup): void {
|
|
const registry = getBeforeExitRegistry();
|
|
registry.cleanups.add(cleanup);
|
|
|
|
if (registry.listener) {
|
|
return;
|
|
}
|
|
|
|
registry.listener = () => runBeforeExitCleanups(registry);
|
|
process.on("beforeExit", registry.listener);
|
|
}
|
|
|
|
/** @internal Test-only helper for deterministic process-lifecycle assertions. */
|
|
export function __getBeforeExitCleanupCount(): number {
|
|
return getBeforeExitRegistry().cleanups.size;
|
|
}
|
|
|
|
/** @internal Test-only helper for deterministic process-lifecycle assertions. */
|
|
export function __runBeforeExitCleanupsForTests(): void {
|
|
runBeforeExitCleanups(getBeforeExitRegistry());
|
|
}
|
|
|
|
/** @internal Test-only helper for deterministic process-lifecycle assertions. */
|
|
export function __resetBeforeExitRegistryForTests(): void {
|
|
const registry = getBeforeExitRegistry();
|
|
if (registry.listener) {
|
|
process.off("beforeExit", registry.listener);
|
|
}
|
|
registry.cleanups.clear();
|
|
registry.listener = undefined;
|
|
}
|