feat: implement node status signal management with computed preconditions and blockedByFailure
- 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
This commit is contained in:
@@ -1 +1,81 @@
|
||||
export {};
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user