Skip to main content

Module watcher

Module watcher 

Source
Expand description

File-watch + auto-rebuild loop for mesofact-static workloads.

Watches <workload>/src/ via notify, debounces edits, rebuilds (BuildDriver — in-process by default, sh -c when workload.toml declares a [build] command), then snapshots <workload>/dist/ into <workload>/.mesofact-dev/gen-<N>/ and flips a DistPointer so the running server starts serving the new artifact on the next request. Build stdout/stderr inherits the parent’s, so output shows up in the operator’s terminal or the Run-tab log surface.

Atomicity story: each generation is a separate directory. dist/ is copied into .mesofact-dev/gen-<N>-staging/ and then atomic-renamed to .mesofact-dev/gen-<N>/ on the same filesystem; the pointer flip is a single RwLock write; in-flight reads keep using the PathBuf they already cloned, so a request that started reading gen-<N-1>/html/index.html doesn’t observe a torn write. dist/ itself is left intact so concurrent publishers (mesofact-publisher to R2, qed-run to pond/MinIO) can read it without racing the watcher. GC keeps the last two generations.

@yah:ticket(R255-S5, “Decide tier-1 object store: s3s-fs vs serve-off-disk vs self-mock”) @yah:assignee(agent:claude) @yah:at(2026-05-25T20:08:09Z) @yah:kind(spike) @yah:status(review) @yah:parent(R255) @yah:next(“decide on the single fact: does the almanac/runtime fetch via S3 API calls or plain CDN GET? if S3, tier-1 needs s3s-fs regardless”) @yah:next(“if s3s-fs: run the existing publish_to_local_sim unchanged against it so tier-1 becomes a strict subset of tier-2 (only diff: in-process s3s-fs vs containerized MinIO+Caddy+yubaba)”) @yah:next(“reject self-mock: reimplementing SigV4 + bucket-policy + error shapes drifts from MinIO and defeats the point”) @yah:gotcha(“license-check s3s before adopting — must be MIT/BSD/Apache-2.0/ISC (believed Apache-2.0)”) @yah:assumes(“tier-1 read path is plain HTTP-GET (Caddy proxies the public bucket), so serve-off-disk is faithful for reads; only the build->PUT->read publish contract is unexercised at tier 1”) @yah:handoff(“Spike closed: serve-off-disk wins for tier-1 (dev). Read path is plain HTTP GET via axum ServeDir — browsers never call S3 APIs. The build→PUT→serve publish contract is exercised at tier-2 (sim+MinIO, R256 T1–T5 in review). No s3s-fs needed at tier-1; self-mock rejected as before. The @yah:assumes fact was correct. Only a tier-2 concern: R256-F8 tracks the publish-to-MinIO watcher sink for hot-ish sim reload, which uses the real MinIO container (not s3s-fs).”) @yah:verify(“No code change needed — tier-1 is already serve-off-disk. Verify the assumes by checking mesofact-dev routes: nothing in app/yah/web/src/ or mesofact-runtime makes S3 API calls client-side.”)

@yah:ticket(R256-F8, “Parameterize watcher sink: DistPointer (serve off disk) vs publish-to-MinIO”) @yah:assignee(agent:claude) @yah:at(2026-05-25T20:30:17Z) @yah:status(review) @yah:parent(R256) @yah:next(“introduce a sink abstraction so the same watch->rebuild loop can target either the in-process DistPointer (dev tier) or publish_to_local_sim (sim tier -> MinIO container)”) @yah:next(“this is what gives the containerized sim a hot-ish reload (edit -> rebuild -> republish -> Caddy serves new artifact) WITHOUT putting mesofact-dev inside a container”) @yah:next(“keep the host-side watcher as the only dev-mode component; the sim containers stay pure mesofact-core + Caddy + MinIO”) @yah:assumes(“today the watcher’s only sink is the in-process DistPointer (build_and_swap -> pointer.set at watcher.rs:253); there is no publish sink”) @arch:see(.yah/docs/working/mesofact-dev-camp-embedding.md) @yah:handoff(“PostBuildFn + PostBuildFuture type aliases added to watcher.rs. Watcher gains post_build: Option field and with_post_build(self, f) builder. build_and_swap calls the hook after the pointer flip — failure logs a warning but does not fail the rebuild (in-process dev server stays healthy). No new dep on cloud: the hook is a generic async closure; camp.rs (which already depends on both mesofact-dev and cloud) will wire publish_to_local_sim into the closure. Two new tests: post_build_hook_receives_gen_dir_on_success + post_build_hook_failure_does_not_fail_rebuild. All 20 mesofact-dev tests pass; cargo check cloud+yah+desktop clean.”) @yah:verify(“cargo test -p mesofact-dev –locked # 20 passed”) @yah:verify(“cargo check -p cloud -p yah -p desktop –locked”)

@arch:see(app/yah/cli/src/camp.rs)

Structs§

WatchOptions
Knobs for the watch loop.
Watcher
File-watch + rebuild loop. Construct with Watcher::new and drive with Watcher::run; pair with a crate::Server sharing the same DistPointer.
WatcherHandle
Handle that keeps a spawned watcher alive. Dropping it does not cancel the task (tokio detaches on drop); use WatcherHandle::is_running to observe lifecycle.

Enums§

BuildDriver
How a rebuild is produced.

Constants§

LEGACY_SHELL_BUILD
The pre-R759-T4 default. Retained as the --no-default-features fallback and named so the one place it still applies is greppable.

Functions§

spawn
Spawn a watch loop on a background tokio task. Returns a handle that stops the loop when dropped (the watcher’s internal channel closes when the task exits, which is fine for a process-lifetime dev server).

Type Aliases§

PostBuildFn
Optional post-build hook. Called with the snapshot directory (the gen-N/ directory, whose html/ subdirectory is what the DistPointer points at) after every successful build, immediately after the pointer flip.
PostBuildFuture
Boxed future returned by a PostBuildFn.