d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
217 lines
6.9 KiB
TypeScript
217 lines
6.9 KiB
TypeScript
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<string, BlockState>
|
|
): 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<string, BlockState>
|
|
): 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, BlockState>): 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, BlockState>
|
|
): string[] {
|
|
const descendants: string[] = []
|
|
const visited = new Set<string>()
|
|
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<string, BlockState>): 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<string, BlockState>): 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<string, BlockState>): Record<string, Loop> {
|
|
const loops: Record<string, Loop> = {}
|
|
|
|
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<string, BlockState>
|
|
): Record<string, Parallel> {
|
|
const parallels: Record<string, Parallel> = {}
|
|
|
|
Object.entries(blocks)
|
|
.filter(([_, block]) => block.type === 'parallel')
|
|
.forEach(([id, block]) => {
|
|
const parallel = convertParallelBlockToParallel(id, blocks)
|
|
if (parallel) {
|
|
parallels[id] = parallel
|
|
}
|
|
})
|
|
|
|
return parallels
|
|
}
|