Skip to content

Vault File Watcher & Live Reload

Chazona Baum edited this page Jun 24, 2026 · 1 revision

Relevant source files

The vault file watcher provides a seamless "live reload" experience when the user modifies Markdown notes using external tools (e.g., Obsidian or VS Code). It monitors the local filesystem for changes, classifies them into entity types, and notifies the frontend to refresh its global stores.

Implementation Overview

The system is built on the notify and notify-debouncer-full crates to handle cross-platform filesystem events with event coalescing. The watcher is designed to ignore the app's own writes to prevent "reload echoes" where an internal save triggers a redundant refresh.

The Watcher Module (watcher.rs)

The core logic resides in src-tauri/src/watcher.rs. It defines which directories are watched and how file paths are mapped back to application entities.

Constant/Type Role
WATCHED A whitelist of directories (companies, jobs, domains, checks) and their corresponding entity kinds. src-tauri/src/watcher.rs#23-28
DEBOUNCE A 500ms window used to coalesce multiple rapid filesystem events (common during file saves) into a single notification. src-tauri/src/watcher.rs#31
WatcherState A Tauri-managed state struct holding a Mutex<Option<Debouncer>>. This ensures the watcher persists for the app lifecycle and can be replaced if the vault path changes. src-tauri/src/watcher.rs#43-49

Data Flow: Filesystem to Frontend

The following diagram illustrates the path of a file change event from the OS through the Rust backend to the Svelte stores.

File Change Event Propagation

flowchart TD
    subgraph subGraph2 ["Frontend (src/lib/vaultSync.ts)"]
        Listen["onRecordChanged()"]
        Stores["Global Stores (companiesStore, etc)"]
        UI["Svelte UI Update"]
    end
    subgraph subGraph1 ["Rust Backend (src-tauri/src/watcher.rs)"]
        Notify["notify-debouncer-full"]
        Handler["Event Handler Loop"]
        Classifier["Is Eligible?"]
        SelfWrite["was_self_write?"]
        Emitter["AppHandle.emit('record:changed')"]
    end
    subgraph subGraph0 ["External Space"]
        Obsidian["Obsidian / External Editor"]
        FS["Local Filesystem"]
    end
    Obsidian --> FS
    FS --> Notify
    Notify --> Handler
    Handler --> Classifier
    Classifier --> SelfWrite
    SelfWrite --> Emitter
    Emitter --> Listen
    Listen --> Stores
    Stores --> UI
Loading

Sources:src-tauri/src/watcher.rs#65-97src/lib/vaultSync.ts#33-48

Change Classification Logic

The classify_change function is responsible for determining if a filesystem event should trigger a UI refresh.

  1. Slug Extraction: It uses note_slug to extract the filename stem. This naturally ignores files starting with underscores (templates/sidecars) and non-Markdown files. src-tauri/src/watcher.rs#56
  2. Parent Directory: It checks if the file's parent directory is one of the four tracked entity folders. src-tauri/src/watcher.rs#57-58
  3. Eligibility: Files in nested subdirectories (like companies/_jd/) or the profile/ directory are explicitly ignored to avoid noise. src-tauri/src/watcher.rs#51-54

Sources:src-tauri/src/watcher.rs#55-60src-tauri/src/watcher.rs#138-144

Self-Write Suppression

To prevent infinite loops and redundant UI flickering, the watcher must distinguish between a user editing a file in Obsidian and the app updating a file (e.g., during a pipeline run or an internal CRUD operation).

Sources:src-tauri/src/watcher.rs#1-7src-tauri/src/watcher.rs#88-90

TypeScript Bridge & Store Routing

The frontend initiates the watcher via startVaultSync. Because the vault path is a user configuration managed by the frontend, the watcher cannot start until the frontend provides the path via the start_vault_watcher command. src-tauri/src/watcher.rs#65-70

startVaultSync(vaultPath)

This function in src/lib/vaultSync.ts performs two roles:

  1. Initialization: Invokes the Rust command to start the OS-level watcher. src/lib/vaultSync.ts#34
  2. Routing: Listens for record:changed events and dispatches them to the appropriate Svelte store. src/lib/vaultSync.ts#35-47

Store Routing Table

Event kind Action
company Calls companiesStore.load()
domain Calls domainsStore.reload()
check Calls checksStore.reload()
job Intentionally ignored in the global sync. Jobs are refreshed by individual company workspace components that subscribe to onRecordChanged locally. src/lib/vaultSync.ts#28-31

Sources:src/lib/vaultSync.ts#33-48src-tauri/src/watcher.rs#65-70

Entity Component Mapping

The following diagram maps the code entities involved in the watcher lifecycle.

Watcher System Architecture

classDiagram
    class WatcherState {
        +Mutex<Option<Debouncer>> 0
    }
    class watcher_rs {
        +WATCHED: const
        +start_vault_watcher(vault_path)
        +classify_change(path)
    }
    class vaultSync_ts {
        +startVaultSync(vaultPath)
        +onRecordChanged(callback)
    }
    class note_rs {
        +write_note(path, content)
        +was_self_write(path)
    }
    class companiesStore
    watcher_rs ..> WatcherState
    watcher_rs ..> note_rs
    vaultSync_ts ..> watcher_rs
    vaultSync_ts ..> companiesStore
Loading

Sources:src-tauri/src/watcher.rs#43-49src-tauri/src/watcher.rs#65-70src/lib/vaultSync.ts#33-48src-tauri/src/lib.rs#40

Clone this wiki locally