- Add computePreconditions and computeBlockedByFailure functions to node-status.ts - Add registerStartEffect and registerAbortEffect for automatic state transitions - Start effect: idle/waiting -> ready when preconditions met - Abort effect: idle/waiting -> aborted when blockedByFailure true - Refactor WorkflowReactiveRoot to use node-status.ts functions - Root nodes auto-transition from idle to ready (no predecessors = preconditions true) - Add AbortEffectOptions with abortDependents policy support - Add comprehensive unit tests for all precondition and failure isolation scenarios
81 lines
2.1 KiB
TypeScript
81 lines
2.1 KiB
TypeScript
import { effect } from "@preact/signals-core";
|
|
import type { Signal, ReadonlySignal } from "@preact/signals-core";
|
|
import type { NodeStatus } from "../schema/enums.js";
|
|
|
|
const TERMINAL_STATUSES: Set<NodeStatus> = new Set([
|
|
"completed",
|
|
"failed",
|
|
"aborted",
|
|
"skipped",
|
|
]);
|
|
|
|
export interface NodeStatusContext {
|
|
statusMap: Map<string, Signal<NodeStatus>>;
|
|
predecessors: string[];
|
|
}
|
|
|
|
export function computePreconditions(
|
|
_nodeKey: string,
|
|
ctx: NodeStatusContext,
|
|
): boolean {
|
|
if (ctx.predecessors.length === 0) return true;
|
|
return ctx.predecessors.every((pred: string) => {
|
|
const predStatus = ctx.statusMap.get(pred);
|
|
if (!predStatus) return false;
|
|
return predStatus.value === "completed" || predStatus.value === "skipped";
|
|
});
|
|
}
|
|
|
|
export function computeBlockedByFailure(
|
|
_nodeKey: string,
|
|
ctx: NodeStatusContext,
|
|
): boolean {
|
|
return ctx.predecessors.some((pred: string) => {
|
|
const predStatus = ctx.statusMap.get(pred);
|
|
if (!predStatus) return false;
|
|
return predStatus.value === "failed" || predStatus.value === "aborted";
|
|
});
|
|
}
|
|
|
|
export function registerStartEffect(
|
|
status: Signal<NodeStatus>,
|
|
preconditions: ReadonlySignal<boolean>,
|
|
effectDisposers: (() => void)[],
|
|
): void {
|
|
const disposer = effect(() => {
|
|
if (preconditions.value) {
|
|
const current = status.value;
|
|
if (current === "idle" || current === "waiting") {
|
|
status.value = "ready";
|
|
}
|
|
}
|
|
});
|
|
effectDisposers.push(disposer);
|
|
}
|
|
|
|
export interface AbortEffectOptions {
|
|
abortDependents?: boolean;
|
|
}
|
|
|
|
export function registerAbortEffect(
|
|
status: Signal<NodeStatus>,
|
|
blockedByFailure: ReadonlySignal<boolean>,
|
|
effectDisposers: (() => void)[],
|
|
options?: AbortEffectOptions,
|
|
): void {
|
|
const disposer = effect(() => {
|
|
if (blockedByFailure.value) {
|
|
const current = status.value;
|
|
if (options?.abortDependents) {
|
|
if (!TERMINAL_STATUSES.has(current)) {
|
|
status.value = "aborted";
|
|
}
|
|
} else {
|
|
if (current === "idle" || current === "waiting") {
|
|
status.value = "aborted";
|
|
}
|
|
}
|
|
}
|
|
});
|
|
effectDisposers.push(disposer);
|
|
} |