Skip to main content

Module revalidate

Module revalidate 

Source
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//mirrors/.toml inside mesofact — that couples an independently-exportable workspace to yah’s config schema, and PublishConfig (mesofact-publisher) is deliberately yah-agnostic (env-named creds, no yah types). tenants/.toml entry = { id, mirror_key (or *_env name), workload (dir containing dist/), publish_config (path to that tenant’s mesofact.config.toml [publish]), routes? (optional allowlist) }. Registry maps mirror_key -> tenant -> (workload, publish_config) — a clean generalization of today’s single-tenant RevalidateConfig. Glimmerstone’s ‘thin deref / compose provider ref’ goal is RIGHT but belongs on the YAH side: a yah reconciler generates each tenant’s mesofact.config.toml from the mirror toml (no such generator exists yet — separate yah-side ticket under R330-F12’s producer track).”) @yah:next(“IMPL: add a TenantRegistry (tenants/.toml parse/load: sorted, missing-dir=empty, id==stem invariant) + a multi-tenant router that routes {route, mirror_key} through it — bearer matches no tenant -> 403; tenant doesn’t serve route (allowlist) -> 404; match -> revalidate_once(tenant.workload, tenant.publish_config, route). serve.rs bin: add –tenants

mode, mutually exclusive with single-tenant –workload/–publish-config. Unit-test routing with a fake render/publish callback (mirror revalidate.rs’s existing serve_receiver_on split) — no V8, no network.”) @yah:next(“RE-HOME CONTEXT: receiver half of yah-root R330-F12. almanac-serve BINARY retired for mesofact-serve –revalidate (W225 §3/§4). The gh-releases FETCH -> releases.json is the upstream PRODUCER (yubaba almanac, landed) and is OUT of this receiver’s scope — it pokes this receiver after writing fresh data. F11 runner hosts mesofact-serve –revalidate, not almanac-serve.”) @yah:assumes(“DIVERGENCE flagged to Glimmerstone: they suggested a thinner shape (tenant = {service, env, data_inputs}, deref the yah mirror toml for bucket/prefix/zone/provider). Overriding to mesofact-native on the export-boundary rationale above. The routing CORE (mirror_key->tenant, 403/404, revalidate_once dispatch) is invariant across both shapes; only the config-source detail differs. Awaiting their ack/objection before finalizing field names, but not blocked on it — routing can land first behind the config seam.”) @yah:assumes(“data_inputs do NOT belong in the tenant registry: route<-data bindings already live in the mesofact manifest.json (RenderRequest.data), and the data SOURCE (gh-releases fetch) is the producer’s concern, out of the receiver’s scope.”) @yah:handoff(“LANDED (code-complete, mesofact-dev, feature=ssr): new crate::tenants module + –tenants CLI mode. (1) tenants.rs: TenantFile (id/workload/publish_config/mirror_key_env from tenants/.toml) -> ResolvedTenant (bearer resolved) -> TenantRegistry.tenant_for(mirror_key) routing; TenantJob{tenant_id,workload,publish_config,route}; load_tenants(dir) (sorted, missing-dir=empty, id==stem fail-loud) + resolve_tenants(files, env-lookup closure) (bearer via mirror_key_env, never a literal secret in git); axum router (POST /revalidate {route,mirror_key} -> bearer selects tenant -> enqueue TenantJob -> 202; absent/empty/unknown bearer -> 403) + serve() draining TenantJob through the EXISTING crate::revalidate::revalidate_once (render+publish unchanged, only multiplied). (2) lib.rs: pub mod tenants (ssr). (3) serve.rs bin: –tenants mode; workload now optional; mutually exclusive with single-tenant –workload/–publish-config. Boundary held: a tenant references its OWN mesofact.config.toml, NOT yah’s mirror toml. Tests: 11 new (registry routing incl. unroutable-without-bearer; HTTP 202/403 + whole-site None-route; load sorted/missing-dir/stem-mismatch; resolve env present/absent). mesofact-dev 76->87 green; clippy clean on tenants.rs/serve.rs.”) @yah:handoff(“REMAINING (not code in this crate): (a) YAH-SIDE generator — a yah reconciler emits each tenant’s mesofact.config.toml [publish] from .yah/services//mirrors/.toml (Glimmerstone’s ‘thin deref’ goal, kept on the yah side to preserve the export boundary); file under R330-F12’s producer track. (b) DEPLOY: F11 runner hosts mesofact-serve --tenants <dir> (not almanac-serve), with tenants/.toml + the mirror_key_env bearers set. (c) SMOKE: POST runner /revalidate {route:‘/releases’, mirror_key:‘’} -> renders+publishes to yah-marketing’s R2. (d) Glimmerstone ack on the mesofact-native shape (divergence flagged; routing core is shape-invariant either way).”) @yah:verify(“cargo test -p mesofact –features ssr tenants:: # 22 pass (registry routing, per-tenant allowlist, validate, load/resolve). NOTE: crate is 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 –listen 127.0.0.1:38446 with two tenant tomls; /releases+mkt-bearer 202, /pricing+mkt-bearer 403 (outside that tenant’s allowlist), /pricing+acme-bearer 202 (acme declares none), wrong/absent bearer 403, whole-site poke 202, escaping data_inputs 400. Renders then fail on the absent fixture workload — which is the proof the worker reached revalidate_once per tenant.”) @yah:verify(“CLI guards on the real binary: --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§

RevalidateConfig
Runtime configuration for the receiver. Built by the mesofact serve binary from CLI flags / env.
RevalidateReport
Outcome of one revalidation cycle.

Constants§

MAX_REVALIDATE_BODY_BYTES
Largest POST /revalidate body 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 (all static/spa, non-deferred).
serve
Run the receiver: bind port, serve the router, and drain pokes through revalidate_once one 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§

DataInputs
Render inputs carried by a poke: the workload-relative path a route declares in its data_inputs → the JSON that path should hold.