Files
flowgraph/src/reactive/node-status.ts
glm-5.1 e98204161d 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
2026-05-21 22:16:46 +00:00

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);
}