UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

189 lines (188 loc) 7.57 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PluginLifecycleManager = void 0; /** * Phase configuration - single source of truth. * Key order defines phase execution order. * Hooks within each phase can execute in any order. */ const HOOKS_BY_PHASE = { graph: ['createNodes', 'createDependencies', 'createMetadata'], 'pre-task': ['preTasksExecution'], 'post-task': ['postTasksExecution'], }; const PHASE_BY_HOOK = Object.entries(HOOKS_BY_PHASE).reduce((acc, [phase, hooks]) => { for (const hook of hooks) { acc[hook] = phase; } return acc; }, {}); const PHASE_ORDER = keys(HOOKS_BY_PHASE); /** * Manages the lifecycle of an isolated plugin worker. * * Tracks phase entry/exit to determine when a worker can safely shut down. * A "phase session" starts when a caller enters the first registered hook * of a phase and ends when they exit the last registered hook. * * Shutdown occurs when: * 1. All phase sessions complete (ref count reaches 0) * 2. No later phases have registered hooks */ class PluginLifecycleManager { constructor(registeredHooks) { /** Phases where this plugin has at least one registered hook */ this.registeredPhases = {}; /** Number of active "phase sessions" (callers between first and last hook) */ this.phaseSessionCount = {}; /** Ordered list of registered phases (derived from HOOKS_BY_PHASE key order, narrowed to registered phases) */ this.registeredPhaseOrder = []; const registered = new Set(registeredHooks); // Determine which phases are registered and find first/last hooks per phase this.registeredPhases = {}; for (const phase of PHASE_ORDER) { const hooksInPhase = HOOKS_BY_PHASE[phase].filter((hook) => registered.has(hook)); if (hooksInPhase.length > 0) { this.registeredPhases[phase] = hooksInPhase; this.registeredPhaseOrder.push(phase); } } // Initialize session counts to 0 for (const phase of this.registeredPhaseOrder) { this.phaseSessionCount[phase] = 0; } } wrapHook(hook, fn, shutdown) { return async (...args) => { this.enterHook(hook); try { return await fn(...args); } finally { const shouldShutdown = this.exitHook(hook); if (shouldShutdown) { await shutdown(); } } }; } /** * Called when entering a hook. Increments phase session count if this * is the first registered hook in the phase. */ enterHook(hook) { const phase = PHASE_BY_HOOK[hook]; const phaseHooks = this.registeredPhases[phase]; if (!phaseHooks || !phaseHooks.includes(hook)) { throw new Error(`Hook "${hook}" is not registered for this plugin. Registered phases: ${this.registeredPhaseOrder.join(', ')}`); } // If this is the first hook registered by this plugin in the phase, increment session count const firstHook = phaseHooks[0]; if (hook === firstHook) { this.phaseSessionCount[phase] ??= 0; this.phaseSessionCount[phase]++; } } /** * Called when exiting a hook. Decrements phase session count if this * is the last registered hook in the phase. * * @returns true if the worker should shut down, false otherwise */ exitHook(hook) { const phase = PHASE_BY_HOOK[hook]; const phaseHooks = this.registeredPhases[phase]; if (!phaseHooks) { throw new Error(`Hook "${hook}" is not registered for this plugin. Registered phases: ${this.registeredPhaseOrder.join(', ')}`); } // Only process shutdown logic on last hook (exiting the phase) if (hook !== phaseHooks[phaseHooks.length - 1]) { return false; } // Decrement session count const count = this.phaseSessionCount[phase] ?? 0; if (count === 0) { throw new Error(`Mismatched exitHook call for phase "${phase}". No active sessions.`); } const newCount = count - 1; this.phaseSessionCount[phase] = newCount; // Can only shut down if no more active sessions in this phase if (newCount > 0) return false; // Can only shut down if this is the last registered phase return this.isLastRegisteredPhase(phase); } /** * Returns true if the worker should shut down immediately after construction. * This happens when the plugin has no graph-phase hooks, meaning it won't * be needed until task execution time (pre-task or post-task). */ shouldShutdownImmediately() { return !this.registeredPhases['graph']; } /** * Returns true if the plugin has any hooks in the given phase. */ hasHooksInPhase(phase) { return !!this.registeredPhases[phase]; } /** * Returns true if this is the last phase with registered hooks. */ isLastRegisteredPhase(phase) { const phaseIndex = this.registeredPhaseOrder.indexOf(phase); if (phaseIndex === -1) { throw new Error(`Phase "${phase}" is not registered for this plugin.`); } return phaseIndex === this.registeredPhaseOrder.length - 1; } /** * Aborts a single session within a phase by decrementing its count, * but only if the caller's session is still open. * * Called when graph construction is abandoned mid-flight (e.g., a newer * recomputation supersedes the current one). Without this, hooks that * were entered but whose phase never completed would leave the session * count elevated, preventing the worker from ever shutting down. * * @param phase The phase to abort * @param lastCompletedHook The last hook the caller executed before aborting. * Used to determine whether the caller's session is still open: if there * are registered hooks after this one, the session is open and needs * cleanup. If this IS the last registered hook, exitHook already closed * the session — decrementing again would steal another caller's count. * @returns true if the worker should shut down, false otherwise */ notifyPhaseAborted(phase, lastCompletedHook) { const phaseHooks = this.registeredPhases[phase]; if (!phaseHooks) { return false; } const hookIndex = phaseHooks.indexOf(lastCompletedHook); // Hook not registered → this caller never entered the phase → no session to abort. // Hook is the last registered hook → exitHook already closed the session. if (hookIndex === -1 || hookIndex === phaseHooks.length - 1) { return false; } const count = this.phaseSessionCount[phase] ?? 0; if (count === 0) { return false; } const newCount = count - 1; this.phaseSessionCount[phase] = newCount; if (newCount > 0) { return false; } return this.isLastRegisteredPhase(phase); } /** * Returns the current session count for a phase. Useful for testing. */ getPhaseRefCount(phase) { return this.phaseSessionCount[phase] ?? 0; } } exports.PluginLifecycleManager = PluginLifecycleManager; function keys(obj) { return Object.keys(obj); }