fswatch

    Cross-platform filesystem watcher for MoonBit: FSEvents on macOS, inotify on Linux, polling fallback elsewhere.

    moonbit
    filesystem
    watch
    fswatch
    fsevents
    inotify
    Download zip
    Author
    Version
    0.2.1
    License
    Apache-2.0
    Last updated
    2 months ago
    Downloads
    1K

    Dependencies

    #mizchi/fswatch

    Cross-platform filesystem watcher for MoonBit.

    @fswatch.start(roots) returns a Watcher that emits Event { path, kind: Created | Modified | Removed } for changes under each root. The backend is selected at runtime:

    Target / OSBackendLatency
    native / macOSFSEvents (via dlopen, no -framework link dep)~50 ms drain interval, OS-driven detection
    native / Linuxinotify~50 ms drain interval, kernel-driven detection
    native / WindowsReadDirectoryChangesW (per-root HANDLE + OVERLAPPED, single reader thread via WaitForMultipleObjects)~50 ms drain interval, kernel-driven detection
    native / otherPolling (mtime + size fingerprint)~500 ms scan interval
    js / Nodefs.watch per root with recursive: true~50 ms drain interval, Node event-loop-driven detection

    All backends share a single normalization layer (watch_normalize.mbt): OS-canonical paths (e.g. /private/tmp/... on macOS) are rewritten back to the user-supplied root form, and Created vs Modified is resolved against an initial directory walk so flag-bit quirks don't cross the API surface.

    #Usage

    async fn watch_src() -> Unit raise {
    let watcher = @fswatch.start(["src", "Taskfile.pkl"])
    defer watcher.close()
    for ;; {
    let events = watcher.next()
    if events.length() == 0 { break }
    for e in events {
    println("\{e.kind} \{e.path}")
    }
    }
    }

    #Options

    • interval_ms?: drain cadence in ms. Default 50 (native) / 500 (polling).
    • exclude?: (String) -> Bool. Returning true drops the entry — for directories, descendants are pruned too.
    • start_polling(...): force the polling backend (deterministic, lets tests bypass platform quirks).

    #Limitations

    • Linux + Windows native runtime is validated only via CI (developing on darwin). The first CI run after the relevant commits is the first time inotify / RDCW execute end-to-end.
    • inotify's per-user watch limit (/proc/sys/fs/inotify/max_user_watches) caps deeply-recursive trees.
    • RDCW caps at 63 simultaneous roots per watcher (MAXIMUM_WAIT_OBJECTS - 1 for the stop event). Most projects use 1–3 roots so this is moot in practice.
    • Node fs.watch with recursive: true requires Node 20+ on Linux. On older versions only the top-level directory is observed.
    • File paths containing literal newlines are not supported on the js backend (the FFI boundary uses '\n' as a separator).
    • No event-time wakeup; the drain loop polls at interval_ms. Sub-50ms latency would need a pipe-wake bridge from the reader thread (or fs.watch listener) into the MoonBit async runtime.

    Event

    pub(all) struct Event {
    path : String
    kind : EventKind
    }

    impl Show for Event

    EventKind

    pub(all) enum EventKind {
    Created
    Modified
    Removed
    } derive(Eq)

    Cross-target filesystem watcher event types. The concrete Watcher struct and its start / next / close methods are defined per target — native dispatches across FSEvents (macOS) / inotify (Linux) / ReadDirectoryChangesW (Windows) / polling; js uses Node fs.watch.
    impl Show for EventKind

    Watcher

    pub struct Watcher {
    // private fields
    }

    A filesystem watcher. Calls to next block until at least one change is observed, then return the batch of events seen during that scan.

    The watcher transparently picks the fastest available backend at start: FSEvents on macOS, inotify on Linux, ReadDirectoryChangesW on Windows, polling fallback elsewhere. The public surface is identical across backends.

    Watcher::close

    fn Watcher::close(self : Watcher) -> Unit

    Mark the watcher closed and release backend resources. Idempotent.

    Watcher::next

    async fn Watcher::next(self : Watcher) -> Array[Event]

    Block until at least one event is observed (or the watcher is closed), then return all events from that scan. Returns [] only on close.

    start

    async fn start(roots : Array[String], interval_ms? : Int, exclude? : (String) -> Bool) -> Watcher

    Start a watcher rooted at roots. Each entry must be a directory; paths are tracked recursively. interval_ms controls how often next re-checks for events (default 500 ms for polling, 50 ms for FSEvents/inotify). exclude is applied per path; returning true drops both the entry and (for directories) its descendants from tracking.

    start_polling

    async fn start_polling(roots : Array[String], interval_ms? : Int, exclude? : (String) -> Bool) -> Watcher

    Force the polling backend, ignoring FSEvents/inotify/etc. Useful for tests that need deterministic snapshot semantics, or for environments where the native backend misbehaves.