Use this document to quickly onboard and resume development from scratch. It details the system architecture, component design, and critical, hard-earned bug fixes.
graph TD
App[BookmarkSyncApp] --> Tray[TrayMenu Popover]
App --> VM[AppViewModel @MainActor]
VM --> SE[SyncEngine @MainActor]
SE --> DB[(SwiftData: BookmarkNode / BrowserConfig)]
SE --> Parsers[Browser Parsers]
SE --> WQ[WriteQueue]
Parsers --> Safari[SafariParser plist]
Parsers --> Chrome[ChromeParser JSON]
Parsers --> Firefox[FirefoxParser SQLite]
WQ --> Files[Browser Files]
BookmarkSyncApp.swift: Entry point. Sets up popoverMenuBarExtraand launches the primarySyncEngineon start.TrayMenu.swift/BackupsView.swift: SwiftUI popover views. Enables toggling browser profiles, viewing backups, and displaying live diff queue logs.AppViewModel.swift: Unified reactive state holder. Manages list of discovered browser profiles, diff history queue, and full disk access checks.SyncEngine.swift: The synchronization heart. Fetches current browser bookmark trees, merges them against the global state, calculates differences, and triggers database/file updates.FileWatcher.swift: Wrapper around macOSFSEventssystem. Watches active browser directories recursively on a background thread.WriteQueue.swift: Thread-safe serial write pipeline. Queues updates for active browsers and defer-writes them if the target browser process is running to avoid in-memory cache clobbering.
- The Issue: Manual changes inside Xcode (
.xcodeproj) were wiped out wheneverxcodegen generatewas executed. - The Fix:
propertiesmust be nested under targetinfo(notsettings) inproject.ymlforLSUIElement: trueto compile correctly toInfo.plist(hiding the app from the Dock).- Code signing configurations must be defined in
project.ymltargetsettingsto persist through generations:settings: CODE_SIGN_STYLE: Automatic DEVELOPMENT_TEAM: MCC7MZGLZ8
- The Issue:
- Initializing optional
@Stateselection tonilcaused a persistent console warning: "Picker: the selection 'nil' is invalid... undefined results". - Wrapping
ScrollViewin dynamic popovers caused the height to collapse to0.
- Initializing optional
- The Fix:
- Wrapped
Pickerinsideif selectedConfigId != niland populated selection immediately via parentVStack.onAppearand.onChange(of: configs). - Set explicit ScrollView height dynamically using
min(count * 24, 150)when items exist, rendering static text directly when empty.
- Wrapped
- The Issue: Accessing deleted/modified
BookmarkNodeentities inside the serial background write queue caused a fatal crash: "This backing data was detached from a context without resolving attribute faults". - The Fix:
- Before calling
modelContext.deleteon stale database nodes, the sync engine maps/clonesmergednodes into brand new, completely unmanagedBookmarkNodeinstances. - The clean unmanaged nodes are safely saved to the database and passed to the asynchronous
WriteQueuewithout any active context coupling.
- Before calling
- The Issue: Watching entire application support directories recursively triggered high CPU usage and loops due to unrelated browser writes (history, cookies, session restore).
- The Fix:
SyncEnginemanages theFileWatcherdynamically, only watching the specific parent directories of currently enabled profiles.- In FSEvents callback, all events are strictly filtered to ignore unrelated files and trigger sync only when the changed path matches the exact bookmark file (
Bookmarks,Bookmarks.plist,places.sqlite). - Added workspace notification observer for
NSWorkspace.didTerminateApplicationNotificationinWriteQueue. Pending writes are flushed instantly the moment a user closes a running browser.
- The Issue:
- Chrome included its root folders in results (nesting child bookmarks one level deep), whereas Safari and Firefox omitted root folders (placing child bookmarks at
parentId: nil). This mismatch caused endless delete/add loops. - Safari has no bookmark-level
mtimemodification timestamps, resulting in fallbackDate()(current time) marking Safari bookmarks as "always newer" than Chrome's actual modifications.
- Chrome included its root folders in results (nesting child bookmarks one level deep), whereas Safari and Firefox omitted root folders (placing child bookmarks at
- The Fix:
- Flattened Roots:
ChromeParser.read()updated to skip traversing root folders, setting immediate children toparentId: nil. Flat structures now align globally. - Robust URL Normalization: Added
normalizeURLutilizing SwiftURLparsing to strip protocols (http/https), host prefixes (www.), trailing slashes (/), query strings, and hashes from the compositeid. Complex URL variations map to the identical node ID. - Safari Timestamp Pollution: Set Safari node
mtimefallback to a constant epoch dateDate(timeIntervalSince1970: 0)(Jan 1, 1970). - SyncEngine Rename Sorter: Implemented logic in
SyncEngine.mergethat prioritizes renames by checking which browser changed relative to the DB sync state (stateNode.title). Conflicting dual-direction queues are resolved cleanly in a single winner direction.
- Flattened Roots:
- The Issue: When
SyncEnginedispatched writes to browsers that were currently open (e.g., Safari lockingBookmarks.plist), the write queued up. On the next sync cycle, comparing the old disk file against the speculative "Hub" state falsely made the engine believe the user locally deleted the pending items, triggering cascading deletions across all synced browsers. - The Fix:
SyncEnginenow strictly maintains anobservedStateData(JSON-encodedBookmarkNodeRecords) attached toBrowserConfig.- The observed state is only ever updated to match what is successfully parsed from the physical disk—never speculatively.
- If a file write is blocked, the disk file matches the old observed state natively, registering
0changes, and simply deferring the operation until the browser is closed and accepts the write.
- The Issue:
SafariParsergenerated folder IDs using unique UUIDs, whileChromeParsergenerated them using the folder's name. This caused massive duplication/deletion loops because the Hub couldn't reconcile identically-named folders across browsers. Additionally, concurrent modification (one profile deletes, another renames) heavily favored deletion. - The Fix:
- Title-Based Folder IDs:
SafariParserandFirefoxParsernow explicitly generate unique folder IDs based on their title (name), perfectly aligning with Chrome's approach. Same-named folders now cleanly merge natively in the Hub. - Robust Prefix Stripping:
BookmarkNode.stripProfileSetPrefixwas hardened using segment-splitting (max 2 splits) to prevent folder names containing colons (e.g.,My: Folder) from corrupting the ID routing logic. - Updates Trump Deletes: Deletions are now strictly validated against
observedStateData. If a profile attempts to delete a node that has been modified (renamed/moved) by another profile since its last sync, the deletion is rejected and the node is preserved or resurrected.
- Title-Based Folder IDs:
