fix(sockets): smoother throttling + fix re-render bug (#541)

* fix(sockets movement): smoother throttling strategy and re-render bug fix

* works

---------

Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net>
This commit is contained in:
Vikhyath Mondreti
2025-06-24 20:53:07 -07:00
committed by GitHub
parent d084ecdcb1
commit 0a5bf5a821
2 changed files with 69 additions and 48 deletions

View File

@@ -99,7 +99,12 @@ const WorkflowContent = React.memo(() => {
const { workflows, activeWorkflowId, isLoading, setActiveWorkflow, createWorkflow } =
useWorkflowRegistry()
const { blocks, edges, updateNodeDimensions } = useWorkflowStore()
const {
blocks,
edges,
updateNodeDimensions,
updateBlockPosition: storeUpdateBlockPosition,
} = useWorkflowStore()
// Use collaborative operations for real-time sync
const currentWorkflow = useMemo(() => workflows[workflowId], [workflows, workflowId])
const workspaceId = currentWorkflow?.workspaceId
@@ -117,7 +122,7 @@ const WorkflowContent = React.memo(() => {
collaborativeAddBlock: addBlock,
collaborativeAddEdge: addEdge,
collaborativeRemoveEdge: removeEdge,
collaborativeUpdateBlockPosition: updateBlockPosition,
collaborativeUpdateBlockPosition,
collaborativeUpdateParentId: updateParentId,
isConnected,
currentWorkflowId,
@@ -186,12 +191,12 @@ const WorkflowContent = React.memo(() => {
nodeId,
newParentId,
getNodes,
updateBlockPosition,
collaborativeUpdateBlockPosition,
updateParentId,
() => resizeLoopNodes(getNodes, updateNodeDimensions, blocks)
)
},
[getNodes, updateBlockPosition, updateParentId, updateNodeDimensions, blocks]
[getNodes, collaborativeUpdateBlockPosition, updateParentId, updateNodeDimensions, blocks]
)
// Function to resize all loop nodes with improved hierarchy handling
@@ -256,13 +261,20 @@ const WorkflowContent = React.memo(() => {
[detectedOrientation]
)
applyAutoLayoutSmooth(blocks, edges, updateBlockPosition, fitView, resizeLoopNodesWrapper, {
...orientationConfig,
alignByLayer: true,
animationDuration: 500, // Smooth 500ms animation
isSidebarCollapsed,
handleOrientation: detectedOrientation, // Explicitly set the detected orientation
})
applyAutoLayoutSmooth(
blocks,
edges,
collaborativeUpdateBlockPosition,
fitView,
resizeLoopNodesWrapper,
{
...orientationConfig,
alignByLayer: true,
animationDuration: 500, // Smooth 500ms animation
isSidebarCollapsed,
handleOrientation: detectedOrientation, // Explicitly set the detected orientation
}
)
const orientationMessage =
detectedOrientation === 'vertical'
@@ -273,7 +285,14 @@ const WorkflowContent = React.memo(() => {
orientation: detectedOrientation,
blockCount: Object.keys(blocks).length,
})
}, [blocks, edges, updateBlockPosition, fitView, isSidebarCollapsed, resizeLoopNodesWrapper])
}, [
blocks,
edges,
collaborativeUpdateBlockPosition,
fitView,
isSidebarCollapsed,
resizeLoopNodesWrapper,
])
const debouncedAutoLayout = useCallback(() => {
const debounceTimer = setTimeout(() => {
@@ -841,7 +860,7 @@ const WorkflowContent = React.memo(() => {
return
}
// Always call setActiveWorkflow when workflow ID changes to ensure proper state
// Get current active workflow state
const { activeWorkflowId } = useWorkflowRegistry.getState()
if (activeWorkflowId !== currentId) {
@@ -955,18 +974,20 @@ const WorkflowContent = React.memo(() => {
return nodeArray
}, [blocks, activeBlockIds, pendingBlocks, isDebugModeEnabled, nestedSubflowErrors])
// Update nodes
// Update nodes - use store version to avoid collaborative feedback loops
const onNodesChange = useCallback(
(changes: any) => {
changes.forEach((change: any) => {
if (change.type === 'position' && change.position) {
const node = nodes.find((n) => n.id === change.id)
if (!node) return
updateBlockPosition(change.id, change.position)
// Use store version to avoid collaborative feedback loop
// React Flow position changes can be triggered by collaborative updates
storeUpdateBlockPosition(change.id, change.position)
}
})
},
[nodes, updateBlockPosition]
[nodes, storeUpdateBlockPosition]
)
// Effect to resize loops when nodes change (add/remove/position change)
@@ -1001,11 +1022,11 @@ const WorkflowContent = React.memo(() => {
const absolutePosition = getNodeAbsolutePositionWrapper(id)
// Update the node to remove parent reference and use absolute position
updateBlockPosition(id, absolutePosition)
collaborativeUpdateBlockPosition(id, absolutePosition)
updateParentId(id, '', 'parent')
}
})
}, [blocks, updateBlockPosition, updateParentId, getNodeAbsolutePositionWrapper])
}, [blocks, collaborativeUpdateBlockPosition, updateParentId, getNodeAbsolutePositionWrapper])
// Validate nested subflows whenever blocks change
useEffect(() => {
@@ -1108,6 +1129,9 @@ const WorkflowContent = React.memo(() => {
// Store currently dragged node ID
setDraggedNodeId(node.id)
// Emit collaborative position update during drag for smooth real-time movement
collaborativeUpdateBlockPosition(node.id, node.position)
// Get the current parent ID of the node being dragged
const currentParentId = blocks[node.id]?.data?.parentId || null
@@ -1254,6 +1278,7 @@ const WorkflowContent = React.memo(() => {
getNodeHierarchyWrapper,
getNodeAbsolutePositionWrapper,
getNodeDepthWrapper,
collaborativeUpdateBlockPosition,
]
)
@@ -1276,7 +1301,11 @@ const WorkflowContent = React.memo(() => {
})
document.body.style.cursor = ''
// Don't process if the node hasn't actually changed parent or is being moved within same parent
// Emit collaborative position update for the final position
// This ensures other users see the smooth final position
collaborativeUpdateBlockPosition(node.id, node.position)
// Don't process parent changes if the node hasn't actually changed parent or is being moved within same parent
if (potentialParentId === dragStartParentId) return
// Check if this is a starter block - starter blocks should never be in containers
@@ -1319,7 +1348,14 @@ const WorkflowContent = React.memo(() => {
setDraggedNodeId(null)
setPotentialParentId(null)
},
[getNodes, dragStartParentId, potentialParentId, updateNodeParent, getNodeHierarchyWrapper]
[
getNodes,
dragStartParentId,
potentialParentId,
updateNodeParent,
getNodeHierarchyWrapper,
collaborativeUpdateBlockPosition,
]
)
// Update onPaneClick to only handle edge selection

View File

@@ -323,8 +323,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
}
}, [socket, currentWorkflowId])
// Position update throttling at 60fps (16ms)
const THROTTLE_DELAY = 16 // 60fps standard
// Light throttling for position updates to ensure smooth collaborative movement
const positionUpdateTimeouts = useRef<Map<string, number>>(new Map())
const pendingPositionUpdates = useRef<Map<string, any>>(new Map())
@@ -333,13 +332,13 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
(operation: string, target: string, payload: any) => {
if (!socket || !currentWorkflowId) return
// Check if this is a position update that should be throttled
// Apply light throttling only to position updates for smooth collaborative experience
const isPositionUpdate = operation === 'update-position' && target === 'block'
if (isPositionUpdate && payload.id) {
const blockId = payload.id
// Store the latest position update for this block
// Store the latest position update
pendingPositionUpdates.current.set(blockId, {
operation,
target,
@@ -347,33 +346,19 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
timestamp: Date.now(),
})
// Check if we have an active interval for this block
const existingTimeout = positionUpdateTimeouts.current.get(blockId)
if (!existingTimeout) {
// No active interval - start emitting at regular intervals
const intervalId = window.setInterval(() => {
// Check if we already have a pending timeout for this block
if (!positionUpdateTimeouts.current.has(blockId)) {
// Schedule emission with light throttling (120fps = ~8ms)
const timeoutId = window.setTimeout(() => {
const latestUpdate = pendingPositionUpdates.current.get(blockId)
if (latestUpdate) {
socket.emit('workflow-operation', latestUpdate)
pendingPositionUpdates.current.delete(blockId)
} else {
// No more updates pending - stop the interval
clearInterval(intervalId)
positionUpdateTimeouts.current.delete(blockId)
}
}, THROTTLE_DELAY)
positionUpdateTimeouts.current.delete(blockId)
}, 8) // 120fps for smooth movement
positionUpdateTimeouts.current.set(blockId, intervalId)
// Set a cleanup timeout to stop the interval if no updates come in
setTimeout(() => {
if (positionUpdateTimeouts.current.get(blockId) === intervalId) {
clearInterval(intervalId)
positionUpdateTimeouts.current.delete(blockId)
pendingPositionUpdates.current.delete(blockId)
}
}, 50) // Stop interval after 50ms of no updates
positionUpdateTimeouts.current.set(blockId, timeoutId)
}
} else {
// For all non-position updates, emit immediately
@@ -411,14 +396,14 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
[socket, currentWorkflowId]
)
// Throttled cursor updates (lower priority than position updates)
// Minimal cursor throttling (reduced from 30fps to 120fps)
const lastCursorEmit = useRef(0)
const emitCursorUpdate = useCallback(
(cursor: { x: number; y: number }) => {
if (socket && currentWorkflowId) {
const now = performance.now()
// Throttle cursor updates to 30fps to reduce noise
if (now - lastCursorEmit.current >= 33) {
// Very light throttling at 120fps (8ms) to prevent excessive spam
if (now - lastCursorEmit.current >= 8) {
socket.emit('cursor-update', { cursor })
lastCursorEmit.current = now
}