feat(hooks): refactor to initialise hook-manager only once per req execution

This commit is contained in:
Bijin A B
2026-02-12 22:18:32 +05:30
parent 521f7332fd
commit bc09a26fa6
11 changed files with 300 additions and 332 deletions

View File

@@ -15,10 +15,11 @@ const { rpad } = require('../utils/common');
const { getOptions } = require('../utils/bru');
const { parseDotEnv, parseEnvironment } = require('@usebruno/filestore');
const constants = require('../constants');
const { findItemInCollection, createCollectionJsonFromPathname, getCallStack, FORMAT_CONFIG, HOOK_EVENTS } = require('../utils/collection');
const { findItemInCollection, createCollectionJsonFromPathname, getCallStack, FORMAT_CONFIG } = require('../utils/collection');
const { hasExecutableTestInScript } = require('../utils/request');
const { createSkippedFileResults } = require('../utils/run');
const { HooksRuntime } = require('@usebruno/js');
const { HooksRuntime, HookManager } = require('@usebruno/js');
const HOOK_EVENTS = HookManager.EVENTS;
const decomment = require('decomment');
const command = 'run [paths...]';
const desc = 'Run one or more requests/folders';
@@ -622,7 +623,7 @@ const handler = async function (argv) {
console[type](...args);
};
// Define runSingleRequestByPathname before executeCollectionHooks so it's available at all hook levels
// Define runSingleRequestByPathname before hooks initialization so it's available at all hook levels
const runSingleRequestByPathname = async (relativeItemPathname) => {
const ext = FORMAT_CONFIG[collection.format].ext;
return new Promise(async (resolve, reject) => {
@@ -650,44 +651,41 @@ const handler = async function (argv) {
});
};
// Helper function to execute collection-level hooks at runtime
const executeCollectionHooks = async (hookEvent, eventData) => {
// Initialize collection-level hooks once (evaluated once, reused for before/after events)
let collectionHooksCtx = null;
{
collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionHooks = get(collectionRoot, 'request.script.hooks', '');
if (!collectionHooks || !collectionHooks.trim()) {
return;
}
try {
const hooksRuntime = new HooksRuntime({ runtime: scriptingConfig?.runtime });
const result = await hooksRuntime.runHooks({
hooksFile: decomment(collectionHooks),
request: {}, // Placeholder request for collection-level hooks
envVariables: envVars,
runtimeVariables,
collectionPath,
onConsoleLog,
processEnvVars,
scriptingConfig,
runRequestByItemPathname: runSingleRequestByPathname,
collectionName
});
if (result?.hookManager) {
await result.hookManager.call(hookEvent, eventData);
// Dispose HookManager to free VM resources
if (result.hookManager && typeof result.hookManager.dispose === 'function') {
result.hookManager.dispose();
}
if (collectionHooks && collectionHooks.trim()) {
try {
const hooksRuntime = new HooksRuntime({ runtime: scriptingConfig?.runtime });
collectionHooksCtx = await hooksRuntime.runHooks({
hooksFile: decomment(collectionHooks),
request: {}, // Placeholder request for collection-level hooks
envVariables: envVars,
runtimeVariables,
collectionPath,
onConsoleLog,
processEnvVars,
scriptingConfig,
runRequestByItemPathname: runSingleRequestByPathname,
collectionName
});
} catch (error) {
console.error('Error initializing collection-level hooks:', error);
}
} catch (error) {
console.error(`Error executing collection-level hooks for ${hookEvent}:`, error);
}
};
}
// Call onBeforeCollectionRun hook before starting to run requests
await executeCollectionHooks(HOOK_EVENTS.RUNNER_BEFORE_COLLECTION_RUN, { collection });
if (collectionHooksCtx?.hookManager) {
try {
await collectionHooksCtx.hookManager.call(HOOK_EVENTS.RUNNER_BEFORE_COLLECTION_RUN, { collection });
} catch (error) {
console.error('Error executing beforeCollectionRun hook:', error);
}
}
let currentRequestIndex = 0;
let nJumps = 0; // count the number of jumps to avoid infinite loops
@@ -802,7 +800,15 @@ const handler = async function (argv) {
results.push(...skippedFileResults);
// Call onAfterCollectionRun hook after all requests are done
await executeCollectionHooks(HOOK_EVENTS.RUNNER_AFTER_COLLECTION_RUN, { collection });
if (collectionHooksCtx?.hookManager) {
try {
await collectionHooksCtx.hookManager.call(HOOK_EVENTS.RUNNER_AFTER_COLLECTION_RUN, { collection });
} catch (error) {
console.error('Error executing afterCollectionRun hook:', error);
}
collectionHooksCtx.hookManager.dispose();
collectionHooksCtx = null;
}
const summary = printRunSummary(results);
const runCompletionTime = new Date().toISOString();

View File

@@ -6,10 +6,11 @@ const { forOwn, isUndefined, isNull, each, extend, get, compact } = require('lod
const prepareRequest = require('./prepare-request');
const interpolateVars = require('./interpolate-vars');
const { interpolateString, interpolateObject } = require('./interpolate-string');
const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime, HooksRuntime } = require('@usebruno/js');
const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime, HooksRuntime, HookManager, BrunoResponse } = require('@usebruno/js');
const HOOK_EVENTS = HookManager.EVENTS;
const { stripExtension } = require('../utils/filesystem');
const { getOptions } = require('../utils/bru');
const { getTreePathFromCollectionToItem, HOOK_EVENTS } = require('../utils/collection');
const { getTreePathFromCollectionToItem } = require('../utils/collection');
const https = require('https');
const { HttpProxyAgent } = require('http-proxy-agent');
const { SocksProxyAgent } = require('socks-proxy-agent');
@@ -215,53 +216,64 @@ const runSingleRequest = async function (
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
const collectionName = collection?.brunoConfig?.name;
// Helper function to execute merged hooks (hooks are merged in prepareRequest via mergeScripts)
const executeHooks = async (hookEvent, eventData) => {
const hooksRuntime = new HooksRuntime({ runtime: scriptingConfig?.runtime });
const result = await hooksRuntime.runHooks({
hooksFile: request.script?.hooks,
request,
response: eventData.response,
envVariables,
runtimeVariables,
collectionPath,
onConsoleLog,
processEnvVars,
scriptingConfig,
runRequestByItemPathname: runSingleRequestByPathname,
collectionName
});
// Initialize hooks once for this request lifecycle (reused for beforeRequest + afterResponse)
let hooksCtx = null;
const hooksFile = request.script?.hooks;
if (hooksFile && hooksFile.trim()) {
try {
const hooksRuntime = new HooksRuntime({ runtime: scriptingConfig?.runtime });
hooksCtx = await hooksRuntime.runHooks({
hooksFile,
request,
envVariables,
runtimeVariables,
collectionPath,
onConsoleLog,
processEnvVars,
scriptingConfig,
runRequestByItemPathname: runSingleRequestByPathname,
collectionName
});
} catch (error) {
console.error('Error initializing hooks:', error);
}
}
if (result?.hookManager) {
// Enrich eventData with runtime-created req/res wrappers
/**
* Trigger a specific hook event using the initialized hooks context.
* Re-reads runner control signals from bru instance after handlers execute.
*/
const triggerHookEvent = async (hookEvent, eventData) => {
if (!hooksCtx?.hookManager) return null;
try {
const enrichedEventData = {
...eventData,
req: result.req || eventData.req,
res: result.res || eventData.res
req: hooksCtx.req || eventData.req,
res: eventData.response ? new BrunoResponse(eventData.response) : (hooksCtx.res || eventData.res)
};
await result.hookManager.call(hookEvent, enrichedEventData);
await hooksCtx.hookManager.call(hookEvent, enrichedEventData);
// Re-read runner control signals from bru instance AFTER handlers have executed
// Handlers may have called bru.runner.setNextRequest(), skipRequest(), or stopExecution()
// which update the bru instance but were not captured in the initial result
const bru = result.__bru;
const bru = hooksCtx.__bru;
if (bru) {
result.nextRequestName = bru.nextRequest;
result.skipRequest = bru.skipRequest;
result.stopExecution = bru.stopExecution;
hooksCtx.nextRequestName = bru.nextRequest;
hooksCtx.skipRequest = bru.skipRequest;
hooksCtx.stopExecution = bru.stopExecution;
}
result.hookManager.dispose();
return hooksCtx;
} catch (error) {
console.error(`Error executing hooks for ${hookEvent}:`, error);
return null;
}
return result;
};
// Call beforeRequest hooks before running pre-request scripts
// Hooks are called in registration order: collection -> folder(s) -> request
const beforeRequestEventData = { request, collection };
const beforeRequestHooksResult = await executeHooks(
const beforeRequestHooksResult = await triggerHookEvent(
HOOK_EVENTS.HTTP_BEFORE_REQUEST,
beforeRequestEventData
);
@@ -738,11 +750,16 @@ const runSingleRequest = async function (
// Hooks are called in registration order: collection -> folder(s) -> request
const afterResponseEventData = { request, response, collection };
const afterResponseHooksResult = await executeHooks(
const afterResponseHooksResult = await triggerHookEvent(
HOOK_EVENTS.HTTP_AFTER_RESPONSE,
afterResponseEventData
);
// Dispose hooks context — done with all events for this request
if (hooksCtx?.hookManager) {
hooksCtx.hookManager.dispose();
}
// Check runner control from hooks
applyRunnerControlFromResult(afterResponseHooksResult, runnerState);
nextRequestName = runnerState.nextRequestName;

View File

@@ -391,17 +391,6 @@ const mergeAuth = (collection, request, requestTreePath) => {
}
};
/**
* Hook event names used throughout the application.
* This object is frozen to prevent accidental modifications and improve maintainability.
*/
const HOOK_EVENTS = Object.freeze({
HTTP_BEFORE_REQUEST: 'http:beforeRequest',
HTTP_AFTER_RESPONSE: 'http:afterResponse',
RUNNER_BEFORE_COLLECTION_RUN: 'runner:beforeCollectionRun',
RUNNER_AFTER_COLLECTION_RUN: 'runner:afterCollectionRun'
});
const getAllRequestsInFolder = (folderItems = [], recursive = true) => {
let requests = [];
@@ -631,6 +620,5 @@ module.exports = {
mergeAuth,
getAllRequestsInFolder,
getAllRequestsAtFolderRoot,
getCallStack,
HOOK_EVENTS
getCallStack
};