mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-01 00:24:08 +00:00
feat(hooks): refactor to initialise hook-manager only once per req execution
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user