import { filterAcyclicEdges as filterAcyclicWorkflowEdges, isWorkflowBlockAncestorLocked, isWorkflowBlockProtected, wouldCreateCycle as wouldCreateWorkflowEdgeCycle, } from '@sim/workflow-types/workflow' import type { Edge } from 'reactflow' import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types' const DEFAULT_LOOP_ITERATIONS = 5 const DEFAULT_PARALLEL_BATCH_SIZE = 20 const MAX_PARALLEL_BATCH_SIZE = 20 export function clampParallelBatchSize(batchSize: unknown): number { const parsed = typeof batchSize === 'number' ? batchSize : Number.parseInt(String(batchSize), 10) if (Number.isNaN(parsed)) { return DEFAULT_PARALLEL_BATCH_SIZE } return Math.max(1, Math.min(MAX_PARALLEL_BATCH_SIZE, parsed)) } /** * Check if adding an edge would create a cycle in the graph. * Delegates to the shared implementation in `@sim/workflow-types` so the * client store, the collaborative queueing layer, and the realtime * persistence layer all agree on the same cyclic edges. */ export function wouldCreateCycle(edges: Edge[], sourceId: string, targetId: string): boolean { return wouldCreateWorkflowEdgeCycle(edges, sourceId, targetId) } /** * Filters a batch of candidate edges down to the ones that do not create a * cycle against `currentEdges`, evaluated incrementally within the batch. */ export function filterAcyclicEdges(edgesToAdd: Edge[], currentEdges: Edge[]): Edge[] { return filterAcyclicWorkflowEdges(edgesToAdd, currentEdges) } /** * Convert UI loop block to executor Loop format * * @param loopBlockId - ID of the loop block to convert * @param blocks - Record of all blocks in the workflow * @returns Loop object for execution engine or undefined if not a valid loop */ export function convertLoopBlockToLoop( loopBlockId: string, blocks: Record ): Loop | undefined { const loopBlock = blocks[loopBlockId] if (!loopBlock || loopBlock.type !== 'loop') return undefined const loopType = loopBlock.data?.loopType || 'for' const loop: Loop = { id: loopBlockId, nodes: findChildNodes(loopBlockId, blocks), iterations: loopBlock.data?.count || DEFAULT_LOOP_ITERATIONS, loopType, enabled: loopBlock.enabled, } loop.forEachItems = loopBlock.data?.collection || '' loop.whileCondition = loopBlock.data?.whileCondition || '' loop.doWhileCondition = loopBlock.data?.doWhileCondition || '' return loop } /** * Convert UI parallel block to executor Parallel format * * @param parallelBlockId - ID of the parallel block to convert * @param blocks - Record of all blocks in the workflow * @returns Parallel object for execution engine or undefined if not a valid parallel block */ export function convertParallelBlockToParallel( parallelBlockId: string, blocks: Record ): Parallel | undefined { const parallelBlock = blocks[parallelBlockId] if (!parallelBlock || parallelBlock.type !== 'parallel') return undefined const parallelType = parallelBlock.data?.parallelType || 'count' const validParallelTypes = ['collection', 'count'] as const const validatedParallelType = validParallelTypes.includes(parallelType as any) ? parallelType : 'collection' const distribution = validatedParallelType === 'collection' ? parallelBlock.data?.collection || '' : undefined const count = parallelBlock.data?.count || 5 const batchSize = clampParallelBatchSize(parallelBlock.data?.batchSize) return { id: parallelBlockId, nodes: findChildNodes(parallelBlockId, blocks), distribution, count, parallelType: validatedParallelType, batchSize, enabled: parallelBlock.enabled, } } /** * Find all nodes that are children of this container (loop or parallel) * * @param containerId - ID of the container to find children for * @param blocks - Record of all blocks in the workflow * @returns Array of node IDs that are direct children of this container */ export function findChildNodes(containerId: string, blocks: Record): string[] { return Object.values(blocks) .filter((block) => block.data?.parentId === containerId) .map((block) => block.id) } /** * Find all descendant nodes, including children, grandchildren, etc. * * @param containerId - ID of the container to find descendants for * @param blocks - Record of all blocks in the workflow * @returns Array of node IDs that are descendants of this container */ export function findAllDescendantNodes( containerId: string, blocks: Record ): string[] { const descendants: string[] = [] const visited = new Set() const stack = [containerId] while (stack.length > 0) { const current = stack.pop()! if (visited.has(current)) continue visited.add(current) for (const block of Object.values(blocks)) { if (block.data?.parentId === current) { descendants.push(block.id) stack.push(block.id) } } } return descendants } /** * Checks if any ancestor container of a block is locked. * Unlike {@link isBlockProtected}, this ignores the block's own locked state. * * @param blockId - The ID of the block to check * @param blocks - Record of all blocks in the workflow * @returns True if any ancestor is locked */ export function isAncestorProtected(blockId: string, blocks: Record): boolean { return isWorkflowBlockAncestorLocked(blockId, blocks) } /** * Checks if a block is protected from editing/deletion. * A block is protected if it is locked or if any ancestor container is locked. * * @param blockId - The ID of the block to check * @param blocks - Record of all blocks in the workflow * @returns True if the block is protected */ export function isBlockProtected(blockId: string, blocks: Record): boolean { return isWorkflowBlockProtected(blockId, blocks) } /** * Builds a complete collection of loops from the UI blocks * * @param blocks - Record of all blocks in the workflow * @returns Record of Loop objects for execution engine */ export function generateLoopBlocks(blocks: Record): Record { const loops: Record = {} Object.entries(blocks) .filter(([_, block]) => block.type === 'loop') .forEach(([id, block]) => { const loop = convertLoopBlockToLoop(id, blocks) if (loop) { loops[id] = loop } }) return loops } /** * Builds a complete collection of parallel blocks from the UI blocks * * @param blocks - Record of all blocks in the workflow * @returns Record of Parallel objects for execution engine */ export function generateParallelBlocks( blocks: Record ): Record { const parallels: Record = {} Object.entries(blocks) .filter(([_, block]) => block.type === 'parallel') .forEach(([id, block]) => { const parallel = convertParallelBlockToParallel(id, blocks) if (parallel) { parallels[id] = parallel } }) return parallels }