RFC: Integrate watch() / watchFile() into @rush-fs/core
Motivation
@rush-fs/core already provides high-performance replacements for readdir, readFile, writeFile, glob, cp, rm, etc. But it lacks fs.watch() / fs.watchFile() — users still need to rely on chokidar or other third-party libraries for file watching.
Node.js's native fs.watch() has well-known issues (documented by chokidar):
- macOS doesn't report filenames — FSEvents batch pushes often omit path info
- Duplicate events — same change triggers multiple notifications
rename is ambiguous — create, delete, and rename all report as rename, requiring manual stat checks
- Inconsistent recursive support — only macOS supports it natively
- Poor atomic write handling — editor temp-file + rename patterns produce false unlink→add events
Chokidar solves these in JavaScript, but the cost is single-threaded CPU overhead.
Proposal
Integrate watch() and watchFile() into @rush-fs/core using Rust + notify-rs as the backend, with API aligned to fs.watch() / fs.watchFile(). Rush-FS extensions (precise events, debounce, gitignore filtering) are all opt-in — default behavior matches native fs.watch() for zero migration cost.
Architecture
Node.js/TypeScript
↓ (NAPI ThreadsafeFunction)
Rust Event Processing
├─ Debounce (notify-debouncer-mini)
├─ Dedup (inode-based, file-id crate)
├─ Event normalization (rename → add/change/unlink when preciseEvents=true)
└─ Gitignore filter (optional, ignore-files crate)
↓
notify-rs (backend)
├─ macOS: FSEvents
├─ Linux: inotify
├─ Windows: ReadDirectoryChangesW
└─ All: Polling fallback
API Design
watch() — aligned with fs.watch()
import { watch } from '@rush-fs/core'
// Native API compatible
const watcher = watch('src/', { recursive: true }, (eventType, filename) => {
console.log(eventType, filename)
// eventType: 'rename' | 'change' (native compatible)
})
// Rush-FS extensions (opt-in)
const watcher = watch('src/', {
recursive: true,
debounce: 100, // event coalesce window (ms), default 0
preciseEvents: true, // rename → add/change/unlink + addDir/unlinkDir
gitignore: true, // auto-ignore .gitignore/.ignore paths
}, (eventType, filename) => {
// preciseEvents enabled:
// eventType: 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir' | 'rename'
})
watcher.close()
watchFile() — aligned with fs.watchFile()
import { watchFile, unwatchFile } from '@rush-fs/core'
watchFile('file.txt', { interval: 1000 }, (curr, prev) => {
console.log('changed', curr.mtimeMs, prev.mtimeMs)
})
unwatchFile('file.txt')
Rush-FS implementation prioritizes notify-rs native notifications over stat polling, falling back to polling only when native APIs are unavailable (network filesystems, etc.).
Rush-FS Extension Options
| Option |
Default |
Description |
debounce |
0 (off) |
Event coalesce window (ms). Set to ~100 to merge editor save events |
preciseEvents |
false |
Split rename into add/change/unlink + addDir/unlinkDir |
gitignore |
false |
Auto-parse .gitignore/.ignore to skip matching paths |
Design principle: All extensions are off by default. Rush-FS watch() defaults to native fs.watch() behavior for zero migration cost.
Core Dependencies
| Crate |
Purpose |
notify |
Cross-platform file notification (main backend) |
notify-debouncer-mini |
Event coalescing/debouncing |
notify-types |
Event type definitions |
file-id |
Inode-based file identification (dedup) |
napi + napi-derive |
NAPI-RS bindings |
ignore-files (watchexec) |
.gitignore/.ignore parsing (optional, Phase 2) |
Milestones
| Phase |
Content |
Estimate |
| P0 |
Rust basic binding: watch() + close() |
2-3 days |
| P1 |
Event transformation (native compatible mode) |
1-2 days |
| P2 |
preciseEvents extension |
1 day |
| P3 |
watchFile() + unwatchFile() |
2 days |
| P4 |
debounce option |
1 day |
| P5 |
TS types + docs + benchmark |
2 days |
Total: ~1-2 weeks for MVP
Compatibility Matrix
| Scenario |
fs.watch() |
Rush-FS default |
Rush-FS + preciseEvents |
| File created |
rename |
rename |
add |
| File modified |
change |
change |
change |
| File deleted |
rename |
rename |
unlink |
| File renamed |
rename |
rename |
unlink + add |
| Dir created |
rename |
rename |
addDir |
| Dir deleted |
rename |
rename |
unlinkDir |
Benchmark Goals
| Scenario |
fs.watch() + chokidar processing |
Rush-FS watch() |
| 100-file directory change |
JS processes ~100 raw events |
Rust coalesces to ~10 events |
| 10k files continuous monitoring CPU |
~2-3% |
< 0.5% |
| Editor save (atomic write) |
Triggers 2-3 events |
debounce merges to 1 |
| macOS large directory scan |
Often loses filenames |
FSEvents native + path resolution |
Full RFC document available at: rush-fs-watch-rfc.md (to be added)
Happy to drive implementation if this gets green-lit 🦀
RFC: Integrate
watch()/watchFile()into @rush-fs/coreMotivation
@rush-fs/corealready provides high-performance replacements forreaddir,readFile,writeFile,glob,cp,rm, etc. But it lacksfs.watch()/fs.watchFile()— users still need to rely on chokidar or other third-party libraries for file watching.Node.js's native
fs.watch()has well-known issues (documented by chokidar):renameis ambiguous — create, delete, and rename all report asrename, requiring manual stat checksChokidar solves these in JavaScript, but the cost is single-threaded CPU overhead.
Proposal
Integrate
watch()andwatchFile()into@rush-fs/coreusing Rust + notify-rs as the backend, with API aligned tofs.watch()/fs.watchFile(). Rush-FS extensions (precise events, debounce, gitignore filtering) are all opt-in — default behavior matches nativefs.watch()for zero migration cost.Architecture
API Design
watch()— aligned withfs.watch()watchFile()— aligned withfs.watchFile()Rush-FS implementation prioritizes notify-rs native notifications over stat polling, falling back to polling only when native APIs are unavailable (network filesystems, etc.).
Rush-FS Extension Options
debounce0(off)preciseEventsfalserenameintoadd/change/unlink+addDir/unlinkDirgitignorefalseDesign principle: All extensions are off by default. Rush-FS
watch()defaults to nativefs.watch()behavior for zero migration cost.Core Dependencies
notifynotify-debouncer-mininotify-typesfile-idnapi+napi-deriveignore-files(watchexec)Milestones
watch()+close()preciseEventsextensionwatchFile()+unwatchFile()debounceoptionTotal: ~1-2 weeks for MVP
Compatibility Matrix
fs.watch()preciseEventsrenamerenameaddchangechangechangerenamerenameunlinkrenamerenameunlink+addrenamerenameaddDirrenamerenameunlinkDirBenchmark Goals
fs.watch()+ chokidar processingwatch()Full RFC document available at: rush-fs-watch-rfc.md (to be added)
Happy to drive implementation if this gets green-lit 🦀