diff --git a/packages/bruno-app/jsconfig.json b/packages/bruno-app/jsconfig.json
index 257b99e10..a71bc3138 100644
--- a/packages/bruno-app/jsconfig.json
+++ b/packages/bruno-app/jsconfig.json
@@ -13,8 +13,7 @@
"api/*": ["src/api/*"],
"pageComponents/*": ["src/pageComponents/*"],
"providers/*": ["src/providers/*"],
- "utils/*": ["src/utils/*"],
- "store/*": ["src/store/*"]
+ "utils/*": ["src/utils/*"]
}
},
"exclude": ["node_modules", "dist"]
diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js
index 99051b5f4..888149f78 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js
@@ -1,6 +1,7 @@
-import React, { useMemo } from 'react';
+import React from 'react';
import { getTotalRequestCountInCollection } from 'utils/collections/';
import { IconFolder, IconWorld, IconApi, IconShare, IconBook } from '@tabler/icons';
+import { areItemsLoading, getItemsLoadStats } from 'utils/collections/index';
import { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import ShareCollection from 'components/ShareCollection/index';
@@ -10,13 +11,10 @@ import StyledWrapper from './StyledWrapper';
const Info = ({ collection }) => {
const dispatch = useDispatch();
+ const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
- const isCollectionLoading = collection.isLoading;
-
- const totalRequestsInCollection = useMemo(
- () => getTotalRequestCountInCollection(collection),
- [collection.items]
- );
+ const isCollectionLoading = areItemsLoading(collection);
+ const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection);
const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false);
const [showGenerateDocumentationModal, setShowGenerateDocumentationModal] = useState(false);
@@ -97,9 +95,7 @@ const Info = ({ collection }) => {
Requests
{
- isCollectionLoading
- ? 'Loading requests...'
- : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
+ isCollectionLoading ? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
}
diff --git a/packages/bruno-app/src/components/Preferences/Cache/StyledWrapper.js b/packages/bruno-app/src/components/Preferences/Cache/StyledWrapper.js
deleted file mode 100644
index 81dd38e70..000000000
--- a/packages/bruno-app/src/components/Preferences/Cache/StyledWrapper.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import styled from 'styled-components';
-
-const StyledWrapper = styled.div`
- color: ${(props) => props.theme.text};
-
- .cache-stats {
- padding: 1rem;
- border-radius: ${(props) => props.theme.border.radius.md};
- background-color: ${(props) => props.theme.input.bg};
- border: 1px solid ${(props) => props.theme.input.border};
- margin-bottom: 1rem;
- }
-
- .stat-item {
- display: flex;
- justify-content: space-between;
- padding: 0.5rem 0;
- border-bottom: 1px solid ${(props) => props.theme.input.border};
-
- &:last-child {
- border-bottom: none;
- }
- }
-
- .stat-label {
- font-size: ${(props) => props.theme.font.size.sm};
- color: ${(props) => props.theme.colors.text.muted};
- }
-
- .stat-value {
- font-size: ${(props) => props.theme.font.size.sm};
- font-weight: 500;
- }
-
- .purge-button {
- padding: 0.5rem 1rem;
- border-radius: ${(props) => props.theme.border.radius.sm};
- font-size: ${(props) => props.theme.font.size.sm};
- cursor: pointer;
- background-color: ${(props) => props.theme.input.bg};
- border: 1px solid ${(props) => props.theme.input.border};
- color: ${(props) => props.theme.text};
-
- &:hover:not(:disabled) {
- border-color: ${(props) => props.theme.input.focusBorder};
- }
-
- &:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- }
-
- .description {
- font-size: ${(props) => props.theme.font.size.sm};
- color: ${(props) => props.theme.colors.text.muted};
- margin-top: 0.5rem;
- }
-
- .section-title {
- font-weight: 600;
- font-size: 0.875rem;
- margin-bottom: 0.75rem;
- }
-`;
-
-export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/Preferences/Cache/index.js b/packages/bruno-app/src/components/Preferences/Cache/index.js
deleted file mode 100644
index a810a2a45..000000000
--- a/packages/bruno-app/src/components/Preferences/Cache/index.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import React, { useState, useEffect, useCallback } from 'react';
-import toast from 'react-hot-toast';
-import StyledWrapper from './StyledWrapper';
-
-const Cache = () => {
- const [stats, setStats] = useState(null);
- const [loading, setLoading] = useState(true);
- const [purging, setPurging] = useState(false);
-
- const fetchStats = useCallback(async () => {
- try {
- const cacheStats = await window.ipcRenderer.invoke('renderer:get-cache-stats');
- setStats(cacheStats);
- } catch (error) {
- console.error('Error fetching cache stats:', error);
- setStats({ error: error.message });
- } finally {
- setLoading(false);
- }
- }, []);
-
- useEffect(() => {
- fetchStats();
- }, [fetchStats]);
-
- const handlePurgeCache = async () => {
- setPurging(true);
- try {
- const result = await window.ipcRenderer.invoke('renderer:purge-cache');
- if (result.success) {
- toast.success('Cache purged successfully');
- await fetchStats();
- } else {
- toast.error(result.error || 'Failed to purge cache');
- }
- } catch (error) {
- console.error('Error purging cache:', error);
- toast.error('Failed to purge cache');
- } finally {
- setPurging(false);
- }
- };
-
- return (
-
- Collection Cache
-
- Bruno caches parsed collection files to improve loading performance. Clearing the cache will cause collections to be fully re-parsed on next load.
-
-
-
- {loading ? (
-
- Loading...
-
- ) : stats?.error ? (
-
- Error: {stats.error}
-
- ) : (
- <>
-
- Cached Collections
- {stats?.totalCollections ?? 0}
-
-
- Cached Files
- {stats?.totalFiles ?? 0}
-
-
- Cache Version
- {stats?.version ?? 'N/A'}
-
- >
- )}
-
-
-
-
- );
-};
-
-export default Cache;
diff --git a/packages/bruno-app/src/components/Preferences/index.js b/packages/bruno-app/src/components/Preferences/index.js
index ea7c188bc..547ffd09a 100644
--- a/packages/bruno-app/src/components/Preferences/index.js
+++ b/packages/bruno-app/src/components/Preferences/index.js
@@ -9,8 +9,7 @@ import {
IconUserCircle,
IconKeyboard,
IconZoomQuestion,
- IconSquareLetterB,
- IconDatabase
+ IconSquareLetterB
} from '@tabler/icons';
import Support from './Support';
@@ -20,7 +19,6 @@ import Proxy from './ProxySettings';
import Display from './Display';
import Keybindings from './Keybindings';
import Beta from './Beta';
-import Cache from './Cache';
import StyledWrapper from './StyledWrapper';
@@ -64,10 +62,6 @@ const Preferences = () => {
return ;
}
- case 'cache': {
- return ;
- }
-
case 'support': {
return ;
}
@@ -98,10 +92,6 @@ const Preferences = () => {
Keybindings
- setTab('cache')}>
-
- Cache
-
setTab('support')}>
Support
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
index b643092e4..80bd84617 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
@@ -67,7 +67,7 @@ const Collection = ({ collection, searchText }) => {
const [isKeyboardFocused, setIsKeyboardFocused] = useState(false);
const [showEmptyState, setShowEmptyState] = useState(false);
const dispatch = useDispatch();
- const isLoading = areItemsLoading(collection);
+ const isLoading = collection.isLoading;
const collectionRef = useRef(null);
const itemCount = collection.items?.length || 0;
diff --git a/packages/bruno-app/src/providers/App/index.js b/packages/bruno-app/src/providers/App/index.js
index 6304997eb..1a2d9a925 100644
--- a/packages/bruno-app/src/providers/App/index.js
+++ b/packages/bruno-app/src/providers/App/index.js
@@ -5,7 +5,6 @@ import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
import ConfirmAppClose from './ConfirmAppClose';
import useIpcEvents from './useIpcEvents';
import useTelemetry from './useTelemetry';
-import useParsedFileCacheIpc from './useParsedFileCacheIpc';
import StyledWrapper from './StyledWrapper';
import { version } from '../../../package.json';
@@ -14,7 +13,6 @@ export const AppContext = React.createContext();
export const AppProvider = (props) => {
useTelemetry({ version });
useIpcEvents();
- useParsedFileCacheIpc();
const dispatch = useDispatch();
useEffect(() => {
diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js
index 1f8e6fbe5..84b95e1a6 100644
--- a/packages/bruno-app/src/providers/App/useIpcEvents.js
+++ b/packages/bruno-app/src/providers/App/useIpcEvents.js
@@ -11,7 +11,6 @@ import {
brunoConfigUpdateEvent,
collectionAddDirectoryEvent,
collectionAddFileEvent,
- collectionBatchAddItems,
collectionChangeFileEvent,
collectionRenamedEvent,
collectionUnlinkDirectoryEvent,
@@ -102,50 +101,6 @@ const useIpcEvents = () => {
}
};
- // Batch handler for collection tree updates (performance optimization)
- // Uses a single Redux dispatch to process all items, avoiding multiple re-renders
- const _collectionTreeBatchUpdated = (batch) => {
- if (!batch || !Array.isArray(batch) || batch.length === 0) {
- return;
- }
-
- if (window.__IS_DEV__) {
- console.log('Batch update received:', batch.length, 'items');
- }
-
- // Separate batch items into those that can be bulk-processed vs those that need individual handling
- const bulkItems = []; // addFile, addDir - can be processed in single reducer
- const individualItems = []; // change, unlink, etc - need individual dispatches
-
- batch.forEach(({ eventType, payload }) => {
- if (eventType === 'addDir' || eventType === 'addFile') {
- bulkItems.push({ eventType, payload });
- } else {
- individualItems.push({ eventType, payload });
- }
- });
-
- // Process bulk items in a single dispatch (addFile and addDir)
- if (bulkItems.length > 0) {
- dispatch(collectionBatchAddItems({ items: bulkItems }));
- }
-
- // Process remaining items individually (these are typically rare during mount)
- individualItems.forEach(({ eventType, payload }) => {
- if (eventType === 'change') {
- dispatch(collectionChangeFileEvent({ file: payload }));
- } else if (eventType === 'unlink') {
- dispatch(collectionUnlinkFileEvent({ file: payload }));
- } else if (eventType === 'unlinkDir') {
- dispatch(collectionUnlinkDirectoryEvent({ directory: payload }));
- } else if (eventType === 'addEnvironmentFile') {
- dispatch(collectionAddEnvFileEvent(payload));
- } else if (eventType === 'unlinkEnvironmentFile') {
- dispatch(collectionUnlinkEnvFileEvent(payload));
- }
- });
- };
-
const _apiSpecTreeUpdated = (type, val) => {
if (window.__IS_DEV__) {
console.log('API Spec update:', type);
@@ -163,8 +118,6 @@ const useIpcEvents = () => {
const removeCollectionTreeUpdateListener = ipcRenderer.on('main:collection-tree-updated', _collectionTreeUpdated);
- const removeCollectionTreeBatchUpdateListener = ipcRenderer.on('main:collection-tree-batch-updated', _collectionTreeBatchUpdated);
-
const removeApiSpecTreeUpdateListener = ipcRenderer.on('main:apispec-tree-updated', _apiSpecTreeUpdated);
const removeOpenCollectionListener = ipcRenderer.on('main:collection-opened', (pathname, uid, brunoConfig) => {
@@ -387,7 +340,6 @@ const useIpcEvents = () => {
return () => {
removeCollectionTreeUpdateListener();
- removeCollectionTreeBatchUpdateListener();
removeApiSpecTreeUpdateListener();
removeOpenCollectionListener();
removeOpenWorkspaceListener();
diff --git a/packages/bruno-app/src/providers/App/useParsedFileCacheIpc.js b/packages/bruno-app/src/providers/App/useParsedFileCacheIpc.js
deleted file mode 100644
index 426bf8c14..000000000
--- a/packages/bruno-app/src/providers/App/useParsedFileCacheIpc.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import { useEffect } from 'react';
-import { isElectron } from 'utils/common/platform';
-import { parsedFileCacheStore } from 'store/parsedFileCache';
-
-const useParsedFileCacheIpc = () => {
- useEffect(() => {
- if (!isElectron()) {
- return () => {};
- }
-
- const { ipcRenderer } = window;
-
- const handleCacheRequest = async (operation, requestId, ...args) => {
- try {
- let result = null;
- switch (operation) {
- case 'getEntry':
- result = await parsedFileCacheStore.getEntry(...args);
- break;
- case 'setEntry':
- await parsedFileCacheStore.setEntry(...args);
- break;
- case 'invalidate':
- await parsedFileCacheStore.invalidate(...args);
- break;
- case 'invalidateCollection':
- await parsedFileCacheStore.invalidateCollection(...args);
- break;
- case 'invalidateDirectory':
- await parsedFileCacheStore.invalidateDirectory(...args);
- break;
- case 'getStats':
- result = await parsedFileCacheStore.getStats();
- break;
- case 'clear':
- await parsedFileCacheStore.clear();
- break;
- default:
- throw new Error(`Unknown cache operation: ${operation}`);
- }
- ipcRenderer.send('renderer:parsed-file-cache-response', { requestId, success: true, data: result });
- } catch (error) {
- ipcRenderer.send('renderer:parsed-file-cache-response', { requestId, success: false, error: error.message });
- }
- };
-
- const removeListener = ipcRenderer.on('main:parsed-file-cache-request', handleCacheRequest);
-
- // Prune old cache entries on startup
- parsedFileCacheStore.prune().catch((err) => {
- console.error('ParsedFileCacheStore: Error during startup prune:', err);
- });
-
- return () => {
- removeListener();
- };
- }, []);
-};
-
-export default useParsedFileCacheIpc;
diff --git a/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js b/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js
index 969dd8d62..3d0730e42 100644
--- a/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js
+++ b/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js
@@ -4,7 +4,7 @@ import filter from 'lodash/filter';
import { createListenerMiddleware } from '@reduxjs/toolkit';
import { removeTaskFromQueue } from 'providers/ReduxStore/slices/app';
import { addTab } from 'providers/ReduxStore/slices/tabs';
-import { collectionAddFileEvent, collectionChangeFileEvent, collectionBatchAddItems } from 'providers/ReduxStore/slices/collections';
+import { collectionAddFileEvent, collectionChangeFileEvent } from 'providers/ReduxStore/slices/collections';
import { findCollectionByUid, findItemInCollectionByPathname, getDefaultRequestPaneTab, findItemInCollectionByItemUid } from 'utils/collections/index';
import { taskTypes } from './utils';
@@ -51,57 +51,6 @@ taskMiddleware.startListening({
}
});
-/*
- * When files are added via batch processing (e.g., during collection mount or when new files are created),
- * we need to check if any of the added files match pending OPEN_REQUEST tasks.
- * This handles the case where file additions go through the batch reducer instead of individual events.
- */
-taskMiddleware.startListening({
- actionCreator: collectionBatchAddItems,
- effect: (action, listenerApi) => {
- const state = listenerApi.getState();
- const items = action.payload?.items || [];
-
- // Extract all addFile events from the batch
- const addFileItems = items.filter((item) => item.eventType === 'addFile');
- if (addFileItems.length === 0) return;
-
- const openRequestTasks = filter(state.app.taskQueue, { type: taskTypes.OPEN_REQUEST });
- if (openRequestTasks.length === 0) return;
-
- each(addFileItems, ({ payload: file }) => {
- const collectionUid = file?.meta?.collectionUid;
- if (!collectionUid) return;
-
- each(openRequestTasks, (task) => {
- if (collectionUid === task.collectionUid && file?.meta?.pathname === task.itemPathname) {
- const collection = findCollectionByUid(state.collections.collections, collectionUid);
- if (collection && collection.mountStatus === 'mounted' && !collection.isLoading) {
- const item = findItemInCollectionByPathname(collection, task.itemPathname);
- const isTransient = item?.isTransient ?? false;
- if (item) {
- listenerApi.dispatch(
- addTab({
- uid: item.uid,
- collectionUid: collection.uid,
- requestPaneTab: getDefaultRequestPaneTab(item),
- preview: !isTransient
- })
- );
- }
- }
-
- listenerApi.dispatch(
- removeTaskFromQueue({
- taskUid: task.uid
- })
- );
- }
- });
- });
- }
-});
-
/*
* When an example is created or cloned, a task to open the example is added to the queue.
* We wait for the File IO to complete, after which the "collectionChangeFileEvent" gets dispatched.
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
index d1c49b4ae..dc7220c74 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -162,7 +162,6 @@ export const collectionsSlice = createSlice({
collection.settingsSelectedTab = 'overview';
collection.folderLevelSettingsSelectedTab = {};
collection.allTags = []; // Initialize collection-level tags
- collection.isLoading = false;
// Collection mount status is used to track the mount status of the collection
// values can be 'unmounted', 'mounting', 'mounted'
@@ -2770,224 +2769,6 @@ export const collectionsSlice = createSlice({
addDepth(collection.items);
}
},
- // Batch reducer for adding multiple files/directories in a single state update
- // This is a performance optimization to avoid multiple re-renders during collection mount
- collectionBatchAddItems: (state, action) => {
- const { items } = action.payload;
- if (!items || !Array.isArray(items) || items.length === 0) {
- return;
- }
-
- // Group items by collection to minimize lookups
- const itemsByCollection = new Map();
- for (const item of items) {
- const collectionUid = item.payload?.meta?.collectionUid || item.payload?.collectionUid;
- if (!collectionUid) continue;
-
- if (!itemsByCollection.has(collectionUid)) {
- itemsByCollection.set(collectionUid, []);
- }
- itemsByCollection.get(collectionUid).push(item);
- }
-
- // Process each collection's items
- for (const [collectionUid, collectionItems] of itemsByCollection) {
- const collection = findCollectionByUid(state.collections, collectionUid);
- if (!collection) continue;
- const tempDirectory = state.tempDirectories?.[collectionUid];
- const folderIndex = new Map();
- const folderStack = [...collection.items];
- while (folderStack.length) {
- const item = folderStack.pop();
- if (item?.type === 'folder' && item.pathname) {
- folderIndex.set(item.pathname, item);
- if (item.items && item.items.length) {
- folderStack.push(...item.items);
- }
- }
- }
-
- for (const { eventType, payload } of collectionItems) {
- if (eventType === 'addDir') {
- const dir = payload;
- const isTransientDir = tempDirectory && dir.meta.pathname.startsWith(tempDirectory);
- const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dir.meta.pathname);
- let currentPath = collection.pathname;
- let currentSubItems = collection.items;
- let lastFolder = null;
-
- for (const directoryName of subDirectories) {
- currentPath = path.join(currentPath, directoryName);
- let childItem = folderIndex.get(currentPath);
- if (!childItem) {
- childItem = currentSubItems.find((f) => f.type === 'folder' && f.filename === directoryName);
- }
- if (!childItem) {
- childItem = {
- uid: dir?.meta?.uid || uuid(),
- pathname: currentPath,
- name: dir?.meta?.name || directoryName,
- seq: dir?.meta?.seq,
- filename: directoryName,
- collapsed: true,
- type: 'folder',
- items: [],
- isTransient: isTransientDir
- };
- currentSubItems.push(childItem);
- folderIndex.set(currentPath, childItem);
- } else if (isTransientDir && !childItem.isTransient) {
- childItem.isTransient = true;
- }
-
- currentSubItems = childItem.items;
- lastFolder = childItem;
- }
-
- if (lastFolder) {
- if (dir?.meta?.name) {
- lastFolder.name = dir.meta.name;
- }
- if (dir?.meta?.seq) {
- lastFolder.seq = dir.meta.seq;
- }
- }
- continue;
- }
-
- if (eventType === 'addFile') {
- const file = payload;
- const isCollectionRoot = file.meta.collectionRoot ? true : false;
- const isFolderRoot = file.meta.folderRoot ? true : false;
- const isTransientFile = tempDirectory && file.meta.pathname.startsWith(tempDirectory);
-
- if (isCollectionRoot) {
- collection.root = mergeRootWithPreservedUids(collection.root, file.data);
- continue;
- }
-
- if (isFolderRoot) {
- const folderPath = path.dirname(file.meta.pathname);
- const subDirectories = getSubdirectoriesFromRoot(collection.pathname, folderPath);
- let currentPath = collection.pathname;
- let currentSubItems = collection.items;
- let folderItem = folderIndex.get(folderPath);
-
- if (!folderItem) {
- for (const directoryName of subDirectories) {
- currentPath = path.join(currentPath, directoryName);
- let childItem = folderIndex.get(currentPath);
- if (!childItem) {
- childItem = currentSubItems.find((f) => f.type === 'folder' && f.filename === directoryName);
- }
- if (!childItem) {
- childItem = {
- uid: uuid(),
- pathname: currentPath,
- name: directoryName,
- collapsed: true,
- type: 'folder',
- items: [],
- isTransient: isTransientFile
- };
- currentSubItems.push(childItem);
- folderIndex.set(currentPath, childItem);
- } else if (isTransientFile && !childItem.isTransient) {
- childItem.isTransient = true;
- }
- currentSubItems = childItem.items;
- if (currentPath === folderPath) {
- folderItem = childItem;
- }
- }
- }
-
- if (folderItem) {
- if (file?.data?.meta?.name) {
- folderItem.name = file?.data?.meta?.name;
- }
- folderItem.root = mergeRootWithPreservedUids(folderItem.root, file.data);
- if (file?.data?.meta?.seq) {
- folderItem.seq = file.data?.meta?.seq;
- }
- }
- continue;
- }
-
- const dirname = path.dirname(file.meta.pathname);
- const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dirname);
- let currentPath = collection.pathname;
- let currentSubItems = collection.items;
- for (const directoryName of subDirectories) {
- currentPath = path.join(currentPath, directoryName);
- let childItem = folderIndex.get(currentPath);
- if (!childItem) {
- childItem = currentSubItems.find((f) => f.type === 'folder' && f.filename === directoryName);
- }
- if (!childItem) {
- childItem = {
- uid: uuid(),
- pathname: currentPath,
- name: directoryName,
- collapsed: true,
- type: 'folder',
- items: [],
- isTransient: isTransientFile
- };
- currentSubItems.push(childItem);
- folderIndex.set(currentPath, childItem);
- } else if (isTransientFile && !childItem.isTransient) {
- childItem.isTransient = true;
- }
- currentSubItems = childItem.items;
- }
-
- if (file.meta.name !== 'folder.bru' && !currentSubItems.find((f) => f.name === file.meta.name)) {
- const currentItem = find(currentSubItems, (i) => i.uid === file.data.uid);
- if (currentItem) {
- currentItem.name = file.data.name;
- currentItem.type = file.data.type;
- currentItem.seq = file.data.seq;
- currentItem.tags = file.data.tags;
- currentItem.request = mergeRequestWithPreservedUids(currentItem.request, file.data.request);
- currentItem.filename = file.meta.name;
- currentItem.pathname = file.meta.pathname;
- currentItem.settings = file.data.settings;
- currentItem.examples = file.data.examples;
- currentItem.draft = null;
- currentItem.partial = file.partial;
- currentItem.loading = file.loading;
- currentItem.size = file.size;
- currentItem.error = file.error;
- currentItem.isTransient = isTransientFile;
- } else {
- currentSubItems.push({
- uid: file.data.uid,
- name: file.data.name,
- type: file.data.type,
- seq: file.data.seq,
- tags: file.data.tags,
- request: file.data.request,
- settings: file.data.settings,
- examples: file.data.examples,
- filename: file.meta.name,
- pathname: file.meta.pathname,
- draft: null,
- partial: file.partial,
- loading: file.loading,
- size: file.size,
- error: file.error,
- isTransient: isTransientFile
- });
- }
- }
- }
- }
-
- // Call addDepth once per collection after all items are added
- addDepth(collection.items);
- }
- },
collectionChangeFileEvent: (state, action) => {
const { file } = action.payload;
const isCollectionRoot = file.meta.collectionRoot ? true : false;
@@ -3002,33 +2783,7 @@ export const collectionsSlice = createSlice({
if (isFolderRoot) {
const folderPath = path.dirname(file.meta.pathname);
- let folderItem = findItemInCollectionByPathname(collection, folderPath);
-
- if (!folderItem && collection) {
- const subDirectories = getSubdirectoriesFromRoot(collection.pathname, folderPath);
- let currentPath = collection.pathname;
- let currentSubItems = collection.items;
- for (const directoryName of subDirectories) {
- let childItem = currentSubItems.find((f) => f.type === 'folder' && f.filename === directoryName);
- currentPath = path.join(currentPath, directoryName);
- if (!childItem) {
- childItem = {
- uid: uuid(),
- pathname: currentPath,
- name: directoryName,
- collapsed: true,
- type: 'folder',
- items: []
- };
- currentSubItems.push(childItem);
- }
- currentSubItems = childItem.items;
- if (currentPath === folderPath) {
- folderItem = childItem;
- }
- }
- }
-
+ const folderItem = findItemInCollectionByPathname(collection, folderPath);
if (folderItem) {
if (file?.data?.meta?.name) {
folderItem.name = file?.data?.meta?.name;
@@ -3907,7 +3662,6 @@ export const {
updateCollectionProtobuf,
collectionAddFileEvent,
collectionAddDirectoryEvent,
- collectionBatchAddItems,
collectionChangeFileEvent,
collectionUnlinkFileEvent,
collectionUnlinkDirectoryEvent,
diff --git a/packages/bruno-app/src/store/parsedFileCache.js b/packages/bruno-app/src/store/parsedFileCache.js
deleted file mode 100644
index 416e3577c..000000000
--- a/packages/bruno-app/src/store/parsedFileCache.js
+++ /dev/null
@@ -1,249 +0,0 @@
-import { openDB } from 'idb';
-import path from 'utils/common/path';
-
-const DB_NAME = 'bruno-parsed-file-cache';
-const STORE_NAME = 'parsedFiles';
-const DB_VERSION = 1;
-const CACHE_VERSION = '1.0.0';
-const DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
-
-let dbPromise = null;
-
-const getDB = () => {
- if (!dbPromise) {
- dbPromise = openDB(DB_NAME, DB_VERSION, {
- upgrade(db) {
- if (!db.objectStoreNames.contains(STORE_NAME)) {
- const store = db.createObjectStore(STORE_NAME, { keyPath: 'key' });
- store.createIndex('collectionPath', 'collectionPath');
- store.createIndex('parsedAt', 'parsedAt');
- }
- }
- }).catch((err) => {
- dbPromise = null;
- throw err;
- });
- }
- return dbPromise;
-};
-
-const generateKey = (collectionPath, filePath) => {
- return `${collectionPath}↝${filePath}`;
-};
-
-export const parsedFileCacheStore = {
- async getEntry(collectionPath, filePath) {
- try {
- const db = await getDB();
- const key = generateKey(collectionPath, filePath);
- const entry = await db.get(STORE_NAME, key);
-
- if (entry && typeof entry.mtimeMs === 'number' && entry.parsedData) {
- return {
- mtimeMs: entry.mtimeMs,
- parsedData: entry.parsedData
- };
- }
- } catch (error) {
- console.error('ParsedFileCacheStore: Error reading cache entry:', error);
- }
- return null;
- },
-
- async setEntry(collectionPath, filePath, entry, retryAfterEviction = true) {
- try {
- const db = await getDB();
- const key = generateKey(collectionPath, filePath);
- const cacheEntry = {
- key,
- collectionPath,
- filePath,
- mtimeMs: entry.mtimeMs,
- parsedData: entry.parsedData,
- parsedAt: Date.now()
- };
- await db.put(STORE_NAME, cacheEntry);
- } catch (error) {
- // Handle QuotaExceededError by evicting old entries and retrying
- const isQuotaError
- = error.name === 'QuotaExceededError'
- || error.code === 22 // Legacy Safari
- || (error.code === 1014 && error.name === 'NS_ERROR_DOM_QUOTA_REACHED'); // Firefox
-
- if (isQuotaError && retryAfterEviction) {
- console.warn('ParsedFileCacheStore: Quota exceeded, evicting old entries...');
- const evicted = await this.evictLRU();
- if (evicted > 0) {
- // Retry the write after eviction
- return this.setEntry(collectionPath, filePath, entry, false);
- }
- console.warn('ParsedFileCacheStore: No entries to evict, cache write skipped');
- } else {
- console.error('ParsedFileCacheStore: Error writing cache entry:', error);
- }
- }
- },
-
- async invalidate(collectionPath, filePath) {
- try {
- const db = await getDB();
- const key = generateKey(collectionPath, filePath);
- await db.delete(STORE_NAME, key);
- } catch (error) {
- console.error('ParsedFileCacheStore: Error invalidating cache entry:', error);
- }
- },
-
- async invalidateCollection(collectionPath) {
- try {
- const db = await getDB();
- const tx = db.transaction(STORE_NAME, 'readwrite');
- const index = tx.store.index('collectionPath');
-
- let cursor = await index.openCursor(IDBKeyRange.only(collectionPath));
- while (cursor) {
- await cursor.delete();
- cursor = await cursor.continue();
- }
- await tx.done;
- } catch (error) {
- console.error('ParsedFileCacheStore: Error invalidating collection cache:', error);
- }
- },
-
- async invalidateDirectory(collectionPath, dirPath) {
- try {
- const db = await getDB();
- const tx = db.transaction(STORE_NAME, 'readwrite');
- const index = tx.store.index('collectionPath');
- const normalizedDirPath = dirPath.endsWith(path.sep) ? dirPath : `${dirPath}${path.sep}`;
-
- let cursor = await index.openCursor(IDBKeyRange.only(collectionPath));
- while (cursor) {
- if (cursor.value.filePath.startsWith(normalizedDirPath)) {
- await cursor.delete();
- }
- cursor = await cursor.continue();
- }
- await tx.done;
- } catch (error) {
- console.error('ParsedFileCacheStore: Error invalidating directory cache:', error);
- }
- },
-
- async moveEntry(collectionPath, oldFilePath, newFilePath) {
- try {
- const entry = await this.getEntry(collectionPath, oldFilePath);
- if (entry) {
- await this.invalidate(collectionPath, oldFilePath);
- await this.setEntry(collectionPath, newFilePath, {
- mtimeMs: entry.mtimeMs,
- parsedData: entry.parsedData
- });
- }
- } catch (error) {
- console.error('ParsedFileCacheStore: Error moving cache entry:', error);
- }
- },
-
- async prune(maxAgeMs = DEFAULT_MAX_AGE_MS) {
- try {
- const db = await getDB();
- const cutoff = Date.now() - maxAgeMs;
- const tx = db.transaction(STORE_NAME, 'readwrite');
- const index = tx.store.index('parsedAt');
-
- let cursor = await index.openCursor(IDBKeyRange.upperBound(cutoff));
- while (cursor) {
- await cursor.delete();
- cursor = await cursor.continue();
- }
- await tx.done;
- } catch (error) {
- console.error('ParsedFileCacheStore: Error pruning cache:', error);
- }
- },
-
- /**
- * Evict least recently used entries when quota is exceeded.
- * Removes approximately 20% of the oldest entries to free up space.
- * @returns {Promise} Number of entries evicted
- */
- async evictLRU(percentageToEvict = 0.2) {
- try {
- const db = await getDB();
- const totalCount = await db.count(STORE_NAME);
-
- if (totalCount === 0) {
- return 0;
- }
-
- const countToEvict = Math.max(1, Math.floor(totalCount * percentageToEvict));
-
- const tx = db.transaction(STORE_NAME, 'readwrite');
- const index = tx.store.index('parsedAt');
-
- let cursor = await index.openCursor();
- let evicted = 0;
-
- while (cursor && evicted < countToEvict) {
- await cursor.delete();
- evicted++;
- cursor = await cursor.continue();
- }
-
- await tx.done;
- console.log(`ParsedFileCacheStore: Evicted ${evicted} LRU entries to free up space`);
- return evicted;
- } catch (error) {
- console.error('ParsedFileCacheStore: Error during LRU eviction:', error);
- return 0;
- }
- },
-
- async clear() {
- try {
- const db = await getDB();
- await db.clear(STORE_NAME);
- } catch (error) {
- console.error('ParsedFileCacheStore: Error clearing cache:', error);
- }
- },
-
- async getStats() {
- try {
- const db = await getDB();
-
- // Use count() for O(1) total files count
- const totalFiles = await db.count(STORE_NAME);
-
- // Count unique collections using index with unique cursor
- const tx = db.transaction(STORE_NAME, 'readonly');
- const index = tx.store.index('collectionPath');
- let totalCollections = 0;
-
- // Use openKeyCursor with 'nextunique' to count unique collection paths
- let cursor = await index.openKeyCursor(null, 'nextunique');
- while (cursor) {
- totalCollections++;
- cursor = await cursor.continue();
- }
-
- return {
- version: CACHE_VERSION,
- totalCollections,
- totalFiles
- };
- } catch (error) {
- console.error('ParsedFileCacheStore: Error getting stats:', error);
- return {
- version: CACHE_VERSION,
- totalCollections: 0,
- totalFiles: 0,
- error: error.message
- };
- }
- }
-};
-
-export default parsedFileCacheStore;
diff --git a/packages/bruno-electron/src/app/collection-tree-batcher.js b/packages/bruno-electron/src/app/collection-tree-batcher.js
deleted file mode 100644
index 1fefcf8a7..000000000
--- a/packages/bruno-electron/src/app/collection-tree-batcher.js
+++ /dev/null
@@ -1,201 +0,0 @@
-/**
- * CollectionTreeBatcher - Batches IPC events to reduce Redux dispatch overhead.
- *
- * Instead of sending individual 'main:collection-tree-updated' events for each file,
- * this batcher collects events and sends them in batches, reducing the number of
- * Redux updates and improving UI performance during collection mounting.
- *
- * Flush triggers:
- * - Time-based: Every DISPATCH_INTERVAL_MS (200ms)
- * - Size-based: When batch reaches MAX_BATCH_SIZE (300 items)
- * - Manual: Call flush() directly (e.g., on watcher 'ready' event)
- */
-
-const DISPATCH_INTERVAL_MS = 200;
-const MAX_BATCH_SIZE = 200;
-
-class CollectionTreeBatcher {
- constructor(win, collectionUid) {
- this.win = win;
- this.queue = [];
- this.timer = null;
- this.isDestroyed = false;
- // Bind methods
- // We need to bind the methods because these are being called as callbacks to
- // chokidar's add, addDir, change, unlink, unlinkDir events
-
- this.add = this.add.bind(this);
- this.flush = this.flush.bind(this);
- this._scheduleFlush = this._scheduleFlush.bind(this);
- }
-
- /**
- * Check if the window is still valid for sending events
- */
- _isWindowValid() {
- return this.win && !this.win.isDestroyed() && !this.isDestroyed;
- }
-
- /**
- * Schedule a flush after the dispatch interval
- */
- _scheduleFlush() {
- if (this.timer || !this._isWindowValid()) {
- return;
- }
-
- this.timer = setTimeout(() => {
- this.timer = null;
- this.flush();
- }, DISPATCH_INTERVAL_MS);
- }
-
- /**
- * Add an event to the batch queue
- * @param {string} eventType - The event type ('addFile', 'addDir', 'change', 'unlink', 'unlinkDir')
- * @param {object} payload - The event payload
- */
- add(eventType, payload) {
- if (!this._isWindowValid()) {
- return;
- }
-
- this.queue.push({
- eventType,
- payload
- });
-
- // Flush immediately if batch is full
- if (this.queue.length >= MAX_BATCH_SIZE) {
- this.flush();
- } else {
- this._scheduleFlush();
- }
- }
-
- /**
- * Flush the current batch to the renderer
- */
- flush() {
- // Clear any pending timer
- if (this.timer) {
- clearTimeout(this.timer);
- this.timer = null;
- }
-
- if (this.queue.length === 0 || !this._isWindowValid()) {
- return;
- }
-
- // Take all items from the queue
- // This is a copy-type operation to avoid mutating the original
- // Splice returns the deleted items
- const batch = this.queue.splice(0);
-
- try {
- // Send the batch to the renderer
- this.win.webContents.send('main:collection-tree-batch-updated', batch);
- } catch (error) {
- console.error('CollectionTreeBatcher: Error sending batch:', error);
- this.queue.push(...batch);
- }
- }
-
- /**
- * Get the current queue size
- * @returns {number} - The number of items in the queue
- */
- size() {
- return this.queue.length;
- }
-
- /**
- * Clear the queue without sending
- */
- clear() {
- if (this.timer) {
- clearTimeout(this.timer);
- this.timer = null;
- }
- this.queue = [];
- }
-
- /**
- * Mark this batcher as destroyed (e.g., when window closes)
- */
- destroy() {
- this.isDestroyed = true;
- this.clear();
- this.win = null;
- }
-}
-
-// Store for managing batchers per collection
-const batchers = new Map();
-
-/**
- * Get the batcher key for a window and collection UID
- * @param {BrowserWindow} win - The Electron BrowserWindow
- * @param {string} collectionUid - The collection UID
- * @returns {string} - The batcher key
- */
-const getBatcherKey = (win, collectionUid) => {
- return `${win.id}-${collectionUid}`;
-};
-
-/**
- * Get or create a CollectionTreeBatcher for a window
- * @param {BrowserWindow} win - The Electron BrowserWindow
- * @param {string} collectionUid - The collection UID
- * @returns {CollectionTreeBatcher} - The batcher instance
- */
-const getBatcher = (win, collectionUid) => {
- const batcherKey = getBatcherKey(win, collectionUid);
-
- if (!batchers.has(batcherKey)) {
- const batcher = new CollectionTreeBatcher(win, collectionUid);
-
- // Clean up when window is closed
- win.once('closed', () => {
- const b = batchers.get(batcherKey);
- if (b) {
- b.destroy();
- batchers.delete(batcherKey);
- }
- });
-
- batchers.set(batcherKey, batcher);
- }
-
- return batchers.get(batcherKey);
-};
-
-/**
- * Remove a batcher for a window
- * @param {BrowserWindow} win - The Electron BrowserWindow
- * @param {string} collectionUid - The collection UID
- */
-const removeBatcher = (win, collectionUid) => {
- const batcherKey = getBatcherKey(win, collectionUid);
- const batcher = batchers.get(batcherKey);
-
- if (batcher) {
- batcher.destroy();
- batchers.delete(batcherKey);
- }
-};
-
-// Export with backward-compatible aliases
-module.exports = {
- CollectionTreeBatcher,
- getBatcher,
- removeBatcher,
- // Backward-compatible aliases
- BatchAggregator: CollectionTreeBatcher,
- getAggregator: getBatcher,
- removeAggregator: removeBatcher,
- constants: {
- MAX_BATCH_SIZE,
- DISPATCH_INTERVAL_MS
- }
-};
diff --git a/packages/bruno-electron/src/app/collection-watcher.js b/packages/bruno-electron/src/app/collection-watcher.js
index f66dfb639..a1f5621c2 100644
--- a/packages/bruno-electron/src/app/collection-watcher.js
+++ b/packages/bruno-electron/src/app/collection-watcher.js
@@ -1,6 +1,5 @@
const _ = require('lodash');
const fs = require('fs');
-const fsPromises = require('fs').promises;
const path = require('path');
const chokidar = require('chokidar');
const {
@@ -27,8 +26,6 @@ const UiStateSnapshot = require('../store/ui-state-snapshot');
const { parseFileMeta, hydrateRequestWithUuid } = require('../utils/collection');
const { parseLargeRequestWithRedaction } = require('../utils/parse');
const { transformBrunoConfigAfterRead } = require('../utils/transformBrunoConfig');
-const { parsedFileCacheStore } = require('../store/parsed-file-cache-idb');
-const { getBatcher } = require('./collection-tree-batcher');
const dotEnvWatcher = require('./dotenv-watcher');
const MAX_FILE_SIZE = 2.5 * 1024 * 1024;
@@ -226,8 +223,6 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
return addEnvironmentFile(win, pathname, collectionUid, collectionPath);
}
- const batcher = getBatcher(win, collectionUid);
-
if (isCollectionRootFile(pathname, collectionPath)) {
const format = getCollectionFormat(collectionPath);
const file = {
@@ -294,8 +289,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
file.data = await parseFolder(content, { format });
hydrateCollectionRootWithUuid(file.data);
- // win.webContents.send('main:collection-tree-updated', 'addFile', file);
- batcher.add('addFile', file);
+ win.webContents.send('main:collection-tree-updated', 'addFile', file);
return;
} catch (err) {
console.error(err);
@@ -315,69 +309,61 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
}
};
- try {
- const fileStats = await fsPromises.stat(pathname);
+ const fileStats = fs.statSync(pathname);
+ let content = fs.readFileSync(pathname, 'utf8');
- const cachedEntry = await parsedFileCacheStore.getEntry(collectionPath, pathname);
- if (cachedEntry && cachedEntry.mtimeMs === fileStats.mtimeMs) {
- // Cache hit
- file.data = cachedEntry.parsedData;
- file.partial = false;
- file.loading = false;
- file.size = sizeInMB(fileStats?.size);
- hydrateRequestWithUuid(file.data, pathname);
- batcher.add('addFile', file);
- watcher.markFileAsProcessed(win, collectionUid, pathname);
- return;
- }
-
- // Cache miss
- const content = await fsPromises.readFile(pathname, 'utf8');
-
- if (!useWorkerThread) {
+ // If worker thread is not used, we can directly parse the file
+ if (!useWorkerThread) {
+ try {
file.data = await parseRequest(content, { format });
file.partial = false;
file.loading = false;
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
- batcher.add('addFile', file);
-
- await parsedFileCacheStore.setEntry(collectionPath, pathname, {
- mtimeMs: fileStats.mtimeMs,
- parsedData: file.data
- });
+ win.webContents.send('main:collection-tree-updated', 'addFile', file);
+ } catch (error) {
+ console.error(error);
+ } finally {
watcher.markFileAsProcessed(win, collectionUid, pathname);
- return;
}
+ return;
+ }
+
+ try {
+ // we need to send a partial file info to the UI
+ // so that the UI can display the file in the collection tree
+ file.data = {
+ name: path.basename(pathname),
+ type: 'http-request'
+ };
+
+ const metaJson = parseFileMeta(content, format);
+ file.data = metaJson;
+ file.partial = true;
+ file.loading = false;
+ file.size = sizeInMB(fileStats?.size);
+ hydrateRequestWithUuid(file.data, pathname);
+ win.webContents.send('main:collection-tree-updated', 'addFile', file);
if (fileStats.size < MAX_FILE_SIZE) {
+ // This is to update the loading indicator in the UI
+ file.data = metaJson;
+ file.partial = false;
+ file.loading = true;
+ hydrateRequestWithUuid(file.data, pathname);
+ win.webContents.send('main:collection-tree-updated', 'addFile', file);
+
+ // This is to update the file info in the UI
file.data = await parseRequestViaWorker(content, {
format,
filename: pathname
});
file.partial = false;
file.loading = false;
- file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
- batcher.add('addFile', file);
-
- await parsedFileCacheStore.setEntry(collectionPath, pathname, {
- mtimeMs: fileStats.mtimeMs,
- parsedData: file.data
- });
- } else {
- const metaJson = parseFileMeta(content, format);
- file.data = metaJson;
- file.partial = true;
- file.loading = false;
- file.size = sizeInMB(fileStats?.size);
- hydrateRequestWithUuid(file.data, pathname);
- batcher.add('addFile', file);
+ win.webContents.send('main:collection-tree-updated', 'addFile', file);
}
-
- watcher.markFileAsProcessed(win, collectionUid, pathname);
} catch (error) {
- console.error(`Error processing file ${pathname}:`, error);
file.data = {
name: path.basename(pathname),
type: 'http-request'
@@ -387,8 +373,10 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
};
file.partial = true;
file.loading = false;
+ file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
- batcher.add('addFile', file);
+ win.webContents.send('main:collection-tree-updated', 'addFile', file);
+ } finally {
watcher.markFileAsProcessed(win, collectionUid, pathname);
}
}
@@ -408,16 +396,15 @@ const addDirectory = async (win, pathname, collectionUid, collectionPath) => {
const folderFilePath = path.join(pathname, `folder.${format}`);
try {
- await fsPromises.access(folderFilePath);
- const folderFileContent = await fsPromises.readFile(folderFilePath, 'utf8');
- const folderData = await parseFolder(folderFileContent, { format });
- name = folderData?.meta?.name || name;
- seq = folderData?.meta?.seq;
- } catch (error) {
- if (error.code !== 'ENOENT') {
- console.error(`Error occurred while parsing folder.${format} file`);
- console.error(error);
+ if (fs.existsSync(folderFilePath)) {
+ let folderFileContent = fs.readFileSync(folderFilePath, 'utf8');
+ let folderData = await parseFolder(folderFileContent, { format });
+ name = folderData?.meta?.name || name;
+ seq = folderData?.meta?.seq;
}
+ } catch (error) {
+ console.error(`Error occured while parsing folder.${format} file`);
+ console.error(error);
}
const directory = {
@@ -430,8 +417,7 @@ const addDirectory = async (win, pathname, collectionUid, collectionPath) => {
}
};
- const batcher = getBatcher(win, collectionUid);
- batcher.add('addDir', directory);
+ win.webContents.send('main:collection-tree-updated', 'addDir', directory);
};
const change = async (win, pathname, collectionUid, collectionPath) => {
@@ -538,9 +524,6 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
const format = getCollectionFormat(collectionPath);
if (hasRequestExtension(pathname, format)) {
- // Invalidate cache for this file since it changed
- await parsedFileCacheStore.invalidate(collectionPath, pathname);
-
try {
const file = {
meta: {
@@ -561,14 +544,6 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
-
- // Update cache with new parsed data
- await parsedFileCacheStore.setEntry(collectionPath, pathname, {
- mtimeMs: fileStats.mtimeMs,
- parsedData: file.data
- });
-
- // Change events are not batched - they need immediate feedback
win.webContents.send('main:collection-tree-updated', 'change', file);
} catch (err) {
console.error(err);
@@ -576,7 +551,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
}
};
-const unlink = async (win, pathname, collectionUid, collectionPath) => {
+const unlink = (win, pathname, collectionUid, collectionPath) => {
console.log(`watcher unlink: ${pathname}`);
if (isEnvironmentsFolder(pathname, collectionPath)) {
@@ -585,9 +560,6 @@ const unlink = async (win, pathname, collectionUid, collectionPath) => {
const format = getCollectionFormat(collectionPath);
if (hasRequestExtension(pathname, format)) {
- // Invalidate cache for deleted file
- await parsedFileCacheStore.invalidate(collectionPath, pathname);
-
const basename = path.basename(pathname);
const dirname = path.dirname(pathname);
@@ -613,8 +585,6 @@ const unlinkDir = async (win, pathname, collectionUid, collectionPath) => {
return;
}
- await parsedFileCacheStore.invalidateDirectory(collectionPath, pathname);
-
const format = getCollectionFormat(collectionPath);
const folderFilePath = path.join(pathname, `folder.${format}`);
@@ -637,9 +607,7 @@ const unlinkDir = async (win, pathname, collectionUid, collectionPath) => {
};
const onWatcherSetupComplete = (win, watchPath, collectionUid, watcher) => {
- const batcher = getBatcher(win, collectionUid);
- batcher.flush();
-
+ // Mark discovery as complete
watcher.completeCollectionDiscovery(win, collectionUid);
const UiStateSnapshotStore = new UiStateSnapshot();
diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js
index d207ad372..830cb2f32 100644
--- a/packages/bruno-electron/src/index.js
+++ b/packages/bruno-electron/src/index.js
@@ -39,7 +39,6 @@ const registerNetworkIpc = require('./ipc/network');
const registerCollectionsIpc = require('./ipc/collection');
const registerFilesystemIpc = require('./ipc/filesystem');
const registerPreferencesIpc = require('./ipc/preferences');
-const { parsedFileCacheStore } = require('./store/parsed-file-cache-idb');
const registerSystemMonitorIpc = require('./ipc/system-monitor');
const registerWorkspaceIpc = require('./ipc/workspace');
const registerApiSpecIpc = require('./ipc/apiSpec');
@@ -461,9 +460,6 @@ app.on('ready', async () => {
});
});
- // Initialize the parsed file cache IPC handlers
- parsedFileCacheStore.initialize(mainWindow);
-
// register all ipc handlers
registerNetworkIpc(mainWindow);
registerGlobalEnvironmentsIpc(mainWindow, globalEnvironmentsManager);
diff --git a/packages/bruno-electron/src/ipc/preferences.js b/packages/bruno-electron/src/ipc/preferences.js
index 6aea6aa0a..305e87895 100644
--- a/packages/bruno-electron/src/ipc/preferences.js
+++ b/packages/bruno-electron/src/ipc/preferences.js
@@ -2,7 +2,6 @@ const { ipcMain, nativeTheme } = require('electron');
const { getPreferences, savePreferences } = require('../store/preferences');
const { getGitVersion } = require('../utils/git');
const { globalEnvironmentsStore } = require('../store/global-environments');
-const { parsedFileCacheStore } = require('../store/parsed-file-cache-idb');
const { getCachedSystemProxy, fetchSystemProxy } = require('../store/system-proxy');
const { resolveDefaultLocation } = require('../utils/default-location');
const onboardUser = require('../app/onboarding');
@@ -61,25 +60,6 @@ const registerPreferencesIpc = (mainWindow) => {
nativeTheme.themeSource = theme;
});
- ipcMain.handle('renderer:get-cache-stats', async () => {
- try {
- return await parsedFileCacheStore.getStats();
- } catch (error) {
- console.error('Error getting cache stats:', error);
- return { error: error.message };
- }
- });
-
- ipcMain.handle('renderer:purge-cache', async () => {
- try {
- await parsedFileCacheStore.clear();
- return { success: true };
- } catch (error) {
- console.error('Error purging cache:', error);
- return { success: false, error: error.message };
- }
- });
-
ipcMain.handle('renderer:get-system-proxy-variables', async () => {
return await getCachedSystemProxy();
});
diff --git a/packages/bruno-electron/src/store/parsed-file-cache-idb.js b/packages/bruno-electron/src/store/parsed-file-cache-idb.js
deleted file mode 100644
index 6a65109b8..000000000
--- a/packages/bruno-electron/src/store/parsed-file-cache-idb.js
+++ /dev/null
@@ -1,157 +0,0 @@
-const { ipcMain } = require('electron');
-const { v4: uuidv4 } = require('uuid');
-
-// Pending requests waiting for renderer response
-const pendingRequests = new Map();
-
-// Timeout for IPC requests (5 seconds)
-const REQUEST_TIMEOUT = 5000;
-
-// Store reference to main window
-let mainWindow = null;
-
-// Initialize the IPC response handler
-const initializeCacheIpc = (win) => {
- mainWindow = win;
-
- ipcMain.on('renderer:parsed-file-cache-response', (event, response) => {
- const { requestId, success, data, error } = response;
- const pending = pendingRequests.get(requestId);
-
- if (pending) {
- pendingRequests.delete(requestId);
- clearTimeout(pending.timeout);
-
- if (success) {
- pending.resolve(data);
- } else {
- pending.reject(new Error(error || 'Unknown error'));
- }
- }
- });
-};
-
-// Send a request to the renderer and wait for response
-const sendCacheRequest = (operation, ...args) => {
- return new Promise((resolve, reject) => {
- if (!mainWindow || mainWindow.isDestroyed()) {
- resolve(null);
- return;
- }
-
- const requestId = uuidv4();
-
- const timeout = setTimeout(() => {
- pendingRequests.delete(requestId);
- resolve(null);
- }, REQUEST_TIMEOUT);
-
- pendingRequests.set(requestId, { resolve, reject, timeout });
-
- mainWindow.webContents.send('main:parsed-file-cache-request', operation, requestId, ...args);
- });
-};
-
-class ParsedFileCacheStore {
- constructor() {
- this.initialized = false;
- }
-
- initialize(win) {
- if (!this.initialized) {
- initializeCacheIpc(win);
- this.initialized = true;
- }
- }
-
- async getEntry(collectionPath, filePath) {
- try {
- return await sendCacheRequest('getEntry', collectionPath, filePath);
- } catch (error) {
- console.error('ParsedFileCacheStore: Error reading cache entry:', error);
- return null;
- }
- }
-
- async setEntry(collectionPath, filePath, entry) {
- try {
- await sendCacheRequest('setEntry', collectionPath, filePath, entry);
- } catch (error) {
- console.error('ParsedFileCacheStore: Error writing cache entry:', error);
- }
- }
-
- async invalidate(collectionPath, filePath) {
- try {
- await sendCacheRequest('invalidate', collectionPath, filePath);
- } catch (error) {
- console.error('ParsedFileCacheStore: Error invalidating cache entry:', error);
- }
- }
-
- async invalidateCollection(collectionPath) {
- try {
- await sendCacheRequest('invalidateCollection', collectionPath);
- } catch (error) {
- console.error('ParsedFileCacheStore: Error invalidating collection cache:', error);
- }
- }
-
- async invalidateDirectory(collectionPath, dirPath) {
- try {
- await sendCacheRequest('invalidateDirectory', collectionPath, dirPath);
- } catch (error) {
- console.error('ParsedFileCacheStore: Error invalidating directory cache:', error);
- }
- }
-
- async moveEntry(collectionPath, oldFilePath, newFilePath) {
- const entry = await this.getEntry(collectionPath, oldFilePath);
- if (entry) {
- await this.invalidate(collectionPath, oldFilePath);
- await this.setEntry(collectionPath, newFilePath, {
- mtimeMs: entry.mtimeMs,
- parsedData: entry.parsedData
- });
- }
- }
-
- async getStats() {
- try {
- const stats = await sendCacheRequest('getStats');
- return stats || {
- version: '1.0.0',
- totalCollections: 0,
- totalFiles: 0
- };
- } catch (error) {
- console.error('ParsedFileCacheStore: Error getting stats:', error);
- return {
- version: '1.0.0',
- totalCollections: 0,
- totalFiles: 0,
- error: error.message
- };
- }
- }
-
- async clear() {
- try {
- await sendCacheRequest('clear');
- } catch (error) {
- console.error('ParsedFileCacheStore: Error clearing cache:', error);
- }
- }
-
- async close() {
- // No-op for IndexedDB version (managed by browser)
- }
-}
-
-// Singleton instance
-const parsedFileCacheStore = new ParsedFileCacheStore();
-
-module.exports = {
- parsedFileCacheStore,
- ParsedFileCacheStore
-};
diff --git a/packages/bruno-electron/tests/app/collection-tree-batcher.spec.js b/packages/bruno-electron/tests/app/collection-tree-batcher.spec.js
deleted file mode 100644
index d6dd778e7..000000000
--- a/packages/bruno-electron/tests/app/collection-tree-batcher.spec.js
+++ /dev/null
@@ -1,312 +0,0 @@
-const { CollectionTreeBatcher, getBatcher, removeBatcher, constants } = require('../../src/app/collection-tree-batcher');
-
-// Mock BrowserWindow
-const createMockWindow = (id = 1) => {
- const listeners = {};
- return {
- id,
- isDestroyed: jest.fn(() => false),
- once: jest.fn((event, callback) => {
- listeners[event] = callback;
- }),
- emit: (event) => {
- if (listeners[event]) {
- listeners[event]();
- }
- },
- webContents: {
- send: jest.fn()
- }
- };
-};
-
-describe('CollectionTreeBatcher', () => {
- beforeEach(() => {
- jest.useFakeTimers();
- });
-
- afterEach(() => {
- jest.useRealTimers();
- });
-
- describe('constructor', () => {
- it('should initialize with empty queue and no timer', () => {
- const win = createMockWindow();
- const batcher = new CollectionTreeBatcher(win, 'collection-1');
-
- expect(batcher.queue).toEqual([]);
- expect(batcher.timer).toBeNull();
- expect(batcher.isDestroyed).toBe(false);
- });
- });
-
- describe('add()', () => {
- it('should add events to the queue', () => {
- const win = createMockWindow();
- const batcher = new CollectionTreeBatcher(win, 'collection-1');
-
- batcher.add('addFile', { path: '/test/file.bru' });
-
- expect(batcher.queue).toHaveLength(1);
- expect(batcher.queue[0]).toEqual({
- eventType: 'addFile',
- payload: { path: '/test/file.bru' }
- });
- });
-
- it('should schedule a flush after adding an event', () => {
- const win = createMockWindow();
- const batcher = new CollectionTreeBatcher(win, 'collection-1');
-
- batcher.add('addFile', { path: '/test/file.bru' });
-
- expect(batcher.timer).not.toBeNull();
- });
-
- it('should not add events if window is destroyed', () => {
- const win = createMockWindow();
- win.isDestroyed.mockReturnValue(true);
- const batcher = new CollectionTreeBatcher(win, 'collection-1');
-
- batcher.add('addFile', { path: '/test/file.bru' });
-
- expect(batcher.queue).toHaveLength(0);
- });
-
- it('should not add events if batcher is destroyed', () => {
- const win = createMockWindow();
- const batcher = new CollectionTreeBatcher(win, 'collection-1');
- batcher.destroy();
-
- batcher.add('addFile', { path: '/test/file.bru' });
-
- expect(batcher.queue).toHaveLength(0);
- });
- });
-
- describe('flush()', () => {
- it('should send batch to renderer and clear queue', () => {
- const win = createMockWindow();
- const batcher = new CollectionTreeBatcher(win, 'collection-1');
-
- batcher.add('addFile', { path: '/test/file1.bru' });
- batcher.add('addDir', { path: '/test/folder' });
-
- batcher.flush();
-
- expect(win.webContents.send).toHaveBeenCalledWith('main:collection-tree-batch-updated', [
- { eventType: 'addFile', payload: { path: '/test/file1.bru' } },
- { eventType: 'addDir', payload: { path: '/test/folder' } }
- ]);
- expect(batcher.queue).toHaveLength(0);
- });
-
- it('should not send if queue is empty', () => {
- const win = createMockWindow();
- const batcher = new CollectionTreeBatcher(win, 'collection-1');
-
- batcher.flush();
-
- expect(win.webContents.send).not.toHaveBeenCalled();
- });
-
- it('should clear pending timer on flush', () => {
- const win = createMockWindow();
- const batcher = new CollectionTreeBatcher(win, 'collection-1');
-
- batcher.add('addFile', { path: '/test/file.bru' });
- expect(batcher.timer).not.toBeNull();
-
- batcher.flush();
- expect(batcher.timer).toBeNull();
- });
-
- it('should not send if window is destroyed', () => {
- const win = createMockWindow();
- const batcher = new CollectionTreeBatcher(win, 'collection-1');
-
- batcher.add('addFile', { path: '/test/file.bru' });
- win.isDestroyed.mockReturnValue(true);
-
- batcher.flush();
-
- expect(win.webContents.send).not.toHaveBeenCalled();
- });
- });
-
- describe('time-based flush', () => {
- it('should auto-flush after DISPATCH_INTERVAL_MS (200ms)', () => {
- const win = createMockWindow();
- const batcher = new CollectionTreeBatcher(win, 'collection-1');
-
- batcher.add('addFile', { path: '/test/file.bru' });
-
- expect(win.webContents.send).not.toHaveBeenCalled();
-
- jest.advanceTimersByTime(200);
-
- expect(win.webContents.send).toHaveBeenCalledWith('main:collection-tree-batch-updated', [
- { eventType: 'addFile', payload: { path: '/test/file.bru' } }
- ]);
- });
-
- it('should not schedule multiple timers', () => {
- const win = createMockWindow();
- const batcher = new CollectionTreeBatcher(win, 'collection-1');
-
- batcher.add('addFile', { path: '/test/file1.bru' });
- const firstTimer = batcher.timer;
-
- batcher.add('addFile', { path: '/test/file2.bru' });
-
- expect(batcher.timer).toBe(firstTimer);
- });
- });
-
- describe('size-based flush', () => {
- it('should auto-flush when reaching MAX_BATCH_SIZE', () => {
- const win = createMockWindow();
- const batcher = new CollectionTreeBatcher(win, 'collection-1');
- const eventCount = constants.MAX_BATCH_SIZE - 1;
- // Add events - should not flush
- for (let i = 0; i < eventCount; i++) {
- batcher.add('addFile', { path: `/test/file${i}.bru` });
- }
- expect(win.webContents.send).not.toHaveBeenCalled();
- expect(batcher.queue).toHaveLength(eventCount);
-
- // Add 300th event - should trigger flush
- batcher.add('addFile', { path: '/test/file299.bru' });
-
- expect(win.webContents.send).toHaveBeenCalledTimes(1);
- expect(batcher.queue).toHaveLength(0);
- });
- });
-
- describe('size()', () => {
- it('should return current queue size', () => {
- const win = createMockWindow();
- const batcher = new CollectionTreeBatcher(win, 'collection-1');
-
- expect(batcher.size()).toBe(0);
-
- batcher.add('addFile', { path: '/test/file1.bru' });
- expect(batcher.size()).toBe(1);
-
- batcher.add('addFile', { path: '/test/file2.bru' });
- expect(batcher.size()).toBe(2);
- });
- });
-
- describe('clear()', () => {
- it('should clear the queue without sending', () => {
- const win = createMockWindow();
- const batcher = new CollectionTreeBatcher(win, 'collection-1');
-
- batcher.add('addFile', { path: '/test/file.bru' });
- batcher.clear();
-
- expect(batcher.queue).toHaveLength(0);
- expect(batcher.timer).toBeNull();
- expect(win.webContents.send).not.toHaveBeenCalled();
- });
- });
-
- describe('destroy()', () => {
- it('should mark batcher as destroyed and clear queue', () => {
- const win = createMockWindow();
- const batcher = new CollectionTreeBatcher(win, 'collection-1');
-
- batcher.add('addFile', { path: '/test/file.bru' });
- batcher.destroy();
-
- expect(batcher.isDestroyed).toBe(true);
- expect(batcher.queue).toHaveLength(0);
- expect(batcher.win).toBeNull();
- });
- });
-
- describe('error handling', () => {
- it('should handle send errors gracefully', () => {
- const win = createMockWindow();
- win.webContents.send.mockImplementation(() => {
- throw new Error('Window closed');
- });
- const batcher = new CollectionTreeBatcher(win, 'collection-1');
- const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
-
- batcher.add('addFile', { path: '/test/file.bru' });
- batcher.flush();
-
- expect(consoleSpy).toHaveBeenCalledWith('CollectionTreeBatcher: Error sending batch:', expect.any(Error));
- consoleSpy.mockRestore();
- });
- });
-});
-
-describe('getBatcher / removeBatcher', () => {
- beforeEach(() => {
- jest.useFakeTimers();
- });
-
- afterEach(() => {
- jest.useRealTimers();
- });
-
- it('should create and return a batcher for a window', () => {
- const win = createMockWindow(100);
- const batcher = getBatcher(win, 'collection-1');
-
- expect(batcher).toBeInstanceOf(CollectionTreeBatcher);
- });
-
- it('should return the same batcher for the same window and collection', () => {
- const win = createMockWindow(101);
- const batcher1 = getBatcher(win, 'collection-1');
- const batcher2 = getBatcher(win, 'collection-1');
-
- expect(batcher1).toBe(batcher2);
- });
-
- it('should return different batchers for different collections', () => {
- const win = createMockWindow(102);
- const batcher1 = getBatcher(win, 'collection-1');
- const batcher2 = getBatcher(win, 'collection-2');
-
- expect(batcher1).not.toBe(batcher2);
- });
-
- it('should return different batchers for different windows', () => {
- const win1 = createMockWindow(103);
- const win2 = createMockWindow(104);
- const batcher1 = getBatcher(win1, 'collection-1');
- const batcher2 = getBatcher(win2, 'collection-1');
-
- expect(batcher1).not.toBe(batcher2);
- });
-
- it('should clean up batcher when window is closed', () => {
- const win = createMockWindow(105);
- const batcher = getBatcher(win, 'collection-1');
-
- batcher.add('addFile', { path: '/test/file.bru' });
-
- // Simulate window close
- win.emit('closed');
-
- expect(batcher.isDestroyed).toBe(true);
- });
-
- it('should remove batcher with removeBatcher', () => {
- const win = createMockWindow(106);
- const batcher = getBatcher(win, 'collection-1');
-
- removeBatcher(win, 'collection-1');
-
- expect(batcher.isDestroyed).toBe(true);
-
- // Getting batcher again should create a new one
- const newBatcher = getBatcher(win, 'collection-1');
- expect(newBatcher).not.toBe(batcher);
- });
-});