157 lines
6.2 KiB
TypeScript
157 lines
6.2 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 { emit } from "@novox/mesh-sdk/events";
|
|
import { broker } from "@novox/mesh-sdk/messaging";
|
|
import { invokeTool } from "@novox/mesh-sdk/tools";
|
|
import { state } from "@novox/mesh-sdk/state";
|
|
import { Graph } from "./store.js";
|
|
import {
|
|
answerOf, builtFrom, placeable, BUILDER, BUILDS_SINCE_TOOL, BUILDS_STATE, MANIFESTS_STATE,
|
|
type Built, type BuildsSince, type LogEntry,
|
|
} from "./heard.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 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` in heard.ts.
|
|
*/
|
|
const placeTheBuild = async (event: { body: unknown }): Promise<void> => {
|
|
const body = (event.body ?? {}) as Built;
|
|
const placing = placeable(body);
|
|
if (placing.placed === "no") {
|
|
// A check, a try, a dry run and a failure are the build seat's ordinary outcomes: passed over. One
|
|
// that names no module or commit is a fault in the builder, said rather than dropped, so it does
|
|
// not look like a missing event.
|
|
if (placing.fault) console.error(`mesh-catalog: a build event was not placed: ${placing.why}`, body);
|
|
return;
|
|
}
|
|
const { module, commit } = placing;
|
|
|
|
const { upgraded, previous } = await graph.register({
|
|
module,
|
|
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, 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, 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,
|
|
});
|
|
}
|
|
};
|
|
|
|
// **From the builder's log** (novox/hq ADR 0571 clauses 1 and 9). The builder keeps every build's end in a log of its
|
|
// own and answers the builds that ended since a time by its tool. This module asks at its start for what it missed (all
|
|
// the log holds, taken in as history: registered, announced to nobody), and again whenever the builder's state of done
|
|
// keys changes, for what ended since the last build it took in. Registering a build already held changes nothing, and a
|
|
// build is taken in once per run of this module.
|
|
const taken = new Set<string>();
|
|
let since = new Date(0).toISOString();
|
|
let caughtUp = false;
|
|
let asking: Promise<void> | undefined;
|
|
let askAgain = false;
|
|
|
|
const manifestOf = async (digest: string | undefined): Promise<unknown> => {
|
|
if (!digest) return {};
|
|
const held = await state<{ manifest?: string }>(MANIFESTS_STATE).get(digest);
|
|
if (!held?.value.manifest) return {};
|
|
return JSON.parse(held.value.manifest);
|
|
};
|
|
|
|
const takeIn = async (e: LogEntry, replay: boolean): Promise<void> => {
|
|
const key = `${e.module}/${e.fp}`;
|
|
if (taken.has(key)) return;
|
|
const built = builtFrom(e, await manifestOf(e.manifest), replay);
|
|
if (!built) return;
|
|
await placeTheBuild({ body: built });
|
|
taken.add(key);
|
|
};
|
|
|
|
const catchUp = async (): Promise<void> => {
|
|
for (;;) {
|
|
const answer = answerOf<BuildsSince>(
|
|
await invokeTool(broker(), BUILDER, BUILDS_SINCE_TOOL, { since }),
|
|
);
|
|
for (const e of answer.builds ?? []) {
|
|
await takeIn(e, !caughtUp);
|
|
if (e.at > since) since = e.at;
|
|
}
|
|
if (!answer.more) break;
|
|
}
|
|
caughtUp = true;
|
|
};
|
|
|
|
/** One ask at a time; a change heard while asking is asked for again once, so none is missed. */
|
|
const ask = (): Promise<void> => {
|
|
if (asking) {
|
|
askAgain = true;
|
|
return asking;
|
|
}
|
|
asking = (async () => {
|
|
try {
|
|
do {
|
|
askAgain = false;
|
|
await catchUp();
|
|
} while (askAgain);
|
|
} catch (err) {
|
|
console.error("mesh-catalog: the builder's builds since", since, "could not be taken in:", err);
|
|
} finally {
|
|
asking = undefined;
|
|
}
|
|
})();
|
|
return asking;
|
|
};
|
|
|
|
// Asked AFTER watching begins, so a build ending while the first ask runs asks again rather than being missed: the watch
|
|
// hands every current key first (the start's ask), then every change.
|
|
await state(BUILDS_STATE).watch(async (change) => {
|
|
if (!change.current) await ask();
|
|
});
|
|
await ask();
|
|
|
|
// **And say that this catalogue has caught up** (novox/hq 04-ISSUES/050).
|
|
await emit("catching-up", {});
|