Expand description
revalidate — the ephemeral revalidate receiver: the mesofact-native
replacement for the standalone almanac-serve binary (W225 §3/§4).
§What it is
§3 splits two verbs: build (source → bundle, CI-gated, carries the
bundler) and revalidate (data → SSG output on the already-built
bundle, no recompilation). This module is the revalidate half: on an
invalidation poke it re-runs the render path against fresh data and
republishes to the CDN. Per §4 the receiver is “a route mesofact mounts,”
not its own service binary — so it ships as a mode of mesofact serve
(mesofact serve <workload> --revalidate), not a separate executable.
§Why it is ephemeral (the memory-footprint property)
Unlike mesofact serve’s SSR-serving mode — which boots a resident V8
isolate and holds it for the process lifetime — the receiver spins V8 up
per poke and drops it (render_route_all calls SsgRuntime::start()
then discards it). Resident cost is just axum + config; V8 memory is spent
only while a re-render is actively running. One receiver node can therefore
back many static sites without holding one isolate per site.
§Bundler-free (W225 §3)
serve must not link the bundler. The render half comes from the
bundler-free mesofact-render crate (extracted from mesofact-build for
exactly this reason, R535-T9); the publish half from mesofact-publisher.
Neither pulls rolldown / lightningcss.
§Scope (single-tenant, v1)
The receiver serves one workload directory, matching what
runner.yah.dev actually runs today (almanac-serve’s single ALMANAC_DIR
shape). The optional mirror_key bearer is ported from
almanac::receiver as the cross-mirror-pollution guard. A multi-tenant
tenants/<id>.toml registry — which finally settles the long-open
R330-F12 config format — is a follow-up; the ephemeral-V8 property is
identical either way.
§Payload-carrying pokes (yah R330-F33)
Getting fresh data onto disk (the almanac feed-fetch: a release manifest →
data/*.json) is an upstream trigger that plugs into the seam and then
pokes this receiver (§3a “domain-triggered invalidation”). Producing that
data is still out of scope here — but receiving it is not.
A poke may carry the render inputs it wants used (DataInputs); the
receiver writes them into the workload before rendering. This exists because
the inputs used to be node-local while the output is global: with several
instances behind one hostname, whichever one serviced a poke published its
own copy of the data to the shared bucket, so a poke landing on an instance
whose feed sidecar had not yet ticked would overwrite fresher output with
staler — silently, since last write wins and nothing errors. A poke that
carries its data can be serviced by any instance with identical results, so
routing becomes an optimisation rather than a correctness input.
A poke with no data_inputs is still valid and still means “re-render from
whatever is on disk” — that is what a whole-site poke, a manual curl, and a
genuinely poll-driven feed all send.
@yah:relay(R446, “mesofact-serve –revalidate: multi-tenant tenants/<id>.toml registry (R330-F12 receiver re-home)”)
@yah:status(review)
@yah:at(2026-08-13T19:10:19Z)
@yah:assignee(agent:bundle-anthropic-ashguard)
@yah:gotcha(“COORDINATE revalidate.rs edits with Glimmerstone (chat, sigil g-polar-star) — they are live in the mesofact tree with in-flight fixes: per-extension Content-Type in object-store r2.rs put/publish (landed, uncommitted) + the clean-URL extensionless->.html router fix (mesofact R443-B4). Those are general infra; this relay must not duplicate or collide with them. Sync before substantive revalidate.rs edits.”)
@yah:gotcha(“/releases is a STATIC prerender (releases.html) re-rendered from releases.json on revalidate — NOT a serveInstance/pointer route (W059 §3 ‘materialisation = build-time static, style a’). The registry routes pokes to render+publish; it does not add per-request dynamic serving.”)
@yah:next(“DESIGN (boundary decision): keep the tenant registry MESOFACT-NATIVE. Do NOT deref yah’s .yah/services/mesofact-serve --tenants <dir> (not almanac-serve), with tenants/mesofact, not mesofact-dev — the serving engine moved here in W225 §2a; the old mesofact-dev verify lines were stale.”)
@yah:verify(“cargo test -p mesofact –features ssr # 131 lib + 5 integration pass”)
@yah:verify(“cargo clippy -p mesofact –features ssr –all-targets # clean”)
@yah:verify(“LIVE SMOKE (done, no infra needed): mesofact serve –revalidate –tenants --tenants X <workload> -> clap conflict error; empty –tenants dir -> refuses to boot; two tenants sharing a bearer -> refuses to boot naming the ids (never the bearer).”)
@yah:handoff(“R446 receiver half is COMPLETE in-crate. This session closed the last modelled gap: the per-tenant routes allowlist that tenants.rs carried as a &[] TODO at the revalidate_once call. TenantFile.routes / ResolvedTenant.routes / TenantJob.allow now thread it end-to-end, enforced in the SAME two places as the single-tenant receiver (yah R752-B7): an explicit out-of-list route is refused 403 in the handler, a whole-site poke (route:None) is NARROWED by the worker. 403 not 404 deliberately, matching revalidate.rs:474 — the route may exist, the caller lacks authority over it.”)
@yah:verify(“Hardening found while implementing (all in R446’s own files, all tested): (1) TenantRegistry::validate() — two tenants resolving to the same bearer meant tenant_for() silently gave every poke to the first, i.e. rendering one tenant’s workload into the other’s bucket with a 202 on the wire. Now refuses at boot, naming ids and never the bearer. (2) –tenants now clap-conflicts with workload/–publish-config/–allow-route instead of silently winning — a silently-ignored –allow-route is an allowlist an operator believes is enforced. –mirror-key only warns (it carries env=MESOFACT_MIRROR_KEY, which a runner may set process-wide). (3) An empty/missing –tenants dir refuses to boot: it produced a receiver that 403s everything while passing /readyz — the yah R330-T35 silent-failure shape. load_tenants keeps missing-dir=empty (library contract, tested); the CLI is where it becomes fatal. (4) serde(deny_unknown_fields) on TenantFile: route for routes would have parsed clean and yielded an unscoped tenant. (5) Per-tenant startup log lines (workload, publish_config, routable, allowed_routes).”)
@yah:gotcha(“Verify lines that referenced -p mesofact-dev were STALE and are corrected: the serving engine (revalidate + tenants) moved into crates/mesofact in W225 §2a. Test with -p mesofact --features ssr.”)
@yah:gotcha(“Working tree is DIRTY and uncommitted by design — tenants.rs + cli/serve.rs. No git write was made (not requested).”)
@yah:assumes(“Glimmerstone (g-polar-star) is no longer in this camp, so the flagged mesofact-native-vs-thin-deref divergence never got an explicit ack. The mesofact-native shape is what shipped and is now tested end-to-end; the routing core is shape-invariant either way, so a later thin-deref would change only where publish_config comes from, not the registry.”)
Structs§
- Revalidate
Config - Runtime configuration for the receiver. Built by the
mesofact servebinary from CLI flags / env. - Revalidate
Report - Outcome of one revalidation cycle.
Constants§
- MAX_
REVALIDATE_ BODY_ BYTES - Largest
POST /revalidatebody this receiver accepts, in bytes.
Functions§
- apply_
data_ inputs - Write a poke’s carried inputs into the workload, replacing whatever this node’s own feed sidecar last left there.
- check_
data_ input_ paths - Check every key in a poke’s payload is a path that stays inside the workload. Returns the offending key on the first violation.
- revalidate_
once - One full revalidation cycle: apply the poke’s carried inputs, render
(ephemeral V8, off the async runtime), then publish.
route:Some→ that route only;None→ every render-eligible route in the manifest (allstatic/spa, non-deferred). - serve
- Run the receiver: bind
port, serve the router, and drain pokes throughrevalidate_onceone at a time (renders are serialized — one V8 boot at a time keeps the footprint bounded). Runs until a hard I/O error.
Type Aliases§
- Data
Inputs - Render inputs carried by a poke: the workload-relative path a route
declares in its
data_inputs→ the JSON that path should hold.