/** The hosted persistence boundary for implementation graphs. */ import { claim, Graphs } from "./graphs"; import * as Comments from "../comments/service"; import { drain, exclusive, persistExclusive } from "../plan/service"; import type { Plan } from "../plan/service"; import type { ClaimInput, ClaimResult, Graph, GraphAdapter, Result } from "./graphs"; const adapter: GraphAdapter = { async transact(plan, change): Promise> { return exclusive(plan, async () => { // Reading the revisions, changing the graph, and committing its sidecar // state are one room operation. A plan change therefore cannot land // between the comparison and the durable record. let result = change({ graph: plan.graph, revision: plan.revision }); if (!result.ok) return result; let previous = plan.graph; plan.graph = result.value; try { await persistExclusive(plan); return result; } catch (error) { plan.graph = previous; throw error; } }); }, }; /** The graph service for a live hosted plan. */ export function implementationGraphs(): Graphs { return new Graphs(adapter); } /** Drain accepted work, then durably lock one approved graph for implementation. */ export async function claimImplementation(plan: Plan, input: ClaimInput): Promise { if (plan.execution) { return claim({ graph: plan.graph, revision: plan.revision, execution: plan.execution }, input); } plan.claiming = true; try { await drain(plan); return await exclusive(plan, async () => { let result = claim({ graph: plan.graph, revision: plan.revision, execution: plan.execution, }, input); if (result.kind !== "started") return result; let previous = plan.graph; plan.graph = result.graph; plan.execution = result.run; try { await persistExclusive(plan); return result; } catch { plan.graph = previous; plan.execution = undefined; return { kind: "refused", reason: "durability" }; } }); } finally { plan.claiming = false; } } /** Whether the settled plan can be turned into implementation work. */ export function implementationReadiness( plan: Plan, revision: unknown, ): { ok: true; revision: number } | { ok: false; blockers: string[] } { let blockers: string[] = []; if ([...plan.records.values()].some(record => record.status === "open")) { blockers.push("unanswered questionnaires"); } if (Comments.outstanding(plan).length > 0) { blockers.push("accepted comments awaiting plan changes"); } if (typeof revision !== "number" || !Number.isInteger(revision) || revision !== plan.revision) { blockers.push("invalid plan revision"); } return blockers.length > 0 ? { ok: false, blockers } : { ok: true, revision: plan.revision }; }