feat(watcher): adaptive filesystem watchdog with zero CPU when idle

Monitors all registered folders using OS-native events (ReadDirectoryChangesW
on Windows) so new, modified, and deleted files are reflected in the gallery
automatically without manual reindexing.

Key design points:
- Adaptive blocking: recv() when no events pending (zero CPU), switches to
  recv_timeout(earliest_deadline) only when debounce timers are running — wakes
  exactly when the soonest event is ready, no busy-polling
- 500 ms per-path debounce coalesces rapid OS event bursts into one action
- Change detection preserved: build_record skips upsert if file_size + mtime
  unchanged, preventing spurious thumbnail/embedding re-queues and avoiding
  clobbering of existing metadata like thumbnail_path
- Access events (reads) filtered out; only Create/Modify/Remove trigger work
- Deletion path: emits watcher-deleted event with image IDs; frontend removes
  those images from state and clears selectedImage if it was deleted

WatcherHandle stored in app state; add_folder / remove_folder / update_folder_path
commands keep the watched path set in sync with the DB.
This commit is contained in:
2026-06-08 06:50:01 +01:00
parent 9ee5b08c93
commit ae9e806e61
7 changed files with 396 additions and 7 deletions
+19
View File
@@ -1747,6 +1747,24 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
});
});
const unlistenWatcherDeleted = await listen<number[]>("watcher-deleted", (event) => {
const deletedIds = new Set(event.payload);
set((state) => {
const removed = state.images.filter((img) => deletedIds.has(img.id)).length;
const images = state.images.filter((img) => !deletedIds.has(img.id));
const selectedImage =
state.selectedImage && deletedIds.has(state.selectedImage.id)
? null
: state.selectedImage;
return {
images,
totalImages: Math.max(0, state.totalImages - removed),
loadedCount: Math.max(0, state.loadedCount - removed),
selectedImage,
};
});
});
return () => {
unlistenProgress();
unlistenMediaJobs();
@@ -1754,6 +1772,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
unlistenTaggerModelProgress();
unlistenImages();
unlistenThumbnails();
unlistenWatcherDeleted();
};
},
}));