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

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 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

// 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