Files
mesh-catalog/modules/mesh-catalog/index.ts
T
jschoubben 016ddb2b3a The catalogue hears what it missed
It asks what it missed on every start and the answer never arrived: the control plane replayed each
build it held as a module's event from a module called "control-plane", which does not exist, so its
own account refused the publish and the graph kept the gap. The control plane now states those under
the seat it holds (novox/hq ADR 0134, mesh-controller #129), so this consumes that too — one handler,
because what a build means for the graph is the same whether the build machine says it as it happens
or the mesh says what it already held.
2026-09-28 16:08:46 +02:00

119 lines
5.3 KiB
TypeScript

// mesh-catalog's entrypoint — the module graph's consumer (novox/hq ADR 0070, ADR 0072).
//
// The build-machine role announces what it built; this places it in the graph and announces what that means.
// The control plane hooks the *meaning* — a module was upgraded — rather than the build output, so
// it never has to interpret an artifact or ask this module anything.
//
// **Ordering is not computed here.** When a registration makes something else stale, the modules
// whose own dependencies are all current are announced as needing a rebuild; the rest stay stale
// and appear the next time round, once whatever they were waiting for is registered. A chain and a
// diamond need no special handling, and nothing holds a plan.
import { on, emit } from "@novox/mesh-sdk/events";
import { Graph, type Made } from "./store.js";
const graph = Graph.fromEnv();
// The schema is not brought up here. The mesh prepares this module's state before it starts this
// version, and does not start it if that failed (novox/hq ADR 0135) — see prepare/index.ts. Doing it
// at start made a schema that could not be reached a crash loop instead of a stop, with the graph
// keeping a gap and nothing saying so. The reason it used to be here — that a step blocking the apply
// would block the very apply that brings the overlay up — stopped being true when a step's failure
// became this module's business and not the machine's (ADR 0136).
/** What the builder says when it has built something. */
interface Built {
module?: string;
commit?: string;
repository?: string;
path?: string;
ref?: string;
manifest?: unknown;
/** What this build published, each pinned as anything else would name it. */
made?: Made[];
/**
* Every artifact this was built on top of, so the edge is derived rather than declared.
*
* References, not module names: the builder sees a pinned image and cannot see which module
* produced it. Turning that into an edge between module-versions is this module's job.
*/
against?: string[];
/**
* This is history, not news — the mesh re-announcing a build this catalogue was not there for.
*
* Registered exactly as any other, and announced as nothing. A module that moved months ago is
* not something anything should act on now: emitting `upgraded` would have the control plane
* decide about a rollout, and `rebuild-needed` would ask for builds of things that are already
* current.
*/
replay?: boolean;
}
/**
* What a build means for the graph, wherever it came from.
*
* Two emitters say the same thing and neither is a mistake: the build machine says it as it happens,
* and the control plane says what it already held when this module asks what it missed
* (novox/hq ADR 0134). A replay is marked as one in its body, so nothing acts on a module that moved
* months ago — see `replay` above.
*/
const placeTheBuild = async (event: { body: unknown }): Promise<void> => {
const body = event.body as Built;
if (!body.module || !body.commit) {
// Said rather than dropped: a build that announced itself without saying what it built is a
// fault in the builder, and a silent discard here would make it look like a missing event.
console.error("mesh-catalog: a build event named no module or no commit; ignored", body);
return;
}
const { upgraded, previous } = await graph.register({
module: body.module,
commit: body.commit,
repository: body.repository ?? "",
path: body.path ?? "",
ref: body.ref ?? "",
manifest: body.manifest ?? {},
}, body.made ?? [], body.against ?? []);
// **A replay is registered and announced to nobody.** See `replay` above: the graph gains what
// it was missing, and the mesh is told nothing happened, because nothing did.
if (body.replay) return;
await emit("registered", {
module: body.module, commit: body.commit, upgraded,
});
// **A rebuild that changed nothing is not an upgrade.** Announcing it would ripple outward
// through modules that did not change, forever (ADR 0072).
if (!upgraded) return;
await emit("upgraded", {
module: body.module, commit: body.commit, previous,
});
// What can be built now — stale, and waiting on nothing that is itself stale.
for (const next of await graph.buildable()) {
await emit("rebuild-needed", {
module: next.module,
builtAt: next.commit,
because: next.because,
});
}
};
// As it happens, and what the mesh already held when this module asked what it missed.
await on("mesh-build-machine.built", placeTheBuild);
await on("mesh-controller.built-before", placeTheBuild);
// **And ask for what was built before this catalogue existed** (novox/hq 04-ISSUES/050).
//
// The queue above is durable, so nothing is missed once this is running. What it cannot have is
// what was announced before it first ran — and on a fresh mesh that is never arbitrary: the shared
// base, the store this runs on, and this module itself are each necessarily built BEFORE a
// catalogue exists to hear about them. The graph's foundation is the part it never sees.
//
// Asked on every start, not only the first. A catalogue cannot tell whether it has a gap, and the
// answer is idempotent: registering a build already held changes nothing and announces nothing.
// Asked AFTER subscribing, so a build arriving during the replay is not lost between the two.
await emit("catching-up", {});