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.
119 lines
5.3 KiB
TypeScript
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", {});
|