A replayed build is registered exactly as any other and announced to nobody. 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 already current. Asked on every start rather than only the first, because a catalogue cannot tell whether it has a gap — and the answer is idempotent, so asking when there is none costs a message. Asked after subscribing, so a build arriving during the replay is not lost between the two. Closes novox/hq 04-ISSUES/050 with mesh-control. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
105 lines
4.5 KiB
TypeScript
105 lines
4.5 KiB
TypeScript
// mesh-catalog's entrypoint — the module graph's consumer (novox/hq ADR 0070, ADR 0072).
|
|
//
|
|
// The builder 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();
|
|
|
|
// Before subscribing, and idempotent. The runtime is restarted until its store is reachable, which
|
|
// is the same arrangement model-usage uses: a schema step that had to reach the provider over the
|
|
// overlay would block the very apply that brings the overlay up.
|
|
await graph.migrate();
|
|
|
|
/** 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;
|
|
}
|
|
|
|
await on("module.builder.built", async (event) => {
|
|
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("module.mesh-catalog.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("module.mesh-catalog.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("module.mesh-catalog.rebuild-needed", {
|
|
module: next.module,
|
|
builtAt: next.commit,
|
|
because: next.because,
|
|
});
|
|
}
|
|
});
|
|
|
|
// **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("module.mesh-catalog.catching-up", {});
|