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
192 lines
6.6 KiB
TypeScript
192 lines
6.6 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import type { Edge } from 'reactflow'
|
|
import { ApiClientError } from '@/lib/api/client/errors'
|
|
import { requestJson } from '@/lib/api/client/request'
|
|
import {
|
|
putWorkflowNormalizedStateContract,
|
|
type WorkflowStateContractInput,
|
|
} from '@/lib/api/contracts/workflows'
|
|
import { stripWorkflowDiffMarkers } from '@/lib/workflows/diff'
|
|
import { useVariablesStore } from '@/stores/variables/store'
|
|
import type { Variable } from '@/stores/variables/types'
|
|
import { useWorkflowRegistry } from '../workflows/registry/store'
|
|
import { useSubBlockStore } from '../workflows/subblock/store'
|
|
import { mergeSubblockState } from '../workflows/utils'
|
|
import { useWorkflowStore } from '../workflows/workflow/store'
|
|
import type { WorkflowState } from '../workflows/workflow/types'
|
|
import type { WorkflowDiffState } from './types'
|
|
|
|
const logger = createLogger('WorkflowDiffStore')
|
|
export const WORKFLOW_DIFF_SETTLED_EVENT = 'workflow-diff-settled'
|
|
|
|
export function cloneWorkflowState(state: WorkflowState): WorkflowState {
|
|
return {
|
|
...state,
|
|
blocks: structuredClone(state.blocks || {}),
|
|
edges: structuredClone(state.edges || []),
|
|
loops: structuredClone(state.loops || {}),
|
|
parallels: structuredClone(state.parallels || {}),
|
|
}
|
|
}
|
|
|
|
export function extractSubBlockValues(
|
|
workflowState: WorkflowState
|
|
): Record<string, Record<string, any>> {
|
|
const values: Record<string, Record<string, any>> = {}
|
|
Object.entries(workflowState.blocks || {}).forEach(([blockId, block]) => {
|
|
values[blockId] = {}
|
|
Object.entries(block.subBlocks || {}).forEach(([subBlockId, subBlock]) => {
|
|
values[blockId][subBlockId] = subBlock?.value ?? null
|
|
})
|
|
})
|
|
return values
|
|
}
|
|
|
|
export function applyWorkflowStateToStores(
|
|
workflowId: string,
|
|
workflowState: WorkflowState,
|
|
options?: { updateLastSaved?: boolean }
|
|
) {
|
|
logger.debug('[applyWorkflowStateToStores] Applying state', {
|
|
workflowId,
|
|
blockCount: Object.keys(workflowState.blocks || {}).length,
|
|
edgeCount: workflowState.edges?.length ?? 0,
|
|
edgePreview: workflowState.edges?.slice(0, 3).map((e) => `${e.source} -> ${e.target}`),
|
|
})
|
|
const workflowStore = useWorkflowStore.getState()
|
|
const cloned = cloneWorkflowState(workflowState)
|
|
logger.debug('[applyWorkflowStateToStores] Cloned state edges', {
|
|
clonedEdgeCount: cloned.edges?.length ?? 0,
|
|
})
|
|
workflowStore.replaceWorkflowState(cloned, options)
|
|
const subBlockValues = extractSubBlockValues(workflowState)
|
|
useSubBlockStore.getState().setWorkflowValues(workflowId, subBlockValues)
|
|
if (Object.hasOwn(workflowState, 'variables')) {
|
|
applyWorkflowVariablesToStore(workflowId, workflowState.variables)
|
|
}
|
|
|
|
// Verify what's in the store after apply
|
|
const afterState = workflowStore.getWorkflowState()
|
|
logger.info('[applyWorkflowStateToStores] Applied workflow state to stores', {
|
|
workflowId,
|
|
afterEdgeCount: afterState.edges?.length ?? 0,
|
|
})
|
|
}
|
|
|
|
export function applyWorkflowVariablesToStore(
|
|
workflowId: string,
|
|
variables?: WorkflowState['variables'] | null
|
|
) {
|
|
const stampedVariables: Record<string, Variable> = {}
|
|
|
|
Object.entries(variables || {}).forEach(([id, variable]) => {
|
|
if (!variable?.name) return
|
|
stampedVariables[id] = {
|
|
id: variable.id || id,
|
|
workflowId,
|
|
name: variable.name,
|
|
type: variable.type || 'plain',
|
|
value: Object.hasOwn(variable, 'value') ? variable.value : '',
|
|
}
|
|
})
|
|
|
|
useVariablesStore.setState((state) => ({
|
|
variables: {
|
|
...Object.fromEntries(
|
|
Object.entries(state.variables).filter(([, variable]) => variable.workflowId !== workflowId)
|
|
),
|
|
...stampedVariables,
|
|
},
|
|
}))
|
|
}
|
|
|
|
export function captureBaselineSnapshot(workflowId: string): WorkflowState {
|
|
const workflowStore = useWorkflowStore.getState()
|
|
const currentState = workflowStore.getWorkflowState()
|
|
const mergedBlocks = mergeSubblockState(currentState.blocks, workflowId)
|
|
|
|
return {
|
|
...cloneWorkflowState(currentState),
|
|
blocks: structuredClone(mergedBlocks),
|
|
}
|
|
}
|
|
|
|
export async function persistWorkflowStateToServer(
|
|
workflowId: string,
|
|
workflowState: WorkflowState
|
|
): Promise<boolean> {
|
|
try {
|
|
const cleanState = stripWorkflowDiffMarkers(cloneWorkflowState(workflowState))
|
|
const { dragStartPosition: _dropDragStart, ...stateToSave } = cleanState
|
|
|
|
type ContractEdgeInput = WorkflowStateContractInput['edges'][number]
|
|
|
|
// Mirror auto-layout-utils sanitization: schema rejects nullable
|
|
// sourceHandle/targetHandle (input type is `string | undefined`), but the
|
|
// store's Edge type carries `string | null | undefined`. Drop nulls before
|
|
// sending so the contract input parses cleanly.
|
|
const sanitizedEdges: ContractEdgeInput[] = (stateToSave.edges || []).map((edge: Edge) => {
|
|
const { sourceHandle, targetHandle, ...rest } = edge
|
|
const sanitized: ContractEdgeInput = { ...rest } as ContractEdgeInput
|
|
if (typeof sourceHandle === 'string' && sourceHandle.length > 0) {
|
|
sanitized.sourceHandle = sourceHandle
|
|
}
|
|
if (typeof targetHandle === 'string' && targetHandle.length > 0) {
|
|
sanitized.targetHandle = targetHandle
|
|
}
|
|
return sanitized
|
|
})
|
|
|
|
const cleanedWorkflowState: WorkflowStateContractInput = {
|
|
...stateToSave,
|
|
loops: stateToSave.loops || {},
|
|
parallels: stateToSave.parallels || {},
|
|
edges: sanitizedEdges,
|
|
lastSaved: Date.now(),
|
|
}
|
|
|
|
await requestJson(putWorkflowNormalizedStateContract, {
|
|
params: { id: workflowId },
|
|
body: cleanedWorkflowState,
|
|
})
|
|
|
|
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
|
if (activeWorkflowId === workflowId) {
|
|
useWorkflowStore.setState({ lastSaved: Date.now() })
|
|
}
|
|
|
|
return true
|
|
} catch (error) {
|
|
if (error instanceof ApiClientError) {
|
|
logger.error('Failed to persist workflow state after copilot edit', {
|
|
status: error.status,
|
|
message: error.message,
|
|
})
|
|
} else {
|
|
logger.error('Failed to persist workflow state after copilot edit', error)
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
export async function getLatestUserMessageId(): Promise<string | null> {
|
|
return null
|
|
}
|
|
|
|
export function createBatchedUpdater(set: (updates: Partial<WorkflowDiffState>) => void) {
|
|
let updateTimer: NodeJS.Timeout | null = null
|
|
const UPDATE_DEBOUNCE_MS = 16
|
|
let pendingUpdates: Partial<WorkflowDiffState> = {}
|
|
return (updates: Partial<WorkflowDiffState>) => {
|
|
Object.assign(pendingUpdates, updates)
|
|
if (updateTimer) {
|
|
clearTimeout(updateTimer)
|
|
}
|
|
updateTimer = setTimeout(() => {
|
|
set(pendingUpdates)
|
|
pendingUpdates = {}
|
|
updateTimer = null
|
|
}, UPDATE_DEBOUNCE_MS)
|
|
}
|
|
}
|