Introduces FileSystemWatcher to monitor file changes and collect updates for the syncer, replacing direct wiki updates in WatchFileSystemAdaptor. Updates documentation to describe the new syncer-driven architecture, echo prevention, and event handling. WatchFileSystemAdaptor now delegates file change detection and lazy loading to FileSystemWatcher, improving batch change handling and eliminating echo loops.
12 KiB
Sync Architecture: IPC and Watch-FS Plugins
This document describes how the tidgi-ipc-syncadaptor (frontend) and watch-filesystem-adaptor (backend) plugins work together to provide real-time bidirectional synchronization between the TiddlyWiki in-memory store and the file system.
Architecture Overview
Frontend (Browser) Backend (Node.js Worker)
┌─────────────────────────────┐ ┌─────────────────────────────────────┐
│ TiddlyWiki │ │ TiddlyWiki (Server) │
│ In-Memory Store │ │ In-Memory Store │
└──────────┬──────────────────┘ └──────────┬──────────────────────────┘
│ │
│ TidGiIPCSyncAdaptor │ WatchFileSystemAdaptor
│ (syncadaptor) │ (syncadaptor)
│ │
│ ├── FileSystemWatcher
│ │ (monitors files via nsfw)
│ │
├─── Save via IPC ──────────────────►│
│ │
│◄────── IPC Observable ─────────────┤
│ (Change Events) │
│ │
│ ├── FileSystemAdaptor
│ │ (read/write files)
│ │
│ ▼
│ File System
└────────────────────────────────────┘
Key Design Principles
1. Single Source of Truth: File System
- Backend watch-fs monitors the file system using
nsfwlibrary - All file changes (external edits, saves from frontend) flow through file system
- Backend wiki state reflects file system state
2. Syncer-Driven Updates (Refactored Architecture)
Previous Approach (Problematic):
- FileSystemWatcher directly called
wiki.addTiddler()when files changed - Led to echo problems and complex edge case handling
Current Approach (Syncer-Driven):
- FileSystemWatcher only collects changes into
updatedTiddlerslist - Triggers
$tw.syncer.syncFromServer()to let TiddlyWiki's syncer handle updates - Syncer calls
getUpdatedTiddlers()to get change list - Syncer calls
loadTiddler()for each modified tiddler - Syncer uses
storeTiddler()which properly updates changeCount to prevent echo
Benefits:
- Leverages TiddlyWiki's built-in sync queue and throttling
- Proper handling of batch changes (git checkout)
- Eliminates echo loops via syncer's changeCount tracking
3. Two-Layer Echo Prevention
IPC Layer (First Defense):
ipcServerRoutes.tstracks recently saved tiddlers inrecentlySavedTiddlersSet- When
wiki.addTiddler()triggers change event, filter out tiddlers in the Set - Prevents frontend from receiving its own save operations as change notifications
Watch-FS Layer (Second Defense):
- When saving/deleting, watch-fs temporarily excludes file from monitoring
- Prevents watcher from detecting the file write operation
- Re-includes file after operation completes (with delay for nsfw debounce)
4. IPC Observable for Change Notification
- Backend sends change events to frontend via IPC Observable pattern (via
ipc-cat) - Frontend subscribes to
getWikiChangeObserver$observable fromwindow.observables.wiki - Change events trigger frontend's
syncFromServer()to pull updates
Module Responsibilities
FileSystemWatcher (Backend - New)
Purpose: Monitor file system changes without directly modifying wiki state
Key Features:
- Uses
nsfwlibrary for native file system watching - Maintains
updatedTiddlerslist for pending changes - Implements
getUpdatedTiddlers()for syncer integration - Implements
loadTiddler()for lazy loading from file system - Handles git revert/checkout via delayed deletion processing
- Manages file exclusion list for echo prevention
Key Methods:
getUpdatedTiddlers(syncer, callback): Returns collected changesloadTiddler(title, callback): Loads tiddler content from fileexcludeFile(path): Temporarily exclude file from watchingscheduleFileInclusion(path): Re-include file after delay
WatchFileSystemAdaptor (Backend)
Purpose: Coordinate between FileSystemWatcher and syncer, implement syncadaptor interface
Key Features:
- Extends FileSystemAdaptor for file save/delete operations
- Delegates file watching to FileSystemWatcher
- Implements full syncadaptor interface for Node.js syncer
- Coordinates file exclusion during save/delete operations
Key Methods:
getUpdatedTiddlers(): Delegates to FileSystemWatcherloadTiddler(): Delegates to FileSystemWatchersaveTiddler(): Saves to file with exclusion handlingdeleteTiddler(): Deletes file with exclusion handling
FileSystemAdaptor (Backend - Base Class)
Purpose: Handle tiddler file save/delete operations with sub-wiki routing
Key Features:
- Routes tiddlers to sub-wikis based on tags
- Generates file paths using TiddlyWiki's FileSystemPaths
- Handles external attachment file movement
- Provides retry logic for file lock errors
TidGiIPCSyncAdaptor (Frontend)
Purpose: Bridge between frontend TiddlyWiki and backend file system
Key Features:
- Communicates via IPC using
tidgi://custom protocol - Subscribes to change events via IPC Observable
- Maintains
updatedTiddlerslist from IPC events - Implements full syncadaptor interface for browser syncer
Data Flow Examples
Example 1: User Edits in Frontend
1. User clicks save in browser
├─► Frontend syncer calls saveTiddler()
│
2. TidGiIPCSyncAdaptor.saveTiddler()
├─► IPC call to putTiddler in ipcServerRoutes.ts
│
3. ipcServerRoutes.putTiddler()
├─► Marks tiddler in recentlySavedTiddlers (IPC echo prevention)
├─► Calls wiki.addTiddler() (triggers change event)
├─► Change event filtered by recentlySavedTiddlers
│
4. Backend syncer detects change
├─► Calls WatchFileSystemAdaptor.saveTiddler()
│
5. WatchFileSystemAdaptor.saveTiddler()
├─► Excludes file path from watching
├─► Calls FileSystemAdaptor.saveTiddler()
├─► Writes file to disk
├─► Schedules file re-inclusion after delay
│
6. nsfw might detect file change
├─► FileSystemWatcher checks exclusion list
├─► File is excluded, event ignored
Example 2: External Editor Modifies File
1. User edits file in VSCode/Vim
├─► File content changes on disk
│
2. nsfw detects file change
├─► File NOT in excludedFiles
│
3. FileSystemWatcher.handleFileAddOrChange()
├─► Adds title to updatedTiddlers.modifications
├─► Stores file info in pendingFileLoads
├─► Schedules syncer trigger (debounced 200ms)
│
4. $tw.syncer.syncFromServer() called
├─► Creates SyncFromServerTask
│
5. SyncFromServerTask.run()
├─► Calls getUpdatedTiddlers()
├─► Gets modifications/deletions list
├─► Adds titles to titlesToBeLoaded
│
6. For each title to load:
├─► LoadTiddlerTask.run()
├─► Calls loadTiddler(title)
├─► FileSystemWatcher loads from file
├─► syncer.storeTiddler() updates wiki
├─► Properly sets changeCount (prevents echo save)
│
7. wiki.addTiddler() triggers change event
├─► getWikiChangeObserver sends to frontend
│
8. Frontend receives change
├─► TidGiIPCSyncAdaptor adds to updatedTiddlers
├─► Frontend syncer.syncFromServer()
├─► Frontend loadTiddler() via IPC
├─► Frontend wiki updated
Example 3: Git Checkout (Batch Changes)
1. User runs git checkout
├─► Many files deleted/created/modified
│
2. nsfw debounces events (100ms)
├─► Multiple events batched together
│
3. FileSystemWatcher.handleNsfwEvents()
├─► For each DELETED file:
│ └─► Schedule deletion with 100ms delay
│ (handles git revert/checkout pattern)
├─► For each CREATED/MODIFIED file:
│ └─► Cancel any pending deletion for same path
│ └─► Add to updatedTiddlers.modifications
│
4. Syncer trigger debounced (200ms)
├─► All changes collected before sync starts
│
5. Single SyncFromServerTask processes all changes
├─► All modifications queued for loading
├─► All deletions processed (wiki.deleteTiddler)
│
6. LoadTiddlerTasks process sequentially
├─► Each tiddler loaded from file
├─► Frontend notified via IPC Observable
Key Configuration
Timing Constants
// FileSystemWatcher.ts
FILE_DELETION_DELAY_MS = 100 // Delay before processing DELETE events
FILE_INCLUSION_DELAY_MS = 150 // Delay before re-including file after save
GIT_NOTIFICATION_DELAY_MS = 1000 // Debounce for git status notification
SYNCER_TRIGGER_DELAY_MS = 200 // Debounce for syncer trigger
Syncer Configuration
- Frontend:
pollTimerInterval = 2_147_483_647(effectively disabled) - All updates come via IPC Observable (event-driven)
Troubleshooting
Changes Not Appearing in Frontend
- Check IPC Observable connection: Look for
[test-id-SSE_READY]in logs - Verify watch-fs is running: Look for
[test-id-WATCH_FS_STABILIZED] - Check file exclusion: Should see file being excluded then included
Echo/Duplicate Updates
- Check
recentlySavedTiddlersfiltering in ipcServerRoutes.ts - Verify file exclusion during save/delete operations
- Check syncer's changeCount tracking
Git Checkout Issues
- Ensure FILE_DELETION_DELAY_MS is working (files not prematurely deleted)
- Check that SYNCER_TRIGGER_DELAY_MS allows batch collection
- Verify syncer processes all changes in single SyncFromServerTask
Sub-Wiki Not Syncing
- Check sub-wiki watcher initialization: Look for
[WATCH_FS_SUBWIKI]logs - Verify tiddlywiki.info has correct configuration
- Check workspace
subWikiFolderssetting
Design Decisions
Why Syncer-Driven Instead of Direct Updates?
- Echo Prevention: Syncer's
storeTiddler()properly updates changeCount, preventing save loops - Batch Handling: Syncer queues all changes and processes them sequentially
- Throttling: Built-in throttle prevents rapid-fire saves
- Error Recovery: Syncer has built-in retry logic
Why Two-Layer Echo Prevention?
- IPC Layer: Prevents frontend from seeing its own saves via IPC
- Watch-FS Layer: Prevents file watcher from seeing our own file writes
- Both needed: IPC prevents wiki→wiki echo, Watch-FS prevents file→wiki echo
Why Delay DELETE Events?
Git operations often delete then recreate files quickly. The delay allows:
- CREATE event to arrive and cancel pending DELETE
- Treat as modification instead of delete+create
- Prevents "missing tiddler" errors during git operations