/*
activation — component activation state tracking for the component runtime lifecycle.
- Do not render components — activation only tracks state.
- RFC-1097: sweep — werkstatt-engine clean
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.
*/
import type {
ResolvedComponentSetV1,
ResolvedComponentIdentityV1,
} from "@warpgogol/werkstatt-shared/component";
import type { ComponentFiber } from "./fiber.ts";
import type { QuiescenceResult, Deadline } from "./fiber.ts";
import type { EffectUnwindReportV1 } from "./effects.ts";
export type ActivationState =
"idle" | "preparing" | "committing" | "aborting" | "committed" | "aborted" | "quarantined";
export interface ActivationTransactionV1 {
readonly transactionId: string;
readonly priorSetHash: string;
readonly proposedSetHash: string;
prepare(): Promise;
commit(): Promise;
abort(reason: string): Promise;
}
export interface ActivationResult {
readonly state: ActivationState;
readonly transactionId: string;
readonly priorSetHash: string;
readonly proposedSetHash: string;
readonly fibers: readonly ComponentFiber[];
readonly drainResults: readonly QuiescenceResult[];
readonly unwindReports: readonly EffectUnwindReportV1[];
readonly error: string | null;
readonly quarantined: boolean;
}
interface ActivationConfig {
readonly transactionId: string;
readonly priorSet: ResolvedComponentSetV1 | null;
readonly proposedSet: ResolvedComponentSetV1;
readonly createFiber: (identity: ResolvedComponentIdentityV1) => ComponentFiber;
readonly drainDeadline: Deadline;
}
function sortByDependency(
components: readonly ResolvedComponentIdentityV1[],
): readonly ResolvedComponentIdentityV1[] {
return [...components].sort((a, b) => {
const ka = `${a.componentId}@${a.version}`;
const kb = `${b.componentId}@${b.version}`;
return ka < kb ? -1 : ka > kb ? 1 : 0;
});
}
export class ActivationTransaction implements ActivationTransactionV1 {
readonly transactionId: string;
readonly priorSetHash: string;
readonly proposedSetHash: string;
private _state: ActivationState = "idle";
private readonly config: ActivationConfig;
private newFibers: ComponentFiber[] = [];
private priorFibers: ComponentFiber[] = [];
private drainResults: QuiescenceResult[] = [];
private unwindReports: EffectUnwindReportV1[] = [];
private error: string | null = null;
constructor(config: ActivationConfig) {
this.config = config;
this.transactionId = config.transactionId;
this.priorSetHash = config.priorSet?.setHash ?? "sha256:" + "0".repeat(64);
this.proposedSetHash = config.proposedSet.setHash;
}
get state(): ActivationState {
return this._state;
}
get fibers(): readonly ComponentFiber[] {
return this.newFibers;
}
get result(): ActivationResult {
return {
state: this._state,
transactionId: this.transactionId,
priorSetHash: this.priorSetHash,
proposedSetHash: this.proposedSetHash,
fibers: this.newFibers,
drainResults: this.drainResults,
unwindReports: this.unwindReports,
error: this.error,
quarantined: this._state === "quarantined",
};
}
async prepare(): Promise {
if (this._state !== "idle") {
throw new Error(`ACTIVATION-01: prepare requires idle state, got ${this._state}`);
}
this._state = "preparing";
try {
const sorted = sortByDependency(this.config.proposedSet.components);
this.newFibers = sorted.map((id) => this.config.createFiber(id));
for (const fiber of this.newFibers) {
const r = fiber.transitionTo("waiting");
if (!r.ok) {
throw new Error(
`ACTIVATION-02: cannot transition ${fiber.component.componentId} to waiting: ${r.message}`,
);
}
}
for (const fiber of this.newFibers) {
const r = fiber.transitionTo("loading");
if (!r.ok) {
throw new Error(
`ACTIVATION-03: cannot transition ${fiber.component.componentId} to loading: ${r.message}`,
);
}
}
for (const fiber of this.newFibers) {
const r = fiber.transitionTo("active");
if (!r.ok) {
throw new Error(
`ACTIVATION-04: cannot transition ${fiber.component.componentId} to active: ${r.message}`,
);
}
}
} catch (e) {
this.error = e instanceof Error ? e.message : String(e);
await this.abort(this.error);
throw e;
}
}
async commit(): Promise {
if (this._state !== "preparing") {
throw new Error(`ACTIVATION-05: commit requires preparing state, got ${this._state}`);
}
this._state = "committing";
if (this.config.priorSet) {
const priorSorted = sortByDependency(this.config.priorSet.components);
this.priorFibers = priorSorted.map((id) => this.config.createFiber(id));
const reversed = [...this.priorFibers].reverse();
for (const fiber of reversed) {
const drainResult = await fiber.drain(this.config.drainDeadline);
this.drainResults.push(drainResult);
if (drainResult.timedOut) {
this.error = `ACTIVATION-06: drain timeout for ${fiber.component.componentId}`;
await this.abort(this.error);
throw new Error(this.error);
}
}
for (const fiber of reversed) {
const report = await fiber.dispose();
this.unwindReports.push(report);
if (report.quarantined) {
this.error = `ACTIVATION-07: quarantine during prior-set unwind for ${fiber.component.componentId}`;
this._state = "quarantined";
throw new Error(this.error);
}
}
}
this._state = "committed";
}
async abort(reason: string): Promise {
if (this._state === "aborted" || this._state === "quarantined") {
return;
}
this._state = "aborting";
this.error = reason;
const reversed = [...this.newFibers].reverse();
for (const fiber of reversed) {
if (!fiber.isTerminal) {
const report = await fiber.dispose();
this.unwindReports.push(report);
if (report.quarantined) {
this._state = "quarantined";
return;
}
}
}
this._state = "aborted";
}
get isCommitted(): boolean {
return this._state === "committed";
}
get isAborted(): boolean {
return this._state === "aborted";
}
get isQuarantined(): boolean {
return this._state === "quarantined";
}
}
export function createActivationTransaction(
transactionId: string,
priorSet: ResolvedComponentSetV1 | null,
proposedSet: ResolvedComponentSetV1,
createFiber: (identity: ResolvedComponentIdentityV1) => ComponentFiber,
drainDeadline: Deadline,
): ActivationTransaction {
return new ActivationTransaction({
transactionId,
priorSet,
proposedSet,
createFiber,
drainDeadline,
});
}