@@ -22,137 +22,60 @@ import AudioSharing from './AudioSharing';
22
22
interface CollaborationEditorProps {
23
23
matchId : string | null ;
24
24
}
25
- type AwarenessStates = Map < number , AwarenessState > ;
26
25
27
26
const CollaborationEditor = ( { matchId } : CollaborationEditorProps ) => {
28
27
const { user } = useAuthStore ( ) ;
29
28
const [ language , setLanguage ] = useState ( SUPPORTED_PROGRAMMING_LANGUAGES [ 0 ] ) ;
30
29
const [ connectedClients , setConnectedClients ] = useState <
31
30
Map < number , ConnectedClient >
32
31
> ( new Map ( ) ) ;
33
-
34
32
const providerRef = useRef < WebsocketProvider | null > ( null ) ;
35
33
const bindingRef = useRef < MonacoBinding | null > ( null ) ;
36
34
const editorRef = useRef < MonacoEditor . IStandaloneCodeEditor | null > ( null ) ;
37
- const docRef = useRef < Y . Doc | null > ( null ) ;
38
35
const prevClientsRef = useRef < Map < number , ConnectedClient > > ( new Map ( ) ) ;
39
- const mountCountRef = useRef ( 0 ) ;
40
- const lastUpdateTimeRef = useRef ( 0 ) ;
41
- const clientChangeTimeoutRef = useRef < NodeJS . Timeout | null > ( null ) ;
42
-
43
36
const sockServerURI =
44
37
process . env . NEXT_PUBLIC_SOCK_SERVER_URL || 'ws://localhost:4444' ;
45
38
const { toast } = useToast ( ) ;
46
39
const { clearLastMatchId } = useCollaborationStore ( ) ;
47
40
const router = useRouter ( ) ;
48
41
49
- const TOAST_DEBOUNCE = 1000 ;
50
-
51
42
const onLanguageChange = ( language : string ) => {
52
43
setLanguage ( language ) ;
53
44
} ;
54
45
55
- const handleClientStateChange = ( states : AwarenessStates ) => {
56
- const now = Date . now ( ) ;
57
- if ( now - lastUpdateTimeRef . current < TOAST_DEBOUNCE ) {
46
+ const handleEditorMount = ( editor : MonacoEditor . IStandaloneCodeEditor ) => {
47
+ if ( ! matchId ) {
48
+ console . error ( 'Cannot mount editor: Match ID is undefined' ) ;
58
49
return ;
59
50
}
51
+ editorRef . current = editor ;
52
+ const doc = new Y . Doc ( ) ;
60
53
61
- const newClients = new Map < number , ConnectedClient > ( ) ;
62
- states . forEach ( ( state : AwarenessState ) => {
63
- if ( state . client ) {
64
- newClients . set ( state . client , {
65
- id : state . client ,
66
- user : state . user ,
67
- } ) ;
68
- }
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
69
62
} ) ;
70
63
71
- if ( clientChangeTimeoutRef . current ) {
72
- clearTimeout ( clientChangeTimeoutRef . current ) ;
73
- }
74
-
75
- clientChangeTimeoutRef . current = setTimeout ( ( ) => {
76
- if ( newClients . size !== prevClientsRef . current . size ) {
77
- const newConnectedUsers = Array . from ( newClients . values ( ) )
78
- . filter (
79
- ( client ) =>
80
- ! Array . from ( prevClientsRef . current . values ( ) ) . some (
81
- ( c ) => c . id === client . id ,
82
- ) && client . id . toString ( ) !== user ?. id ,
83
- )
84
- . map ( ( client ) => client . user . name ) ;
85
-
86
- if ( newConnectedUsers . length > 0 ) {
87
- lastUpdateTimeRef . current = now ;
88
- const description =
89
- newConnectedUsers . length === 1
90
- ? `${ newConnectedUsers [ 0 ] } joined the session`
91
- : `${ newConnectedUsers . slice ( 0 , - 1 ) . join ( ', ' ) } and ${
92
- newConnectedUsers [ newConnectedUsers . length - 1 ]
93
- } joined the session`;
94
-
95
- toast ( {
96
- title : 'User Connected!' ,
97
- description,
98
- variant : 'success' ,
99
- } ) ;
100
- }
101
-
102
- Array . from ( prevClientsRef . current . values ( ) ) . forEach ( ( prevClient ) => {
103
- if (
104
- ! Array . from ( newClients . values ( ) ) . some (
105
- ( client ) => client . id === prevClient . id ,
106
- ) &&
107
- prevClient . id . toString ( ) !== user ?. id
108
- ) {
109
- lastUpdateTimeRef . current = now ;
110
- toast ( {
111
- title : 'User Disconnected' ,
112
- description : `${ prevClient . user . name } left the session` ,
113
- variant : 'warning' ,
114
- } ) ;
115
- }
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
+ } ,
116
74
} ) ;
117
75
}
76
+ } ) ;
118
77
119
- prevClientsRef . current = newClients ;
120
- setConnectedClients ( newClients ) ;
121
- } , 500 ) ;
122
- } ;
123
-
124
- const initializeWebSocket = ( editor : MonacoEditor . IStandaloneCodeEditor ) => {
125
- if ( ! matchId ) {
126
- console . error ( 'Cannot initialize: Match ID is undefined' ) ;
127
- return ;
128
- }
129
-
130
- if ( providerRef . current ?. wsconnected ) {
131
- console . log ( 'Reusing existing WebSocket connection' ) ;
132
- return ;
133
- }
134
-
135
- console . log ( 'Initializing new WebSocket connection' ) ;
136
-
137
- if ( ! docRef . current ) {
138
- docRef . current = new Y . Doc ( ) ;
139
- }
140
-
141
- providerRef . current = new WebsocketProvider (
142
- sockServerURI ,
143
- matchId ,
144
- docRef . current ,
145
- {
146
- connect : true ,
147
- resyncInterval : 3000 ,
148
- disableBc : true ,
149
- params : {
150
- version : '1.0.0' ,
151
- } ,
152
- } ,
153
- ) ;
154
-
155
- const type = docRef . current . getText ( 'monaco' ) ;
78
+ const type = doc . getText ( 'monaco' ) ;
156
79
157
80
providerRef . current . awareness . setLocalState ( {
158
81
client : user ?. id ,
@@ -162,49 +85,89 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
162
85
} ,
163
86
} ) ;
164
87
165
- providerRef . current . on ( 'status' , ( { status } : { status : string } ) => {
166
- console . log ( 'WebSocket status:' , status ) ;
167
- } ) ;
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
+ } ) ;
168
102
169
- providerRef . current . on ( 'connection-error' , ( event : Event ) => {
170
- console . error ( 'WebSocket connection error:' , event ) ;
171
- } ) ;
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`;
172
128
173
- let changeTimeout : NodeJS . Timeout ;
174
- providerRef . current . awareness . on ( 'change' , ( ) => {
175
- clearTimeout ( changeTimeout ) ;
176
- changeTimeout = setTimeout ( ( ) => {
177
- const states =
178
- providerRef . current ?. awareness . getStates ( ) as AwarenessStates ;
179
- if ( states ) {
180
- handleClientStateChange ( states ) ;
129
+ toast ( {
130
+ title : 'User Connected!' ,
131
+ description,
132
+ variant : 'success' ,
133
+ } ) ;
134
+ }
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
+ } ) ;
181
151
}
182
- } , 100 ) ;
152
+
153
+ prevClientsRef . current = newClients ;
154
+ setConnectedClients ( newClients ) ;
155
+ }
183
156
} ) ;
184
157
185
- const model = editor . getModel ( ) ;
186
- if ( editor && model ) {
158
+ const model = editorRef . current ? .getModel ( ) ;
159
+ if ( editorRef . current && model ) {
187
160
bindingRef . current = new MonacoBinding (
188
161
type ,
189
162
model ,
190
- new Set ( [ editor ] ) ,
163
+ new Set ( [ editorRef . current ] ) ,
191
164
providerRef . current . awareness ,
192
165
) ;
193
166
}
194
167
} ;
195
168
196
- const handleEditorMount = ( editor : MonacoEditor . IStandaloneCodeEditor ) => {
197
- editorRef . current = editor ;
198
- initializeWebSocket ( editor ) ;
199
- } ;
200
-
201
- const cleanup = ( force = false ) => {
202
- if ( clientChangeTimeoutRef . current ) {
203
- clearTimeout ( clientChangeTimeoutRef . current ) ;
204
- clientChangeTimeoutRef . current = null ;
205
- }
206
-
207
- if ( force ) {
169
+ useEffect ( ( ) => {
170
+ return ( ) => {
208
171
if ( bindingRef . current ) {
209
172
bindingRef . current . destroy ( ) ;
210
173
bindingRef . current = null ;
@@ -215,47 +178,18 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
215
178
providerRef . current = null ;
216
179
}
217
180
218
- if ( docRef . current ) {
219
- docRef . current . destroy ( ) ;
220
- docRef . current = null ;
221
- }
222
-
223
181
if ( editorRef . current ) {
224
182
editorRef . current . dispose ( ) ;
225
183
editorRef . current = null ;
226
184
}
227
-
228
- prevClientsRef . current = new Map ( ) ;
229
- setConnectedClients ( new Map ( ) ) ;
230
- }
231
- } ;
232
-
233
- useEffect ( ( ) => {
234
- const currentMountCount = mountCountRef . current + 1 ;
235
- mountCountRef . current = currentMountCount ;
236
- console . log ( `Editor component mounted (count: ${ currentMountCount } )` ) ;
237
-
238
- return ( ) => {
239
- const finalMountCount = currentMountCount - 1 ;
240
- mountCountRef . current = finalMountCount ;
241
- console . log ( `Editor component unmounting (count: ${ finalMountCount } )` ) ;
242
- cleanup ( finalMountCount === 0 ) ;
243
- } ;
244
- } , [ ] ) ;
245
-
246
- useEffect ( ( ) => {
247
- const handleUnload = ( ) => {
248
- cleanup ( true ) ;
249
- } ;
250
-
251
- window . addEventListener ( 'beforeunload' , handleUnload ) ;
252
- return ( ) => {
253
- window . removeEventListener ( 'beforeunload' , handleUnload ) ;
254
185
} ;
255
186
} , [ ] ) ;
256
187
257
188
const handleLeaveSession = ( ) => {
258
- cleanup ( true ) ;
189
+ // Clear awareness state before leaving
190
+ if ( providerRef . current ?. awareness ) {
191
+ providerRef . current . awareness . setLocalState ( null ) ;
192
+ }
259
193
clearLastMatchId ( ) ;
260
194
router . push ( '/' ) ;
261
195
} ;
0 commit comments