UNPKG

nx

Version:

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

150 lines (149 loc) 5.75 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; } /** * 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); }