mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +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
|
||||
};
|
||||
|
||||
@@ -8,7 +8,8 @@ const mime = require('mime-types');
|
||||
const { ipcMain } = require('electron');
|
||||
const { each, get, extend, cloneDeep, merge } = require('lodash');
|
||||
const { NtlmClient } = require('axios-ntlm');
|
||||
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime, HooksRuntime } = require('@usebruno/js');
|
||||
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime, HooksRuntime, HookManager, BrunoResponse } = require('@usebruno/js');
|
||||
const HOOK_EVENTS = HookManager.EVENTS;
|
||||
const { encodeUrl } = require('@usebruno/common').utils;
|
||||
const { extractPromptVariables } = require('@usebruno/common').utils;
|
||||
const { interpolateString } = require('./interpolate-string');
|
||||
@@ -24,7 +25,7 @@ const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseData
|
||||
const { chooseFileToSave, writeFile, getCollectionFormat, hasRequestExtension } = require('../../utils/filesystem');
|
||||
const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies');
|
||||
const { createFormData } = require('../../utils/form-data');
|
||||
const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars, sortByNameThenSequence, HOOK_EVENTS } = require('../../utils/collection');
|
||||
const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars, sortByNameThenSequence } = require('../../utils/collection');
|
||||
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant, updateCollectionOauth2Credentials } = require('../../utils/oauth2');
|
||||
const { preferencesUtil } = require('../../store/preferences');
|
||||
const { getProcessEnvVars } = require('../../store/process-env');
|
||||
@@ -490,19 +491,21 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute merged hooks for a specific event
|
||||
* @param {string} hookEvent - Hook event to trigger
|
||||
* @param {object} eventData - Data to pass to hook handlers
|
||||
* Initialize hooks by evaluating the hooks file once.
|
||||
* Returns context for subsequent event triggering via triggerHookEvent().
|
||||
* Caller must dispose hooksCtx.hookManager when done with all events.
|
||||
* @param {object} options - Configuration options
|
||||
* @returns {Promise<object|null>} Execution result or null if error
|
||||
* @returns {Promise<object|null>} Hooks context or null if no hooks/error
|
||||
*/
|
||||
const executeHooks = async (hookEvent, eventData, options) => {
|
||||
const initHooks = async (options) => {
|
||||
const hooksFile = options.request?.script?.hooks;
|
||||
if (!hooksFile || !hooksFile.trim()) return null;
|
||||
|
||||
try {
|
||||
const hooksRuntime = new HooksRuntime({ runtime: options.scriptingConfig?.runtime });
|
||||
const result = await hooksRuntime.runHooks({
|
||||
hooksFile: options.request?.script?.hooks,
|
||||
hooksFile,
|
||||
request: options.request || {},
|
||||
response: eventData.response,
|
||||
envVariables: options.envVars,
|
||||
runtimeVariables: options.runtimeVariables,
|
||||
collectionPath: options.collectionPath,
|
||||
@@ -513,29 +516,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionName: options.collectionName
|
||||
});
|
||||
|
||||
if (result?.hookManager) {
|
||||
// Enrich eventData with runtime-created req/res wrappers
|
||||
const enrichedEventData = {
|
||||
...eventData,
|
||||
req: result.req || eventData.req,
|
||||
res: result.res || eventData.res
|
||||
};
|
||||
await result.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;
|
||||
if (bru) {
|
||||
result.nextRequestName = bru.nextRequest;
|
||||
result.skipRequest = bru.skipRequest;
|
||||
result.stopExecution = bru.stopExecution;
|
||||
}
|
||||
|
||||
result.hookManager.dispose();
|
||||
}
|
||||
|
||||
// Send UI updates if we have a result
|
||||
if (result) {
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult: result,
|
||||
@@ -545,23 +525,55 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
updateCookies: true
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error initializing hooks:', error);
|
||||
options.onConsoleLog?.('error', [`Error initializing hooks: ${error.message}`]);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Trigger a specific hook event using an already-initialized hooks context.
|
||||
* Re-reads runner control signals from bru instance after handlers execute.
|
||||
* @param {object} hooksCtx - Context from initHooks()
|
||||
* @param {string} hookEvent - Hook event to trigger
|
||||
* @param {object} eventData - Data to pass to hook handlers
|
||||
* @param {object} options - Configuration options
|
||||
* @returns {Promise<object|null>} Updated hooks context or null if error
|
||||
*/
|
||||
const triggerHookEvent = async (hooksCtx, hookEvent, eventData, options) => {
|
||||
if (!hooksCtx?.hookManager) return null;
|
||||
|
||||
try {
|
||||
const enrichedEventData = {
|
||||
...eventData,
|
||||
req: hooksCtx.req || eventData.req,
|
||||
res: eventData.response ? new BrunoResponse(eventData.response) : (hooksCtx.res || eventData.res)
|
||||
};
|
||||
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()
|
||||
const bru = hooksCtx.__bru;
|
||||
if (bru) {
|
||||
hooksCtx.nextRequestName = bru.nextRequest;
|
||||
hooksCtx.skipRequest = bru.skipRequest;
|
||||
hooksCtx.stopExecution = bru.stopExecution;
|
||||
}
|
||||
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult: hooksCtx,
|
||||
collection: options.collection,
|
||||
collectionUid: options.collectionUid,
|
||||
requestUid: options.requestUid,
|
||||
updateCookies: true
|
||||
});
|
||||
|
||||
return hooksCtx;
|
||||
} catch (error) {
|
||||
console.error(`Error executing hooks for ${hookEvent}:`, error);
|
||||
options.onConsoleLog?.('error', [`Error executing hooks for ${hookEvent}: ${error.message}`]);
|
||||
if (!options.runInBackground && options.notifyScriptExecution && typeof options.notifyScriptExecution === 'function') {
|
||||
options.notifyScriptExecution({
|
||||
channel: 'main:run-request-event',
|
||||
basePayload: {
|
||||
requestUid: options.requestUid,
|
||||
collectionUid: options.collectionUid,
|
||||
itemUid: options.itemUid
|
||||
},
|
||||
scriptType: 'hooks',
|
||||
error
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -766,10 +778,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
request.signal = abortController.signal;
|
||||
saveCancelToken(cancelTokenUid, abortController);
|
||||
|
||||
// Call beforeRequest hooks before running pre-request scripts
|
||||
// Initialize hooks once for this request lifecycle
|
||||
// Hooks are merged in prepareRequest via mergeScripts
|
||||
// Hooks are called in registration order: collection -> folder(s) -> request
|
||||
const beforeRequestEventData = { request, collection, collectionUid };
|
||||
const hookOptions = {
|
||||
request,
|
||||
envVars,
|
||||
@@ -788,12 +799,11 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
notifyScriptExecution
|
||||
};
|
||||
|
||||
// Call beforeRequest hooks using merged hooks
|
||||
await executeHooks(
|
||||
HOOK_EVENTS.HTTP_BEFORE_REQUEST,
|
||||
beforeRequestEventData,
|
||||
hookOptions
|
||||
);
|
||||
const hooksCtx = await initHooks(hookOptions);
|
||||
|
||||
// Call beforeRequest hooks using initialized hooks context
|
||||
const beforeRequestEventData = { request, collection, collectionUid };
|
||||
await triggerHookEvent(hooksCtx, HOOK_EVENTS.HTTP_BEFORE_REQUEST, beforeRequestEventData, hookOptions);
|
||||
|
||||
let preRequestScriptResult = null;
|
||||
let preRequestError = null;
|
||||
@@ -963,12 +973,13 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
// Hooks are called in registration order: collection -> folder(s) -> request
|
||||
const afterResponseEventData = { request, response, collection, collectionUid };
|
||||
|
||||
// Call afterResponse hooks using merged hooks
|
||||
await executeHooks(
|
||||
HOOK_EVENTS.HTTP_AFTER_RESPONSE,
|
||||
afterResponseEventData,
|
||||
hookOptions
|
||||
);
|
||||
// Call afterResponse hooks using already-initialized hooks context
|
||||
await triggerHookEvent(hooksCtx, HOOK_EVENTS.HTTP_AFTER_RESPONSE, afterResponseEventData, hookOptions);
|
||||
|
||||
// Dispose hooks context — done with all events for this request
|
||||
if (hooksCtx?.hookManager) {
|
||||
hooksCtx.hookManager.dispose();
|
||||
}
|
||||
|
||||
const runPostScripts = async () => {
|
||||
let postResponseScriptResult = null;
|
||||
@@ -1286,61 +1297,61 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
const isCollectionRun = folder?.uid === collection?.uid;
|
||||
|
||||
// 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;
|
||||
if (isCollectionRun) {
|
||||
const 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: (type, args) => {
|
||||
console[type](...args);
|
||||
mainWindow.webContents.send('main:console-log', {
|
||||
type,
|
||||
args
|
||||
});
|
||||
},
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName: collection?.name
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// Send UI updates after collection-level hooks execution
|
||||
if (result) {
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult: result,
|
||||
collection,
|
||||
collectionUid,
|
||||
updateCookies: true
|
||||
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: (type, args) => {
|
||||
console[type](...args);
|
||||
mainWindow.webContents.send('main:console-log', {
|
||||
type,
|
||||
args
|
||||
});
|
||||
},
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName: collection?.name
|
||||
});
|
||||
|
||||
if (collectionHooksCtx) {
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult: collectionHooksCtx,
|
||||
collection,
|
||||
collectionUid,
|
||||
updateCookies: true
|
||||
});
|
||||
}
|
||||
} 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
|
||||
if (isCollectionRun) {
|
||||
await executeCollectionHooks(HOOK_EVENTS.RUNNER_BEFORE_COLLECTION_RUN, { collection, collectionUid });
|
||||
if (collectionHooksCtx?.hookManager) {
|
||||
try {
|
||||
await collectionHooksCtx.hookManager.call(HOOK_EVENTS.RUNNER_BEFORE_COLLECTION_RUN, { collection, collectionUid });
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult: collectionHooksCtx,
|
||||
collection,
|
||||
collectionUid,
|
||||
updateCookies: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error executing beforeCollectionRun hook:', error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1493,11 +1504,15 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize hooks once per request iteration (reused for beforeRequest + afterResponse)
|
||||
const hooksCtx = await initHooks(hookOptions);
|
||||
|
||||
try {
|
||||
// Call beforeRequest hooks using merged hooks
|
||||
// Call beforeRequest hooks using initialized hooks context
|
||||
const beforeRequestEventData = { request, collection, collectionUid };
|
||||
|
||||
const beforeRequestHooksResult = await executeHooks(
|
||||
const beforeRequestHooksResult = await triggerHookEvent(
|
||||
hooksCtx,
|
||||
HOOK_EVENTS.HTTP_BEFORE_REQUEST,
|
||||
beforeRequestEventData,
|
||||
hookOptions
|
||||
@@ -1748,15 +1763,21 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Call afterResponse hooks using merged hooks
|
||||
// Call afterResponse hooks using already-initialized hooks context
|
||||
const afterResponseEventData = { request, response, collection, collectionUid };
|
||||
|
||||
const afterResponseHooksResult = await executeHooks(
|
||||
const afterResponseHooksResult = await triggerHookEvent(
|
||||
hooksCtx,
|
||||
HOOK_EVENTS.HTTP_AFTER_RESPONSE,
|
||||
afterResponseEventData,
|
||||
hookOptions
|
||||
);
|
||||
|
||||
// Dispose hooks context — done with all events for this request
|
||||
if (hooksCtx?.hookManager) {
|
||||
hooksCtx.hookManager.dispose();
|
||||
}
|
||||
|
||||
// Check runner control from hooks
|
||||
if (afterResponseHooksResult?.nextRequestName !== undefined) {
|
||||
nextRequestName = afterResponseHooksResult.nextRequestName;
|
||||
@@ -1939,8 +1960,20 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
// Call onAfterCollectionRun hook after all requests are done
|
||||
if (isCollectionRun) {
|
||||
await executeCollectionHooks(HOOK_EVENTS.RUNNER_AFTER_COLLECTION_RUN, { collection, collectionUid });
|
||||
if (collectionHooksCtx?.hookManager) {
|
||||
try {
|
||||
await collectionHooksCtx.hookManager.call(HOOK_EVENTS.RUNNER_AFTER_COLLECTION_RUN, { collection, collectionUid });
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult: collectionHooksCtx,
|
||||
collection,
|
||||
collectionUid,
|
||||
updateCookies: true
|
||||
});
|
||||
} catch (hookError) {
|
||||
console.error('Error executing afterCollectionRun hook:', hookError);
|
||||
}
|
||||
collectionHooksCtx.hookManager.dispose();
|
||||
collectionHooksCtx = null;
|
||||
}
|
||||
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
@@ -1954,8 +1987,20 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
console.log('error', error);
|
||||
|
||||
// Call onAfterCollectionRun hook even on error
|
||||
if (isCollectionRun) {
|
||||
await executeCollectionHooks(HOOK_EVENTS.RUNNER_AFTER_COLLECTION_RUN, { collection, collectionUid });
|
||||
if (collectionHooksCtx?.hookManager) {
|
||||
try {
|
||||
await collectionHooksCtx.hookManager.call(HOOK_EVENTS.RUNNER_AFTER_COLLECTION_RUN, { collection, collectionUid });
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult: collectionHooksCtx,
|
||||
collection,
|
||||
collectionUid,
|
||||
updateCookies: true
|
||||
});
|
||||
} catch (hookError) {
|
||||
console.error('Error executing afterCollectionRun hook:', hookError);
|
||||
}
|
||||
collectionHooksCtx.hookManager.dispose();
|
||||
collectionHooksCtx = null;
|
||||
}
|
||||
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
|
||||
@@ -249,17 +249,6 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
request.script.hooks = compact(hooksScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 flattenItems = (items = []) => {
|
||||
const flattenedItems = [];
|
||||
|
||||
@@ -785,6 +774,5 @@ module.exports = {
|
||||
getEnvVars,
|
||||
getFormattedCollectionOauth2Credentials,
|
||||
sortByNameThenSequence,
|
||||
resolveInheritedSettings,
|
||||
HOOK_EVENTS
|
||||
resolveInheritedSettings
|
||||
};
|
||||
|
||||
@@ -312,29 +312,17 @@ class Bru {
|
||||
this.hooks = {
|
||||
runner: {
|
||||
onBeforeCollectionRun: (handler) => {
|
||||
if (!this.#hookManager) {
|
||||
throw new Error('HookManager is not available');
|
||||
}
|
||||
return this.#hookManager.on('runner:beforeCollectionRun', handler);
|
||||
},
|
||||
onAfterCollectionRun: (handler) => {
|
||||
if (!this.#hookManager) {
|
||||
throw new Error('HookManager is not available');
|
||||
}
|
||||
return this.#hookManager.on('runner:afterCollectionRun', handler);
|
||||
}
|
||||
},
|
||||
http: {
|
||||
onBeforeRequest: (handler) => {
|
||||
if (!this.#hookManager) {
|
||||
throw new Error('HookManager is not available');
|
||||
}
|
||||
return this.#hookManager.on('http:beforeRequest', handler);
|
||||
},
|
||||
onAfterResponse: (handler) => {
|
||||
if (!this.#hookManager) {
|
||||
throw new Error('HookManager is not available');
|
||||
}
|
||||
return this.#hookManager.on('http:afterResponse', handler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* HookManager provides a simple event system for registering and calling hooks (event listeners).
|
||||
*
|
||||
* Hooks can be registered for specific string patterns or arrays of patterns. The special pattern '*' acts as a wildcard.
|
||||
* Hooks can be registered for specific string patterns or arrays of patterns.
|
||||
*
|
||||
* Usage examples:
|
||||
*
|
||||
@@ -36,6 +36,17 @@ class HookManager {
|
||||
DISPOSED: 'disposed'
|
||||
});
|
||||
|
||||
/**
|
||||
* Standard hook event names used throughout the application.
|
||||
* @readonly
|
||||
*/
|
||||
static 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'
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.listeners = {};
|
||||
this._state = HookManager.State.ACTIVE;
|
||||
@@ -87,8 +98,6 @@ class HookManager {
|
||||
/**
|
||||
* Call all registered handlers for the given pattern(s)
|
||||
* Supports both sync and async handlers - all handlers are awaited
|
||||
* Wildcard handlers ('*') are called for every pattern, in addition to specific pattern handlers
|
||||
*
|
||||
* Error Isolation: Errors in one handler don't stop other handlers from running.
|
||||
* Errors are logged to console but don't affect execution flow.
|
||||
*
|
||||
@@ -108,31 +117,11 @@ class HookManager {
|
||||
}
|
||||
|
||||
const patternList = [].concat(pattern).map((d) => String(d).trim());
|
||||
const hasWildcard = patternList.includes('*');
|
||||
|
||||
/**
|
||||
* Execute a single handler with error handling
|
||||
* @param {Function} handler - Handler to execute
|
||||
* @param {string} event - Event name
|
||||
*/
|
||||
const executeHandler = async (handler, event) => {
|
||||
await callHandler(handler, data, event);
|
||||
};
|
||||
|
||||
if (hasWildcard) {
|
||||
for (const ptn of Object.keys(this.listeners)) {
|
||||
const handlers = this.listeners[ptn];
|
||||
for (const handler of handlers) {
|
||||
await executeHandler(handler, ptn);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Call handlers for each specific pattern
|
||||
for (const ptn of patternList) {
|
||||
if (!this.listeners[ptn]) continue;
|
||||
for (const handler of this.listeners[ptn]) {
|
||||
await executeHandler(handler, ptn);
|
||||
}
|
||||
for (const ptn of patternList) {
|
||||
if (!this.listeners[ptn]) continue;
|
||||
for (const handler of this.listeners[ptn]) {
|
||||
await callHandler(handler, data, ptn);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,12 +145,6 @@ class HookManager {
|
||||
}
|
||||
|
||||
const patternList = [].concat(pattern).map((d) => String(d).trim());
|
||||
const hasWildcard = patternList.includes('*');
|
||||
|
||||
if (hasWildcard) {
|
||||
(this.listeners['*'] ||= []).push(handler);
|
||||
return this._createUnhook(patternList, handler);
|
||||
}
|
||||
|
||||
for (const ptn of patternList) {
|
||||
this.listeners[ptn] ||= [];
|
||||
@@ -192,12 +175,6 @@ class HookManager {
|
||||
patterns = patternList;
|
||||
}
|
||||
|
||||
const hasStar = patterns.includes('*');
|
||||
|
||||
if (hasStar && self.listeners['*']) {
|
||||
self.listeners['*'] = self.listeners['*'].filter((d) => !Object.is(d, handler));
|
||||
}
|
||||
|
||||
for (const ptn of patterns) {
|
||||
if (!self.listeners[ptn]) continue;
|
||||
self.listeners[ptn] = self.listeners[ptn].filter((d) => !Object.is(d, handler));
|
||||
@@ -217,9 +194,7 @@ class HookManager {
|
||||
const patternList = [].concat(pattern).map((d) => String(d).trim());
|
||||
|
||||
for (const ptn of patternList) {
|
||||
if (ptn === '*') {
|
||||
delete this.listeners['*'];
|
||||
} else if (this.listeners[ptn]) {
|
||||
if (this.listeners[ptn]) {
|
||||
delete this.listeners[ptn];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ const VarsRuntime = require('./runtime/vars-runtime');
|
||||
const AssertRuntime = require('./runtime/assert-runtime');
|
||||
const HooksRuntime = require('./runtime/hooks-runtime');
|
||||
const HookManager = require('./hook-manager');
|
||||
const BrunoResponse = require('./bruno-response');
|
||||
const { runScriptInNodeVm } = require('./sandbox/node-vm');
|
||||
|
||||
module.exports = {
|
||||
@@ -13,5 +14,6 @@ module.exports = {
|
||||
AssertRuntime,
|
||||
HooksRuntime,
|
||||
HookManager,
|
||||
BrunoResponse,
|
||||
runScriptInNodeVm
|
||||
};
|
||||
|
||||
@@ -110,56 +110,8 @@ class HooksRuntime {
|
||||
context.bru.runRequest = runRequestByItemPathname;
|
||||
}
|
||||
|
||||
// Lazy VM creation: If no hooks file, return early without creating a VM
|
||||
if (this._isEmptyContent(hooksFile)) {
|
||||
return {
|
||||
hookManager: activeHookManager,
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
persistentEnvVariables: bru.persistentEnvVariables,
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
nextRequestName: bru.nextRequest,
|
||||
skipRequest: bru.skipRequest,
|
||||
stopExecution: bru.stopExecution,
|
||||
__bru: bru,
|
||||
req,
|
||||
res
|
||||
};
|
||||
}
|
||||
|
||||
// Execute hooks script
|
||||
// Note: Hooks need the VM to persist so registered handlers can be called later
|
||||
if (this.runtime === 'nodevm') {
|
||||
await runScriptInNodeVm({
|
||||
script: hooksFile,
|
||||
context,
|
||||
collectionPath,
|
||||
scriptingConfig
|
||||
});
|
||||
|
||||
return {
|
||||
hookManager: activeHookManager,
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
persistentEnvVariables: bru.persistentEnvVariables,
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
nextRequestName: bru.nextRequest,
|
||||
skipRequest: bru.skipRequest,
|
||||
stopExecution: bru.stopExecution,
|
||||
__bru: bru,
|
||||
req,
|
||||
res
|
||||
};
|
||||
}
|
||||
|
||||
// For QuickJS, persist the VM so hook handlers can be called later during the collection run
|
||||
await executeQuickJsVmAsync({
|
||||
script: hooksFile,
|
||||
context: context,
|
||||
collectionPath
|
||||
});
|
||||
|
||||
return {
|
||||
// Build result from current state — shared across all return paths
|
||||
const buildResult = () => ({
|
||||
hookManager: activeHookManager,
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
@@ -171,7 +123,32 @@ class HooksRuntime {
|
||||
__bru: bru,
|
||||
req,
|
||||
res
|
||||
};
|
||||
});
|
||||
|
||||
// Lazy VM creation: If no hooks file, return early without creating a VM
|
||||
if (this._isEmptyContent(hooksFile)) {
|
||||
return buildResult();
|
||||
}
|
||||
|
||||
// Execute hooks script
|
||||
// Note: Hooks need the VM to persist so registered handlers can be called later
|
||||
if (this.runtime === 'nodevm') {
|
||||
await runScriptInNodeVm({
|
||||
script: hooksFile,
|
||||
context,
|
||||
collectionPath,
|
||||
scriptingConfig
|
||||
});
|
||||
} else {
|
||||
// QuickJS: persist the VM so hook handlers can be called later during the collection run
|
||||
await executeQuickJsVmAsync({
|
||||
script: hooksFile,
|
||||
context: context,
|
||||
collectionPath
|
||||
});
|
||||
}
|
||||
|
||||
return buildResult();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -398,8 +398,6 @@ const addBruShimToContext = (vm, bru) => {
|
||||
|
||||
// Add hooks shim if bru.hooks exists
|
||||
if (bru.hooks) {
|
||||
const hooksObject = vm.newObject();
|
||||
|
||||
// Execute handler using the original function handle from the VM
|
||||
// Returns a Promise that resolves when the handler completes (supports async handlers)
|
||||
const executeHandler = async (handlerHandle, vmInstance, data) => {
|
||||
|
||||
@@ -61,12 +61,6 @@ describe('HookManager', () => {
|
||||
expect(() => hookManager.on('test', handler)).toThrow();
|
||||
});
|
||||
|
||||
it('should register wildcard handlers', () => {
|
||||
const handler = jest.fn();
|
||||
hookManager.on('*', handler);
|
||||
expect(hookManager.listeners['*']).toContain(handler);
|
||||
});
|
||||
|
||||
it('should throw if HookManager is disposed', () => {
|
||||
hookManager.dispose();
|
||||
expect(() => hookManager.on('test', jest.fn())).toThrow(/disposed/);
|
||||
@@ -119,16 +113,6 @@ describe('HookManager', () => {
|
||||
expect(results).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('should call all handlers when called with wildcard pattern', async () => {
|
||||
const handler1 = jest.fn();
|
||||
const handler2 = jest.fn();
|
||||
hookManager.on('event1', handler1);
|
||||
hookManager.on('event2', handler2);
|
||||
await hookManager.call('*', {});
|
||||
expect(handler1).toHaveBeenCalled();
|
||||
expect(handler2).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors without stopping execution', async () => {
|
||||
const errorHandler = jest.fn(() => { throw new Error('Test error'); });
|
||||
const normalHandler = jest.fn();
|
||||
|
||||
Reference in New Issue
Block a user