Skip to content

Commit 2879ab6

Browse files
committed
improve sockets with reconnecting
1 parent afead54 commit 2879ab6

File tree

6 files changed

+275
-178
lines changed

6 files changed

+275
-178
lines changed

apps/sim/app/workspace/[workspaceId]/providers/workspace-permissions-provider.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
'use client'
22

33
import type React from 'react'
4-
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
4+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
55
import { createLogger } from '@sim/logger'
66
import { useQueryClient } from '@tanstack/react-query'
77
import { useParams } from 'next/navigation'
8+
import { useSocket } from '@/app/workspace/providers/socket-provider'
89
import {
910
useWorkspacePermissionsQuery,
1011
type WorkspacePermissions,
@@ -57,14 +58,35 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP
5758
const [hasShownOfflineNotification, setHasShownOfflineNotification] = useState(false)
5859
const hasOperationError = useOperationQueueStore((state) => state.hasOperationError)
5960
const addNotification = useNotificationStore((state) => state.addNotification)
61+
const removeNotification = useNotificationStore((state) => state.removeNotification)
62+
const { isReconnecting } = useSocket()
63+
const reconnectingNotificationIdRef = useRef<string | null>(null)
6064

6165
const isOfflineMode = hasOperationError
6266

67+
useEffect(() => {
68+
if (isReconnecting && !reconnectingNotificationIdRef.current && !isOfflineMode) {
69+
const id = addNotification({
70+
level: 'error',
71+
message: 'Reconnecting...',
72+
})
73+
reconnectingNotificationIdRef.current = id
74+
} else if (!isReconnecting && reconnectingNotificationIdRef.current) {
75+
removeNotification(reconnectingNotificationIdRef.current)
76+
reconnectingNotificationIdRef.current = null
77+
}
78+
}, [isReconnecting, isOfflineMode, addNotification, removeNotification])
79+
6380
useEffect(() => {
6481
if (!isOfflineMode || hasShownOfflineNotification) {
6582
return
6683
}
6784

85+
if (reconnectingNotificationIdRef.current) {
86+
removeNotification(reconnectingNotificationIdRef.current)
87+
reconnectingNotificationIdRef.current = null
88+
}
89+
6890
try {
6991
addNotification({
7092
level: 'error',
@@ -78,7 +100,7 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP
78100
} catch (error) {
79101
logger.error('Failed to add offline notification', { error })
80102
}
81-
}, [addNotification, hasShownOfflineNotification, isOfflineMode])
103+
}, [addNotification, removeNotification, hasShownOfflineNotification, isOfflineMode])
82104

83105
const {
84106
data: workspacePermissions,

apps/sim/app/workspace/providers/socket-provider.tsx

Lines changed: 72 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ interface SocketContextType {
4949
socket: Socket | null
5050
isConnected: boolean
5151
isConnecting: boolean
52+
isReconnecting: boolean
5253
authFailed: boolean
5354
currentWorkflowId: string | null
5455
currentSocketId: string | null
@@ -88,6 +89,7 @@ const SocketContext = createContext<SocketContextType>({
8889
socket: null,
8990
isConnected: false,
9091
isConnecting: false,
92+
isReconnecting: false,
9193
authFailed: false,
9294
currentWorkflowId: null,
9395
currentSocketId: null,
@@ -122,6 +124,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
122124
const [socket, setSocket] = useState<Socket | null>(null)
123125
const [isConnected, setIsConnected] = useState(false)
124126
const [isConnecting, setIsConnecting] = useState(false)
127+
const [isReconnecting, setIsReconnecting] = useState(false)
125128
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null)
126129
const [currentSocketId, setCurrentSocketId] = useState<string | null>(null)
127130
const [presenceUsers, setPresenceUsers] = useState<PresenceUser[]>([])
@@ -236,20 +239,19 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
236239
setCurrentWorkflowId(null)
237240
setPresenceUsers([])
238241

239-
logger.info('Socket disconnected', {
240-
reason,
241-
})
242+
// socket.active indicates if auto-reconnect will happen
243+
if (socketInstance.active) {
244+
setIsReconnecting(true)
245+
logger.info('Socket disconnected, will auto-reconnect', { reason })
246+
} else {
247+
setIsReconnecting(false)
248+
logger.info('Socket disconnected, no auto-reconnect', { reason })
249+
}
242250
})
243251

244-
socketInstance.on('connect_error', (error: any) => {
252+
socketInstance.on('connect_error', (error: Error) => {
245253
setIsConnecting(false)
246-
logger.error('Socket connection error:', {
247-
message: error.message,
248-
stack: error.stack,
249-
description: error.description,
250-
type: error.type,
251-
transport: error.transport,
252-
})
254+
logger.error('Socket connection error:', { message: error.message })
253255

254256
// Check if this is an authentication failure
255257
const isAuthError =
@@ -261,43 +263,41 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
261263
logger.warn(
262264
'Authentication failed - stopping reconnection attempts. User may need to refresh/re-login.'
263265
)
264-
// Stop reconnection attempts to prevent infinite loop
265266
socketInstance.disconnect()
266-
// Reset state to allow re-initialization when session is restored
267267
setSocket(null)
268268
setAuthFailed(true)
269+
setIsReconnecting(false)
269270
initializedRef.current = false
271+
} else if (socketInstance.active) {
272+
// Temporary failure, will auto-reconnect
273+
setIsReconnecting(true)
270274
}
271275
})
272276

273-
socketInstance.on('reconnect', (attemptNumber) => {
277+
// Reconnection events are on the Manager (socket.io), not the socket itself
278+
socketInstance.io.on('reconnect', (attemptNumber) => {
274279
setIsConnected(true)
280+
setIsReconnecting(false)
275281
setCurrentSocketId(socketInstance.id ?? null)
276282
logger.info('Socket reconnected successfully', {
277283
attemptNumber,
278284
socketId: socketInstance.id,
279285
transport: socketInstance.io.engine?.transport?.name,
280286
})
281-
// Note: join-workflow is handled by the useEffect watching isConnected
282287
})
283288

284-
socketInstance.on('reconnect_attempt', (attemptNumber) => {
285-
logger.info('Socket reconnection attempt (fresh token will be generated)', {
286-
attemptNumber,
287-
timestamp: new Date().toISOString(),
288-
})
289+
socketInstance.io.on('reconnect_attempt', (attemptNumber) => {
290+
setIsReconnecting(true)
291+
logger.info('Socket reconnection attempt', { attemptNumber })
289292
})
290293

291-
socketInstance.on('reconnect_error', (error: any) => {
292-
logger.error('Socket reconnection error:', {
293-
message: error.message,
294-
attemptNumber: error.attemptNumber,
295-
type: error.type,
296-
})
294+
socketInstance.io.on('reconnect_error', (error: Error) => {
295+
logger.error('Socket reconnection error:', { message: error.message })
297296
})
298297

299-
socketInstance.on('reconnect_failed', () => {
298+
socketInstance.io.on('reconnect_failed', () => {
300299
logger.error('Socket reconnection failed - all attempts exhausted')
300+
setIsReconnecting(false)
301301
setIsConnecting(false)
302302
})
303303

@@ -629,6 +629,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
629629

630630
if (commit) {
631631
socket.emit('workflow-operation', {
632+
workflowId: currentWorkflowId,
632633
operation,
633634
target,
634635
payload,
@@ -645,6 +646,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
645646
}
646647

647648
pendingPositionUpdates.current.set(blockId, {
649+
workflowId: currentWorkflowId,
648650
operation,
649651
target,
650652
payload,
@@ -666,6 +668,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
666668
}
667669
} else {
668670
socket.emit('workflow-operation', {
671+
workflowId: currentWorkflowId,
669672
operation,
670673
target,
671674
payload,
@@ -678,47 +681,53 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
678681
)
679682

680683
const emitSubblockUpdate = useCallback(
681-
(blockId: string, subblockId: string, value: any, operationId?: string) => {
682-
if (socket && currentWorkflowId) {
683-
socket.emit('subblock-update', {
684-
blockId,
685-
subblockId,
686-
value,
687-
timestamp: Date.now(),
688-
operationId,
689-
})
690-
} else {
691-
logger.warn('Cannot emit subblock update: no socket connection or workflow room', {
692-
hasSocket: !!socket,
693-
currentWorkflowId,
694-
blockId,
695-
subblockId,
696-
})
684+
(
685+
blockId: string,
686+
subblockId: string,
687+
value: any,
688+
operationId?: string,
689+
workflowId?: string
690+
) => {
691+
if (!workflowId) {
692+
logger.error('emitSubblockUpdate called without workflowId', { blockId, subblockId })
693+
return
694+
}
695+
if (!socket) {
696+
logger.warn('Cannot emit subblock update: no socket connection', { workflowId, blockId })
697+
return
697698
}
699+
socket.emit('subblock-update', {
700+
workflowId,
701+
blockId,
702+
subblockId,
703+
value,
704+
timestamp: Date.now(),
705+
operationId,
706+
})
698707
},
699-
[socket, currentWorkflowId]
708+
[socket]
700709
)
701710

702711
const emitVariableUpdate = useCallback(
703-
(variableId: string, field: string, value: any, operationId?: string) => {
704-
if (socket && currentWorkflowId) {
705-
socket.emit('variable-update', {
706-
variableId,
707-
field,
708-
value,
709-
timestamp: Date.now(),
710-
operationId,
711-
})
712-
} else {
713-
logger.warn('Cannot emit variable update: no socket connection or workflow room', {
714-
hasSocket: !!socket,
715-
currentWorkflowId,
716-
variableId,
717-
field,
718-
})
712+
(variableId: string, field: string, value: any, operationId?: string, workflowId?: string) => {
713+
if (!workflowId) {
714+
logger.error('emitVariableUpdate called without workflowId', { variableId, field })
715+
return
716+
}
717+
if (!socket) {
718+
logger.warn('Cannot emit variable update: no socket connection', { workflowId, variableId })
719+
return
719720
}
721+
socket.emit('variable-update', {
722+
workflowId,
723+
variableId,
724+
field,
725+
value,
726+
timestamp: Date.now(),
727+
operationId,
728+
})
720729
},
721-
[socket, currentWorkflowId]
730+
[socket]
722731
)
723732

724733
const lastCursorEmit = useRef(0)
@@ -794,6 +803,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
794803
socket,
795804
isConnected,
796805
isConnecting,
806+
isReconnecting,
797807
authFailed,
798808
currentWorkflowId,
799809
currentSocketId,
@@ -820,6 +830,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
820830
socket,
821831
isConnected,
822832
isConnecting,
833+
isReconnecting,
823834
authFailed,
824835
currentWorkflowId,
825836
currentSocketId,

0 commit comments

Comments
 (0)