mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
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:
committed by
GitHub
parent
d084ecdcb1
commit
0a5bf5a821
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user