@@ -43,6 +43,8 @@ import type {
4343 VariableUpdateEmit ,
4444 WorkflowOperationEmit ,
4545} from '@/stores/operation-queue/types'
46+ import { usePresenceStore } from '@/stores/presence/store'
47+ import type { PresenceUser } from '@/stores/presence/types'
4648import { useWorkflowRegistry as useWorkflowRegistryStore } from '@/stores/workflows/registry/store'
4749
4850const logger = createLogger ( 'SocketContext' )
@@ -71,15 +73,6 @@ interface User {
7173 email ?: string
7274}
7375
74- interface PresenceUser {
75- socketId : string
76- userId : string
77- userName : string
78- avatarUrl ?: string | null
79- cursor ?: { x : number ; y : number } | null
80- selection ?: { type : 'block' | 'edge' | 'none' ; id ?: string }
81- }
82-
8376interface SocketContextType {
8477 socket : Socket | null
8578 isConnected : boolean
@@ -95,7 +88,6 @@ interface SocketContextType {
9588 blockedJoinWorkflowId : string | null
9689 currentWorkflowId : string | null
9790 currentSocketId : string | null
98- presenceUsers : PresenceUser [ ]
9991 joinWorkflow : ( workflowId : string ) => void
10092 leaveWorkflow : ( ) => void
10193 retryConnection : ( ) => void
@@ -129,7 +121,6 @@ const SocketContext = createContext<SocketContextType>({
129121 blockedJoinWorkflowId : null ,
130122 currentWorkflowId : null ,
131123 currentSocketId : null ,
132- presenceUsers : [ ] ,
133124 joinWorkflow : ( ) => { } ,
134125 leaveWorkflow : ( ) => { } ,
135126 retryConnection : ( ) => { } ,
@@ -166,7 +157,6 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
166157 const [ isRetryingWorkflowJoin , setIsRetryingWorkflowJoin ] = useState ( false )
167158 const [ currentWorkflowId , setCurrentWorkflowId ] = useState < string | null > ( null )
168159 const [ currentSocketId , setCurrentSocketId ] = useState < string | null > ( null )
169- const [ presenceUsers , setPresenceUsers ] = useState < PresenceUser [ ] > ( [ ] )
170160 const [ authFailed , setAuthFailed ] = useState ( false )
171161 const [ blockedJoinWorkflowId , setBlockedJoinWorkflowId ] = useState < string | null > ( null )
172162 const [ explicitWorkflowId , setExplicitWorkflowId ] = useState < string | null > ( null )
@@ -202,6 +192,21 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
202192 const positionUpdateTimeouts = useRef < Map < string , number > > ( new Map ( ) )
203193 const pendingPositionUpdates = useRef < Map < string , any > > ( new Map ( ) )
204194
195+ /**
196+ * Presence is high-frequency (cursor frames many times per second) so it lives
197+ * in {@link usePresenceStore}, not the broad socket context — writing it here no
198+ * longer mints a new context value, so emitter-only `useSocket()` consumers stop
199+ * re-rendering on every cursor frame. These thin wrappers delegate to the store's
200+ * stable actions read via `getState()`.
201+ */
202+ const setPresenceUsers = useCallback ( ( users : PresenceUser [ ] ) => {
203+ usePresenceStore . getState ( ) . setPresenceUsers ( users )
204+ } , [ ] )
205+
206+ const updatePresenceUsers = useCallback ( ( updater : ( prev : PresenceUser [ ] ) => PresenceUser [ ] ) => {
207+ usePresenceStore . getState ( ) . updatePresenceUsers ( updater )
208+ } , [ ] )
209+
205210 const setVisibleWorkflowId = useCallback ( ( workflowId : string | null ) => {
206211 currentWorkflowIdRef . current = workflowId
207212 setCurrentWorkflowId ( workflowId )
@@ -255,7 +260,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
255260 setPresenceUsers ( [ ] )
256261 setVisibleWorkflowId ( null )
257262 } ,
258- [ resetVisibleWorkflowState , setVisibleWorkflowId ]
263+ [ resetVisibleWorkflowState , setPresenceUsers , setVisibleWorkflowId ]
259264 )
260265
261266 const executeJoinCommands = useCallback (
@@ -501,7 +506,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
501506 return
502507 }
503508
504- setPresenceUsers ( ( prev ) => {
509+ updatePresenceUsers ( ( prev ) => {
505510 const prevMap = new Map ( prev . map ( ( u ) => [ u . socketId , u ] ) )
506511
507512 return users . map ( ( user ) => {
@@ -675,7 +680,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
675680 return
676681 }
677682
678- setPresenceUsers ( ( prev ) => {
683+ updatePresenceUsers ( ( prev ) => {
679684 const existingIndex = prev . findIndex ( ( user ) => user . socketId === data . socketId )
680685 if ( existingIndex === - 1 ) {
681686 logger . debug ( 'Received cursor-update for unknown user' , { socketId : data . socketId } )
@@ -693,7 +698,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
693698 return
694699 }
695700
696- setPresenceUsers ( ( prev ) => {
701+ updatePresenceUsers ( ( prev ) => {
697702 const existingIndex = prev . findIndex ( ( user ) => user . socketId === data . socketId )
698703 if ( existingIndex === - 1 ) {
699704 logger . debug ( 'Received selection-update for unknown user' , {
@@ -779,6 +784,11 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
779784 socketRef . current . close ( )
780785 socketRef . current = null
781786 }
787+
788+ // Presence lives in a module-global store (not provider React state), so it
789+ // must be cleared explicitly on unmount/user-switch to match the prior
790+ // per-provider lifetime — otherwise the previous room's collaborators linger.
791+ usePresenceStore . getState ( ) . clearPresenceUsers ( )
782792 }
783793 } , [ user ?. id ] )
784794
@@ -1116,7 +1126,6 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
11161126 blockedJoinWorkflowId,
11171127 currentWorkflowId,
11181128 currentSocketId,
1119- presenceUsers,
11201129 joinWorkflow,
11211130 leaveWorkflow,
11221131 retryConnection,
@@ -1147,7 +1156,6 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
11471156 blockedJoinWorkflowId ,
11481157 currentWorkflowId ,
11491158 currentSocketId ,
1150- presenceUsers ,
11511159 joinWorkflow ,
11521160 leaveWorkflow ,
11531161 retryConnection ,
0 commit comments