@@ -33,6 +33,9 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
3333 const bindingRef = useRef < MonacoBinding | null > ( null ) ;
3434 const editorRef = useRef < MonacoEditor . IStandaloneCodeEditor | null > ( null ) ;
3535 const prevClientsRef = useRef < Map < number , ConnectedClient > > ( new Map ( ) ) ;
36+ const connectionTimeoutRef = useRef < NodeJS . Timeout | null > ( null ) ;
37+ const awarenessUpdateTimeoutRef = useRef < NodeJS . Timeout | null > ( null ) ;
38+
3639 const sockServerURI =
3740 process . env . NEXT_PUBLIC_SOCK_SERVER_URL || 'ws://localhost:4444' ;
3841 const { toast } = useToast ( ) ;
@@ -43,118 +46,126 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
4346 setLanguage ( language ) ;
4447 } ;
4548
46- const handleEditorMount = ( editor : MonacoEditor . IStandaloneCodeEditor ) => {
47- if ( ! matchId ) {
48- console . error ( 'Cannot mount editor: Match ID is undefined' ) ;
49- return ;
49+ const updateLocalAwareness = ( ) => {
50+ if ( providerRef . current ?. awareness && user ) {
51+ providerRef . current . awareness . setLocalState ( {
52+ client : user . id ,
53+ user : {
54+ name : user . username ,
55+ color : stringToColor ( user . id || '' ) ,
56+ } ,
57+ } ) ;
5058 }
51- editorRef . current = editor ;
52- const doc = new Y . Doc ( ) ;
59+ } ;
5360
54- // Configure the WebsocketProvider with keepalive settings
55- providerRef . current = new WebsocketProvider ( sockServerURI , matchId , doc , {
56- connect : true ,
57- params : {
58- keepalive : 'true' , // Enable keepalive
59- } ,
60- resyncInterval : 3000 , // More frequent resyncs (3 seconds)
61- maxBackoffTime : 500 , // Faster reconnection attempts
62- } ) ;
61+ const handleAwarenessUpdate = ( ) => {
62+ const states = providerRef . current ?. awareness . getStates ( ) ;
63+ if ( ! states ) return ;
6364
64- // Listen for connection status changes
65- providerRef . current . on ( 'status' , ( { status } : { status : string } ) => {
66- if ( status === 'connected' ) {
67- // Re-set local state when reconnected to ensure presence
68- providerRef . current ?. awareness . setLocalState ( {
69- client : user ?. id ,
70- user : {
71- name : user ?. username ,
72- color : stringToColor ( user ?. id || '' ) ,
73- } ,
74- } ) ;
75- }
76- } ) ;
65+ // Clear any pending awareness update
66+ if ( awarenessUpdateTimeoutRef . current ) {
67+ clearTimeout ( awarenessUpdateTimeoutRef . current ) ;
68+ }
7769
78- const type = doc . getText ( 'monaco' ) ;
70+ // Debounce awareness updates
71+ awarenessUpdateTimeoutRef . current = setTimeout ( ( ) => {
72+ const newClients = new Map < number , ConnectedClient > ( ) ;
7973
80- providerRef . current . awareness . setLocalState ( {
81- client : user ?. id ,
82- user : {
83- name : user ?. username ,
84- color : stringToColor ( user ?. id || '' ) ,
85- } ,
86- } ) ;
74+ states . forEach ( ( value ) => {
75+ const state = value as AwarenessState ;
76+ if ( state . client ) {
77+ newClients . set ( state . client , {
78+ id : state . client ,
79+ user : state . user ,
80+ } ) ;
81+ }
82+ } ) ;
8783
88- providerRef . current . awareness . on ( 'change' , ( ) => {
89- const states = providerRef . current ?. awareness . getStates ( ) ;
90- if ( states ) {
91- const newClients = new Map < number , ConnectedClient > ( ) ;
92- // Build new clients map
93- states . forEach ( ( value ) => {
94- const state = value as AwarenessState ;
95- if ( state . client ) {
96- newClients . set ( state . client , {
97- id : state . client ,
98- user : state . user ,
99- } ) ;
100- }
101- } ) ;
84+ // Only process changes if the client list has actually changed
85+ const currentClientIds = Array . from ( prevClientsRef . current . keys ( ) ) . sort ( ) ;
86+ const newClientIds = Array . from ( newClients . keys ( ) ) . sort ( ) ;
10287
103- // Compare entire client lists instead of just size
104- const currentClients = Array . from ( prevClientsRef . current . keys ( ) )
105- . sort ( )
106- . join ( ',' ) ;
107- const newClientsList = Array . from ( newClients . keys ( ) ) . sort ( ) . join ( ',' ) ;
108- const clientsChanged = currentClients !== newClientsList ;
109-
110- if ( clientsChanged ) {
111- // Check for new connections
112- const newConnectedUsers = Array . from ( newClients . values ( ) )
113- . filter (
114- ( client ) =>
115- ! Array . from ( prevClientsRef . current . values ( ) ) . some (
116- ( c ) => c . id === client . id ,
117- ) && client . id . toString ( ) !== user ?. id ,
118- )
119- . map ( ( client ) => client . user . name ) ;
120-
121- if ( newConnectedUsers . length > 0 ) {
122- const description =
123- newConnectedUsers . length === 1
124- ? `${ newConnectedUsers [ 0 ] } joined the session`
125- : `${ newConnectedUsers . slice ( 0 , - 1 ) . join ( ', ' ) } and ${
126- newConnectedUsers [ newConnectedUsers . length - 1 ]
127- } joined the session`;
88+ if ( JSON . stringify ( currentClientIds ) !== JSON . stringify ( newClientIds ) ) {
89+ // Handle new connections
90+ const newConnections = newClientIds . filter (
91+ ( id ) => ! currentClientIds . includes ( id ) && id . toString ( ) !== user ?. id ,
92+ ) ;
12893
94+ if ( newConnections . length > 0 ) {
95+ const newUsers = newConnections
96+ . map ( ( id ) => newClients . get ( id ) ?. user . name )
97+ . filter ( Boolean ) ;
98+
99+ if ( newUsers . length > 0 ) {
129100 toast ( {
130101 title : 'User Connected!' ,
131- description,
102+ description :
103+ newUsers . length === 1
104+ ? `${ newUsers [ 0 ] } joined the session`
105+ : `${ newUsers . slice ( 0 , - 1 ) . join ( ', ' ) } and ${ newUsers [ newUsers . length - 1 ] } joined the session` ,
132106 variant : 'success' ,
133107 } ) ;
134108 }
135-
136- // Check for disconnections
137- Array . from ( prevClientsRef . current . values ( ) ) . forEach ( ( prevClient ) => {
138- if (
139- ! Array . from ( newClients . values ( ) ) . some (
140- ( client ) => client . id === prevClient . id ,
141- ) &&
142- prevClient . id . toString ( ) !== user ?. id
143- ) {
144- toast ( {
145- title : 'User Disconnected' ,
146- description : `${ prevClient . user . name } left the session` ,
147- variant : 'warning' ,
148- } ) ;
149- }
150- } ) ;
151109 }
152110
111+ // Handle disconnections
112+ const disconnections = currentClientIds . filter (
113+ ( id ) => ! newClientIds . includes ( id ) && id . toString ( ) !== user ?. id ,
114+ ) ;
115+
116+ disconnections . forEach ( ( id ) => {
117+ const disconnectedUser = prevClientsRef . current . get ( id ) ;
118+ if ( disconnectedUser ) {
119+ toast ( {
120+ title : 'User Disconnected' ,
121+ description : `${ disconnectedUser . user . name } left the session` ,
122+ variant : 'warning' ,
123+ } ) ;
124+ }
125+ } ) ;
126+
153127 prevClientsRef . current = newClients ;
154128 setConnectedClients ( newClients ) ;
155129 }
130+ } , 1000 ) ; // Debounce for 1 second
131+ } ;
132+
133+ const handleEditorMount = ( editor : MonacoEditor . IStandaloneCodeEditor ) => {
134+ if ( ! matchId ) {
135+ console . error ( 'Cannot mount editor: Match ID is undefined' ) ;
136+ return ;
137+ }
138+
139+ editorRef . current = editor ;
140+ const doc = new Y . Doc ( ) ;
141+
142+ providerRef . current = new WebsocketProvider ( sockServerURI , matchId , doc , {
143+ connect : true ,
144+ params : { keepalive : 'true' } ,
145+ WebSocketPolyfill : WebSocket ,
146+ resyncInterval : 5000 ,
147+ maxBackoffTime : 2500 ,
148+ disableBc : true , // Disable broadcast channel to prevent duplicate events
149+ } ) ;
150+
151+ providerRef . current . on ( 'status' , ( { status } : { status : string } ) => {
152+ if ( status === 'connected' ) {
153+ // Clear any pending connection timeout
154+ if ( connectionTimeoutRef . current ) {
155+ clearTimeout ( connectionTimeoutRef . current ) ;
156+ }
157+
158+ // Update awareness state
159+ updateLocalAwareness ( ) ;
160+ }
156161 } ) ;
157162
163+ const type = doc . getText ( 'monaco' ) ;
164+ updateLocalAwareness ( ) ;
165+
166+ // Set up awareness change handler
167+ providerRef . current . awareness . on ( 'change' , handleAwarenessUpdate ) ;
168+
158169 const model = editorRef . current ?. getModel ( ) ;
159170 if ( editorRef . current && model ) {
160171 bindingRef . current = new MonacoBinding (
@@ -164,16 +175,37 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
164175 providerRef . current . awareness ,
165176 ) ;
166177 }
178+
179+ // Set up periodic awareness state refresh
180+ const refreshInterval = setInterval ( ( ) => {
181+ if ( providerRef . current ?. wsconnected ) {
182+ updateLocalAwareness ( ) ;
183+ }
184+ } , 30000 ) ; // Refresh every 30 seconds
185+
186+ return ( ) => {
187+ clearInterval ( refreshInterval ) ;
188+ } ;
167189 } ;
168190
169191 useEffect ( ( ) => {
170192 return ( ) => {
193+ // Clear all timeouts
194+ if ( connectionTimeoutRef . current ) {
195+ clearTimeout ( connectionTimeoutRef . current ) ;
196+ }
197+ if ( awarenessUpdateTimeoutRef . current ) {
198+ clearTimeout ( awarenessUpdateTimeoutRef . current ) ;
199+ }
200+
201+ // Clean up provider and binding
171202 if ( bindingRef . current ) {
172203 bindingRef . current . destroy ( ) ;
173204 bindingRef . current = null ;
174205 }
175206
176207 if ( providerRef . current ) {
208+ providerRef . current . disconnect ( ) ;
177209 providerRef . current . destroy ( ) ;
178210 providerRef . current = null ;
179211 }
@@ -186,7 +218,6 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
186218 } , [ ] ) ;
187219
188220 const handleLeaveSession = ( ) => {
189- // Clear awareness state before leaving
190221 if ( providerRef . current ?. awareness ) {
191222 providerRef . current . awareness . setLocalState ( null ) ;
192223 }
0 commit comments