TidGi-Desktop/docs/internal/IPCSyncAdaptorAndFSAdaptor.md
lin onetwo d04f497a02 Refactor file system sync to use $tw.syncer.syncFromServer()
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.
2026-01-05 15:28:06 +08:00

294 lines
12 KiB
Markdown

# 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 `nsfw` library
- 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 `updatedTiddlers` list
- 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.ts` tracks recently saved tiddlers in `recentlySavedTiddlers` Set
- 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 from `window.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 `nsfw` library for native file system watching
- Maintains `updatedTiddlers` list 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 changes
- `loadTiddler(title, callback)`: Loads tiddler content from file
- `excludeFile(path)`: Temporarily exclude file from watching
- `scheduleFileInclusion(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 FileSystemWatcher
- `loadTiddler()`: Delegates to FileSystemWatcher
- `saveTiddler()`: Saves to file with exclusion handling
- `deleteTiddler()`: 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 `updatedTiddlers` list 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
```typescript
// 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
1. Check IPC Observable connection: Look for `[test-id-SSE_READY]` in logs
2. Verify watch-fs is running: Look for `[test-id-WATCH_FS_STABILIZED]`
3. Check file exclusion: Should see file being excluded then included
### Echo/Duplicate Updates
1. Check `recentlySavedTiddlers` filtering in ipcServerRoutes.ts
2. Verify file exclusion during save/delete operations
3. Check syncer's changeCount tracking
### Git Checkout Issues
1. Ensure FILE_DELETION_DELAY_MS is working (files not prematurely deleted)
2. Check that SYNCER_TRIGGER_DELAY_MS allows batch collection
3. Verify syncer processes all changes in single SyncFromServerTask
### Sub-Wiki Not Syncing
1. Check sub-wiki watcher initialization: Look for `[WATCH_FS_SUBWIKI]` logs
2. Verify tiddlywiki.info has correct configuration
3. Check workspace `subWikiFolders` setting
## Design Decisions
### Why Syncer-Driven Instead of Direct Updates?
1. **Echo Prevention**: Syncer's `storeTiddler()` properly updates changeCount, preventing save loops
2. **Batch Handling**: Syncer queues all changes and processes them sequentially
3. **Throttling**: Built-in throttle prevents rapid-fire saves
4. **Error Recovery**: Syncer has built-in retry logic
### Why Two-Layer Echo Prevention?
1. **IPC Layer**: Prevents frontend from seeing its own saves via IPC
2. **Watch-FS Layer**: Prevents file watcher from seeing our own file writes
3. **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:
1. CREATE event to arrive and cancel pending DELETE
2. Treat as modification instead of delete+create
3. Prevents "missing tiddler" errors during git operations