Skip to content

Commit 1d0e118

Browse files
authored
fix(socket): sync deploy button state across collaborators (#4206)
* fix(socket): sync deploy button state across collaborators Broadcast workflow-deployed events via socket so all connected users invalidate their deployment query cache when any user deploys, undeploys, activates a version, or triggers a deploy through chat/form endpoints. * fix(socket): check response status on deployment notification Log a warning when the socket server returns a non-2xx status for deployment notifications, matching the pattern in lifecycle.ts. * improvement(config): consolidate socket server URL into getSocketServerUrl/getSocketUrl Replace all inline `env.SOCKET_SERVER_URL || 'http://localhost:3002'` and `getEnv('NEXT_PUBLIC_SOCKET_URL') || 'http://localhost:3002'` with centralized utility functions in urls.ts, matching the getBaseUrl() pattern. * improvement(config): consolidate Ollama URL and CSP socket/Ollama hardcodes Add getOllamaUrl() to urls.ts and replace inline env.OLLAMA_URL fallbacks in the provider and API route. Update CSP to use getSocketUrl(), getOllamaUrl(), and a local toWebSocketUrl() helper instead of hardcoded localhost strings. * lint * fix(tests): add missing mocks for new URL utility exports Update lifecycle, async execute, and chat manage test mocks to include getSocketServerUrl, getOllamaUrl, and notifySocketDeploymentChanged. * fix(csp): remove urls.ts import to fix next.config.ts build CSP is loaded by next.config.ts which transpiles outside the @/ alias context. Use local constants instead of importing from urls.ts. * fix(queries): invalidate chat and form status on deployment change Add chatStatus and formStatus to invalidateDeploymentQueries so all deployment-related queries refresh when any user deploys or undeploys.
1 parent c06361b commit 1d0e118

File tree

23 files changed

+277
-119
lines changed

23 files changed

+277
-119
lines changed

apps/sim/app/api/chat/manage/[id]/route.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ vi.mock('@/lib/workflows/persistence/utils', () => ({
8989
}))
9090
vi.mock('@/lib/workflows/orchestration', () => ({
9191
performChatUndeploy: mockPerformChatUndeploy,
92+
notifySocketDeploymentChanged: vi.fn().mockResolvedValue(undefined),
9293
}))
9394
vi.mock('drizzle-orm', () => ({
9495
and: vi.fn((...conditions: unknown[]) => ({ type: 'and', conditions })),

apps/sim/app/api/chat/manage/[id]/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { getSession } from '@/lib/auth'
99
import { isDev } from '@/lib/core/config/feature-flags'
1010
import { encryptSecret } from '@/lib/core/security/encryption'
1111
import { getEmailDomain } from '@/lib/core/utils/urls'
12-
import { performChatUndeploy } from '@/lib/workflows/orchestration'
12+
import { notifySocketDeploymentChanged, performChatUndeploy } from '@/lib/workflows/orchestration'
1313
import { deployWorkflow } from '@/lib/workflows/persistence/utils'
1414
import { checkChatAccess } from '@/app/api/chat/utils'
1515
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
@@ -155,6 +155,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
155155
logger.info(
156156
`Redeployed workflow ${existingChat[0].workflowId} for chat update (v${deployResult.version})`
157157
)
158+
await notifySocketDeploymentChanged(existingChat[0].workflowId)
158159
}
159160

160161
let encryptedPassword

apps/sim/app/api/form/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { isDev } from '@/lib/core/config/feature-flags'
1010
import { encryptSecret } from '@/lib/core/security/encryption'
1111
import { getEmailDomain } from '@/lib/core/utils/urls'
1212
import { generateId } from '@/lib/core/utils/uuid'
13+
import { notifySocketDeploymentChanged } from '@/lib/workflows/orchestration'
1314
import { deployWorkflow } from '@/lib/workflows/persistence/utils'
1415
import {
1516
checkWorkflowAccessForFormCreation,
@@ -152,6 +153,8 @@ export async function POST(request: NextRequest) {
152153
`${workflowRecord.isDeployed ? 'Redeployed' : 'Auto-deployed'} workflow ${workflowId} for form (v${result.version})`
153154
)
154155

156+
await notifySocketDeploymentChanged(workflowId)
157+
155158
let encryptedPassword = null
156159
if (authType === 'password' && password) {
157160
const { encrypted } = await encryptSecret(password)

apps/sim/app/api/providers/ollama/models/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
3-
import { env } from '@/lib/core/config/env'
3+
import { getOllamaUrl } from '@/lib/core/utils/urls'
44
import type { ModelsObject } from '@/providers/ollama/types'
55
import { filterBlacklistedModels, isProviderBlacklisted } from '@/providers/utils'
66

77
const logger = createLogger('OllamaModelsAPI')
8-
const OLLAMA_HOST = env.OLLAMA_URL || 'http://localhost:11434'
8+
const OLLAMA_HOST = getOllamaUrl()
99

1010
/**
1111
* Get available Ollama models

apps/sim/app/api/workflows/[id]/execute/route.async.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ vi.mock('@/lib/core/utils/request', () => ({
5353

5454
vi.mock('@/lib/core/utils/urls', () => ({
5555
getBaseUrl: vi.fn().mockReturnValue('http://localhost:3000'),
56+
getOllamaUrl: vi.fn().mockReturnValue('http://localhost:11434'),
5657
}))
5758

5859
vi.mock('@/lib/execution/call-chain', () => ({

apps/sim/app/api/workflows/[id]/state/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { z } from 'zod'
77
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
88
import { env } from '@/lib/core/config/env'
99
import { generateRequestId } from '@/lib/core/utils/request'
10+
import { getSocketServerUrl } from '@/lib/core/utils/urls'
1011
import { extractAndPersistCustomTools } from '@/lib/workflows/persistence/custom-tools-persistence'
1112
import {
1213
loadWorkflowFromNormalizedTables,
@@ -305,8 +306,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
305306
logger.info(`[${requestId}] Successfully saved workflow ${workflowId} state in ${elapsed}ms`)
306307

307308
try {
308-
const socketUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002'
309-
const notifyResponse = await fetch(`${socketUrl}/api/workflow-updated`, {
309+
const notifyResponse = await fetch(`${getSocketServerUrl()}/api/workflow-updated`, {
310310
method: 'POST',
311311
headers: {
312312
'Content-Type': 'application/json',

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import { createLogger } from '@sim/logger'
1414
import { useParams } from 'next/navigation'
1515
import type { Socket } from 'socket.io-client'
16-
import { getEnv } from '@/lib/core/config/env'
16+
import { getSocketUrl } from '@/lib/core/utils/urls'
1717
import { generateId } from '@/lib/core/utils/uuid'
1818
import {
1919
type SocketJoinCommand,
@@ -102,6 +102,7 @@ interface SocketContextType {
102102
onWorkflowDeleted: (handler: (data: any) => void) => void
103103
onWorkflowReverted: (handler: (data: any) => void) => void
104104
onWorkflowUpdated: (handler: (data: any) => void) => void
105+
onWorkflowDeployed: (handler: (data: any) => void) => void
105106
onOperationConfirmed: (handler: (data: any) => void) => void
106107
onOperationFailed: (handler: (data: any) => void) => void
107108
}
@@ -132,6 +133,7 @@ const SocketContext = createContext<SocketContextType>({
132133
onWorkflowDeleted: () => {},
133134
onWorkflowReverted: () => {},
134135
onWorkflowUpdated: () => {},
136+
onWorkflowDeployed: () => {},
135137
onOperationConfirmed: () => {},
136138
onOperationFailed: () => {},
137139
})
@@ -176,6 +178,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
176178
workflowDeleted?: (data: any) => void
177179
workflowReverted?: (data: any) => void
178180
workflowUpdated?: (data: any) => void
181+
workflowDeployed?: (data: any) => void
179182
operationConfirmed?: (data: any) => void
180183
operationFailed?: (data: any) => void
181184
}>({})
@@ -337,7 +340,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
337340
const initializeSocket = async () => {
338341
try {
339342
const { io } = await import('socket.io-client')
340-
const socketUrl = getEnv('NEXT_PUBLIC_SOCKET_URL') || 'http://localhost:3002'
343+
const socketUrl = getSocketUrl()
341344

342345
logger.info('Attempting to connect to Socket.IO server', {
343346
url: socketUrl,
@@ -550,6 +553,11 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
550553
eventHandlers.current.workflowUpdated?.(data)
551554
})
552555

556+
socketInstance.on('workflow-deployed', (data) => {
557+
logger.info(`Workflow ${data.workflowId} deployment state changed`)
558+
eventHandlers.current.workflowDeployed?.(data)
559+
})
560+
553561
const rehydrateWorkflowStores = async (workflowId: string, workflowState: any) => {
554562
const [
555563
{ useOperationQueueStore },
@@ -994,6 +1002,10 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
9941002
eventHandlers.current.workflowUpdated = handler
9951003
}, [])
9961004

1005+
const onWorkflowDeployed = useCallback((handler: (data: any) => void) => {
1006+
eventHandlers.current.workflowDeployed = handler
1007+
}, [])
1008+
9971009
const onOperationConfirmed = useCallback((handler: (data: any) => void) => {
9981010
eventHandlers.current.operationConfirmed = handler
9991011
}, [])
@@ -1029,6 +1041,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
10291041
onWorkflowDeleted,
10301042
onWorkflowReverted,
10311043
onWorkflowUpdated,
1044+
onWorkflowDeployed,
10321045
onOperationConfirmed,
10331046
onOperationFailed,
10341047
}),
@@ -1058,6 +1071,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
10581071
onWorkflowDeleted,
10591072
onWorkflowReverted,
10601073
onWorkflowUpdated,
1074+
onWorkflowDeployed,
10611075
onOperationConfirmed,
10621076
onOperationFailed,
10631077
]

apps/sim/hooks/queries/deployments.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export function invalidateDeploymentQueries(queryClient: QueryClient, workflowId
4242
queryClient.invalidateQueries({ queryKey: deploymentKeys.info(workflowId) }),
4343
queryClient.invalidateQueries({ queryKey: deploymentKeys.deployedState(workflowId) }),
4444
queryClient.invalidateQueries({ queryKey: deploymentKeys.versions(workflowId) }),
45+
queryClient.invalidateQueries({ queryKey: deploymentKeys.chatStatus(workflowId) }),
46+
queryClient.invalidateQueries({ queryKey: deploymentKeys.formStatus(workflowId) }),
4547
])
4648
}
4749

apps/sim/hooks/use-collaborative-workflow.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { useCallback, useEffect, useRef } from 'react'
22
import { createLogger } from '@sim/logger'
3+
import { useQueryClient } from '@tanstack/react-query'
34
import type { Edge } from 'reactflow'
45
import { useShallow } from 'zustand/react/shallow'
56
import { useSession } from '@/lib/auth/auth-client'
67
import { generateId } from '@/lib/core/utils/uuid'
78
import { useSocket } from '@/app/workspace/providers/socket-provider'
89
import { getBlock } from '@/blocks'
910
import { normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
11+
import { invalidateDeploymentQueries } from '@/hooks/queries/deployments'
1012
import { useUndoRedo } from '@/hooks/use-undo-redo'
1113
import {
1214
BLOCK_OPERATIONS,
@@ -34,6 +36,7 @@ import { findAllDescendantNodes, isBlockProtected } from '@/stores/workflows/wor
3436
const logger = createLogger('CollaborativeWorkflow')
3537

3638
export function useCollaborativeWorkflow() {
39+
const queryClient = useQueryClient()
3740
const undoRedo = useUndoRedo()
3841
const isUndoRedoInProgress = useRef(false)
3942
const lastDiffOperationId = useRef<string | null>(null)
@@ -125,6 +128,7 @@ export function useCollaborativeWorkflow() {
125128
onWorkflowDeleted,
126129
onWorkflowReverted,
127130
onWorkflowUpdated,
131+
onWorkflowDeployed,
128132
onOperationConfirmed,
129133
onOperationFailed,
130134
} = useSocket()
@@ -645,6 +649,15 @@ export function useCollaborativeWorkflow() {
645649
}
646650
}
647651

652+
const handleWorkflowDeployed = (data: any) => {
653+
const { workflowId } = data
654+
logger.info(`Workflow ${workflowId} deployment state changed`)
655+
656+
if (workflowId !== activeWorkflowId) return
657+
658+
invalidateDeploymentQueries(queryClient, workflowId)
659+
}
660+
648661
const handleOperationConfirmed = (data: any) => {
649662
const { operationId } = data
650663
logger.debug('Operation confirmed', { operationId })
@@ -664,6 +677,7 @@ export function useCollaborativeWorkflow() {
664677
onWorkflowDeleted(handleWorkflowDeleted)
665678
onWorkflowReverted(handleWorkflowReverted)
666679
onWorkflowUpdated(handleWorkflowUpdated)
680+
onWorkflowDeployed(handleWorkflowDeployed)
667681
onOperationConfirmed(handleOperationConfirmed)
668682
onOperationFailed(handleOperationFailed)
669683
}, [
@@ -673,9 +687,11 @@ export function useCollaborativeWorkflow() {
673687
onWorkflowDeleted,
674688
onWorkflowReverted,
675689
onWorkflowUpdated,
690+
onWorkflowDeployed,
676691
onOperationConfirmed,
677692
onOperationFailed,
678693
activeWorkflowId,
694+
queryClient,
679695
confirmOperation,
680696
failOperation,
681697
emitWorkflowOperation,

0 commit comments

Comments
 (0)