/*
effects — component runtime effect outcome type definitions for effect handlers.
- Do not run effects — this module defines the outcome contract.
- 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 {
EffectClass,
EffectDeclarationV1,
CompensationAction,
} from "@warpgogol/werkstatt-shared/component";
import type { ComponentId } from "@warpgogol/werkstatt-shared/component";
export type EffectOutcome =
"committed" | "aborted" | "compensated" | "withheld" | "failed-rollback" | "quarantined";
export interface EffectUnwindEntryV1 {
readonly componentId: ComponentId;
readonly effectClass: EffectClass;
readonly outcome: EffectOutcome;
readonly error: string | null;
}
export interface EffectUnwindReportV1 {
readonly entries: readonly EffectUnwindEntryV1[];
readonly allSucceeded: boolean;
readonly quarantined: boolean;
}
export interface EffectHandlerResult {
readonly ok: boolean;
readonly outcome: EffectOutcome;
readonly error: string | null;
}
export interface EffectHandler {
readonly effectClass: EffectClass;
prepare(): Promise;
commit(): Promise;
abort(): Promise;
compensate(): Promise;
dispose(): Promise;
}
export class RevertibleEffectHandler implements EffectHandler {
readonly effectClass: EffectClass = "revertible";
private disposed = false;
private readonly disposer: () => Promise;
private readonly onFail: (error: string) => void;
constructor(disposer: () => Promise, onFail?: (error: string) => void) {
this.disposer = disposer;
this.onFail = onFail ?? (() => {});
}
async prepare(): Promise {
return { ok: true, outcome: "committed", error: null };
}
async commit(): Promise {
return { ok: true, outcome: "committed", error: null };
}
async abort(): Promise {
if (this.disposed) {
return { ok: true, outcome: "aborted", error: null };
}
return this.dispose();
}
async compensate(): Promise {
return {
ok: false,
outcome: "failed-rollback",
error: "revertible effects do not support compensation; use dispose",
};
}
async dispose(): Promise {
if (this.disposed) {
return { ok: true, outcome: "aborted", error: null };
}
try {
await this.disposer();
this.disposed = true;
return { ok: true, outcome: "aborted", error: null };
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
this.onFail(msg);
return { ok: false, outcome: "failed-rollback", error: msg };
}
}
}
export class TransactionalEffectHandler implements EffectHandler {
readonly effectClass: EffectClass = "transactional";
private prepared = false;
private committed = false;
private aborted = false;
private readonly prepareFn: () => Promise;
private readonly commitFn: () => Promise;
private readonly abortFn: () => Promise;
private readonly idempotencyKey: string;
constructor(
idempotencyKey: string,
prepareFn: () => Promise,
commitFn: () => Promise,
abortFn: () => Promise,
) {
this.idempotencyKey = idempotencyKey;
this.prepareFn = prepareFn;
this.commitFn = commitFn;
this.abortFn = abortFn;
}
async prepare(): Promise {
if (this.prepared || this.committed || this.aborted) {
return { ok: true, outcome: "committed", error: null };
}
try {
await this.prepareFn();
this.prepared = true;
return { ok: true, outcome: "committed", error: null };
} catch (e) {
return {
ok: false,
outcome: "failed-rollback",
error: e instanceof Error ? e.message : String(e),
};
}
}
async commit(): Promise {
if (this.committed) {
return { ok: true, outcome: "committed", error: null };
}
if (!this.prepared) {
return { ok: false, outcome: "failed-rollback", error: "commit called before prepare" };
}
try {
await this.commitFn();
this.committed = true;
return { ok: true, outcome: "committed", error: null };
} catch (e) {
return {
ok: false,
outcome: "failed-rollback",
error: e instanceof Error ? e.message : String(e),
};
}
}
async abort(): Promise {
if (this.aborted || this.committed) {
return { ok: true, outcome: "aborted", error: null };
}
try {
await this.abortFn();
this.aborted = true;
return { ok: true, outcome: "aborted", error: null };
} catch (e) {
return {
ok: false,
outcome: "failed-rollback",
error: e instanceof Error ? e.message : String(e),
};
}
}
async compensate(): Promise {
return {
ok: false,
outcome: "failed-rollback",
error: "transactional effects use abort, not compensation",
};
}
async dispose(): Promise {
if (this.committed) {
return { ok: true, outcome: "committed", error: null };
}
if (!this.aborted) {
return this.abort();
}
return { ok: true, outcome: "aborted", error: null };
}
}
export class CompensatableEffectHandler implements EffectHandler {
readonly effectClass: EffectClass = "compensatable";
private committed = false;
private compensated = false;
private readonly commitFn: () => Promise;
private readonly compensateFn: () => Promise;
private readonly compensationAction: CompensationAction;
constructor(
commitFn: () => Promise,
compensateFn: () => Promise,
compensationAction: CompensationAction,
) {
if (compensationAction.verificationProbes.length === 0) {
throw new Error("compensatable effects require at least one verification probe");
}
this.commitFn = commitFn;
this.compensateFn = compensateFn;
this.compensationAction = compensationAction;
}
get compensation(): CompensationAction {
return this.compensationAction;
}
async prepare(): Promise {
return { ok: true, outcome: "committed", error: null };
}
async commit(): Promise {
if (this.committed) {
return { ok: true, outcome: "committed", error: null };
}
try {
await this.commitFn();
this.committed = true;
return { ok: true, outcome: "committed", error: null };
} catch (e) {
return {
ok: false,
outcome: "failed-rollback",
error: e instanceof Error ? e.message : String(e),
};
}
}
async abort(): Promise {
if (!this.committed) {
return { ok: true, outcome: "aborted", error: null };
}
return this.compensate();
}
async compensate(): Promise {
if (this.compensated) {
return { ok: true, outcome: "compensated", error: null };
}
if (!this.committed) {
return { ok: true, outcome: "aborted", error: null };
}
try {
await this.compensateFn();
this.compensated = true;
return { ok: true, outcome: "compensated", error: null };
} catch (e) {
return {
ok: false,
outcome: "quarantined",
error: e instanceof Error ? e.message : String(e),
};
}
}
async dispose(): Promise {
if (this.committed && !this.compensated) {
return this.compensate();
}
return { ok: true, outcome: this.compensated ? "compensated" : "aborted", error: null };
}
}
export class IrreversibleEmissionEffectHandler implements EffectHandler {
readonly effectClass: EffectClass = "irreversible-emission";
private withheld = true;
private emitted = false;
private readonly emitFn: () => Promise;
constructor(emitFn: () => Promise) {
this.emitFn = emitFn;
}
async prepare(): Promise {
return { ok: true, outcome: "withheld", error: null };
}
async commit(): Promise {
if (this.emitted) {
return { ok: true, outcome: "committed", error: null };
}
try {
await this.emitFn();
this.emitted = true;
this.withheld = false;
return { ok: true, outcome: "committed", error: null };
} catch (e) {
return {
ok: false,
outcome: "failed-rollback",
error: e instanceof Error ? e.message : String(e),
};
}
}
async abort(): Promise {
if (this.emitted) {
return {
ok: false,
outcome: "quarantined",
error: "irreversible emission already emitted; cannot abort",
};
}
this.withheld = false;
return { ok: true, outcome: "aborted", error: null };
}
async compensate(): Promise {
if (this.emitted) {
return {
ok: false,
outcome: "quarantined",
error: "irreversible emissions cannot be compensated",
};
}
return { ok: true, outcome: "withheld", error: null };
}
async dispose(): Promise {
if (this.withheld && !this.emitted) {
return { ok: true, outcome: "withheld", error: null };
}
if (this.emitted) {
return { ok: true, outcome: "committed", error: null };
}
return { ok: true, outcome: "aborted", error: null };
}
get isWithheld(): boolean {
return this.withheld;
}
}
export function createEffectHandler(
declaration: EffectDeclarationV1,
handlers: {
disposer?: () => Promise;
prepare?: () => Promise;
commit?: () => Promise;
abort?: () => Promise;
compensate?: () => Promise;
emit?: () => Promise;
compensationAction?: CompensationAction;
},
): EffectHandler {
switch (declaration.effectClass) {
case "revertible":
return new RevertibleEffectHandler(handlers.disposer ?? (async () => {}));
case "transactional":
return new TransactionalEffectHandler(
declaration.commitMetadata ?? "tx-key",
handlers.prepare ?? (async () => {}),
handlers.commit ?? (async () => {}),
handlers.abort ?? (async () => {}),
);
case "compensatable": {
if (!handlers.compensationAction) {
throw new Error("EFFECT-02: compensatable effects require a compensationAction");
}
return new CompensatableEffectHandler(
handlers.commit ?? (async () => {}),
handlers.compensate ?? (async () => {}),
handlers.compensationAction,
);
}
case "irreversible-emission":
return new IrreversibleEmissionEffectHandler(handlers.emit ?? (async () => {}));
default: {
const _exhaustive: never = declaration.effectClass;
throw new Error(`unknown effect class: ${_exhaustive}`);
}
}
}
export function buildUnwindReport(entries: readonly EffectUnwindEntryV1[]): EffectUnwindReportV1 {
const allSucceeded = entries.every(
(e) =>
e.outcome === "aborted" ||
e.outcome === "compensated" ||
e.outcome === "withheld" ||
e.outcome === "committed",
);
const quarantined = entries.some(
(e) => e.outcome === "quarantined" || e.outcome === "failed-rollback",
);
return { entries, allSucceeded: allSucceeded && !quarantined, quarantined };
}