Overview
Sync Folds is built as a lightweight Obsidian plugin that intercepts localStorage operations to capture fold state changes and synchronizes them through Obsidian’s plugin data storage system. This architecture enables cross-device synchronization while maintaining compatibility with Obsidian’s existing fold management.Core Components
SyncFolds Plugin Class
The main plugin class (main.ts:6-269) extends Obsidian’s Plugin base class and manages the entire fold synchronization lifecycle:
- Initialize localStorage interception
- Manage bidirectional sync between localStorage and plugin settings
- Handle external settings changes from file sync
- Provide manual export/import commands
- Maintain a cached copy of fold states for change detection
localStorage Interception Mechanism
The plugin uses a monkey-patching approach to intercept all localStorage operations related to fold states (main.ts:130-159):
- Obsidian stores fold states in localStorage using the pattern
{appId}-note-fold-{filePath} - By intercepting
setItemandremoveItem, the plugin captures all fold changes without modifying Obsidian’s core behavior - Original methods are preserved and restored on unload to prevent conflicts
Debouncing System
To prevent excessive disk writes, the plugin debounces fold state changes with a 150ms delay (main.ts:170-182):
- Rapid fold/unfold operations (e.g., keyboard shortcuts) trigger only one write
- Reduces I/O operations and prevents settings file thrashing
- Each new change resets the timer, ensuring only the final state is saved
Settings Management
Settings are defined insettings.ts:1-9 with a minimal interface:
External Change Detection
When the plugin settings file (data.json) changes externally (e.g., via Obsidian Sync, iCloud, or Git), the onExternalSettingsChange() method (main.ts:73-107) intelligently merges changes:
- Cache the previous fold states before reloading
- Compare previous and current states by serializing to JSON
- Remove localStorage entries for deleted folds
- Update localStorage entries for modified folds
- Update the cache to reflect the new state
This method bypasses the intercepted
setItem/removeItem by calling originalSetItem.call() and originalRemoveItem.call() to avoid triggering the debounced sync logic.Data Structures
The plugin uses three TypeScript interfaces defined intypes.ts:1-13:
Fold
FoldedProperties
FoldStateData
settings.folds.
appId-Based Key System
Obsidian uses a uniqueappId for each vault instance. The plugin constructs localStorage keys using this pattern (main.ts:132-133):
abc123-note-fold-Daily Notes/2024-03-15.md
Why This Matters:
- Multiple Obsidian vaults on the same device use different
appIdvalues - This prevents fold state collisions between vaults sharing the same localStorage
- The plugin must use the same
appIdwhen reading/writing to match Obsidian’s keys
Sync Flow Diagrams
Local Change → Storage
External Change → Local
Performance Considerations
Caching Strategy
The plugin maintains acachedFolds object (main.ts:11) that stores the last known state:
- Enables efficient change detection without re-parsing JSON repeatedly
- Reduces unnecessary localStorage operations when external changes arrive
- Provides a rollback point if settings loading fails
Debouncing Parameters
The 150ms debounce delay (main.ts:181) was chosen to:
- Be imperceptible to users (< 200ms is considered instant)
- Catch rapid sequences of folds/unfolds (e.g., Ctrl+Shift+[ repeatedly)
- Balance between responsiveness and write reduction
Lazy Initialization
The plugin only initializes ifenableSync is true (main.ts:17-20):
Logging System
The plugin uses a conditional logging system defined inlog.ts:1-7:
- All internal operations are logged via
log()calls throughoutmain.ts - Logs are only output when
DEBUGis true (controlled by build configuration) - Prefixed with
[SyncFolds]for easy filtering in the console - Uses
console.debug()rather thanconsole.log()for semantic clarity
Design Decisions
Why localStorage Interception?
Alternatives Considered:- File system watching: Would miss in-memory fold changes until file save
- DOM mutation observers: Too low-level, tightly coupled to Obsidian’s rendering
- Polling localStorage: Inefficient, high CPU usage, delayed detection
- Captures changes immediately when Obsidian updates folds
- No dependency on Obsidian’s internal APIs (more stable across updates)
- Clean separation: Obsidian manages folds, plugin syncs them
Why Store as JSON String?
Thefolds setting is stored as a JSON string rather than an object:
- Obsidian’s
saveData()API requires serializable values - Storing pre-serialized JSON gives the plugin control over formatting
- Easier to inspect/edit the
data.jsonfile manually if needed - Reduces risk of Obsidian’s serialization introducing inconsistencies
Why Manual Commands?
The plugin provides explicit export/import commands (main.ts:38-54) even though automatic sync is active:
- Force a sync before switching devices
- Recover from a corrupted cache
- Debugging: verify what’s currently stored
- Manual control for users who prefer explicit actions
Thread Safety and Edge Cases
Concurrent Changes
The plugin handles several edge cases:- Rapid local changes: Debouncing ensures only the final state is saved
- External changes during debounce: The cache comparison in
onExternalSettingsChange()detects conflicts - Plugin reload: Fresh load from
data.jsonduringonload()ensures consistency
Cleanup on Unload
Theonunload() method (main.ts:109-116) ensures clean shutdown:
- Restores original localStorage methods to prevent memory leaks
- Cancels any pending debounced writes to avoid orphaned timers
- Prevents the plugin from interfering with localStorage after unload
Mobile Compatibility
The plugin is marked asisDesktopOnly: false in manifest.json:12 because:
- localStorage API is available on mobile WebView environments
- No Node.js or Electron APIs are used
- File I/O is handled through Obsidian’s
Pluginbase class APIs - Debouncing uses
window.setTimeout, available in all browsers
- Desktop: Windows, macOS, Linux
- Mobile: iOS (via Obsidian mobile app), Android
Security and Privacy
The plugin follows Obsidian’s security best practices:- No network requests: All data stays local to the device
- No telemetry: The
log()function outputs to the local console only - Vault-only access: Only reads/writes plugin settings through Obsidian’s API
- No external dependencies: Pure TypeScript, no third-party runtime libraries
- Open source: Code is auditable at the source repository
Future Considerations
Scalability
The current architecture stores all fold states in a single JSON object. For vaults with thousands of files:- Current approach: Manageable up to ~10,000 files (< 1MB of fold data)
- Potential optimization: Shard fold states across multiple setting keys if needed
- Alternative: Implement a custom file-based storage system
Conflict Resolution
The plugin uses a “last write wins” strategy. Future enhancements could include:- Timestamp-based merge conflict detection
- Per-file merge rather than whole-settings replacement
- User-configurable conflict resolution strategies
Selective Sync
Currently, all folds are synced. Potential features:- Exclude patterns (e.g., don’t sync folds in
.trash/folder) - Per-folder sync toggles
- Sync only specific file types