@@ -33,6 +33,9 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
33
33
const bindingRef = useRef < MonacoBinding | null > ( null ) ;
34
34
const editorRef = useRef < MonacoEditor . IStandaloneCodeEditor | null > ( null ) ;
35
35
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
+
36
39
const sockServerURI =
37
40
process . env . NEXT_PUBLIC_SOCK_SERVER_URL || 'ws://localhost:4444' ;
38
41
const { toast } = useToast ( ) ;
@@ -43,118 +46,126 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
43
46
setLanguage ( language ) ;
44
47
} ;
45
48
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
+ } ) ;
50
58
}
51
- editorRef . current = editor ;
52
- const doc = new Y . Doc ( ) ;
59
+ } ;
53
60
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 ;
63
64
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
+ }
77
69
78
- const type = doc . getText ( 'monaco' ) ;
70
+ // Debounce awareness updates
71
+ awarenessUpdateTimeoutRef . current = setTimeout ( ( ) => {
72
+ const newClients = new Map < number , ConnectedClient > ( ) ;
79
73
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
+ } ) ;
87
83
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 ( ) ;
102
87
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
+ ) ;
128
93
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 ) {
129
100
toast ( {
130
101
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` ,
132
106
variant : 'success' ,
133
107
} ) ;
134
108
}
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
- } ) ;
151
109
}
152
110
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
+
153
127
prevClientsRef . current = newClients ;
154
128
setConnectedClients ( newClients ) ;
155
129
}
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
+ }
156
161
} ) ;
157
162
163
+ const type = doc . getText ( 'monaco' ) ;
164
+ updateLocalAwareness ( ) ;
165
+
166
+ // Set up awareness change handler
167
+ providerRef . current . awareness . on ( 'change' , handleAwarenessUpdate ) ;
168
+
158
169
const model = editorRef . current ?. getModel ( ) ;
159
170
if ( editorRef . current && model ) {
160
171
bindingRef . current = new MonacoBinding (
@@ -164,16 +175,37 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
164
175
providerRef . current . awareness ,
165
176
) ;
166
177
}
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
+ } ;
167
189
} ;
168
190
169
191
useEffect ( ( ) => {
170
192
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
171
202
if ( bindingRef . current ) {
172
203
bindingRef . current . destroy ( ) ;
173
204
bindingRef . current = null ;
174
205
}
175
206
176
207
if ( providerRef . current ) {
208
+ providerRef . current . disconnect ( ) ;
177
209
providerRef . current . destroy ( ) ;
178
210
providerRef . current = null ;
179
211
}
@@ -186,7 +218,6 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
186
218
} , [ ] ) ;
187
219
188
220
const handleLeaveSession = ( ) => {
189
- // Clear awareness state before leaving
190
221
if ( providerRef . current ?. awareness ) {
191
222
providerRef . current . awareness . setLocalState ( null ) ;
192
223
}
0 commit comments