From bf4af42a25522d10f44e1fec17af63b3f0db0077 Mon Sep 17 00:00:00 2001 From: Sid Date: Fri, 27 Feb 2026 13:43:15 +0530 Subject: [PATCH] fix(batch-events): fix order of directory file and folder events (#7300) * fix: order of events * fix: update constants handling in CollectionTreeBatcher and related tests --- .../Sidebar/Collections/Collection/index.js | 3 +- .../ReduxStore/slices/collections/index.js | 309 ++++++++++++------ .../src/app/collection-tree-batcher.js | 9 +- .../src/app/collection-watcher.js | 7 +- .../tests/app/collection-tree-batcher.spec.js | 12 +- 5 files changed, 219 insertions(+), 121 deletions(-) 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 358d01131..f88f64c35 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -48,6 +48,7 @@ import { openDevtoolsAndSwitchToTerminal } from 'utils/terminal'; import ActionIcon from 'ui/ActionIcon'; import MenuDropdown from 'ui/MenuDropdown'; import { useSidebarAccordion } from 'components/Sidebar/SidebarAccordionContext'; +import { areItemsLoading } from 'utils/collections'; const Collection = ({ collection, searchText }) => { const { dropdownContainerRef } = useSidebarAccordion(); @@ -61,7 +62,7 @@ const Collection = ({ collection, searchText }) => { const [dropType, setDropType] = useState(null); const [isKeyboardFocused, setIsKeyboardFocused] = useState(false); const dispatch = useDispatch(); - const isLoading = collection.isLoading; + const isLoading = areItemsLoading(collection); const collectionRef = useRef(null); const isCollectionFocused = useSelector(isTabForItemActive({ itemUid: collection.uid })); 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 3f7317b36..d1c49b4ae 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -2795,126 +2795,191 @@ export const collectionsSlice = createSlice({ const collection = findCollectionByUid(state.collections, collectionUid); if (!collection) continue; const tempDirectory = state.tempDirectories?.[collectionUid]; - - // Process directories first to ensure folder structure exists - const directories = collectionItems.filter((i) => i.eventType === 'addDir'); - const files = collectionItems.filter((i) => i.eventType === 'addFile'); - - // Add directories - for (const { payload: dir } of directories) { - const isTransientDir = tempDirectory && dir.meta.pathname.startsWith(tempDirectory); - const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dir.meta.pathname); - 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: 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); - } else if (isTransientDir && !childItem.isTransient) { - childItem.isTransient = true; + 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); } - currentSubItems = childItem.items; } } - // Add files - for (const { payload: file } of files) { - 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; - } + 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; - if (isFolderRoot) { - const folderPath = path.dirname(file.meta.pathname); - const folderItem = findItemInCollectionByPathname(collection, folderPath); - if (folderItem) { - if (file?.data?.meta?.name) { - folderItem.name = file?.data?.meta?.name; + 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); } - folderItem.root = mergeRootWithPreservedUids(folderItem.root, file.data); - if (file?.data?.meta?.seq) { - folderItem.seq = file.data?.meta?.seq; + 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; } - 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) { - 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: [], - isTransient: isTransientFile - }; - currentSubItems.push(childItem); - } else if (isTransientFile && !childItem.isTransient) { - childItem.isTransient = true; - } - currentSubItems = childItem.items; - } + 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 (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 - }); + 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 + }); + } } } } @@ -2937,7 +3002,33 @@ export const collectionsSlice = createSlice({ if (isFolderRoot) { const folderPath = path.dirname(file.meta.pathname); - const folderItem = findItemInCollectionByPathname(collection, folderPath); + 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; + } + } + } + if (folderItem) { if (file?.data?.meta?.name) { folderItem.name = file?.data?.meta?.name; diff --git a/packages/bruno-electron/src/app/collection-tree-batcher.js b/packages/bruno-electron/src/app/collection-tree-batcher.js index 62bf1cbe3..1fefcf8a7 100644 --- a/packages/bruno-electron/src/app/collection-tree-batcher.js +++ b/packages/bruno-electron/src/app/collection-tree-batcher.js @@ -12,7 +12,7 @@ */ const DISPATCH_INTERVAL_MS = 200; -const MAX_BATCH_SIZE = 300; +const MAX_BATCH_SIZE = 200; class CollectionTreeBatcher { constructor(win, collectionUid) { @@ -97,6 +97,7 @@ class CollectionTreeBatcher { this.win.webContents.send('main:collection-tree-batch-updated', batch); } catch (error) { console.error('CollectionTreeBatcher: Error sending batch:', error); + this.queue.push(...batch); } } @@ -192,5 +193,9 @@ module.exports = { // Backward-compatible aliases BatchAggregator: CollectionTreeBatcher, getAggregator: getBatcher, - removeAggregator: removeBatcher + 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 d94c1e11d..b2acdd942 100644 --- a/packages/bruno-electron/src/app/collection-watcher.js +++ b/packages/bruno-electron/src/app/collection-watcher.js @@ -226,6 +226,8 @@ 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 = { @@ -292,7 +294,8 @@ 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); + // win.webContents.send('main:collection-tree-updated', 'addFile', file); + batcher.add('addFile', file); return; } catch (err) { console.error(err); @@ -312,8 +315,6 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread } }; - const batcher = getBatcher(win, collectionUid); - try { const fileStats = await fsPromises.stat(pathname); diff --git a/packages/bruno-electron/tests/app/collection-tree-batcher.spec.js b/packages/bruno-electron/tests/app/collection-tree-batcher.spec.js index 4cbe66c11..d6dd778e7 100644 --- a/packages/bruno-electron/tests/app/collection-tree-batcher.spec.js +++ b/packages/bruno-electron/tests/app/collection-tree-batcher.spec.js @@ -1,4 +1,4 @@ -const { CollectionTreeBatcher, getBatcher, removeBatcher } = require('../../src/app/collection-tree-batcher'); +const { CollectionTreeBatcher, getBatcher, removeBatcher, constants } = require('../../src/app/collection-tree-batcher'); // Mock BrowserWindow const createMockWindow = (id = 1) => { @@ -164,16 +164,16 @@ describe('CollectionTreeBatcher', () => { }); describe('size-based flush', () => { - it('should auto-flush when reaching MAX_BATCH_SIZE (300)', () => { + it('should auto-flush when reaching MAX_BATCH_SIZE', () => { const win = createMockWindow(); const batcher = new CollectionTreeBatcher(win, 'collection-1'); - - // Add 299 events - should not flush - for (let i = 0; i < 299; i++) { + 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(299); + expect(batcher.queue).toHaveLength(eventCount); // Add 300th event - should trigger flush batcher.add('addFile', { path: '/test/file299.bru' });