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
This commit is contained in:
Sid
2026-02-27 13:43:15 +05:30
committed by GitHub
parent 39d6999cb2
commit bf4af42a25
5 changed files with 219 additions and 121 deletions

View File

@@ -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 }));

View File

@@ -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;

View File

@@ -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
}
};

View File

@@ -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);

View File

@@ -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' });