8.1 KiB
smartchok - Technical Hints
Native File Watching (v2.0.0+)
The module now uses native file watching APIs instead of chokidar, providing cross-runtime support for Node.js, Deno, and Bun.
Exported Class
The package exports the Smartwatch class (not Smartchok):
import { Smartwatch } from '@push.rocks/smartchok';
Architecture
ts/
├── smartwatch.classes.smartwatch.ts # Main Smartwatch class
├── smartwatch.plugins.ts # Dependencies (smartenv, picomatch, etc.)
├── watchers/
│ ├── index.ts # Factory with runtime detection
│ ├── interfaces.ts # IWatcher interface and types
│ ├── watcher.node.ts # Node.js/Bun implementation (fs.watch)
│ └── watcher.deno.ts # Deno implementation (Deno.watchFs)
└── utils/
└── write-stabilizer.ts # awaitWriteFinish polling implementation
Runtime Detection
Uses @push.rocks/smartenv v6.x for runtime detection:
- Node.js/Bun: Uses native
fs.watch()with{ recursive: true } - Deno: Uses
Deno.watchFs()async iterable
Dependencies
- picomatch: Glob pattern matching (zero deps, well-maintained)
- @push.rocks/smartenv: Runtime detection (Node.js, Deno, Bun)
- @push.rocks/smartrx: RxJS Subject/Observable management
- @push.rocks/smartpromise: Deferred promise utilities
- @push.rocks/lik: Stringmap for pattern storage
Why picomatch?
Native file watching APIs don't support glob patterns. Picomatch provides glob pattern matching with:
- Zero dependencies
- 164M+ weekly downloads
- Excellent security profile
- Full glob syntax support
Event Handling
Native events are normalized to a consistent interface:
| Node.js/Bun Event | Deno Event | Normalized Event |
|---|---|---|
rename (file exists) |
create |
add |
rename (file gone) |
remove |
unlink |
change |
modify |
change |
awaitWriteFinish Implementation
The WriteStabilizer class replaces chokidar's built-in write stabilization:
- Polls file size until stable (configurable threshold: 300ms default)
- Configurable poll interval (100ms default)
- Handles file deletion during write detection
Platform Requirements
- Node.js 20+: Required for native recursive watching on all platforms
- Deno: Works on all versions with
Deno.watchFs() - Bun: Uses Node.js compatibility layer
Architecture (v6.3.0+) - Chokidar-Inspired
The Node.js watcher has been refactored with elegant patterns inspired by chokidar:
DirEntry Class:
- Tracks directory contents with proper disposal
- Encapsulates file tracking and inode management
dispose()method freezes object to catch use-after-cleanup bugs
Throttler Pattern:
- More sophisticated than simple debounce
- Tracks count of suppressed events
- Returns
falseif already throttled,Throttlerobject otherwise - Used for change events to prevent duplicate emissions
Atomic Write Handling:
- Unlink events are queued with 100ms delay
- If add event arrives for same path within delay, unlink is cancelled
- Emits single
changeevent instead ofunlink+add - Handles editor atomic saves elegantly
Closer Registry:
- Maps watch paths to cleanup functions
- Ensures proper resource cleanup on stop
addCloser()/runClosers()pattern
Event Constants Object:
const EV = {
ADD: 'add',
CHANGE: 'change',
UNLINK: 'unlink',
ADD_DIR: 'addDir',
UNLINK_DIR: 'unlinkDir',
READY: 'ready',
ERROR: 'error',
} as const;
Configuration Constants:
const CONFIG = {
MAX_RETRIES: 3,
INITIAL_RESTART_DELAY: 1000,
MAX_RESTART_DELAY: 30000,
HEALTH_CHECK_INTERVAL: 30000,
ATOMIC_DELAY: 100,
TEMP_FILE_DELAY: 50,
} as const;
Robustness Features (v6.1.0+)
The Node.js watcher includes automatic recovery mechanisms based on learnings from chokidar and known fs.watch issues:
Auto-restart on failure:
- Watchers automatically restart when errors occur
- Exponential backoff (1s → 30s max)
- Maximum 3 retry attempts before giving up
- v6.2.0+: Race condition guards prevent orphan watchers when
stop()is called during restart
Inode tracking (critical for long-running watchers):
fs.watch()watches the inode, not the path!- When directories are replaced (git checkout, atomic saves), the inode changes
- Health check detects inode changes and restarts the watcher
- v6.2.0+: File-level inode tracking detects delete+recreate (common editor save pattern)
- This is the most common cause of "watcher stops working after some time"
Health check monitoring:
- 30-second periodic health checks
- Detects when watched paths disappear
- Detects inode changes (directory replacement)
- Detects ENOSPC errors (inotify limit exceeded)
- v6.2.0+: Protected against dual-restart race conditions (health check + error handler)
ENOSPC detection (Linux inotify limit):
- Detects when
/proc/sys/fs/inotify/max_user_watchesis exceeded - Logs fix command:
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
Error isolation:
- Subscriber errors don't crash the watcher
- All events emitted via
safeEmit()with try-catch
Untracked file handling (v6.2.0+):
- Files created after initial scan are properly detected
- Untracked file deletions emit
unlinkevents instead of being silently dropped
Event Deferral During Initial Scan (v6.2.2+):
- Events are queued until initial scan completes
- Prevents race conditions where events arrive before
watchedFilesis populated - Deferred events are processed after scan completes
Event Sequence Tracking (v6.2.2+):
- Debounce now tracks ALL events in sequence, not just the last one
- Prevents losing intermediate events (e.g., add→change→delete no longer loses add)
- Intelligent processing of event sequences:
- Delete+recreate with inode change → emits
unlinkthenadd - Rapid create+delete → emits both events
- Multiple changes → single
changeevent (debouncing)
- Delete+recreate with inode change → emits
Post-Stop Event Guards (v6.2.2+):
handleFsEvent()returns early if watcher is stopped- Pending emits are cleared BEFORE setting
_isWatching = false - Prevents orphaned timeouts and events after
stop()
Verbose logging:
- All lifecycle events logged with
[smartwatch]prefix - Event sequences logged for debugging complex scenarios
- Helps debug watcher issues in production
Example log output:
[smartwatch] Starting watcher for 1 base path(s)...
[smartwatch] Started watching: ./test/assets/
[smartwatch] Starting health check (every 30s)
[smartwatch] Watcher started with 1 active watcher(s)
[smartwatch] Health check: 1 watchers active
[smartwatch] Processing event sequence for ./src/file.ts: [rename, rename, change]
[smartwatch] File inode changed (delete+recreate): ./src/file.ts
[smartwatch] Previous inode: 12345, current: 67890
Known fs.watch Limitations
- Watches inode, not path - If a directory is replaced, watcher goes stale
- inotify limits on Linux - Default
max_user_watches(8192) may be too low - No events for some atomic writes - Some editors' save patterns may not trigger events
- Platform differences - Linux uses inotify, macOS uses FSEvents/kqueue
Testing
pnpm test
Test files:
- test.basic.ts - Core functionality (add, change, unlink events)
- test.inode.ts - Inode change detection, atomic writes
- test.stress.ts - Rapid modifications, many files, interleaved operations
Tests verify:
- Creating Smartwatch instance
- Adding glob patterns
- Receiving 'add', 'change', 'unlink' events
- Inode change detection (delete+recreate pattern)
- Atomic write pattern (temp file + rename)
- Rapid file modifications (debouncing)
- Many files created rapidly
- Interleaved add/change/delete operations
- Graceful shutdown
Dev Dependencies
- Using
@git.zone/tstestv3.x with tapbundle - Import from
@git.zone/tstest/tapbundle