Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/bruno-app/src/providers/App/useIpcEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ const useIpcEvents = () => {
const removeScriptEnvUpdateListener = ipcRenderer.on('main:script-environment-update', (val) => {
dispatch(scriptEnvironmentUpdateEvent(val));
if (val.collectionUid) {
dispatch(persistActiveEnvironment(val.collectionUid, val.requestUid));
dispatch(persistActiveEnvironment(val.collectionUid));
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,8 +505,6 @@ export const wsConnectOnly = (item, collectionUid) => (dispatch, getState) => {
const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);

// WS connect does not run user scripts — no baseline to clear.
// Wiping baselines here would also wipe collection._scriptRequestUid, opening
// a window where a late HTTP post-response could pass the stale-update gate.

connectWS(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables, { connectOnly: true })
.then(resolve)
Expand Down Expand Up @@ -2412,15 +2410,11 @@ export const clearScriptVariableBaselines = (collectionUid) => (dispatch) => {
dispatch(_clearScriptGlobalEnvBaseline());
};

export const persistActiveEnvironment = (collectionUid, requestUid) => (dispatch, getState) => {
export const persistActiveEnvironment = (collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if (!collection) return;

// Ignore stale updates from superseded requests so an in-flight pre/post
// from request N-1 can't trigger a disk write for request N.
if (requestUid && collection._scriptRequestUid && requestUid !== collection._scriptRequestUid) return;

const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
if (!environment) return;

Expand All @@ -2441,18 +2435,13 @@ export const persistActiveEnvironment = (collectionUid, requestUid) => (dispatch
.catch((err) => console.error('Failed to persist environment during script execution:', err));
};

export const collectionVariablesUpdateEvent = ({ collectionVariables, collectionUid, requestUid }) => (dispatch, getState) => {
export const collectionVariablesUpdateEvent = ({ collectionVariables, collectionUid }) => (dispatch, getState) => {
if (!collectionVariables || !collectionUid) return;

const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if (!collection) return;

// Ignore stale updates from superseded requests.
if (requestUid && collection._scriptRequestUid && requestUid !== collection._scriptRequestUid) {
return;
}

const savedVars = get(collection, 'root.request.vars.req', []);
const draftVars = collection.draft?.root
? get(collection, 'draft.root.request.vars.req', null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -459,12 +459,6 @@ export const collectionsSlice = createSlice({
const collection = findCollectionByUid(state.collections, collectionUid);

if (collection) {
// Ignore stale updates from superseded requests so an in-flight pre/post
// from request N-1 can't clobber state for request N.
if (requestUid && collection._scriptRequestUid && requestUid !== collection._scriptRequestUid) {
return;
}

const activeEnvironment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);

if (activeEnvironment) {
Expand Down Expand Up @@ -503,12 +497,9 @@ export const collectionsSlice = createSlice({
}
},
runtimeVariablesUpdateEvent: (state, action) => {
const { collectionUid, runtimeVariables, requestUid } = action.payload;
const { collectionUid, runtimeVariables } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
if (requestUid && collection._scriptRequestUid && requestUid !== collection._scriptRequestUid) {
return;
}
collection.runtimeVariables = runtimeVariables;
}
},
Expand Down Expand Up @@ -2772,9 +2763,6 @@ export const collectionsSlice = createSlice({
if (!collection) return;
delete collection._scriptEnvBaseline;
delete collection._scriptCollVarBaseline;
// Also drop the inflight request UID so updates from WS/OAuth2 paths (which
// don't dispatch initRunRequestEvent) aren't gated out by a stale HTTP UID.
delete collection._scriptRequestUid;
},
collectionAddFileEvent: (state, action) => {
const file = action.payload.file;
Expand Down Expand Up @@ -3089,7 +3077,6 @@ export const collectionsSlice = createSlice({

delete collection._scriptEnvBaseline;
delete collection._scriptCollVarBaseline;
collection._scriptRequestUid = requestUid;

const item = findItemInCollection(collection, itemUid);
if (!item) return;
Expand Down Expand Up @@ -3248,7 +3235,6 @@ export const collectionsSlice = createSlice({
// pre-flush snapshot.
delete collection._scriptEnvBaseline;
delete collection._scriptCollVarBaseline;
collection._scriptRequestUid = action.payload.requestUid || null;

collection.runnerResult.items.push({
uid: request.uid,
Expand Down Expand Up @@ -3297,8 +3283,10 @@ export const collectionsSlice = createSlice({

if (type === 'runner-request-skipped') {
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
item.status = 'skipped';
item.responseReceived = action.payload.responseReceived;
if (item) {
item.status = 'skipped';
item.responseReceived = action.payload.responseReceived;
}
}

if (type === 'post-response-script-execution') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,19 +284,11 @@ export const deleteGlobalEnvironment = ({ environmentUid }) => (dispatch, getSta
});
};

export const globalEnvironmentsUpdateEvent = ({ globalEnvironmentVariables, collectionUid, requestUid }) => (dispatch, getState) => {
export const globalEnvironmentsUpdateEvent = ({ globalEnvironmentVariables }) => (dispatch, getState) => {
if (!globalEnvironmentVariables) return;

const state = getState();

// Ignore stale updates from superseded requests on the originating collection.
if (collectionUid && requestUid) {
const sourceCollection = state?.collections?.collections?.find((c) => c.uid === collectionUid);
if (sourceCollection?._scriptRequestUid && requestUid !== sourceCollection._scriptRequestUid) {
return;
}
}

const globalEnvironments = state?.globalEnvironments?.globalEnvironments || [];
const environmentUid = state?.globalEnvironments?.activeGlobalEnvironmentUid;
const environment = globalEnvironments?.find((env) => env?.uid == environmentUid);
Expand Down
37 changes: 23 additions & 14 deletions packages/bruno-electron/src/ipc/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ const {
isBrunoConfigFile,
isBruEnvironmentConfig,
isCollectionRootBruFile,
scanForBrunoFiles
scanForBrunoFiles,
withFileLock
} = require('../utils/filesystem');
const { getCollectionConfigFile, openCollection, openCollectionDialog, openCollectionsByPathname, registerScratchCollectionPath } = require('../app/collections');
const { generateUidBasedOnHash, stringifyJson, safeStringifyJSON, safeParseJSON } = require('../utils/common');
Expand Down Expand Up @@ -764,14 +765,21 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
throw new Error(`environment: ${envFilePath} does not exist`);
}

if (envHasSecrets(environment)) {
environmentSecretsStore.storeEnvSecrets(collectionPathname, environment);
}
// Serialize concurrent saves to the same env file. Without the lock the
// read-then-write pattern below can interleave: writer A reads pre-A state,
// writer B reads pre-A state, B writes B-content, A writes A-content —
// dropping B's update. Rapid scripted `bru.setEnvVar(..., {persist:true})`
// calls (e.g. across folder-run requests) hit this without serialization.
await withFileLock(envFilePath, async () => {
if (envHasSecrets(environment)) {
environmentSecretsStore.storeEnvSecrets(collectionPathname, environment);
}

const content = await stringifyEnvironment(environment, { format });
const existing = fs.readFileSync(envFilePath, 'utf8');
if (content === existing) return; // skip write if content unchanged
await writeFile(envFilePath, content);
const content = await stringifyEnvironment(environment, { format });
const existing = fs.readFileSync(envFilePath, 'utf8');
if (content === existing) return; // skip write if content unchanged
await writeFile(envFilePath, content);
});
Comment thread
sanish-bruno marked this conversation as resolved.
} catch (error) {
return Promise.reject(error);
}
Expand Down Expand Up @@ -909,12 +917,13 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
throw new Error(`environment: ${envFilePath} does not exist`);
}

// Read, update color, and write back to file
const fileContent = fs.readFileSync(envFilePath, 'utf8');
const environment = parseEnvironment(fileContent, { format });
environment.color = color;
const updatedContent = stringifyEnvironment(environment, { format });
fs.writeFileSync(envFilePath, updatedContent, 'utf8');
await withFileLock(envFilePath, async () => {
const fileContent = fs.readFileSync(envFilePath, 'utf8');
const environment = parseEnvironment(fileContent, { format });
environment.color = color;
const updatedContent = stringifyEnvironment(environment, { format });
await writeFile(envFilePath, updatedContent);
});
} catch (error) {
return Promise.reject(error);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const { uuid } = require('../../utils/common');

// Mutates `collection.root.request.vars.req` (and draft.root.request.vars.req if
// present) to reflect a script's collection-variable changes, so the next request
// in a folder/collection run's iteration picks them up through mergeVars().
// Without this, bru.setCollectionVar() inside request N would be invisible to
// request N+1 in the same iteration (envVars/globalEnvironmentVariables already
// propagate because they're mutated in place by reference).
//
// TODO: this is a write-back patch, not the structural fix. The root cause
// is that mergeVars (utils/collection.js) rebuilds `collectionVariables` fresh
// from `collection.root.request.vars.req` on every iteration, while envVars is
// built once at runner scope and passed by reference. May need a re-wire.
const applyCollectionVarsToCollectionRoot = (collection, collectionVariables) => {
if (!collectionVariables || typeof collectionVariables !== 'object') return;

const writeBack = (root) => {
if (!root) return;
if (!root.request) root.request = {};
if (!root.request.vars) root.request.vars = {};
const existing = Array.isArray(root.request.vars.req) ? root.request.vars.req : [];

const disabled = existing.filter((v) => !v.enabled);
const enabledByName = new Map(existing.filter((v) => v.enabled).map((v) => [v.name, v]));
const scriptNames = Object.keys(collectionVariables);

// Rebuild the enabled slice from the script's output. Keys present here are
// kept (with updated value); previously-enabled keys missing from the script
// output are treated as `bru.deleteCollectionVar` and dropped. Disabled vars
// (user-disabled in the UI) are preserved untouched.
const updatedEnabled = scriptNames.map((name) => {
const existingVar = enabledByName.get(name);
if (existingVar) {
return { ...existingVar, value: collectionVariables[name] };
}
return { uid: uuid(), name, value: collectionVariables[name], type: 'text', enabled: true };
});

root.request.vars.req = [...updatedEnabled, ...disabled];
};

writeBack(collection.root);
if (collection.draft?.root) writeBack(collection.draft.root);
};

module.exports = { applyCollectionVarsToCollectionRoot };
Loading
Loading