@@ -29,29 +29,138 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
29
29
const [ connectedClients , setConnectedClients ] = useState <
30
30
Map < number , ConnectedClient >
31
31
> ( new Map ( ) ) ;
32
+
33
+ // Refs for persistent state
32
34
const providerRef = useRef < WebsocketProvider | null > ( null ) ;
33
35
const bindingRef = useRef < MonacoBinding | null > ( null ) ;
34
36
const editorRef = useRef < MonacoEditor . IStandaloneCodeEditor | null > ( null ) ;
37
+ const docRef = useRef < Y . Doc | null > ( null ) ;
35
38
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
+
36
43
const sockServerURI =
37
44
process . env . NEXT_PUBLIC_SOCK_SERVER_URL || 'ws://localhost:4444' ;
38
45
const { toast } = useToast ( ) ;
39
46
const { clearLastMatchId } = useCollaborationStore ( ) ;
40
47
const router = useRouter ( ) ;
41
48
49
+ const TOAST_DEBOUNCE = 1000 ; // Minimum time between toasts
50
+
42
51
const onLanguageChange = ( language : string ) => {
43
52
setLanguage ( language ) ;
44
53
} ;
45
54
46
- const handleEditorMount = ( editor : MonacoEditor . IStandaloneCodeEditor ) => {
55
+ const handleClientStateChange = ( states : Map < any , any > ) => {
56
+ const now = Date . now ( ) ;
57
+ if ( now - lastUpdateTimeRef . current < TOAST_DEBOUNCE ) {
58
+ return ;
59
+ }
60
+
61
+ const newClients = new Map < number , ConnectedClient > ( ) ;
62
+ states . forEach ( ( value : { [ x : string ] : any } ) => {
63
+ const state = value as AwarenessState ;
64
+ if ( state . client ) {
65
+ newClients . set ( state . client , {
66
+ id : state . client ,
67
+ user : state . user ,
68
+ } ) ;
69
+ }
70
+ } ) ;
71
+
72
+ // Clear any pending timeout
73
+ if ( clientChangeTimeoutRef . current ) {
74
+ clearTimeout ( clientChangeTimeoutRef . current ) ;
75
+ }
76
+
77
+ // Set a new timeout to handle the change
78
+ clientChangeTimeoutRef . current = setTimeout ( ( ) => {
79
+ if ( newClients . size !== prevClientsRef . current . size ) {
80
+ // Check for new connections
81
+ const newConnectedUsers = Array . from ( newClients . values ( ) )
82
+ . filter (
83
+ ( client ) =>
84
+ ! Array . from ( prevClientsRef . current . values ( ) ) . some (
85
+ ( c ) => c . id === client . id ,
86
+ ) && client . id . toString ( ) !== user ?. id ,
87
+ )
88
+ . map ( ( client ) => client . user . name ) ;
89
+
90
+ if ( newConnectedUsers . length > 0 ) {
91
+ lastUpdateTimeRef . current = now ;
92
+ const description =
93
+ newConnectedUsers . length === 1
94
+ ? `${ newConnectedUsers [ 0 ] } joined the session`
95
+ : `${ newConnectedUsers . slice ( 0 , - 1 ) . join ( ', ' ) } and ${
96
+ newConnectedUsers [ newConnectedUsers . length - 1 ]
97
+ } joined the session`;
98
+
99
+ toast ( {
100
+ title : 'User Connected!' ,
101
+ description,
102
+ variant : 'success' ,
103
+ } ) ;
104
+ }
105
+
106
+ // Check for disconnections
107
+ Array . from ( prevClientsRef . current . values ( ) ) . forEach ( ( prevClient ) => {
108
+ if (
109
+ ! Array . from ( newClients . values ( ) ) . some (
110
+ ( client ) => client . id === prevClient . id ,
111
+ ) &&
112
+ prevClient . id . toString ( ) !== user ?. id
113
+ ) {
114
+ lastUpdateTimeRef . current = now ;
115
+ toast ( {
116
+ title : 'User Disconnected' ,
117
+ description : `${ prevClient . user . name } left the session` ,
118
+ variant : 'warning' ,
119
+ } ) ;
120
+ }
121
+ } ) ;
122
+ }
123
+
124
+ prevClientsRef . current = newClients ;
125
+ setConnectedClients ( newClients ) ;
126
+ } , 500 ) ; // Debounce time for client changes
127
+ } ;
128
+
129
+ const initializeWebSocket = ( editor : MonacoEditor . IStandaloneCodeEditor ) => {
47
130
if ( ! matchId ) {
48
- console . error ( 'Cannot mount editor : Match ID is undefined' ) ;
131
+ console . error ( 'Cannot initialize : Match ID is undefined' ) ;
49
132
return ;
50
133
}
51
- editorRef . current = editor ;
52
- const doc = new Y . Doc ( ) ;
53
- providerRef . current = new WebsocketProvider ( sockServerURI , matchId , doc ) ;
54
- const type = doc . getText ( 'monaco' ) ;
134
+
135
+ // If we already have a connection, don't reinitialize
136
+ if ( providerRef . current ?. wsconnected ) {
137
+ console . log ( 'Reusing existing WebSocket connection' ) ;
138
+ return ;
139
+ }
140
+
141
+ console . log ( 'Initializing new WebSocket connection' ) ;
142
+
143
+ // Create new Y.Doc if it doesn't exist
144
+ if ( ! docRef . current ) {
145
+ docRef . current = new Y . Doc ( ) ;
146
+ }
147
+
148
+ // Create new WebSocket provider with valid configuration options
149
+ providerRef . current = new WebsocketProvider (
150
+ sockServerURI ,
151
+ matchId ,
152
+ docRef . current ,
153
+ {
154
+ connect : true ,
155
+ resyncInterval : 3000 , // Time between resync attempts
156
+ disableBc : true , // Disable broadcast channel to prevent duplicate connections
157
+ params : {
158
+ version : '1.0.0' , // Optional version parameter
159
+ } ,
160
+ } ,
161
+ ) ;
162
+
163
+ const type = docRef . current . getText ( 'monaco' ) ;
55
164
56
165
providerRef . current . awareness . setLocalState ( {
57
166
client : user ?. id ,
@@ -61,84 +170,52 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
61
170
} ,
62
171
} ) ;
63
172
64
- providerRef . current . awareness . on ( 'change' , ( ) => {
65
- const states = providerRef . current ?. awareness . getStates ( ) ;
66
- if ( states ) {
67
- const newClients = new Map < number , ConnectedClient > ( ) ;
68
- // Build new clients map
69
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
- states . forEach ( ( value : { [ x : string ] : any } ) => {
71
- const state = value as AwarenessState ;
72
- if ( state . client ) {
73
- newClients . set ( state . client , {
74
- id : state . client ,
75
- user : state . user ,
76
- } ) ;
77
- }
78
- } ) ;
79
-
80
- // Only check for connections/disconnections if the NUMBER OF CLIENTS HAS CHANGED
81
- if ( newClients . size !== prevClientsRef . current . size ) {
82
- // Check for new connections
83
- const newConnectedUsers = Array . from ( newClients . values ( ) )
84
- . filter (
85
- ( client ) =>
86
- ! Array . from ( prevClientsRef . current . values ( ) ) . some (
87
- ( c ) => c . id === client . id ,
88
- ) && client . id . toString ( ) !== user ?. id ,
89
- )
90
- . map ( ( client ) => client . user . name ) ;
91
-
92
- if ( newConnectedUsers . length > 0 ) {
93
- const description =
94
- newConnectedUsers . length === 1
95
- ? `${ newConnectedUsers [ 0 ] } joined the session`
96
- : `${ newConnectedUsers . slice ( 0 , - 1 ) . join ( ', ' ) } and ${
97
- newConnectedUsers [ newConnectedUsers . length - 1 ]
98
- } joined the session`;
173
+ // Add connection status handlers
174
+ providerRef . current . on ( 'status' , ( { status } : { status : string } ) => {
175
+ console . log ( 'WebSocket status:' , status ) ;
176
+ } ) ;
99
177
100
- toast ( {
101
- title : 'User Connected!' ,
102
- description,
103
- variant : 'success' ,
104
- } ) ;
105
- }
178
+ providerRef . current . on ( 'connection-error' , ( event : Event ) => {
179
+ console . error ( 'WebSocket connection error:' , event ) ;
180
+ } ) ;
106
181
107
- // Check for disconnections
108
- Array . from ( prevClientsRef . current . values ( ) ) . forEach ( ( prevClient ) => {
109
- if (
110
- ! Array . from ( newClients . values ( ) ) . some (
111
- ( client ) => client . id === prevClient . id ,
112
- ) &&
113
- prevClient . id . toString ( ) !== user ?. id
114
- ) {
115
- toast ( {
116
- title : 'User Disconnected' ,
117
- description : `${ prevClient . user . name } left the session` ,
118
- variant : 'warning' ,
119
- } ) ;
120
- }
121
- } ) ;
182
+ // Set up awareness change handler with debouncing
183
+ let changeTimeout : NodeJS . Timeout ;
184
+ providerRef . current . awareness . on ( 'change' , ( ) => {
185
+ clearTimeout ( changeTimeout ) ;
186
+ changeTimeout = setTimeout ( ( ) => {
187
+ const states = providerRef . current ?. awareness . getStates ( ) ;
188
+ if ( states ) {
189
+ handleClientStateChange ( states ) ;
122
190
}
123
-
124
- prevClientsRef . current = newClients ;
125
- setConnectedClients ( newClients ) ;
126
- }
191
+ } , 100 ) ;
127
192
} ) ;
128
193
129
- const model = editorRef . current ?. getModel ( ) ;
130
- if ( editorRef . current && model ) {
194
+ // Set up Monaco binding
195
+ const model = editor . getModel ( ) ;
196
+ if ( editor && model ) {
131
197
bindingRef . current = new MonacoBinding (
132
198
type ,
133
199
model ,
134
- new Set ( [ editorRef . current ] ) ,
200
+ new Set ( [ editor ] ) ,
135
201
providerRef . current . awareness ,
136
202
) ;
137
203
}
138
204
} ;
139
205
140
- useEffect ( ( ) => {
141
- return ( ) => {
206
+ const handleEditorMount = ( editor : MonacoEditor . IStandaloneCodeEditor ) => {
207
+ editorRef . current = editor ;
208
+ initializeWebSocket ( editor ) ;
209
+ } ;
210
+
211
+ // Cleanup function
212
+ const cleanup = ( force = false ) => {
213
+ if ( clientChangeTimeoutRef . current ) {
214
+ clearTimeout ( clientChangeTimeoutRef . current ) ;
215
+ clientChangeTimeoutRef . current = null ;
216
+ }
217
+
218
+ if ( force ) {
142
219
if ( bindingRef . current ) {
143
220
bindingRef . current . destroy ( ) ;
144
221
bindingRef . current = null ;
@@ -149,15 +226,52 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
149
226
providerRef . current = null ;
150
227
}
151
228
229
+ if ( docRef . current ) {
230
+ docRef . current . destroy ( ) ;
231
+ docRef . current = null ;
232
+ }
233
+
152
234
if ( editorRef . current ) {
153
235
editorRef . current . dispose ( ) ;
154
236
editorRef . current = null ;
155
237
}
238
+
239
+ prevClientsRef . current = new Map ( ) ;
240
+ setConnectedClients ( new Map ( ) ) ;
241
+ }
242
+ } ;
243
+
244
+ // Mount/unmount handling
245
+ useEffect ( ( ) => {
246
+ mountCountRef . current ++ ;
247
+ console . log ( `Editor component mounted (count: ${ mountCountRef . current } )` ) ;
248
+
249
+ return ( ) => {
250
+ mountCountRef . current -- ;
251
+ console . log (
252
+ `Editor component unmounting (count: ${ mountCountRef . current } )` ,
253
+ ) ;
254
+
255
+ // Only do full cleanup when last instance unmounts
256
+ cleanup ( mountCountRef . current === 0 ) ;
257
+ } ;
258
+ } , [ ] ) ;
259
+
260
+ // Handle page unload
261
+ useEffect ( ( ) => {
262
+ const handleUnload = ( ) => {
263
+ cleanup ( true ) ;
264
+ } ;
265
+
266
+ window . addEventListener ( 'beforeunload' , handleUnload ) ;
267
+ return ( ) => {
268
+ window . removeEventListener ( 'beforeunload' , handleUnload ) ;
156
269
} ;
157
270
} , [ ] ) ;
158
271
159
272
const handleLeaveSession = ( ) => {
160
- clearLastMatchId ( ) ; // now users last match id will be null
273
+ cleanup ( true ) ;
274
+ clearLastMatchId ( ) ;
161
275
router . push ( '/' ) ;
162
276
} ;
163
277
0 commit comments