watch
js/file/watch.ts
fino:file/watch — Cross-platform filesystem event watcher.
Watches files and directories for changes and exposes a unified async iterator interface. Platform implementations differ, but the event model is the same on both:
interface WatchEvent {
type: 'create' | 'modify' | 'delete' | 'rename';
path: string;
}
This is a Fino runtime watcher, not Node fs.watch parity. The public
release contract accepts string paths, yields events through async
iteration, and stops through explicit close(). The only supported option
is recursive; Node-style persistent, encoding, and AbortSignal
options are not interpreted.
Platform details
macOS — uses kqueue EVFILT_VNODE (via internal:runtime/loop's vnode()
API). One open fd is required per watched path. EV_CLEAR auto-re-arms the
filter after each delivery. Events report which flags fired (NOTE_WRITE,
NOTE_DELETE, etc.) but not which specific file changed within a directory —
a NOTE_WRITE on a directory only means "something changed in this directory".
Linux — uses inotify through the runtime watch backend. A single inotify fd handles all watches. Events include the specific filename that changed, providing finer-grained information than the macOS backend.
Known limitations
- macOS: each watched path requires an open fd. Deep recursive watches may
approach per-process fd limits (default 256; raise with
ulimit -n). - macOS: directory watches only know that something changed, not which entry. Callers that need the specific changed file must re-scan the directory.
- macOS: renames report only the old path (NOTE_RENAME on the source).
- Recursive watching: there is a brief race window between a new subdirectory being created and its watch being registered — a few events may be missed.
Usage
import { Watcher } from 'fino:file/watch';
const watcher = new Watcher({ recursive: true });
watcher.watch('/tmp/mydir');
for await (const event of watcher) {
console.log(event.type, event.path);
if (event.type === 'delete') break; // breaking closes the watcher
}
kqueue reference: https://man.freebsd.org/cgi/man.cgi?kqueue inotify reference: https://man7.org/linux/man-pages/man7/inotify.7.html
Types
type WatchEventType = 'create' | 'modify' | 'delete' | 'rename'
Normalized filesystem event names emitted by Watcher.
Platform backends collapse native event masks into these four names. A single filesystem operation can still produce multiple events, and directory watches may report the directory path rather than the exact changed child on macOS.
import { Watcher, type WatchEventType } from 'fino:file/watch';
const counts: Record<WatchEventType, number> = {
create: 0, modify: 0, delete: 0, rename: 0,
};
const watcher = new Watcher();
watcher.watch('/tmp/mydir');
for await (const event of watcher) {
counts[event.type]++;
}
Interfaces
interface WatchEvent {
Filesystem event yielded by a watcher.
Events are normalized from kqueue on macOS and inotify on Linux. The path
is the watched path or changed child path reported by the backend; callers
that need exact metadata should stat or rescan after receiving the event.
import { Watcher, type WatchEvent } from 'fino:file/watch';
function isSourceChange(event: WatchEvent): boolean {
return event.type === 'modify' && event.path.endsWith('.ts');
}
const watcher = new Watcher({ recursive: true });
watcher.watch('./src');
for await (const event of watcher) {
if (isSourceChange(event)) console.log('rebuild:', event.path);
}
Properties
type: WatchEventType
Normalized event type.
The value is one of 'create', 'modify', 'delete', or 'rename'.
Backends may coalesce or duplicate events, so treat this as a notification
to re-check state rather than a complete change log.
path: string
Absolute or relative path of the affected file or directory.
The path shape follows the path passed to watch() and the platform event
backend. Linux directory events usually include the changed child name;
macOS directory events may only identify the watched directory.
interface WatchOptions {
Options for [Watcher.
The current option surface controls whether watched directories are scanned recursively. Watch events are backend notifications, not a transactional filesystem log: platforms may coalesce rapid changes, directory events may identify only the watched directory, and recursive watches can miss events created between directory discovery and backend registration.
import { Watcher, type WatchOptions } from 'fino:file/watch';
const options: WatchOptions = { recursive: true };
const watcher = new Watcher(options);
watcher.watch('./src');
watcher.close();
Properties
recursive?: boolean
Watch subdirectories recursively. On macOS, this opens one fd per subdirectory. On Linux, it adds one inotify watch per subdirectory. Default: false.
Recursive watching can miss a small number of events between a new subdirectory being created and its watch being installed. Very large trees may also hit fd or inotify watch limits.
Classes
class Watcher {
Async-iterable filesystem watcher. Construct, call watch() for each path,
then iterate events with for await.
The default is non-recursive watching. Call close() to stop watching and
release fds or inotify resources. Iteration ends after close() or after
the iterator's return() method is called by breaking out of for await.
import { Watcher } from 'fino:file/watch';
const watcher = new Watcher({ recursive: true });
watcher.watch('/tmp/mydir');
for await (const { type, path } of watcher) {
console.log(type, path);
}
Constructors
constructor(options: WatchOptions = {})
Create a filesystem watcher.
By default, only the exact paths passed to watch() are watched.
{ recursive: true } scans subdirectories and adds backend watches for
them. Construction allocates an inotify fd and starts the background read
loop on Linux; close the watcher when done.
import { Watcher } from 'fino:file/watch';
const watcher = new Watcher({ recursive: false });
watcher.close();
Methods
watch(path: string): void
Start watching path for changes.
On macOS, opens an fd for each watched path (and each subdirectory if
recursive: true). On Linux, adds an inotify watch.
May be called multiple times to watch multiple paths. Watching a path that is already watched is a no-op.
Throws if the watcher is already closed, if path is not a string, or if
the backend cannot register the path (for example, it does not exist).
On macOS, directory watches do not identify the exact changed child — events carry the watched directory's path, so re-scan the directory if the specific entry matters.
import { Watcher } from 'fino:file/watch';
const watcher = new Watcher();
watcher.watch('/tmp/app.log');
watcher.close();
close(): void
Stop watching all paths and release all resources.
Resolves any pending iterator .next() calls with { done: true }.
Calling close() more than once is allowed. After close, watch() throws
and async iteration completes without yielding more queued events after the
queue is drained.
import { Watcher } from 'fino:file/watch';
const watcher = new Watcher();
watcher.watch('/tmp');
watcher.close();