DEV Community

Cover image for Watch: a content gate, not an event storm
VX
VX

Posted on Originally published at vznjs.github.io

Watch: a content gate, not an event storm

vx watch test --all runs test in every project, then re-runs it on
every change. The interesting design decisions are in what it refuses
to do.

No per-event glob matching

The tempting design is to compare each filesystem event against each
task's cache.inputs.files and re-run only the tasks whose inputs
matched. vx does not do this, because the cache key is already the
source of truth for "does this change matter to this task," and a
second copy of that rule, written in terms of events instead of hashes,
would drift from the first.

Instead, every change triggers a cycle, the cycle is the same code path
as vx run, and the key decides. A change to an irrelevant file
produces a fully cached cycle, typically tens of milliseconds. A change
to a relevant one produces exactly the re-runs the graph implies,
including downstream tasks, because the cascade is the key's, not the
watcher's. The engineering cost of a per-event matcher is much larger
than the cost of a cheap cycle, and the correctness cost of two rules
is larger still.

The content gate

A task that writes into its own project is the classic watch-mode
trap: it writes, the watcher sees the write, the task re-runs, forever.
The usual fix is a list of ignored paths, which fails for any task
whose writes were not declared.

vx ignores the declared outputs of every task in scope (and their
containing directories, so a dist/** glob does not re-trigger on the
dist directory being created). For everything else it gates on
content: a file whose bytes did not change since the loop last saw
it is not an edit. A task that rewrites its own files costs one extra
cycle, which the cache serves, and then settles.

Always ignored regardless: node_modules, .git, .vx, the run's
resolved cache directory wherever --cache-dir put it, .tsbuildinfo
files, editor backup files (a trailing ~), and any untracked path git
ignores (one git check-ignore per debounce window): such a path is in
no key, so a task writing a pid file or a log there would re-run the
loop forever.

Watchers that are actually watching

On macOS a directory watcher can return before its event stream is
live, and an edit in that gap is silently lost. vx writes a probe file
under every watcher and does not print vx watch: watching … until
each probe's event has arrived, re-writing on a short backoff. The line
is a promise, not a hope. A directory whose watcher stays silent for
two seconds is polled instead, with a warning naming the interval.

The workspace root is watched non-recursively so a lockfile or
pnpm-workspace.yaml edit is heard; those move the workspace
fingerprint, or, with @vzn/vx-lockfile, the keys of the projects they
reach.

The rest of the contract

  • Events during a cycle queue and drain after it; re-runs are debounced about 150 ms after the last event.
  • A failed cycle prints its failure and waits for the next change; it does not exit the loop. That matches turbo watch and nx watch.
  • Ctrl-C prints vx watch: stopped, tears down the in-flight cycle's children and exits 0 only once they are gone.
  • Flags that describe one run (--dry, --graph, --summarize, --profile, --report, --report-file, --verbosity above 0, --format) are rejected up front, because a loop has no single run.
  • A persistent task starts once and stays up across cycles; each cycle re-runs what it depends on, as the keys decide. It restarts only when its own config or forwarded args change, or when it dies.

Reference: vx watch.


Originally published on the vx blog. vx is an MIT task runner and build cache for JS monorepos: github.com/vznjs/vx.

Written with AI assistance.

Top comments (1)

Collapse
 
devantibot profile image
Info Comment hidden by post author - thread only accessible via permalink
DEV ANTIBOT •

You need to verify your account .
Link is in the profile.

Some comments have been hidden by the post's author - find out more