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

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();