From 0b7cd0e5408c54e9752f962a561846aafbce37d1 Mon Sep 17 00:00:00 2001 From: Chirag Chandrashekhar Date: Wed, 4 Mar 2026 19:20:26 +0530 Subject: [PATCH] Revert "Performance/file parse and mount (#6975)" (#7360) * Revert "Performance/file parse and mount (#6975)" This reverts commit f76f487211d3859edf2d14f0b11812811df3a758. * fix: import duplication * Revert "fix(batch-events): fix order of directory file and folder events (#7300)" This reverts commit bf4af42a25522d10f44e1fec17af63b3f0db0077. --------- Co-authored-by: Chirag Chandrashekhar Co-authored-by: Sid --- packages/bruno-app/jsconfig.json | 3 +- .../CollectionSettings/Overview/Info/index.js | 16 +- .../Preferences/Cache/StyledWrapper.js | 67 ---- .../src/components/Preferences/Cache/index.js | 89 ----- .../src/components/Preferences/index.js | 12 +- .../Sidebar/Collections/Collection/index.js | 2 +- packages/bruno-app/src/providers/App/index.js | 2 - .../src/providers/App/useIpcEvents.js | 48 --- .../providers/App/useParsedFileCacheIpc.js | 60 ---- .../middlewares/tasks/middleware.js | 53 +-- .../ReduxStore/slices/collections/index.js | 248 +------------- .../bruno-app/src/store/parsedFileCache.js | 249 -------------- .../src/app/collection-tree-batcher.js | 201 ----------- .../src/app/collection-watcher.js | 134 +++----- packages/bruno-electron/src/index.js | 4 - .../bruno-electron/src/ipc/preferences.js | 20 -- .../src/store/parsed-file-cache-idb.js | 157 --------- .../tests/app/collection-tree-batcher.spec.js | 312 ------------------ 18 files changed, 62 insertions(+), 1615 deletions(-) delete mode 100644 packages/bruno-app/src/components/Preferences/Cache/StyledWrapper.js delete mode 100644 packages/bruno-app/src/components/Preferences/Cache/index.js delete mode 100644 packages/bruno-app/src/providers/App/useParsedFileCacheIpc.js delete mode 100644 packages/bruno-app/src/store/parsedFileCache.js delete mode 100644 packages/bruno-electron/src/app/collection-tree-batcher.js delete mode 100644 packages/bruno-electron/src/store/parsed-file-cache-idb.js delete mode 100644 packages/bruno-electron/tests/app/collection-tree-batcher.spec.js 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); - }); -});