1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-26 22:41:17 +02:00

Desktop: Fix infinite loop on startup after quickly moving folders (#12140)

This commit is contained in:
Henry Heino
2025-04-24 00:52:35 -07:00
committed by GitHub
parent e953290810
commit ddc75ecc13
2 changed files with 50 additions and 3 deletions

View File

@@ -163,6 +163,23 @@ describe('models/Folder', () => {
} }
})); }));
it('folder hierarchy cycles should not cause addNoteCounts to loop infinitely', async () => {
const f1 = await Folder.save({ title: 'folder1' });
const f2 = await Folder.save({ title: 'folder2', parent_id: f1.id });
const f3 = await Folder.save({ title: 'folder3', parent_id: f2.id });
await Note.save({ title: 'test', parent_id: f1.id });
// Create a cycle.
// Note: This has been observed to happen, likely as a result of a bug in other code.
await Folder.save({ id: f1.id, parent_id: f3.id });
const folders = await Folder.all();
// Should not loop indefinitely:
await Folder.addNoteCounts(folders);
// Note count may be incorrect
expect(folders.find(folder => folder.id === f1.id)).toHaveProperty('note_count');
});
it('should not count completed to-dos', (async () => { it('should not count completed to-dos', (async () => {
const f1 = await Folder.save({ title: 'folder1' }); const f1 = await Folder.save({ title: 'folder1' });

View File

@@ -187,6 +187,24 @@ export default class Folder extends BaseItem {
}; };
} }
// Checks for invalid state -- whether startId or its parents is part of a cycle
// in the folder graph (which should be a tree).
private static checkForFolderHierarchyCycle_(
idToFolder: Record<string, FolderEntity>,
startId: string,
) {
let folderId = startId;
const seenIds = new Set();
for (; idToFolder[folderId]; folderId = idToFolder[folderId].parent_id) {
if (seenIds.has(folderId)) {
return true;
}
seenIds.add(folderId);
}
return false;
}
// Calculates note counts for all folders and adds the note_count attribute to each folder // Calculates note counts for all folders and adds the note_count attribute to each folder
// Note: this only calculates the overall number of nodes for this folder and all its descendants // Note: this only calculates the overall number of nodes for this folder and all its descendants
public static async addNoteCounts(folders: FolderEntity[], includeCompletedTodos = true) { public static async addNoteCounts(folders: FolderEntity[], includeCompletedTodos = true) {
@@ -226,10 +244,22 @@ export default class Folder extends BaseItem {
} }
const noteCounts: NoteCount[] = await this.db().selectAll(sql); const noteCounts: NoteCount[] = await this.db().selectAll(sql);
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied for (const noteCount of noteCounts) {
noteCounts.forEach((noteCount) => {
let parentId = noteCount.folder_id; let parentId = noteCount.folder_id;
let i = 0;
let checkedForCycle = false;
do { do {
// Handle invalid state, preventing infinite loops -- check whether the current
// folder has itself as a parent.
if (i++ > 100 && !checkedForCycle) {
if (Folder.checkForFolderHierarchyCycle_(foldersById, parentId)) {
logger.warn(`Invalid state: Folder ${parentId} has itself as a parent.`);
break;
}
checkedForCycle = true;
}
const folder = foldersById[parentId]; const folder = foldersById[parentId];
if (!folder) break; // https://github.com/laurent22/joplin/issues/2079 if (!folder) break; // https://github.com/laurent22/joplin/issues/2079
folder.note_count = (folder.note_count || 0) + noteCount.note_count; folder.note_count = (folder.note_count || 0) + noteCount.note_count;
@@ -240,7 +270,7 @@ export default class Folder extends BaseItem {
parentId = folder.parent_id; parentId = folder.parent_id;
} while (parentId); } while (parentId);
}); }
} }
// Folders that contain notes that have been modified recently go on top. // Folders that contain notes that have been modified recently go on top.