Skip to content

Commit d666844

Browse files
committed
improvement(workflow): narrow zustand selectors and optimize log tree builds
- Add useIsCurrentWorkflowExecuting selector; swap broad useCurrentWorkflowExecution in action-bar/chat - Use useShallow for execution-state subset in useWorkflowExecution - Drop useCallback-wrapped object selectors in diff-controls and use-block-state (use primitive selectors) - Stabilize EMPTY_SUBBLOCK_VALUES constant for empty-workflow selector results in api-info-modal, mcp, tag-dropdown - O(n) Map lookups in buildEntryTree (was O(n^2) filter scans) - Skip expanded-paths reset when structured-output data is content-stable - Extract ReactFlow constants out of workflow.tsx into workflow-constants.ts
1 parent a9c12a2 commit d666844

15 files changed

Lines changed: 138 additions & 81 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/provide
77
import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
88
import { validateTriggerPaste } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
99
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
10-
import { useCurrentWorkflowExecution, useExecutionStore } from '@/stores/execution'
10+
import { useExecutionStore, useIsCurrentWorkflowExecuting } from '@/stores/execution'
1111
import { useNotificationStore } from '@/stores/notifications'
1212
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
1313
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -114,7 +114,7 @@ export const ActionBar = memo(
114114
)
115115

116116
const { activeWorkflowId } = useWorkflowRegistry()
117-
const { isExecuting } = useCurrentWorkflowExecution()
117+
const isExecuting = useIsCurrentWorkflowExecuting()
118118
const getLastExecutionSnapshot = useExecutionStore((s) => s.getLastExecutionSnapshot)
119119
const userPermissions = useUserPermissionsContext()
120120
const edges = useWorkflowStore((state) => state.edges)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowI
5555
import type { BlockLog, ExecutionResult } from '@/executor/types'
5656
import { useChatStore } from '@/stores/chat/store'
5757
import { getChatPosition } from '@/stores/chat/utils'
58-
import { useCurrentWorkflowExecution } from '@/stores/execution'
58+
import { useIsCurrentWorkflowExecuting } from '@/stores/execution'
5959
import { useOperationQueue } from '@/stores/operation-queue/store'
6060
import { useTerminalConsoleStore, useWorkflowConsoleEntries } from '@/stores/terminal'
6161
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -269,7 +269,7 @@ export function Chat() {
269269
const entries = useWorkflowConsoleEntries(
270270
hasConsoleHydrated && typeof activeWorkflowId === 'string' ? activeWorkflowId : undefined
271271
)
272-
const { isExecuting } = useCurrentWorkflowExecution()
272+
const isExecuting = useIsCurrentWorkflowExecuting()
273273
const { handleRunWorkflow, handleCancelExecution } = useWorkflowExecution()
274274
const { data: session } = useSession()
275275
const { addToQueue } = useOperationQueue()

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,12 @@ const NOTIFICATION_WIDTH = 240
1212
const NOTIFICATION_GAP = 16
1313

1414
export const DiffControls = memo(function DiffControls() {
15-
const { isDiffReady, hasActiveDiff, acceptChanges, rejectChanges } = useWorkflowDiffStore(
16-
useCallback(
17-
(state) => ({
18-
isDiffReady: state.isDiffReady,
19-
hasActiveDiff: state.hasActiveDiff,
20-
acceptChanges: state.acceptChanges,
21-
rejectChanges: state.rejectChanges,
22-
}),
23-
[]
24-
)
25-
)
15+
const isDiffReady = useWorkflowDiffStore((state) => state.isDiffReady)
16+
const hasActiveDiff = useWorkflowDiffStore((state) => state.hasActiveDiff)
17+
const acceptChanges = useWorkflowDiffStore((state) => state.acceptChanges)
18+
const rejectChanges = useWorkflowDiffStore((state) => state.rejectChanges)
2619

27-
const { activeWorkflowId } = useWorkflowRegistry(
28-
useCallback((state) => ({ activeWorkflowId: state.activeWorkflowId }), [])
29-
)
20+
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
3021

3122
const allNotifications = useNotificationStore((state) => state.notifications)
3223
const hasVisibleNotifications = useMemo(() => {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/api-info-modal.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
2828

2929
type NormalizedField = InputFormatField & { name: string }
3030

31+
const EMPTY_SUBBLOCK_VALUES: Record<string, Record<string, unknown>> = {}
32+
3133
interface ApiInfoModalProps {
3234
open: boolean
3335
onOpenChange: (open: boolean) => void
@@ -38,8 +40,8 @@ export function ApiInfoModal({ open, onOpenChange, workflowId }: ApiInfoModalPro
3840
const { workspaceId } = useParams<{ workspaceId: string }>()
3941
const blocks = useWorkflowStore((state) => state.blocks)
4042
const setValue = useSubBlockStore((state) => state.setValue)
41-
const subBlockValues = useSubBlockStore((state) =>
42-
workflowId ? (state.workflowValues[workflowId] ?? {}) : {}
43+
const subBlockValues = useSubBlockStore(
44+
(state) => (workflowId ? state.workflowValues[workflowId] : undefined) ?? EMPTY_SUBBLOCK_VALUES
4345
)
4446

4547
const { data: workflows = {} } = useWorkflowMap(workspaceId)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ const logger = createLogger('McpToolDeploy')
3535
/** InputFormatField with guaranteed name (after normalization) */
3636
type NormalizedField = InputFormatField & { name: string }
3737

38+
const EMPTY_SUBBLOCK_VALUES: Record<string, Record<string, unknown>> = {}
39+
3840
interface McpDeployProps {
3941
workflowId: string
4042
workflowName: string
@@ -120,8 +122,8 @@ export function McpDeploy({
120122
return null
121123
}, [blocks])
122124

123-
const subBlockValues = useSubBlockStore((state) =>
124-
workflowId ? (state.workflowValues[workflowId] ?? {}) : {}
125+
const subBlockValues = useSubBlockStore(
126+
(state) => (workflowId ? state.workflowValues[workflowId] : undefined) ?? EMPTY_SUBBLOCK_VALUES
125127
)
126128

127129
const inputFormat = useMemo((): NormalizedField[] => {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ const TAG_PREFIXES = {
167167
VARIABLE: 'variable.',
168168
} as const
169169

170+
const EMPTY_SUBBLOCK_VALUES: Record<string, Record<string, unknown>> = {}
171+
170172
/**
171173
* Ensures the root tag is present in the tags array
172174
*/
@@ -980,8 +982,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
980982
return new Set<string>(rawAccessiblePrefixes)
981983
}, [rawAccessiblePrefixes])
982984

983-
const workflowSubBlockValues = useSubBlockStore((state) =>
984-
workflowId ? (state.workflowValues[workflowId] ?? {}) : {}
985+
const workflowSubBlockValues = useSubBlockStore(
986+
(state) => (workflowId ? state.workflowValues[workflowId] : undefined) ?? EMPTY_SUBBLOCK_VALUES
985987
)
986988

987989
const getMergedSubBlocks = useCallback(

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/components/structured-output.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,7 @@ export const StructuredOutput = memo(function StructuredOutput({
682682
computeInitialPaths(data, isError)
683683
)
684684
const prevDataRef = useRef(data)
685+
const prevDataJsonRef = useRef<string>('')
685686
const prevIsErrorRef = useRef(isError)
686687
const internalRef = useRef<HTMLDivElement>(null)
687688
const listRef = useListRef(null)
@@ -712,10 +713,21 @@ export const StructuredOutput = memo(function StructuredOutput({
712713

713714
// Reset expanded paths when data changes
714715
useEffect(() => {
715-
if (prevDataRef.current !== data || prevIsErrorRef.current !== isError) {
716+
if (prevIsErrorRef.current !== isError) {
716717
prevDataRef.current = data
717718
prevIsErrorRef.current = isError
719+
prevDataJsonRef.current = JSON.stringify(data)
718720
setExpandedPaths(computeInitialPaths(data, isError))
721+
return
722+
}
723+
724+
if (prevDataRef.current !== data) {
725+
const newJson = JSON.stringify(data)
726+
if (prevDataJsonRef.current !== newJson) {
727+
prevDataJsonRef.current = newJson
728+
setExpandedPaths(computeInitialPaths(data, isError))
729+
}
730+
prevDataRef.current = data
719731
}
720732
}, [data, isError])
721733

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -366,14 +366,23 @@ export function buildEntryTree(entries: ConsoleEntry[], idPrefix = ''): EntryNod
366366
}
367367
}
368368

369+
const nestedByContainerId = new Map<string, ConsoleEntry[]>()
370+
for (const e of nestedIterationEntries) {
371+
const parent = e.parentIterations?.[0]
372+
if (!parent) continue
373+
const list = nestedByContainerId.get(parent.iterationContainerId)
374+
if (list) {
375+
list.push(e)
376+
} else {
377+
nestedByContainerId.set(parent.iterationContainerId, [e])
378+
}
379+
}
380+
369381
const subflowNodes: EntryNode[] = []
370382
for (const subflowGroup of subflowGroups.values()) {
371383
const { iterationType, iterationContainerId, groups: iterationGroups } = subflowGroup
372384

373-
const nestedForThisSubflow = nestedIterationEntries.filter((e) => {
374-
const parent = e.parentIterations?.[0]
375-
return parent && parent.iterationContainerId === iterationContainerId
376-
})
385+
const nestedForThisSubflow = nestedByContainerId.get(iterationContainerId) ?? []
377386

378387
const allDirectBlocks = iterationGroups.flatMap((g) => g.blocks)
379388
const allRelevantBlocks = [...allDirectBlocks, ...nestedForThisSubflow]
@@ -406,12 +415,21 @@ export function buildEntryTree(entries: ConsoleEntry[], idPrefix = ''): EntryNod
406415
iterationContainerId,
407416
}
408417

418+
const nestedByIteration = new Map<number, ConsoleEntry[]>()
419+
for (const e of nestedForThisSubflow) {
420+
const iterNum = e.parentIterations?.[0]?.iterationCurrent
421+
if (iterNum === undefined) continue
422+
const list = nestedByIteration.get(iterNum)
423+
if (list) {
424+
list.push(e)
425+
} else {
426+
nestedByIteration.set(iterNum, [e])
427+
}
428+
}
429+
409430
const iterationNodes: EntryNode[] = iterationGroups
410431
.map((iterGroup): EntryNode | null => {
411-
const matchingNestedEntries = nestedForThisSubflow.filter((e) => {
412-
const parent = e.parentIterations?.[0]
413-
return parent?.iterationCurrent === iterGroup.iterationCurrent
414-
})
432+
const matchingNestedEntries = nestedByIteration.get(iterGroup.iterationCurrent) ?? []
415433

416434
const strippedNestedEntries: ConsoleEntry[] = matchingNestedEntries.map((e) => ({
417435
...e,

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-block-state.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useCallback } from 'react'
21
import type { DiffStatus } from '@/lib/workflows/diff/types'
32
import { hasDiffStatus } from '@/lib/workflows/diff/types'
43
import { useIsBlockActive } from '@/stores/execution'
@@ -54,15 +53,8 @@ export function useBlockState(
5453
: undefined
5554

5655
// Get diff-related data
57-
const { diffAnalysis, isShowingDiff } = useWorkflowDiffStore(
58-
useCallback(
59-
(state) => ({
60-
diffAnalysis: state.diffAnalysis,
61-
isShowingDiff: state.isShowingDiff,
62-
}),
63-
[]
64-
)
65-
)
56+
const diffAnalysis = useWorkflowDiffStore((state) => state.diffAnalysis)
57+
const isShowingDiff = useWorkflowDiffStore((state) => state.isShowingDiff)
6658

6759
const isDeletedBlock = !isShowingDiff && diffAnalysis?.deleted_blocks?.includes(blockId)
6860

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { subscriptionKeys } from '@/hooks/queries/subscription'
3838
import { getWorkflows } from '@/hooks/queries/utils/workflow-cache'
3939
import { isExecutionStreamHttpError, useExecutionStream } from '@/hooks/use-execution-stream'
4040
import { WorkflowValidationError } from '@/serializer'
41-
import { useCurrentWorkflowExecution, useExecutionStore } from '@/stores/execution'
41+
import { defaultWorkflowExecutionState, useExecutionStore } from '@/stores/execution'
4242
import { useNotificationStore } from '@/stores/notifications'
4343
import {
4444
clearExecutionPointer,
@@ -136,8 +136,20 @@ export function useWorkflowExecution() {
136136
variables: s.variables,
137137
}))
138138
)
139-
const { isExecuting, isDebugging, pendingBlocks, executor, debugContext } =
140-
useCurrentWorkflowExecution()
139+
const { isExecuting, isDebugging, pendingBlocks, executor, debugContext } = useExecutionStore(
140+
useShallow((state) => {
141+
const exec = activeWorkflowId
142+
? (state.workflowExecutions.get(activeWorkflowId) ?? defaultWorkflowExecutionState)
143+
: defaultWorkflowExecutionState
144+
return {
145+
isExecuting: exec.isExecuting,
146+
isDebugging: exec.isDebugging,
147+
pendingBlocks: exec.pendingBlocks,
148+
executor: exec.executor,
149+
debugContext: exec.debugContext,
150+
}
151+
})
152+
)
141153
const setCurrentExecutionId = useExecutionStore((s) => s.setCurrentExecutionId)
142154
const getCurrentExecutionId = useExecutionStore((s) => s.getCurrentExecutionId)
143155
const rawSetIsExecuting = useExecutionStore((s) => s.setIsExecuting)

0 commit comments

Comments
 (0)