-
Notifications
You must be signed in to change notification settings - Fork 0
Vault File Watcher & Live Reload
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.
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 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
|
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
Sources:src-tauri/src/watcher.rs#65-97src/lib/vaultSync.ts#33-48
The classify_change function is responsible for determining if a filesystem event should trigger a UI refresh.
- Slug Extraction: It uses
note_slugto extract the filename stem. This naturally ignores files starting with underscores (templates/sidecars) and non-Markdown files. src-tauri/src/watcher.rs#56 - 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
- Eligibility: Files in nested subdirectories (like
companies/_jd/) or theprofile/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
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).
- Choke Point: Every write performed by Lodestar goes through
note::write_note. src-tauri/src/note.rs (referenced in src-tauri/src/watcher.rs#4-5) - Tracking: When a write occurs, the path is recorded in
note::was_self_write. src-tauri/src/watcher.rs#11 - Filtering: The watcher's event loop calls
was_self_write(path). If it returns true, the event is dropped because the UI has already optimistically updated or will receive a specificrun:*event. src-tauri/src/watcher.rs#88-90
Sources:src-tauri/src/watcher.rs#1-7src-tauri/src/watcher.rs#88-90
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
This function in src/lib/vaultSync.ts performs two roles:
- Initialization: Invokes the Rust command to start the OS-level watcher. src/lib/vaultSync.ts#34
- Routing: Listens for
record:changedevents 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
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
Sources:src-tauri/src/watcher.rs#43-49src-tauri/src/watcher.rs#65-70src/lib/vaultSync.ts#33-48src-tauri/src/lib.rs#40