UNPKG

hardhat

Version:

Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

306 lines 14.5 kB
import { assertHardhatInvariant, HardhatError, } from "@nomicfoundation/hardhat-errors"; import { ensureError } from "@nomicfoundation/hardhat-utils/error"; import { AsyncMutex } from "@nomicfoundation/hardhat-utils/synchronization"; import { detectPluginNpmDependencyProblems } from "./plugins/detect-plugin-npm-dependency-problems.js"; export class HookManagerImplementation { #mutex = new AsyncMutex(); #projectRoot; /** * The context passed to hook handlers, except to the `config` ones, to break * a circular dependency between the config and the hook handler. * * Initially `undefined` to be able to run the config hooks during * initialization. */ #context; /** * Plugins that provide hook handlers for each category, in reverse order. * * Precomputed from the plugin list at construction. */ #pluginsByHookCategory = new Map(); /** * Cached resolved category objects per hook category in reverse plugin * order. * * Only written by #getStaticHookHandlerCategories, which uses a mutex to * ensure that every Hook Category Factory is run once per HookManager * instance. */ #resolvedStaticCategories = new Map(); /** * A map of the dynamically registered handler categories. * * Each array is a list of categories, in reverse order of registration. * * Written by registerHandlers and unregisterHandlers. */ #dynamicHookHandlerCategories = new Map(); /** * Cached combined (dynamic + static) handlers per (category, hook name) in * chained running order. * * Only written by #getHandlersInChainedRunningOrder, and invalidated * per-category on dynamic handlers register/unregister. */ #chainedHandlers = new Map(); /** * Cached combined handlers per (category, hook name) in sequential running * order (reverse of chained). * * Only written by #getHandlersInSequentialRunningOrder, and invalidated * per-category on dynamic handlers register/unregister. */ #sequentialHandlers = new Map(); constructor(projectRoot, plugins) { this.#projectRoot = projectRoot; for (const plugin of plugins.toReversed()) { if (plugin.hookHandlers === undefined) { continue; } for (const hookCategoryName of Object.keys(plugin.hookHandlers)) { if (plugin.hookHandlers[hookCategoryName] === undefined) { continue; } let pluginsForCategory = this.#pluginsByHookCategory.get(hookCategoryName); if (pluginsForCategory === undefined) { pluginsForCategory = []; this.#pluginsByHookCategory.set(hookCategoryName, pluginsForCategory); } pluginsForCategory.push(plugin); } } } setContext(context) { this.#context = context; } registerHandlers(hookCategoryName, hookHandlerCategory) { let categories = this.#dynamicHookHandlerCategories.get(hookCategoryName); if (categories === undefined) { categories = []; this.#dynamicHookHandlerCategories.set(hookCategoryName, categories); } categories.unshift(hookHandlerCategory); this.#invalidateResolvedHandlersCache(hookCategoryName); } unregisterHandlers(hookCategoryName, hookHandlerCategory) { const categories = this.#dynamicHookHandlerCategories.get(hookCategoryName); if (categories === undefined) { return; } this.#dynamicHookHandlerCategories.set(hookCategoryName, categories.filter((c) => c !== hookHandlerCategory)); this.#invalidateResolvedHandlersCache(hookCategoryName); } async runHandlerChain(hookCategoryName, hookName, params, defaultImplementation) { // Synchronous fast path for already cached handlers. This duplicates // the check inside #getHandlersInChainedRunningOrder on purpose: // calling that async method introduces a microtask tick even on a // cache hit, whereas a direct Map lookup stays on the current tick. // That tick matters here because runHandlerChain is on every hook's // hot path, and this path pairs with the empty-handlers shortcut // below to dispatch straight to defaultImplementation with no awaits. const cachedHandlers = this.#chainedHandlers .get(hookCategoryName) ?.get(hookName); const handlers = cachedHandlers ?? (await this.#getHandlersInChainedRunningOrder(hookCategoryName, hookName)); let handlerParams; if (hookCategoryName !== "config") { assertHardhatInvariant(this.#context !== undefined, "Context must be set before running non-config hooks"); handlerParams = [this.#context, ...params]; } else { handlerParams = params; } // Fast path for the common case of no registered handlers: skip building // handlerParams and the `next` closure, and call the default implementation // directly. if (handlers.length === 0) { return (await defaultImplementation(...handlerParams)); } const numberOfHandlers = handlers.length; let index = 0; const next = async (...nextParams) => { const result = index < numberOfHandlers ? await handlers[index++](...nextParams, next) : await defaultImplementation(...nextParams); return result; }; return await next(...handlerParams); } async runSequentialHandlers(hookCategoryName, hookName, params) { const handlers = await this.#getHandlersInSequentialRunningOrder(hookCategoryName, hookName); let handlerParams; if (hookCategoryName !== "config") { assertHardhatInvariant(this.#context !== undefined, "Context must be set before running non-config hooks"); handlerParams = [this.#context, ...params]; } else { handlerParams = params; } const result = []; for (const handler of handlers) { result.push(await handler(...handlerParams)); } return result; } async runParallelHandlers(hookCategoryName, hookName, params) { // The ordering of handlers is unimportant here, as they are run in parallel const handlers = await this.#getHandlersInChainedRunningOrder(hookCategoryName, hookName); let handlerParams; if (hookCategoryName !== "config") { assertHardhatInvariant(this.#context !== undefined, "Context must be set before running non-config hooks"); handlerParams = [this.#context, ...params]; } else { handlerParams = params; } return await Promise.all(handlers.map((handler) => handler(...handlerParams))); } async hasHandlers(hookCategoryName, hookName) { // The ordering of handlers is unimportant here, as we only check if any exist const handlers = await this.#getHandlersInChainedRunningOrder(hookCategoryName, hookName); return handlers.length > 0; } async #getHandlersInChainedRunningOrder(hookCategoryName, hookName) { let handlersByName = this.#chainedHandlers.get(hookCategoryName); if (handlersByName === undefined) { handlersByName = new Map(); this.#chainedHandlers.set(hookCategoryName, handlersByName); } const cached = handlersByName.get(hookName); if (cached !== undefined) { return cached; } const staticCategories = await this.#getStaticHookHandlerCategories(hookCategoryName); // IMPORTANT NOTE: Accessing the dynamic hook handlers MUST happen // after awaiting the static ones. See // #invalidateResolvedHandlersCache for more info. const dynamicCategories = this.#dynamicHookHandlerCategories.get(hookCategoryName); const handlers = []; if (dynamicCategories !== undefined) { for (const category of dynamicCategories) { const handler = category[hookName]; if (handler !== undefined) { handlers.push(handler); } } } for (const category of staticCategories) { const handler = category[hookName]; if (handler !== undefined) { handlers.push(handler); } } handlersByName.set(hookName, handlers); return handlers; } async #getHandlersInSequentialRunningOrder(hookCategoryName, hookName) { let handlersByName = this.#sequentialHandlers.get(hookCategoryName); if (handlersByName === undefined) { handlersByName = new Map(); this.#sequentialHandlers.set(hookCategoryName, handlersByName); } const cached = handlersByName.get(hookName); if (cached !== undefined) { return cached; } const chained = await this.#getHandlersInChainedRunningOrder(hookCategoryName, hookName); const sequential = chained.toReversed(); handlersByName.set(hookName, sequential); return sequential; } async #getStaticHookHandlerCategories(hookCategoryName) { const cached = this.#resolvedStaticCategories.get(hookCategoryName); if (cached !== undefined) { return cached; } const plugins = this.#pluginsByHookCategory.get(hookCategoryName); // We don't need to get the mutex to resolve this case, as it will always // be an empty array, and won't execute any factory. if (plugins === undefined) { this.#resolvedStaticCategories.set(hookCategoryName, []); return []; } return await this.#mutex.exclusiveRun(async () => { // Re-check under the mutex in case another caller just populated it. const recheck = this.#resolvedStaticCategories.get(hookCategoryName); if (recheck !== undefined) { return recheck; } const resolved = await Promise.all(plugins.map(async (plugin) => await this.#getPluginStaticHookCategory(plugin, hookCategoryName))); this.#resolvedStaticCategories.set(hookCategoryName, resolved); return resolved; }); } /** * Returns the hook category object for a plugin that has the hook category * defined. * * @param plugin A plugin that MUST have the given hook category defined. * @param hookCategoryName The name of the hook category. * @returns The hook category object. */ async #getPluginStaticHookCategory(plugin, hookCategoryName) { const hookHandlerCategoryFactory = plugin.hookHandlers?.[hookCategoryName]; assertHardhatInvariant(hookHandlerCategoryFactory !== undefined, "#pluginsByHookCategory only contains plugins with this hook category"); let factory; try { factory = (await hookHandlerCategoryFactory()).default; } catch (error) { ensureError(error); await detectPluginNpmDependencyProblems(this.#projectRoot, plugin, error); throw new HardhatError(HardhatError.ERRORS.CORE.HOOKS.FAILED_TO_LOAD_HOOK_HANDLER_FACTORY, { pluginId: plugin.id, hookCategoryName }, error); } assertHardhatInvariant(typeof factory === "function", `Plugin ${plugin.id} doesn't export a hook factory for category ${hookCategoryName}`); let hookCategory; try { hookCategory = await factory(); } catch (error) { ensureError(error); throw new HardhatError(HardhatError.ERRORS.CORE.HOOKS.FAILED_TO_RUN_HOOK_HANDLER_FACTORY, { pluginId: plugin.id, hookCategoryName }, error); } assertHardhatInvariant(hookCategory !== null && typeof hookCategory === "object", `Plugin ${plugin.id} doesn't export a valid factory for category ${hookCategoryName}, it didn't return an object`); return hookCategory; } #invalidateResolvedHandlersCache(hookCategoryName) { // Invalidation deletes the outer entry rather than clearing the inner // map. This matters under concurrency. // // A reader of #getHandlersInChainedRunningOrder (or its sequential // sibling) captures a reference to the inner map before awaiting the // static categories, and writes its computed array back after the // await. If invalidation runs during that await, deleting the outer // entry leaves the reader's inner map orphaned: its write lands in a // map no longer reachable from #chainedHandlers/#sequentialHandlers, // so it cannot poison the shared cache. The next reader sees // `undefined`, installs a fresh inner map, and rebuilds from the // current dynamic state. // // Two distinct properties make this safe, guaranteed by two different // things: // // 1. The in-flight reader's own return value is correct. This is // because #getHandlersInChainedRunningOrder reads // #dynamicHookHandlerCategories *after* awaiting the static // categories. Any invalidation that happened during the await is // visible to the reader when it resumes, so the array it builds // reflects the current dynamic state. // // 2. The shared cache never holds a stale array. This is guaranteed // by the orphaning-by-delete described above: a reader that // started before the invalidation can only write into an // unreachable inner map. // // Property 1 depends on the ordering of the dynamic handlers read relative // to the await. If that read ever moved *before* the await, a reader // could build a stale array and return it to its caller — the cache // would still be protected by property 2, but the reader's caller // would see the stale result. this.#chainedHandlers.delete(hookCategoryName); this.#sequentialHandlers.delete(hookCategoryName); } } //# sourceMappingURL=hook-manager.js.map