Files
flowgraph/test/helpers/reactive.ts

136 lines
3.7 KiB
TypeScript

import { signal, computed, effect } from "@preact/signals-core";
import { DirectedGraph } from "graphology";
const IDLE: NodeStatus = "idle";
const WAITING: NodeStatus = "waiting";
const READY: NodeStatus = "ready";
const RUNNING: NodeStatus = "running";
const COMPLETED: NodeStatus = "completed";
const FAILED: NodeStatus = "failed";
const ABORTED: NodeStatus = "aborted";
const SKIPPED: NodeStatus = "skipped";
type NodeStatus =
| "idle"
| "waiting"
| "ready"
| "running"
| "completed"
| "failed"
| "aborted"
| "skipped";
export interface TestReactiveRoot {
statusMap: Map<string, ReturnType<typeof signal<NodeStatus>>>;
preconditions: Map<string, ReturnType<typeof computed<boolean>>>;
blockedByFailure: Map<string, ReturnType<typeof computed<boolean>>>;
dispose: () => void;
}
export function createTestReactiveRoot(graph: DirectedGraph): TestReactiveRoot {
const statusMap = new Map<string, ReturnType<typeof signal<NodeStatus>>>();
const preconditions = new Map<string, ReturnType<typeof computed<boolean>>>();
const blockedByFailure = new Map<string, ReturnType<typeof computed<boolean>>>();
const disposers: (() => void)[] = [];
for (const node of graph.nodes()) {
const predecessors = graph.inNeighbors(node);
const status = signal<NodeStatus>(IDLE);
const preconditionsComputed = computed(() => {
return predecessors.every((pred: string) => {
const predStatus = statusMap.get(pred);
return (
predStatus !== undefined &&
(predStatus.value === COMPLETED || predStatus.value === SKIPPED)
);
});
});
const blockedByFailureComputed = computed(() => {
return predecessors.some((pred: string) => {
const predStatus = statusMap.get(pred);
return (
predStatus !== undefined &&
(predStatus.value === FAILED || predStatus.value === ABORTED)
);
});
});
const blockerDisposer = effect(() => {
if (blockedByFailureComputed.value) {
if (status.value === IDLE || status.value === WAITING) {
status.value = ABORTED;
}
}
});
disposers.push(blockerDisposer);
statusMap.set(node, status);
preconditions.set(node, preconditionsComputed);
blockedByFailure.set(node, blockedByFailureComputed);
}
return {
statusMap,
preconditions,
blockedByFailure,
dispose: () => {
for (const disposer of disposers) {
disposer();
}
statusMap.clear();
preconditions.clear();
blockedByFailure.clear();
},
};
}
export function assertStatus(
root: TestReactiveRoot,
nodeId: string,
expected: NodeStatus,
): void {
const status = root.statusMap.get(nodeId);
if (!status) {
throw new Error(`Node ${nodeId} not found in statusMap`);
}
if (status.value !== expected) {
throw new Error(
`Node ${nodeId} status: expected ${expected}, got ${status.value}`,
);
}
}
export function assertPreconditions(
root: TestReactiveRoot,
nodeId: string,
expected: boolean,
): void {
const preconditions = root.preconditions.get(nodeId);
if (!preconditions) {
throw new Error(`Node ${nodeId} not found in preconditions`);
}
if (preconditions.value !== expected) {
throw new Error(
`Node ${nodeId} preconditions: expected ${expected}, got ${preconditions.value}`,
);
}
}
export function assertBlockedByFailure(
root: TestReactiveRoot,
nodeId: string,
expected: boolean,
): void {
const blocked = root.blockedByFailure.get(nodeId);
if (!blocked) {
throw new Error(`Node ${nodeId} not found in blockedByFailure`);
}
if (blocked.value !== expected) {
throw new Error(
`Node ${nodeId} blockedByFailure: expected ${expected}, got ${blocked.value}`,
);
}
}
export type { NodeStatus };