Skip to content

Stdlib · extensions

watch

ergonomic file-watching (over the fs primitive)

extensions.md · 63 lines · 2 min read

use watch

The ergonomic layer of filesystem observation, over the tier 0 primitive fs.watch_raw (the thin wrapper of inotify/kqueue/FSEvents/ReadDirectoryChangesW, which lives in the core, being a syscall and mechanism). The compiler/LSP uses the primitive directly (tier 0 does not depend on tier 1); whoever wants convenience (debounce, recursion, normalization across OSes, ignore-globs) imports this lib. Coherent with signals-as-channel: events arrive on a Channel, not on a global callback.

The primitive (in the core fs, tier 0): reference

Section titled “The primitive (in the core fs, tier 0): reference”
// lives in `fs` (tier 0); listed here only for context:
fn fs.watch_raw(path: string) -> Result[Channel[->RawFsEvent], error{NotFound, Io}]
decl RawFsEvent { pub path: string; pub kind: RawKind }
decl RawKind { Created; Modified; Deleted; MovedFrom; MovedTo } // raw, as the OS reports

The lib (tier 1): normalized and coalesced events

Section titled “The lib (tier 1): normalized and coalesced events”
fn watch(path: string) -> Result[Watcher, error{NotFound, Io}] // one path
fn watch_tree(root: string) -> Result[Watcher, error{NotFound, Io}] // recursive
decl Watcher { pub events: Channel[->FsEvent] }
fn (w: *Watcher) with_debounce(d: Duration) -> Watcher // coalesces bursts (a save fires N raw events)
fn (w: *Watcher) ignore(glob: string) -> Watcher // ignores paths (node_modules, .git)
fn (w: *Watcher) close() // closes the channel / ends the observation
decl FsEvent {
Created(string)
Modified(string)
Deleted(string)
Renamed(from: string, to: string) // rename-tracking: joins the OS's MovedFrom+MovedTo into a single event
}
use watch
w := watch.watch_tree("src").or_panic().with_debounce(200ms).ignore("**/.git/**")
loop ev in w.events {
match ev {
Modified(p) => rebuild(p)
Renamed(from, to) => { drop(from); rebuild(to) }
Created(p) => rebuild(p)
Deleted(p) => drop(p)
}
}
  • Primitive in the core, service in tier 1: the same split as runtime (the trace primitive is core, the observer is tooling) and as MemorySource (syscall core, strategy lib). The part the compiler/LSP needs (fs.watch_raw) is tier 0; only the convenience is tier 1, and the dependency rule holds.
  • Events as channel (signals-as-channel, §4): no global callback; cancelling is closing the channel or killing the process.
  • The lib normalizes what the OS makes messy: burst debounce, coalesced recursion, rename-tracking (joins the OS’s two move half-events), ignore-globs. Policy that churns stays in tier 1, not in the core.