Skip to content

Commit bc455d5

Browse files
authored
feat(variables): multiplayer variables through sockets, persist server side (#933)
* feat(variables): multiplayer variables through sockets, persist server side * remove extraneous comments * breakout variables handler in sockets
1 parent 2a333c7 commit bc455d5

File tree

12 files changed

+1099
-578
lines changed

12 files changed

+1099
-578
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/variables/variables.tsx

Lines changed: 20 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'
1919
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
2020
import { createLogger } from '@/lib/logs/console/logger'
2121
import { validateName } from '@/lib/utils'
22+
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
2223
import { useVariablesStore } from '@/stores/panel/variables/store'
2324
import type { Variable, VariableType } from '@/stores/panel/variables/types'
2425
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -34,19 +35,17 @@ export function Variables() {
3435
deleteVariable,
3536
duplicateVariable,
3637
getVariablesByWorkflowId,
37-
loadVariables,
3838
} = useVariablesStore()
39+
const {
40+
collaborativeUpdateVariable,
41+
collaborativeAddVariable,
42+
collaborativeDeleteVariable,
43+
collaborativeDuplicateVariable,
44+
} = useCollaborativeWorkflow()
3945

4046
// Get variables for the current workflow
4147
const workflowVariables = activeWorkflowId ? getVariablesByWorkflowId(activeWorkflowId) : []
4248

43-
// Load variables when active workflow changes
44-
useEffect(() => {
45-
if (activeWorkflowId) {
46-
loadVariables(activeWorkflowId)
47-
}
48-
}, [activeWorkflowId, loadVariables])
49-
5049
// Track editor references
5150
const editorRefs = useRef<Record<string, HTMLDivElement | null>>({})
5251

@@ -56,16 +55,14 @@ export function Variables() {
5655
// Handle variable name change with validation
5756
const handleVariableNameChange = (variableId: string, newName: string) => {
5857
const validatedName = validateName(newName)
59-
updateVariable(variableId, { name: validatedName })
58+
collaborativeUpdateVariable(variableId, 'name', validatedName)
6059
}
6160

62-
// Auto-save when variables are added/edited
6361
const handleAddVariable = () => {
6462
if (!activeWorkflowId) return
6563

66-
// Create a default variable - naming is handled in the store
67-
const id = addVariable({
68-
name: '', // Store will generate an appropriate name
64+
const id = collaborativeAddVariable({
65+
name: '',
6966
type: 'string',
7067
value: '',
7168
workflowId: activeWorkflowId,
@@ -125,46 +122,32 @@ export function Variables() {
125122
}
126123
}
127124

128-
// Handle editor value changes - store exactly what user types
129125
const handleEditorChange = (variable: Variable, newValue: string) => {
130-
// Store the raw value directly, no parsing or formatting
131-
updateVariable(variable.id, {
132-
value: newValue,
133-
// Clear any previous validation errors so they'll be recalculated
134-
validationError: undefined,
135-
})
126+
collaborativeUpdateVariable(variable.id, 'value', newValue)
136127
}
137128

138-
// Only track focus state for UI purposes
139129
const handleEditorBlur = (variableId: string) => {
140130
setActiveEditors((prev) => ({
141131
...prev,
142132
[variableId]: false,
143133
}))
144134
}
145135

146-
// Track when editor becomes active
147136
const handleEditorFocus = (variableId: string) => {
148137
setActiveEditors((prev) => ({
149138
...prev,
150139
[variableId]: true,
151140
}))
152141
}
153142

154-
// Always return raw value without any formatting
155143
const formatValue = (variable: Variable) => {
156144
if (variable.value === '') return ''
157145

158-
// Always return raw value exactly as typed
159146
return typeof variable.value === 'string' ? variable.value : JSON.stringify(variable.value)
160147
}
161148

162-
// Get validation status based on type and value
163149
const getValidationStatus = (variable: Variable): string | undefined => {
164-
// Empty values don't need validation
165150
if (variable.value === '') return undefined
166-
167-
// Otherwise validate based on type
168151
switch (variable.type) {
169152
case 'number':
170153
return Number.isNaN(Number(variable.value)) ? 'Not a valid number' : undefined
@@ -174,49 +157,38 @@ export function Variables() {
174157
: undefined
175158
case 'object':
176159
try {
177-
// Handle both JavaScript and JSON syntax
178160
const valueToEvaluate = String(variable.value).trim()
179161

180-
// Basic security check to prevent arbitrary code execution
181162
if (!valueToEvaluate.startsWith('{') || !valueToEvaluate.endsWith('}')) {
182163
return 'Not a valid object format'
183164
}
184165

185-
// Use Function constructor to safely evaluate the object expression
186-
// This is safer than eval() and handles all JS object literal syntax
187166
const parsed = new Function(`return ${valueToEvaluate}`)()
188167

189-
// Verify it's actually an object (not array or null)
190168
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
191169
return 'Not a valid object'
192170
}
193171

194-
return undefined // Valid object
172+
return undefined
195173
} catch (e) {
196174
logger.info('Object parsing error:', e)
197175
return 'Invalid object syntax'
198176
}
199177
case 'array':
200178
try {
201-
// Use actual JavaScript evaluation instead of trying to convert to JSON
202-
// This properly handles all valid JS array syntax including mixed types
203179
const valueToEvaluate = String(variable.value).trim()
204180

205-
// Basic security check to prevent arbitrary code execution
206181
if (!valueToEvaluate.startsWith('[') || !valueToEvaluate.endsWith(']')) {
207182
return 'Not a valid array format'
208183
}
209184

210-
// Use Function constructor to safely evaluate the array expression
211-
// This is safer than eval() and handles all JS array syntax correctly
212185
const parsed = new Function(`return ${valueToEvaluate}`)()
213186

214-
// Verify it's actually an array
215187
if (!Array.isArray(parsed)) {
216188
return 'Not a valid array'
217189
}
218190

219-
return undefined // Valid array
191+
return undefined
220192
} catch (e) {
221193
logger.info('Array parsing error:', e)
222194
return 'Invalid array syntax'
@@ -226,9 +198,7 @@ export function Variables() {
226198
}
227199
}
228200

229-
// Clear editor refs when variables change
230201
useEffect(() => {
231-
// Clean up any references to deleted variables
232202
Object.keys(editorRefs.current).forEach((id) => {
233203
if (!workflowVariables.some((v) => v.id === id)) {
234204
delete editorRefs.current[id]
@@ -276,35 +246,35 @@ export function Variables() {
276246
className='min-w-32 rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[#202020]'
277247
>
278248
<DropdownMenuItem
279-
onClick={() => updateVariable(variable.id, { type: 'plain' })}
249+
onClick={() => collaborativeUpdateVariable(variable.id, 'type', 'plain')}
280250
className='flex cursor-pointer items-center rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
281251
>
282252
<div className='mr-2 w-5 text-center font-[380] text-sm'>Abc</div>
283253
<span className='font-[380]'>Plain</span>
284254
</DropdownMenuItem>
285255
<DropdownMenuItem
286-
onClick={() => updateVariable(variable.id, { type: 'number' })}
256+
onClick={() => collaborativeUpdateVariable(variable.id, 'type', 'number')}
287257
className='flex cursor-pointer items-center rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
288258
>
289259
<div className='mr-2 w-5 text-center font-[380] text-sm'>123</div>
290260
<span className='font-[380]'>Number</span>
291261
</DropdownMenuItem>
292262
<DropdownMenuItem
293-
onClick={() => updateVariable(variable.id, { type: 'boolean' })}
263+
onClick={() => collaborativeUpdateVariable(variable.id, 'type', 'boolean')}
294264
className='flex cursor-pointer items-center rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
295265
>
296266
<div className='mr-2 w-5 text-center font-[380] text-sm'>0/1</div>
297267
<span className='font-[380]'>Boolean</span>
298268
</DropdownMenuItem>
299269
<DropdownMenuItem
300-
onClick={() => updateVariable(variable.id, { type: 'object' })}
270+
onClick={() => collaborativeUpdateVariable(variable.id, 'type', 'object')}
301271
className='flex cursor-pointer items-center rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
302272
>
303273
<div className='mr-2 w-5 text-center font-[380] text-sm'>{'{}'}</div>
304274
<span className='font-[380]'>Object</span>
305275
</DropdownMenuItem>
306276
<DropdownMenuItem
307-
onClick={() => updateVariable(variable.id, { type: 'array' })}
277+
onClick={() => collaborativeUpdateVariable(variable.id, 'type', 'array')}
308278
className='flex cursor-pointer items-center rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
309279
>
310280
<div className='mr-2 w-5 text-center font-[380] text-sm'>[]</div>
@@ -329,14 +299,14 @@ export function Variables() {
329299
className='min-w-32 rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[#202020]'
330300
>
331301
<DropdownMenuItem
332-
onClick={() => duplicateVariable(variable.id)}
302+
onClick={() => collaborativeDuplicateVariable(variable.id)}
333303
className='cursor-pointer rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
334304
>
335305
<Copy className='mr-2 h-4 w-4 text-muted-foreground' />
336306
Duplicate
337307
</DropdownMenuItem>
338308
<DropdownMenuItem
339-
onClick={() => deleteVariable(variable.id)}
309+
onClick={() => collaborativeDeleteVariable(variable.id)}
340310
className='cursor-pointer rounded-md px-3 py-2 font-[380] text-destructive text-sm hover:bg-destructive/10 focus:bg-destructive/10 focus:text-destructive'
341311
>
342312
<Trash className='mr-2 h-4 w-4' />

apps/sim/contexts/socket-context.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@ interface SocketContextType {
5050
value: any,
5151
operationId?: string
5252
) => void
53+
emitVariableUpdate: (variableId: string, field: string, value: any, operationId?: string) => void
5354

5455
emitCursorUpdate: (cursor: { x: number; y: number }) => void
5556
emitSelectionUpdate: (selection: { type: 'block' | 'edge' | 'none'; id?: string }) => void
5657
// Event handlers for receiving real-time updates
5758
onWorkflowOperation: (handler: (data: any) => void) => void
5859
onSubblockUpdate: (handler: (data: any) => void) => void
60+
onVariableUpdate: (handler: (data: any) => void) => void
5961

6062
onCursorUpdate: (handler: (data: any) => void) => void
6163
onSelectionUpdate: (handler: (data: any) => void) => void
@@ -77,10 +79,12 @@ const SocketContext = createContext<SocketContextType>({
7779
leaveWorkflow: () => {},
7880
emitWorkflowOperation: () => {},
7981
emitSubblockUpdate: () => {},
82+
emitVariableUpdate: () => {},
8083
emitCursorUpdate: () => {},
8184
emitSelectionUpdate: () => {},
8285
onWorkflowOperation: () => {},
8386
onSubblockUpdate: () => {},
87+
onVariableUpdate: () => {},
8488
onCursorUpdate: () => {},
8589
onSelectionUpdate: () => {},
8690
onUserJoined: () => {},
@@ -113,6 +117,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
113117
const eventHandlers = useRef<{
114118
workflowOperation?: (data: any) => void
115119
subblockUpdate?: (data: any) => void
120+
variableUpdate?: (data: any) => void
116121

117122
cursorUpdate?: (data: any) => void
118123
selectionUpdate?: (data: any) => void
@@ -292,6 +297,11 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
292297
eventHandlers.current.subblockUpdate?.(data)
293298
})
294299

300+
// Variable update events
301+
socketInstance.on('variable-update', (data) => {
302+
eventHandlers.current.variableUpdate?.(data)
303+
})
304+
295305
// Workflow deletion events
296306
socketInstance.on('workflow-deleted', (data) => {
297307
logger.warn(`Workflow ${data.workflowId} has been deleted`)
@@ -697,6 +707,30 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
697707
[socket, currentWorkflowId]
698708
)
699709

710+
// Emit variable value updates
711+
const emitVariableUpdate = useCallback(
712+
(variableId: string, field: string, value: any, operationId?: string) => {
713+
// Only emit if socket is connected and we're in a valid workflow room
714+
if (socket && currentWorkflowId) {
715+
socket.emit('variable-update', {
716+
variableId,
717+
field,
718+
value,
719+
timestamp: Date.now(),
720+
operationId, // Include operation ID for queue tracking
721+
})
722+
} else {
723+
logger.warn('Cannot emit variable update: no socket connection or workflow room', {
724+
hasSocket: !!socket,
725+
currentWorkflowId,
726+
variableId,
727+
field,
728+
})
729+
}
730+
},
731+
[socket, currentWorkflowId]
732+
)
733+
700734
// Cursor throttling optimized for database connection health
701735
const lastCursorEmit = useRef(0)
702736
const emitCursorUpdate = useCallback(
@@ -732,6 +766,10 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
732766
eventHandlers.current.subblockUpdate = handler
733767
}, [])
734768

769+
const onVariableUpdate = useCallback((handler: (data: any) => void) => {
770+
eventHandlers.current.variableUpdate = handler
771+
}, [])
772+
735773
const onCursorUpdate = useCallback((handler: (data: any) => void) => {
736774
eventHandlers.current.cursorUpdate = handler
737775
}, [])
@@ -776,11 +814,13 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
776814
leaveWorkflow,
777815
emitWorkflowOperation,
778816
emitSubblockUpdate,
817+
emitVariableUpdate,
779818

780819
emitCursorUpdate,
781820
emitSelectionUpdate,
782821
onWorkflowOperation,
783822
onSubblockUpdate,
823+
onVariableUpdate,
784824

785825
onCursorUpdate,
786826
onSelectionUpdate,

0 commit comments

Comments
 (0)