Skip to content

Commit 0a5bf5a

Browse files
icecrasher321Vikhyath Mondreti
andauthored
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>
1 parent d084ecd commit 0a5bf5a

2 files changed

Lines changed: 69 additions & 48 deletions

File tree

apps/sim/app/w/[id]/workflow.tsx

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,12 @@ const WorkflowContent = React.memo(() => {
9999
const { workflows, activeWorkflowId, isLoading, setActiveWorkflow, createWorkflow } =
100100
useWorkflowRegistry()
101101

102-
const { blocks, edges, updateNodeDimensions } = useWorkflowStore()
102+
const {
103+
blocks,
104+
edges,
105+
updateNodeDimensions,
106+
updateBlockPosition: storeUpdateBlockPosition,
107+
} = useWorkflowStore()
103108
// Use collaborative operations for real-time sync
104109
const currentWorkflow = useMemo(() => workflows[workflowId], [workflows, workflowId])
105110
const workspaceId = currentWorkflow?.workspaceId
@@ -117,7 +122,7 @@ const WorkflowContent = React.memo(() => {
117122
collaborativeAddBlock: addBlock,
118123
collaborativeAddEdge: addEdge,
119124
collaborativeRemoveEdge: removeEdge,
120-
collaborativeUpdateBlockPosition: updateBlockPosition,
125+
collaborativeUpdateBlockPosition,
121126
collaborativeUpdateParentId: updateParentId,
122127
isConnected,
123128
currentWorkflowId,
@@ -186,12 +191,12 @@ const WorkflowContent = React.memo(() => {
186191
nodeId,
187192
newParentId,
188193
getNodes,
189-
updateBlockPosition,
194+
collaborativeUpdateBlockPosition,
190195
updateParentId,
191196
() => resizeLoopNodes(getNodes, updateNodeDimensions, blocks)
192197
)
193198
},
194-
[getNodes, updateBlockPosition, updateParentId, updateNodeDimensions, blocks]
199+
[getNodes, collaborativeUpdateBlockPosition, updateParentId, updateNodeDimensions, blocks]
195200
)
196201

197202
// Function to resize all loop nodes with improved hierarchy handling
@@ -256,13 +261,20 @@ const WorkflowContent = React.memo(() => {
256261
[detectedOrientation]
257262
)
258263

259-
applyAutoLayoutSmooth(blocks, edges, updateBlockPosition, fitView, resizeLoopNodesWrapper, {
260-
...orientationConfig,
261-
alignByLayer: true,
262-
animationDuration: 500, // Smooth 500ms animation
263-
isSidebarCollapsed,
264-
handleOrientation: detectedOrientation, // Explicitly set the detected orientation
265-
})
264+
applyAutoLayoutSmooth(
265+
blocks,
266+
edges,
267+
collaborativeUpdateBlockPosition,
268+
fitView,
269+
resizeLoopNodesWrapper,
270+
{
271+
...orientationConfig,
272+
alignByLayer: true,
273+
animationDuration: 500, // Smooth 500ms animation
274+
isSidebarCollapsed,
275+
handleOrientation: detectedOrientation, // Explicitly set the detected orientation
276+
}
277+
)
266278

267279
const orientationMessage =
268280
detectedOrientation === 'vertical'
@@ -273,7 +285,14 @@ const WorkflowContent = React.memo(() => {
273285
orientation: detectedOrientation,
274286
blockCount: Object.keys(blocks).length,
275287
})
276-
}, [blocks, edges, updateBlockPosition, fitView, isSidebarCollapsed, resizeLoopNodesWrapper])
288+
}, [
289+
blocks,
290+
edges,
291+
collaborativeUpdateBlockPosition,
292+
fitView,
293+
isSidebarCollapsed,
294+
resizeLoopNodesWrapper,
295+
])
277296

278297
const debouncedAutoLayout = useCallback(() => {
279298
const debounceTimer = setTimeout(() => {
@@ -841,7 +860,7 @@ const WorkflowContent = React.memo(() => {
841860
return
842861
}
843862

844-
// Always call setActiveWorkflow when workflow ID changes to ensure proper state
863+
// Get current active workflow state
845864
const { activeWorkflowId } = useWorkflowRegistry.getState()
846865

847866
if (activeWorkflowId !== currentId) {
@@ -955,18 +974,20 @@ const WorkflowContent = React.memo(() => {
955974
return nodeArray
956975
}, [blocks, activeBlockIds, pendingBlocks, isDebugModeEnabled, nestedSubflowErrors])
957976

958-
// Update nodes
977+
// Update nodes - use store version to avoid collaborative feedback loops
959978
const onNodesChange = useCallback(
960979
(changes: any) => {
961980
changes.forEach((change: any) => {
962981
if (change.type === 'position' && change.position) {
963982
const node = nodes.find((n) => n.id === change.id)
964983
if (!node) return
965-
updateBlockPosition(change.id, change.position)
984+
// Use store version to avoid collaborative feedback loop
985+
// React Flow position changes can be triggered by collaborative updates
986+
storeUpdateBlockPosition(change.id, change.position)
966987
}
967988
})
968989
},
969-
[nodes, updateBlockPosition]
990+
[nodes, storeUpdateBlockPosition]
970991
)
971992

972993
// Effect to resize loops when nodes change (add/remove/position change)
@@ -1001,11 +1022,11 @@ const WorkflowContent = React.memo(() => {
10011022
const absolutePosition = getNodeAbsolutePositionWrapper(id)
10021023

10031024
// Update the node to remove parent reference and use absolute position
1004-
updateBlockPosition(id, absolutePosition)
1025+
collaborativeUpdateBlockPosition(id, absolutePosition)
10051026
updateParentId(id, '', 'parent')
10061027
}
10071028
})
1008-
}, [blocks, updateBlockPosition, updateParentId, getNodeAbsolutePositionWrapper])
1029+
}, [blocks, collaborativeUpdateBlockPosition, updateParentId, getNodeAbsolutePositionWrapper])
10091030

10101031
// Validate nested subflows whenever blocks change
10111032
useEffect(() => {
@@ -1108,6 +1129,9 @@ const WorkflowContent = React.memo(() => {
11081129
// Store currently dragged node ID
11091130
setDraggedNodeId(node.id)
11101131

1132+
// Emit collaborative position update during drag for smooth real-time movement
1133+
collaborativeUpdateBlockPosition(node.id, node.position)
1134+
11111135
// Get the current parent ID of the node being dragged
11121136
const currentParentId = blocks[node.id]?.data?.parentId || null
11131137

@@ -1254,6 +1278,7 @@ const WorkflowContent = React.memo(() => {
12541278
getNodeHierarchyWrapper,
12551279
getNodeAbsolutePositionWrapper,
12561280
getNodeDepthWrapper,
1281+
collaborativeUpdateBlockPosition,
12571282
]
12581283
)
12591284

@@ -1276,7 +1301,11 @@ const WorkflowContent = React.memo(() => {
12761301
})
12771302
document.body.style.cursor = ''
12781303

1279-
// Don't process if the node hasn't actually changed parent or is being moved within same parent
1304+
// Emit collaborative position update for the final position
1305+
// This ensures other users see the smooth final position
1306+
collaborativeUpdateBlockPosition(node.id, node.position)
1307+
1308+
// Don't process parent changes if the node hasn't actually changed parent or is being moved within same parent
12801309
if (potentialParentId === dragStartParentId) return
12811310

12821311
// Check if this is a starter block - starter blocks should never be in containers
@@ -1319,7 +1348,14 @@ const WorkflowContent = React.memo(() => {
13191348
setDraggedNodeId(null)
13201349
setPotentialParentId(null)
13211350
},
1322-
[getNodes, dragStartParentId, potentialParentId, updateNodeParent, getNodeHierarchyWrapper]
1351+
[
1352+
getNodes,
1353+
dragStartParentId,
1354+
potentialParentId,
1355+
updateNodeParent,
1356+
getNodeHierarchyWrapper,
1357+
collaborativeUpdateBlockPosition,
1358+
]
13231359
)
13241360

13251361
// Update onPaneClick to only handle edge selection

apps/sim/contexts/socket-context.tsx

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
323323
}
324324
}, [socket, currentWorkflowId])
325325

326-
// Position update throttling at 60fps (16ms)
327-
const THROTTLE_DELAY = 16 // 60fps standard
326+
// Light throttling for position updates to ensure smooth collaborative movement
328327
const positionUpdateTimeouts = useRef<Map<string, number>>(new Map())
329328
const pendingPositionUpdates = useRef<Map<string, any>>(new Map())
330329

@@ -333,47 +332,33 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
333332
(operation: string, target: string, payload: any) => {
334333
if (!socket || !currentWorkflowId) return
335334

336-
// Check if this is a position update that should be throttled
335+
// Apply light throttling only to position updates for smooth collaborative experience
337336
const isPositionUpdate = operation === 'update-position' && target === 'block'
338337

339338
if (isPositionUpdate && payload.id) {
340339
const blockId = payload.id
341340

342-
// Store the latest position update for this block
341+
// Store the latest position update
343342
pendingPositionUpdates.current.set(blockId, {
344343
operation,
345344
target,
346345
payload,
347346
timestamp: Date.now(),
348347
})
349348

350-
// Check if we have an active interval for this block
351-
const existingTimeout = positionUpdateTimeouts.current.get(blockId)
352-
353-
if (!existingTimeout) {
354-
// No active interval - start emitting at regular intervals
355-
const intervalId = window.setInterval(() => {
349+
// Check if we already have a pending timeout for this block
350+
if (!positionUpdateTimeouts.current.has(blockId)) {
351+
// Schedule emission with light throttling (120fps = ~8ms)
352+
const timeoutId = window.setTimeout(() => {
356353
const latestUpdate = pendingPositionUpdates.current.get(blockId)
357354
if (latestUpdate) {
358355
socket.emit('workflow-operation', latestUpdate)
359356
pendingPositionUpdates.current.delete(blockId)
360-
} else {
361-
// No more updates pending - stop the interval
362-
clearInterval(intervalId)
363-
positionUpdateTimeouts.current.delete(blockId)
364357
}
365-
}, THROTTLE_DELAY)
366-
367-
positionUpdateTimeouts.current.set(blockId, intervalId)
358+
positionUpdateTimeouts.current.delete(blockId)
359+
}, 8) // 120fps for smooth movement
368360

369-
// Set a cleanup timeout to stop the interval if no updates come in
370-
setTimeout(() => {
371-
if (positionUpdateTimeouts.current.get(blockId) === intervalId) {
372-
clearInterval(intervalId)
373-
positionUpdateTimeouts.current.delete(blockId)
374-
pendingPositionUpdates.current.delete(blockId)
375-
}
376-
}, 50) // Stop interval after 50ms of no updates
361+
positionUpdateTimeouts.current.set(blockId, timeoutId)
377362
}
378363
} else {
379364
// For all non-position updates, emit immediately
@@ -411,14 +396,14 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
411396
[socket, currentWorkflowId]
412397
)
413398

414-
// Throttled cursor updates (lower priority than position updates)
399+
// Minimal cursor throttling (reduced from 30fps to 120fps)
415400
const lastCursorEmit = useRef(0)
416401
const emitCursorUpdate = useCallback(
417402
(cursor: { x: number; y: number }) => {
418403
if (socket && currentWorkflowId) {
419404
const now = performance.now()
420-
// Throttle cursor updates to 30fps to reduce noise
421-
if (now - lastCursorEmit.current >= 33) {
405+
// Very light throttling at 120fps (8ms) to prevent excessive spam
406+
if (now - lastCursorEmit.current >= 8) {
422407
socket.emit('cursor-update', { cursor })
423408
lastCursorEmit.current = now
424409
}

0 commit comments

Comments
 (0)