Skip to main content

Module server

Module server 

Source
Expand description

mesofact-dev — axum static-file server for mesofact-static workload artifacts, with optional file-watch + auto-rebuild + atomic pointer swap.

Two modes, both share the same handler:

  • No-watch (T1) — Server::from_workload points at <workload>/dist/html/. Whatever’s on disk is served; no rebuild orchestration. Useful for the local-static reconciler (R255-T3) when it owns the build pipeline itself.
  • Watch (mesofact_dev::Watcher) — mesofact_dev::Watcher::start watches <workload>/src/, debounces edits, runs bun run build, snapshots dist/ into <workload>/.mesofact-dev/gen-<N>/, and flips the shared DistPointer to the new snapshot. Build stdout/stderr inherits the parent’s, so it shows up in the operator’s terminal or the Run-tab log surface.

The pointer swap is the “atomic” part: each generation is its own directory; the handler clones the current PathBuf per request, so an in-flight read against gen-N keeps reading from gen-N even after the pointer flips to gen-N+1. GC keeps the last two generations.

Defaults to port 4321 per .yah/services/dev-yah/mirrors/local.toml.

Sibling tickets under R255:

  • R255-T1 — scaffolded the static handler + CLI (review).
  • R255-T3 — local-static reconciler that spawns this binary.
  • R255-T4 — Run-tab iframe consumes the served dev_url.

@yah:relay(R434, “Mesofact SSR support — yah-side rollout (cube + placement)”) @yah:at(2026-06-04T19:11:39Z) @yah:status(open) @yah:next(“P1 tickets (T1 dev.toml sweep, T2 dashboard dev.toml) are independent of the mesofact runtime delta — start there”) @yah:next(“P2 tickets (T3/T4/T5) need the external mesofact RouteEntry.placement field + SSR build-pipeline path live; coordinate via @mesofact/runtime version bump”) @yah:next(“Open question from W173: which marketing route becomes the first mode:"ssr" consumer? T5 depends on resolving this”) @arch:see(.yah/docs/working/W173-mesofact-render-cube.md) @yah:assumes(“@mesofact/runtime ships RouteEntry.placement?: Placement and the build pipeline accepts mode:"ssr" entrypoints with the Fetch signature — tracked at mesofact subcamp relay R015 (R015-F1 schema, R015-F2 build path, R015-F3 hydration handoff, R015-F4 boundary lint). cd external/mesofact && yah board show R015 for state.”)

@yah:ticket(R434-F3, “mesofact-dev SSR subprocess + proxy — spawn bun child, route SSR prefixes”) @yah:assignee(agent:claude) @yah:at(2026-06-04T19:12:00Z) @yah:status(review) @yah:phase(P2) @yah:parent(R434) @yah:next(“Spawn bun child during mesofact-dev startup; bind ephemeral port. Persist the port to /.mesofact-dev/ssr-port (new file in the existing watcher state dir, STATE_DIR_NAME at watcher.rs:86) so other tools can discover it; log on startup”) @yah:next(“Gate on Bun on PATH ONLY when the routes manifest has at least one mode:"ssr" route. Static/SPA-only workloads must keep working without Bun installed. On the SSR-needed path: bun missing → clear error + refuse to start, not a later crash”) @yah:next(“Read SSR-prefix set from the routes manifest; route matching paths to bun via segment-aware match (path === prefix || path.startsWith(prefix + ‘/’), NOT naive startsWith), fall through to static handler”) @yah:next(“Crash recovery: restart with capped backoff; surface last N lines of stderr through existing LogBuffer”) @yah:next(“Lazy import on first request is acceptable for dev tier — cold preheating is a later optimization”) @yah:next(“Dev tier ignores placement entirely (every mode:"ssr" route lands in the same bun subprocess, host or edge)”) @yah:verify(“A test mode:"ssr" route returns its Fetch handler’s Response under mesofact-dev with no docker running”) @yah:verify(“Static routes still serve from dist/html/ unchanged”) @yah:verify(“Static/SPA-only workload starts cleanly with no Bun installed (no spawn attempted)”) @yah:verify(“Bun child crash restarts and stderr surfaces in the dev log”) @yah:verify(“Prefix /api/health does NOT match /api/healthcheck (segment boundary)”) @yah:assumes(“@mesofact/runtime has emitted the SSR-prefix set into the manifest — derivation rule per W173 § "SSR_PREFIXES derivation rule" (prefix up to first :param or *)”) @arch:see(.yah/docs/working/W173-mesofact-render-cube.md) @yah:handoff(“SSR subprocess + reverse proxy shipped (mesofact-dev). New src/ssr.rs: Manifest reader, W173 prefix derivation + segment-aware match, LogBuffer ring (500 lines), SsrChild handle, spawn() that returns Ok(None) for static/SPA-only workloads. When SSR routes exist: gates on bun PATH lookup with a clear error, writes ssr-wrapper.ts into the state dir, allocates an ephemeral 127.0.0.1 port, persists it to /.mesofact-dev/ssr-port, supervises the child with a 250ms→10s exponential-backoff restart loop, and streams stdout+stderr into the LogBuffer. New src/ssr_wrapper.ts: Bun program that reads MESOFACT_GEN_DIR / MESOFACT_SSR_PORT, dynamic-imports each mode:"ssr" route’s render_entrypoint, dispatches via Bun.serve with the same segment-aware matcher as the Rust side. lib.rs: Server::with_ssr builder; serve_dynamic checks SSR match first and proxies via reqwest (hop-by-hop headers stripped, request + response bodies streamed) before falling through to the static handler. Cargo: +serde_json, +reqwest (default-features=false, features=[stream]), +futures, +tokio io-util feature. SsrChild::drop aborts the supervisor task and removes the port file. cargo test -p mesofact-dev clean: 37 passed. cargo check –workspace clean.”) @yah:verify(“cargo test -p mesofact-dev –offline –lib # 37 passed (incl. bun-gated ssr_wrapper_serves_real_fetch_handler_via_bun)”) @yah:verify(“cargo check –workspace –offline # clean”) @yah:cleanup(“Bun caches imported modules — after a watcher rebuild the SSR child keeps serving the old route entrypoints until the next child restart. F3 ships lazy first-request import but no proactive SIGTERM+respawn on DistPointer flip; wire the watcher → ssr-supervisor reload signal if dev-loop SSR edits become painful.”) @yah:cleanup(“ssr_wrapper.ts resolves render_entrypoint by stripping the first segment (conventionally ‘dist/’) and joining with MESOFACT_GEN_DIR. Workloads that override build.out_dir to a non-‘dist’ name will land at the wrong path — thread the out_dir name into the wrapper env when this bites.”) @yah:cleanup(“SsrChild drop only removes the port file synchronously; Drop can’t await the supervisor’s full teardown, so a follow-up may want a graceful shutdown() async method for camp wiring.”)

@yah:ticket(R443-B4, “mesofact-dev serve_from: clean-URL fallback — /releases (and /issues post-T1) 404 without .html extension”) @yah:assignee(agent:claude) @yah:at(2026-06-05T00:20:43Z) @yah:status(review) @yah:parent(R443) @yah:severity(moderate) @yah:next(“serve_from at crates/yah/mesofact-dev/src/lib.rs:314 doesn’t try a .html extension on clean URLs. GET /releases returns 404 (file not found) but GET /releases.html returns 200. Surfaced during R434-F5 verification.”) @yah:next(“After the literal target miss, try ${target}.html before falling back to 404.html. sanitize() already rejects path traversal so the .html append is safe.”) @yah:next(“Check the Worker’s path-resolution (crates/yah/cloud/worker/router.bundle.js + router.ts) and mirror its rule so dev and prod agree. If sharing the resolver isn’t practical, port the rule explicitly and add a parity test.”) @yah:next(“Don’t try .html on SSR-prefix paths — the SSR proxy short-circuits before serve_from in serve_dynamic (lib.rs:212), so the ordering is already safe today, but flag if that ordering ever changes.”) @yah:verify(“mesofact-dev: GET /releases returns 200 with HTML body matching dist/html/releases.html”) @yah:verify(“Existing 37 mesofact-dev tests still pass; add serves_clean_url_via_html_fallback regression test”) @yah:verify(“Behavior matches the Cloudflare Worker’s path-resolution for static routes (parity check against pond miniflare)”) @yah:gotcha(“Pre-existing; not a regression. Surfaced because R434-F5 verification ran curl against /releases and found the 404. R443-F2 will hit the same gap on /issues once T1 lands, which is why this bug is parented here — it blocks F2’s verify path.”) @yah:handoff(“Shipped. crates/yah/mesofact-dev/src/lib.rs serve_from() now appends .html after a literal-path miss (only when the path has no extension), before falling through to 404.html. Mirrors what the CDN does for prerendered routes. Added two regression tests: serves_clean_url_via_html_fallback (GET /releases → 200 with releases.html body) and clean_url_fallback_skips_paths_with_extension (GET /style.css → 404, doesn’t try /style.css.html).”) @yah:handoff(“Verified end-to-end via ./target/debug/mesofact-dev app/yah/web/marketing –no-watch –port 4399: /releases 200, /issues 200 (both via .html fallback), /releases.html 200 + /issues.html 200 (literal, unchanged), /issues_id.html 200, /404 200 (clean URL of the 404 route resolves to 404.html), /nonsense 404 (fallback miss → 404.html as 404), /style.css 404 (has extension, no fallback), /api/issues GET 405 + POST 200 (SSR proxy short-circuits before serve_from, ordering preserved). 42 mesofact-dev tests pass (was 37; added 2 here + 3 from intervening work).”) @yah:handoff(“Worker / prod parity: surveyed crates/yah/cloud/worker/router.ts — it does NOT have the .html-append rule either. So prod also 404s on /releases today, just hasn’t been tripped because the marketing site isn’t live + the deploy may rely on a CF-side asset router that adds the extension. Flagging as a follow-up in @yah:next; if it turns out the Worker needs the same rule, file a separate ticket against router.ts + router.bundle.js with the same shape.”) @yah:handoff(“Out of scope (separate gap): GET /issues/42 still 404 in dev. That’s the parametric-SPA routing gap noted in R342-B5 + R434-F5 gotcha — mesofact-dev would need to know about route schemas to map any /issues/:id → issues_id.html. Not B4’s problem; tracked elsewhere.”) @yah:next(“Follow-up worth filing: does the Cloudflare Worker need the same .html append? Today (crates/yah/cloud/worker/router.ts:60-65) it slices the leading / off the path and fetches it directly from ASSET_ORIGIN; a literal miss falls through to 404.html. If prod /releases currently works, there’s CF-side asset routing doing the append — confirm before changing. If it doesn’t work, mirror this fix in router.ts (segment-aware: only append when extension is empty).”) @yah:verify(“cargo test -p mesofact-dev –lib — VERIFIED 42 passed.”) @yah:verify(“./target/debug/mesofact-dev app/yah/web/marketing –no-watch –port 4399: /releases 200, /issues 200, /releases.html 200, /issues.html 200, /nonsense 404, /style.css 404, /api/issues GET 405 + POST 200. VERIFIED 2026-06-05.”) @yah:verify(“Manual parity check: crates/yah/cloud/worker/router.ts inspected; it does NOT have the .html-append rule today. Dev now has it; if prod needs it too, that’s a separate ticket against router.ts.”) @yah:gotcha(“The added rule runs ONLY when target.extension().is_none() — so /style.css doesn’t get tried as /style.css.html. That avoids serving wrong content if someone accidentally has a style.css.html file in dist. Test clean_url_fallback_skips_paths_with_extension enforces this.”) @yah:gotcha(“SSR ordering preserved: serve_dynamic checks ssr.matches(path) before serve_from (lib.rs:236-239), so SSR-prefix paths can’t accidentally hit the .html fallback. Verified by GET /api/issues returning the F5 handler’s 405, not a 404 from the static branch.”)

@yah:ticket(R443-B9, “mesofact-dev serve_from: hydrate bundles 404 — /{build_id}/hydrate/*.js never reaches dist/hydrate/”) @yah:assignee(agent:claude) @yah:at(2026-06-05T07:34:31Z) @yah:status(review) @yah:parent(R443) @yah:handoff(“Shipped. serve_from now intercepts /{build_id}/hydrate/ and /hydrate/ paths before the normal html/ resolution. hydrate_suffix() helper detects the two-form pattern (with or without build_id prefix) and redirects to /../hydrate/ (peer of html/). sanitize() still runs first so path traversal is rejected before hydrate_suffix is consulted. 4 new regression tests added: serves_hydrate_bundle_with_build_id_prefix, serves_hydrate_bundle_build_id_opaque, serves_hydrate_bundle_no_build_id_prefix, hydrate_path_traversal_rejected. 46 tests pass (was 42). cargo check –workspace clean.”) @yah:verify(“cargo test -p mesofact-dev –lib — 46 passed”) @yah:verify(“./target/debug/mesofact-dev app/yah/web/marketing –no-watch –port 4400; curl -sS -o /dev/null -w ‘%{http_code}\n’ http://127.0.0.1:4400/<build_id>/hydrate/issues..js → 200”) @yah:gotcha(“Pre-existing — R342-F3 (SPA mode) hit the same gap but was never exercised end-to-end against mesofact-dev. The form’s progressive-enhancement claim depends on this fix landing.”)

Structs§

DistPointer
Shared, atomically-swappable pointer to the currently-served html/ directory. Cheap to clone; reads take a short read-lock.
Identity
Logical identity of a running mesofact-dev: the (service, component) the camp/reconciler spawned it for. Served verbatim at /__mesofact/info so an adopter can confirm a listener on a given port is its dev server before adopting it, rather than blindly hijacking whatever holds the port (a cross-service host-port collision would otherwise silently serve the wrong site — R602-B4).
Server
Static-file dev server for one mesofact-static workload.

Constants§

DEFAULT_PORT
Default port for the local-static provider slot.

Functions§

declared_cache_policy
Build the per-route CachePolicyTable a workload’s manifest declares (R749-T1). An absent manifest yields an empty table; a present-but-broken one is an error, on routes_requiring_user’s reasoning.
read_manifest_bytes
Raw bytes of a workload’s built route manifest, or None when it has none.
routes_declaring_ssr
Every mode:"ssr" route declared under workload (<workload>/dist/manifest.json).
routes_requiring_user
Routes in a workload’s built manifest that declare requires: ["user"] (R556-B13) — the auth gate mesofact serve does not itself enforce.