Files
kandev/apps/web/components/settings/workflow-mutation-guard.ts
T
2026-07-17 18:47:27 +01:00

119 lines
3.4 KiB
TypeScript

"use client";
import { useCallback, useMemo, useRef, useState } from "react";
import type { WorkflowStep } from "@/lib/types/http";
import {
analyzeIntroducedWorkflowReplayCycles,
analyzeWorkflowReplayCycles,
type WorkflowReplayCycleDiagnostic,
type WorkflowReplayCycleSeverity,
} from "@/lib/workflows/replay-cycle-analysis";
export type WorkflowMutationIntent = "apply" | "create";
export type WorkflowMutationProposal = {
diagnostics: WorkflowReplayCycleDiagnostic[];
intent: WorkflowMutationIntent;
severity: WorkflowReplayCycleSeverity;
};
type GuardMutationParams = {
proposedSteps: WorkflowStep[];
operation: () => Promise<void>;
intent?: WorkflowMutationIntent;
baselineSteps?: WorkflowStep[];
};
export type WorkflowMutationGuardController = {
diagnostics: WorkflowReplayCycleDiagnostic[];
proposal: WorkflowMutationProposal | null;
isMutationPending: boolean;
guardMutation: (params: GuardMutationParams) => Promise<void>;
confirmProposal: () => Promise<void>;
cancelProposal: () => void;
};
type MutationPhase = "idle" | "proposal" | "running";
export function useWorkflowMutationGuard(
displayedSteps: WorkflowStep[],
): WorkflowMutationGuardController {
const diagnostics = useMemo(() => analyzeWorkflowReplayCycles(displayedSteps), [displayedSteps]);
const [proposal, setProposal] = useState<WorkflowMutationProposal | null>(null);
const [isMutationPending, setIsMutationPending] = useState(false);
const mutationPhase = useRef<MutationPhase>("idle");
const pendingOperation = useRef<(() => Promise<void>) | null>(null);
const releaseMutation = useCallback(() => {
mutationPhase.current = "idle";
setIsMutationPending(false);
}, []);
const cancelProposal = useCallback(() => {
if (mutationPhase.current !== "proposal") return;
pendingOperation.current = null;
setProposal(null);
releaseMutation();
}, [releaseMutation]);
const confirmProposal = useCallback(async () => {
if (mutationPhase.current !== "proposal") return;
const operation = pendingOperation.current;
if (!operation) {
setProposal(null);
releaseMutation();
return;
}
pendingOperation.current = null;
mutationPhase.current = "running";
setProposal(null);
try {
await operation();
} finally {
releaseMutation();
}
}, [releaseMutation]);
const guardMutation = useCallback(
async ({
proposedSteps,
operation,
intent = "apply",
baselineSteps = displayedSteps,
}: GuardMutationParams) => {
if (mutationPhase.current !== "idle") return;
mutationPhase.current = "running";
setIsMutationPending(true);
try {
const introduced = analyzeIntroducedWorkflowReplayCycles(baselineSteps, proposedSteps);
if (introduced.length === 0) {
await operation();
releaseMutation();
return;
}
const severity = introduced.some((diagnostic) => diagnostic.severity === "blocking")
? "blocking"
: "warning";
pendingOperation.current = severity === "warning" ? operation : null;
mutationPhase.current = "proposal";
setProposal({ diagnostics: introduced, intent, severity });
} catch (error) {
releaseMutation();
throw error;
}
},
[displayedSteps, releaseMutation],
);
return {
diagnostics,
proposal,
isMutationPending,
guardMutation,
confirmProposal,
cancelProposal,
};
}