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_workloadpoints 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::startwatches<workload>/src/, debounces edits, runsbun run build, snapshotsdist/into<workload>/.mesofact-dev/gen-<N>/, and flips the sharedDistPointerto 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 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
@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/
Structs§
- Dist
Pointer - 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/infoso 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-staticworkload.
Constants§
- DEFAULT_
PORT - Default port for the local-static provider slot.
Functions§
- routes_
declaring_ ssr - Every
mode:"ssr"route declared underworkload(<workload>/dist/manifest.json). - routes_
requiring_ user - Routes in a workload’s built manifest that declare
requires: ["user"](R556-B13) — the auth gatemesofact servedoes not itself enforce.