Skip to Content
ExamplesLayout

Layout Animation

import type { MaybeRefOrGetter } from 'vue'; import type { Node } from '@xyflow/vue'; import type { DagreGraph } from './useLayout'; import { shallowRef, toValue } from 'vue'; import { getConnectedEdges, useVueFlow } from '@xyflow/vue'; export enum ProcessStatus { ERROR = 'error', SKIPPED = 'skipped', CANCELLED = 'cancelled', FINISHED = 'finished', RUNNING = 'running', } export type ProcessNodeData = { status: ProcessStatus | null }; export type ProcessNodeType = Node<ProcessNodeData, 'process'>; interface RunProcessOptions { graph: MaybeRefOrGetter<DagreGraph>; cancelOnError?: MaybeRefOrGetter<boolean>; } /** * Simulates running a process tree: it walks the graph, pretends to run an async task on each node and * updates the node's `status`. When a node finishes it starts its children (in parallel), optionally * cancelling everything downstream on error. */ export function useRunProcess({ graph, cancelOnError = true }: RunProcessOptions) { const { updateNodeData, getNode, getEdge, getEdges } = useVueFlow<ProcessNodeType>(); const isRunning = shallowRef(false); /** node id → its scheduled timeout */ const runningTasks = new Map<string, ReturnType<typeof setTimeout>>(); /** node ids that have already run */ const executedNodes = new Set<string>(); /** node ids queued but not yet started */ const upcomingTasks = new Set<string>(); function updateNodeStatus(nodeId: string, status: ProcessStatus | null) { updateNodeData(nodeId, { status }); } async function runNode(nodeId: string, isStart = false): Promise<boolean | void> { if (executedNodes.has(nodeId)) { return; } upcomingTasks.add(nodeId); const node = getNode(nodeId); const connectedEdges = node ? getConnectedEdges([node], [...getEdges.value]) : []; const incomers = connectedEdges.filter((edge) => edge.target === nodeId); // wait for incoming edge animations to finish; re-read the edge each poll since updates replace it await Promise.all(incomers.map((incomer) => until(() => !getEdge(incomer.id)?.data?.isAnimating))); upcomingTasks.clear(); if (!isRunning.value) { return; } executedNodes.add(nodeId); updateNodeStatus(nodeId, ProcessStatus.RUNNING); const delay = Math.floor(Math.random() * 2000) + 1000; return new Promise<boolean>((resolve) => { const timeout = setTimeout( async () => { const children = toValue(graph).successors(nodeId) ?? []; // 15% chance of failure (never on the starting node) const willThrowError = Math.random() < 0.15; if (!isStart && willThrowError) { updateNodeStatus(nodeId, ProcessStatus.ERROR); if (toValue(cancelOnError)) { await skipDescendants(nodeId); runningTasks.delete(nodeId); resolve(true); return; } } updateNodeStatus(nodeId, ProcessStatus.FINISHED); runningTasks.delete(nodeId); if (children.length > 0) { await Promise.all(children.map((child) => runNode(child))); } resolve(true); }, isStart ? 0 : delay, ); runningTasks.set(nodeId, timeout); }); } async function run(nodes: Node[]) { if (isRunning.value) { return; } reset(nodes); isRunning.value = true; const startingNodes = nodes.filter((node) => (toValue(graph).predecessors(node.id) ?? []).length === 0); await Promise.all(startingNodes.map((node) => runNode(node.id, true))); clear(); } function reset(nodes: Node[]) { clear(); for (const node of nodes) { updateNodeStatus(node.id, null); } } async function skipDescendants(nodeId: string) { const children = toValue(graph).successors(nodeId) ?? []; for (const child of children) { updateNodeStatus(child, ProcessStatus.SKIPPED); await skipDescendants(child); } } async function stop() { isRunning.value = false; for (const nodeId of [...upcomingTasks, ...runningTasks.keys()]) { clearTimeout(runningTasks.get(nodeId)); runningTasks.delete(nodeId); updateNodeStatus(nodeId, ProcessStatus.CANCELLED); await skipDescendants(nodeId); } executedNodes.clear(); upcomingTasks.clear(); } function clear() { isRunning.value = false; executedNodes.clear(); runningTasks.clear(); } return { run, stop, reset, isRunning }; } function until(condition: () => boolean) { return new Promise<void>((resolve) => { const interval = setInterval(() => { if (condition()) { clearInterval(interval); resolve(); } }, 100); }); }
Last updated on