Skip to content

Commit e748a64

Browse files
authored
refactor(realtime): type the socket event-handler boundary with @sim/realtime-protocol (#5208)
* refactor(realtime): type the socket event-handler boundary with @sim/realtime-protocol Replace the (data: any) event-handler types in socket-provider.tsx with precise broadcast types that mirror the exact payloads emitted by the realtime Socket.IO server (apps/realtime/src/handlers/** and rooms/**). Add @sim/realtime-protocol/events with the canonical wire types for the broadcast/confirmation events the server emits: WorkflowOperationBroadcast, SubblockUpdateBroadcast, VariableUpdateBroadcast, CursorUpdateBroadcast, SelectionUpdateBroadcast, the four workflow-lifecycle broadcasts, and OperationConfirmed/Failed. Typing change only; zero runtime/logic changes. Store-internal any (rehydrate state, subblock map, emit payloads) is left untouched as out of scope. * fix(realtime): type cursor-update broadcast cursor as nullable The client emits 'cursor-update' with { cursor: null } when a remote user's cursor leaves the canvas, and the server re-broadcasts it verbatim, so receivers genuinely get cursor: null. Type CursorUpdateBroadcast.cursor as CursorPosition | null to match the wire. (selection stays non-null — it signals absence via type: 'none', never null.)
1 parent 9bd8f14 commit e748a64

3 files changed

Lines changed: 189 additions & 47 deletions

File tree

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

Lines changed: 63 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ import {
1111
useState,
1212
} from 'react'
1313
import { createLogger } from '@sim/logger'
14+
import type {
15+
CursorUpdateBroadcast,
16+
OperationConfirmedBroadcast,
17+
OperationFailedBroadcast,
18+
SelectionUpdateBroadcast,
19+
SubblockUpdateBroadcast,
20+
VariableUpdateBroadcast,
21+
WorkflowDeletedBroadcast,
22+
WorkflowDeployedBroadcast,
23+
WorkflowOperationBroadcast,
24+
WorkflowRevertedBroadcast,
25+
WorkflowUpdatedBroadcast,
26+
} from '@sim/realtime-protocol/events'
1427
import { generateId } from '@sim/utils/id'
1528
import { backoffWithJitter } from '@sim/utils/retry'
1629
import { useParams } from 'next/navigation'
@@ -92,18 +105,18 @@ interface SocketContextType {
92105

93106
emitCursorUpdate: (cursor: { x: number; y: number } | null) => void
94107
emitSelectionUpdate: (selection: { type: 'block' | 'edge' | 'none'; id?: string }) => void
95-
onWorkflowOperation: (handler: (data: any) => void) => void
96-
onSubblockUpdate: (handler: (data: any) => void) => void
97-
onVariableUpdate: (handler: (data: any) => void) => void
98-
99-
onCursorUpdate: (handler: (data: any) => void) => void
100-
onSelectionUpdate: (handler: (data: any) => void) => void
101-
onWorkflowDeleted: (handler: (data: any) => void) => void
102-
onWorkflowReverted: (handler: (data: any) => void) => void
103-
onWorkflowUpdated: (handler: (data: any) => void) => void
104-
onWorkflowDeployed: (handler: (data: any) => void) => void
105-
onOperationConfirmed: (handler: (data: any) => void) => void
106-
onOperationFailed: (handler: (data: any) => void) => void
108+
onWorkflowOperation: (handler: (data: WorkflowOperationBroadcast) => void) => void
109+
onSubblockUpdate: (handler: (data: SubblockUpdateBroadcast) => void) => void
110+
onVariableUpdate: (handler: (data: VariableUpdateBroadcast) => void) => void
111+
112+
onCursorUpdate: (handler: (data: CursorUpdateBroadcast) => void) => void
113+
onSelectionUpdate: (handler: (data: SelectionUpdateBroadcast) => void) => void
114+
onWorkflowDeleted: (handler: (data: WorkflowDeletedBroadcast) => void) => void
115+
onWorkflowReverted: (handler: (data: WorkflowRevertedBroadcast) => void) => void
116+
onWorkflowUpdated: (handler: (data: WorkflowUpdatedBroadcast) => void) => void
117+
onWorkflowDeployed: (handler: (data: WorkflowDeployedBroadcast) => void) => void
118+
onOperationConfirmed: (handler: (data: OperationConfirmedBroadcast) => void) => void
119+
onOperationFailed: (handler: (data: OperationFailedBroadcast) => void) => void
107120
}
108121

109122
const SocketContext = createContext<SocketContextType>({
@@ -173,17 +186,17 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
173186
explicitWorkflowIdRef.current = explicitWorkflowId
174187

175188
const eventHandlers = useRef<{
176-
workflowOperation?: (data: any) => void
177-
subblockUpdate?: (data: any) => void
178-
variableUpdate?: (data: any) => void
179-
cursorUpdate?: (data: any) => void
180-
selectionUpdate?: (data: any) => void
181-
workflowDeleted?: (data: any) => void
182-
workflowReverted?: (data: any) => void
183-
workflowUpdated?: (data: any) => void
184-
workflowDeployed?: (data: any) => void
185-
operationConfirmed?: (data: any) => void
186-
operationFailed?: (data: any) => void
189+
workflowOperation?: (data: WorkflowOperationBroadcast) => void
190+
subblockUpdate?: (data: SubblockUpdateBroadcast) => void
191+
variableUpdate?: (data: VariableUpdateBroadcast) => void
192+
cursorUpdate?: (data: CursorUpdateBroadcast) => void
193+
selectionUpdate?: (data: SelectionUpdateBroadcast) => void
194+
workflowDeleted?: (data: WorkflowDeletedBroadcast) => void
195+
workflowReverted?: (data: WorkflowRevertedBroadcast) => void
196+
workflowUpdated?: (data: WorkflowUpdatedBroadcast) => void
197+
workflowDeployed?: (data: WorkflowDeployedBroadcast) => void
198+
operationConfirmed?: (data: OperationConfirmedBroadcast) => void
199+
operationFailed?: (data: OperationFailedBroadcast) => void
187200
}>({})
188201

189202
const positionUpdateTimeouts = useRef<Map<string, number>>(new Map())
@@ -555,19 +568,19 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
555568
executeJoinCommands(result.commands)
556569
})
557570

558-
socketInstance.on('workflow-operation', (data) => {
571+
socketInstance.on('workflow-operation', (data: WorkflowOperationBroadcast) => {
559572
eventHandlers.current.workflowOperation?.(data)
560573
})
561574

562-
socketInstance.on('subblock-update', (data) => {
575+
socketInstance.on('subblock-update', (data: SubblockUpdateBroadcast) => {
563576
eventHandlers.current.subblockUpdate?.(data)
564577
})
565578

566-
socketInstance.on('variable-update', (data) => {
579+
socketInstance.on('variable-update', (data: VariableUpdateBroadcast) => {
567580
eventHandlers.current.variableUpdate?.(data)
568581
})
569582

570-
socketInstance.on('workflow-deleted', (data) => {
583+
socketInstance.on('workflow-deleted', (data: WorkflowDeletedBroadcast) => {
571584
logger.warn(`Workflow ${data.workflowId} has been deleted`)
572585
const result = joinControllerRef.current.handleWorkflowDeleted(data.workflowId)
573586
if (result.shouldClearCurrent) {
@@ -577,17 +590,17 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
577590
eventHandlers.current.workflowDeleted?.(data)
578591
})
579592

580-
socketInstance.on('workflow-reverted', (data) => {
593+
socketInstance.on('workflow-reverted', (data: WorkflowRevertedBroadcast) => {
581594
logger.info(`Workflow ${data.workflowId} has been reverted to deployed state`)
582595
eventHandlers.current.workflowReverted?.(data)
583596
})
584597

585-
socketInstance.on('workflow-updated', (data) => {
598+
socketInstance.on('workflow-updated', (data: WorkflowUpdatedBroadcast) => {
586599
logger.info(`Workflow ${data.workflowId} has been updated externally`)
587600
eventHandlers.current.workflowUpdated?.(data)
588601
})
589602

590-
socketInstance.on('workflow-deployed', (data) => {
603+
socketInstance.on('workflow-deployed', (data: WorkflowDeployedBroadcast) => {
591604
logger.info(`Workflow ${data.workflowId} deployment state changed`)
592605
eventHandlers.current.workflowDeployed?.(data)
593606
})
@@ -647,17 +660,17 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
647660
return true
648661
}
649662

650-
socketInstance.on('operation-confirmed', (data) => {
663+
socketInstance.on('operation-confirmed', (data: OperationConfirmedBroadcast) => {
651664
logger.debug('Operation confirmed', { operationId: data.operationId })
652665
eventHandlers.current.operationConfirmed?.(data)
653666
})
654667

655-
socketInstance.on('operation-failed', (data) => {
668+
socketInstance.on('operation-failed', (data: OperationFailedBroadcast) => {
656669
logger.warn('Operation failed', { operationId: data.operationId, error: data.error })
657670
eventHandlers.current.operationFailed?.(data)
658671
})
659672

660-
socketInstance.on('cursor-update', (data) => {
673+
socketInstance.on('cursor-update', (data: CursorUpdateBroadcast) => {
661674
if (!isWorkflowVisible()) {
662675
return
663676
}
@@ -675,7 +688,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
675688
eventHandlers.current.cursorUpdate?.(data)
676689
})
677690

678-
socketInstance.on('selection-update', (data) => {
691+
socketInstance.on('selection-update', (data: SelectionUpdateBroadcast) => {
679692
if (!isWorkflowVisible()) {
680693
return
681694
}
@@ -1045,47 +1058,50 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
10451058
[socket, currentWorkflowId, isWorkflowVisible]
10461059
)
10471060

1048-
const onWorkflowOperation = useCallback((handler: (data: any) => void) => {
1061+
const onWorkflowOperation = useCallback((handler: (data: WorkflowOperationBroadcast) => void) => {
10491062
eventHandlers.current.workflowOperation = handler
10501063
}, [])
10511064

1052-
const onSubblockUpdate = useCallback((handler: (data: any) => void) => {
1065+
const onSubblockUpdate = useCallback((handler: (data: SubblockUpdateBroadcast) => void) => {
10531066
eventHandlers.current.subblockUpdate = handler
10541067
}, [])
10551068

1056-
const onVariableUpdate = useCallback((handler: (data: any) => void) => {
1069+
const onVariableUpdate = useCallback((handler: (data: VariableUpdateBroadcast) => void) => {
10571070
eventHandlers.current.variableUpdate = handler
10581071
}, [])
10591072

1060-
const onCursorUpdate = useCallback((handler: (data: any) => void) => {
1073+
const onCursorUpdate = useCallback((handler: (data: CursorUpdateBroadcast) => void) => {
10611074
eventHandlers.current.cursorUpdate = handler
10621075
}, [])
10631076

1064-
const onSelectionUpdate = useCallback((handler: (data: any) => void) => {
1077+
const onSelectionUpdate = useCallback((handler: (data: SelectionUpdateBroadcast) => void) => {
10651078
eventHandlers.current.selectionUpdate = handler
10661079
}, [])
10671080

1068-
const onWorkflowDeleted = useCallback((handler: (data: any) => void) => {
1081+
const onWorkflowDeleted = useCallback((handler: (data: WorkflowDeletedBroadcast) => void) => {
10691082
eventHandlers.current.workflowDeleted = handler
10701083
}, [])
10711084

1072-
const onWorkflowReverted = useCallback((handler: (data: any) => void) => {
1085+
const onWorkflowReverted = useCallback((handler: (data: WorkflowRevertedBroadcast) => void) => {
10731086
eventHandlers.current.workflowReverted = handler
10741087
}, [])
10751088

1076-
const onWorkflowUpdated = useCallback((handler: (data: any) => void) => {
1089+
const onWorkflowUpdated = useCallback((handler: (data: WorkflowUpdatedBroadcast) => void) => {
10771090
eventHandlers.current.workflowUpdated = handler
10781091
}, [])
10791092

1080-
const onWorkflowDeployed = useCallback((handler: (data: any) => void) => {
1093+
const onWorkflowDeployed = useCallback((handler: (data: WorkflowDeployedBroadcast) => void) => {
10811094
eventHandlers.current.workflowDeployed = handler
10821095
}, [])
10831096

1084-
const onOperationConfirmed = useCallback((handler: (data: any) => void) => {
1085-
eventHandlers.current.operationConfirmed = handler
1086-
}, [])
1097+
const onOperationConfirmed = useCallback(
1098+
(handler: (data: OperationConfirmedBroadcast) => void) => {
1099+
eventHandlers.current.operationConfirmed = handler
1100+
},
1101+
[]
1102+
)
10871103

1088-
const onOperationFailed = useCallback((handler: (data: any) => void) => {
1104+
const onOperationFailed = useCallback((handler: (data: OperationFailedBroadcast) => void) => {
10891105
eventHandlers.current.operationFailed = handler
10901106
}, [])
10911107

packages/realtime-protocol/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
"types": "./src/constants.ts",
1515
"default": "./src/constants.ts"
1616
},
17+
"./events": {
18+
"types": "./src/events.ts",
19+
"default": "./src/events.ts"
20+
},
1721
"./schemas": {
1822
"types": "./src/schemas.ts",
1923
"default": "./src/schemas.ts"
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import type { OperationTarget, SocketOperation } from './constants'
2+
3+
/**
4+
* Wire types for the broadcast/confirmation events the realtime Socket.IO server
5+
* emits to clients. These mirror the exact object literals emitted by
6+
* `apps/realtime/src/handlers/**` and `apps/realtime/src/rooms/**`, and are the
7+
* canonical types consumed by the client socket transport
8+
* (`apps/sim/app/workspace/providers/socket-provider.tsx`).
9+
*
10+
* Payload bodies that the transport forwards opaquely are typed `unknown` rather
11+
* than a concrete operation union, because the transport never narrows them — the
12+
* collaborative-workflow consumer dispatches on `operation`/`target` itself.
13+
*/
14+
15+
/** A live-presence cursor position broadcast over the socket. */
16+
export interface CursorPosition {
17+
x: number
18+
y: number
19+
}
20+
21+
/** A live-presence selection broadcast over the socket. */
22+
export interface PresenceSelection {
23+
type: 'block' | 'edge' | 'none'
24+
id?: string
25+
}
26+
27+
/**
28+
* `workflow-operation` broadcast. The server re-broadcasts the originating
29+
* operation envelope plus sender identity and operation metadata.
30+
*/
31+
export interface WorkflowOperationBroadcast {
32+
operation: SocketOperation | string
33+
target: OperationTarget | string
34+
payload: unknown
35+
timestamp: number
36+
senderId: string
37+
userId: string
38+
userName: string
39+
metadata: {
40+
workflowId: string
41+
operationId: string
42+
isPositionUpdate?: boolean
43+
isBatchPositionUpdate?: boolean
44+
}
45+
}
46+
47+
/** `subblock-update` broadcast. */
48+
export interface SubblockUpdateBroadcast {
49+
workflowId: string
50+
blockId: string
51+
subblockId: string
52+
value: unknown
53+
timestamp: number
54+
}
55+
56+
/** `variable-update` broadcast. */
57+
export interface VariableUpdateBroadcast {
58+
workflowId: string
59+
variableId: string
60+
field: string
61+
value: unknown
62+
timestamp: number
63+
}
64+
65+
/** `cursor-update` presence broadcast for a single remote user. */
66+
export interface CursorUpdateBroadcast {
67+
socketId: string
68+
userId: string
69+
userName: string
70+
avatarUrl?: string | null
71+
/** `null` when the remote user's cursor leaves the canvas (the client emits `{ cursor: null }`). */
72+
cursor: CursorPosition | null
73+
}
74+
75+
/** `selection-update` presence broadcast for a single remote user. */
76+
export interface SelectionUpdateBroadcast {
77+
socketId: string
78+
userId: string
79+
userName: string
80+
avatarUrl?: string | null
81+
selection: PresenceSelection
82+
}
83+
84+
/** `workflow-deleted` lifecycle broadcast. */
85+
export interface WorkflowDeletedBroadcast {
86+
workflowId: string
87+
message: string
88+
timestamp: number
89+
}
90+
91+
/** `workflow-reverted` lifecycle broadcast. */
92+
export interface WorkflowRevertedBroadcast {
93+
workflowId: string
94+
message: string
95+
timestamp: number
96+
}
97+
98+
/** `workflow-updated` lifecycle broadcast. */
99+
export interface WorkflowUpdatedBroadcast {
100+
workflowId: string
101+
message: string
102+
timestamp: number
103+
}
104+
105+
/** `workflow-deployed` lifecycle broadcast. */
106+
export interface WorkflowDeployedBroadcast {
107+
workflowId: string
108+
timestamp: number
109+
}
110+
111+
/** `operation-confirmed` ack for a previously-emitted operation. */
112+
export interface OperationConfirmedBroadcast {
113+
operationId: string
114+
serverTimestamp: number
115+
}
116+
117+
/** `operation-failed` rejection for a previously-emitted operation. */
118+
export interface OperationFailedBroadcast {
119+
operationId: string
120+
error: string
121+
retryable?: boolean
122+
}

0 commit comments

Comments
 (0)