Skip to content

Commit 607d561

Browse files
authored
fix: preserve bookmark ordering during sync — stop items moving to end of toolbar (#8)
Root cause: addCloudBookmarksToLocal passed the cloud's absolute index to browser.bookmarks.create(), but when the local folder had fewer items than the cloud index value, the browser clamped the index to the end of the list. Sequential insertions also caused cascading index shifts where each create at index N shifted all items at N+ to the right. The reorder pass (reorderLocalToMatchCloud) was designed to fix this, but it was being skipped too often because the locallyModifiedBookmarkIds check was using the live set — which got polluted by sync-driven onMoved events during the add/update phase. Fix: 1. addCloudBookmarksToLocal: create bookmarks/folders WITHOUT specifying index. Let the dedicated reorder pass handle all positioning. 2. updateLocalBookmarksFromCloud: for index-only changes, skip the move entirely and let the reorder pass handle it. For folder changes, move without specifying index. 3. reorderLocalToMatchCloud: accept a userModifiedIds snapshot taken at the START of performSync (before any sync-driven changes). Only skip reordering when the USER rearranged bookmarks, not when sync-driven creates/moves happened to trigger onMoved events. 4. performSync: capture locallyModifiedBookmarkIds snapshot before the pull phase and pass it to the reorder function.
1 parent 392294e commit 607d561

1 file changed

Lines changed: 43 additions & 55 deletions

File tree

apps/extension/src/background/index.js

Lines changed: 43 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1627,6 +1627,12 @@ async function performSync(sourceId) {
16271627

16281628
// Two-way sync with tombstone support
16291629
try {
1630+
// Snapshot user-modified bookmark IDs BEFORE the pull phase.
1631+
// This snapshot is passed to reorderLocalToMatchCloud so it only skips
1632+
// reordering for folders the USER rearranged, not folders where
1633+
// sync-driven creates/moves/deletes happened to trigger onMoved events.
1634+
const userModifiedIdsSnapshot = new Set(locallyModifiedBookmarkIds);
1635+
16301636
// Step 1: Get current local bookmarks and tombstones
16311637
const bookmarkTree = await browser.bookmarks.getTree();
16321638
const localFlat = flattenBookmarkTree(bookmarkTree);
@@ -1782,7 +1788,7 @@ async function performSync(sourceId) {
17821788
// so earlier moves don't shift later targets).
17831789
isSyncDrivenChange = true;
17841790
try {
1785-
await reorderLocalToMatchCloud(cloudBookmarks);
1791+
await reorderLocalToMatchCloud(cloudBookmarks, userModifiedIdsSnapshot);
17861792
} catch (reorderErr) {
17871793
console.warn('[MarkSyncr] Reorder pass failed (non-fatal):', reorderErr.message);
17881794
} finally {
@@ -2340,44 +2346,29 @@ async function addCloudBookmarksToLocal(cloudItems) {
23402346
const targetFolderId = await getOrCreateFolderPath(item._folderPath, rootFolder.id);
23412347

23422348
if (item.type === 'folder') {
2343-
// It's a folder entry - create the folder at the correct index
2344-
// First check if folder already exists
2349+
// It's a folder entry - create the folder WITHOUT specifying index.
2350+
// Index-based positioning is handled by reorderLocalToMatchCloud after
2351+
// all items are created. Setting index here causes cascading shifts:
2352+
// each insertion at index N shifts all items at N+ to the right, so
2353+
// subsequent inserts land at the wrong position. When the cloud index
2354+
// exceeds the local folder's item count, the browser clamps it to the
2355+
// end — the main cause of "bookmarks end up at the END of toolbar."
23452356
const children = await browser.bookmarks.getChildren(targetFolderId);
23462357
const existingFolder = children.find((c) => !c.url && c.title === item.title);
23472358

23482359
if (existingFolder) {
2349-
// Folder exists - check if it's at the correct index
2350-
if (typeof item.index === 'number' && existingFolder.index !== item.index) {
2351-
// Move to correct position
2352-
try {
2353-
await browser.bookmarks.move(existingFolder.id, {
2354-
parentId: targetFolderId,
2355-
index: item.index,
2356-
});
2357-
console.log(
2358-
`[MarkSyncr] Moved existing folder "${item.title}" to index ${item.index}`
2359-
);
2360-
} catch (moveErr) {
2361-
console.warn(`[MarkSyncr] Failed to move folder "${item.title}":`, moveErr);
2362-
}
2363-
}
2364-
// Cache the folder ID
2360+
// Folder already exists - just cache it, reorder pass will fix position
23652361
const folderFullPath = item._folderPath
23662362
? `${item._folderPath}/${item.title}`
23672363
: item.title;
23682364
folderCache.set(`${rootFolder.id}:${folderFullPath}`, existingFolder.id);
23692365
} else {
2370-
// Create new folder at the correct index
2366+
// Create new folder without index — reorder pass will position it
23712367
const createOptions = {
23722368
parentId: targetFolderId,
23732369
title: item.title || 'Untitled Folder',
23742370
};
23752371

2376-
// Set index if available
2377-
if (typeof item.index === 'number' && item.index >= 0) {
2378-
createOptions.index = item.index;
2379-
}
2380-
23812372
const newFolder = await browser.bookmarks.create(createOptions);
23822373
addedFolders++;
23832374

@@ -2387,21 +2378,19 @@ async function addCloudBookmarksToLocal(cloudItems) {
23872378
: item.title;
23882379
folderCache.set(`${rootFolder.id}:${folderFullPath}`, newFolder.id);
23892380

2390-
console.log(`[MarkSyncr] Created folder "${item.title}" at index ${item.index}`);
2381+
console.log(
2382+
`[MarkSyncr] Created folder "${item.title}" (reorder pass will set position)`
2383+
);
23912384
}
23922385
} else if (item.url) {
2393-
// It's a bookmark - create at the correct index
2386+
// It's a bookmark - create WITHOUT index.
2387+
// Reorder pass will position it correctly afterward.
23942388
const createOptions = {
23952389
parentId: targetFolderId,
23962390
title: item.title ?? '',
23972391
url: item.url,
23982392
};
23992393

2400-
// Set index if available
2401-
if (typeof item.index === 'number' && item.index >= 0) {
2402-
createOptions.index = item.index;
2403-
}
2404-
24052394
await browser.bookmarks.create(createOptions);
24062395
addedBookmarks++;
24072396
}
@@ -2439,7 +2428,7 @@ async function addCloudBookmarksToLocal(cloudItems) {
24392428
*
24402429
* @param {Array} cloudBookmarks - Flat array of cloud bookmarks with folderPath and index
24412430
*/
2442-
async function reorderLocalToMatchCloud(cloudBookmarks) {
2431+
async function reorderLocalToMatchCloud(cloudBookmarks, userModifiedIds = null) {
24432432
// Group cloud items by their normalized folder path
24442433
const cloudByFolder = new Map();
24452434
for (const cb of cloudBookmarks) {
@@ -2524,18 +2513,22 @@ async function reorderLocalToMatchCloud(cloudBookmarks) {
25242513

25252514
if (children.length < 2) continue;
25262515

2527-
// Skip reordering only when the pattern of local modifications looks like
2528-
// a local reorder. When a user rearranges bookmarks locally, multiple (often
2529-
// all) siblings in the folder are marked as locally modified. If we reorder
2530-
// to match cloud in that case, we overwrite the user's intended arrangement.
2531-
// The local order will be pushed to cloud in the push step instead.
2516+
// Skip reordering only when the user manually rearranged bookmarks
2517+
// before this sync started. We use `userModifiedIds` — a snapshot of
2518+
// locallyModifiedBookmarkIds taken at the START of performSync, before
2519+
// any sync-driven creates/moves/deletes. This ensures that sync-driven
2520+
// changes (adds, folder moves, tombstone deletions) don't accidentally
2521+
// trigger the skip, which was the root cause of "bookmarks end up at
2522+
// the end of the toolbar" — the reorder pass was being skipped because
2523+
// sync-driven onMoved events polluted locallyModifiedBookmarkIds.
2524+
const idsToCheck = userModifiedIds || locallyModifiedBookmarkIds;
25322525
const modifiedChildrenCount = children.reduce(
2533-
(count, child) => (locallyModifiedBookmarkIds.has(child.id) ? count + 1 : count),
2526+
(count, child) => (idsToCheck.has(child.id) ? count + 1 : count),
25342527
0
25352528
);
25362529
if (modifiedChildrenCount > 1) {
25372530
console.log(
2538-
`[MarkSyncr] 🏠 Skipping reorder for folder "${folderPath}" — contains multiple locally modified bookmarks (${modifiedChildrenCount})`
2531+
`[MarkSyncr] 🏠 Skipping reorder for folder "${folderPath}" — contains multiple user-modified bookmarks (${modifiedChildrenCount})`
25392532
);
25402533
continue;
25412534
}
@@ -2717,27 +2710,22 @@ async function updateLocalBookmarksFromCloud(bookmarksToUpdate) {
27172710
);
27182711
}
27192712

2720-
// Move to new folder or position if needed
2721-
if (folderChanged || indexChanged) {
2713+
// Move to new folder if the folder changed. Index-only changes are
2714+
// handled by the reorderLocalToMatchCloud pass, which processes all
2715+
// children of each folder in one go (last-to-first) to avoid the
2716+
// cascading index shift problem caused by sequential moves.
2717+
if (folderChanged) {
27222718
const rootFolder = rootFolders[cloudRootKey] || rootFolders.other || rootFolders.toolbar;
27232719

27242720
if (rootFolder) {
27252721
const targetFolderId = await getOrCreateFolderPath(cloudRelativePath, rootFolder.id);
27262722

2727-
const moveOptions = { parentId: targetFolderId };
2728-
if (cloud.index !== undefined && cloud.index >= 0) {
2729-
moveOptions.index = cloud.index;
2730-
}
2731-
2732-
await browser.bookmarks.move(local.id, moveOptions);
2723+
// Move to new folder without specifying index — reorder pass will position it
2724+
await browser.bookmarks.move(local.id, { parentId: targetFolderId });
27332725

2734-
if (folderChanged) {
2735-
console.log(
2736-
`[MarkSyncr] Moved ${cloud.url} from "${local.folderPath}" to "${cloud.folderPath}"`
2737-
);
2738-
} else {
2739-
console.log(`[MarkSyncr] Repositioned ${cloud.url} to index ${cloud.index}`);
2740-
}
2726+
console.log(
2727+
`[MarkSyncr] Moved ${cloud.url} from "${local.folderPath}" to "${cloud.folderPath}" (reorder pass will set position)`
2728+
);
27412729
}
27422730
}
27432731

0 commit comments

Comments
 (0)