@@ -22,137 +22,60 @@ import AudioSharing from './AudioSharing';
2222interface CollaborationEditorProps {
2323 matchId : string | null ;
2424}
25- type AwarenessStates = Map < number , AwarenessState > ;
2625
2726const CollaborationEditor = ( { matchId } : CollaborationEditorProps ) => {
2827 const { user } = useAuthStore ( ) ;
2928 const [ language , setLanguage ] = useState ( SUPPORTED_PROGRAMMING_LANGUAGES [ 0 ] ) ;
3029 const [ connectedClients , setConnectedClients ] = useState <
3130 Map < number , ConnectedClient >
3231 > ( new Map ( ) ) ;
33-
3432 const providerRef = useRef < WebsocketProvider | null > ( null ) ;
3533 const bindingRef = useRef < MonacoBinding | null > ( null ) ;
3634 const editorRef = useRef < MonacoEditor . IStandaloneCodeEditor | null > ( null ) ;
37- const docRef = useRef < Y . Doc | null > ( null ) ;
3835 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-
4336 const sockServerURI =
4437 process . env . NEXT_PUBLIC_SOCK_SERVER_URL || 'ws://localhost:4444' ;
4538 const { toast } = useToast ( ) ;
4639 const { clearLastMatchId } = useCollaborationStore ( ) ;
4740 const router = useRouter ( ) ;
4841
49- const TOAST_DEBOUNCE = 1000 ;
50-
5142 const onLanguageChange = ( language : string ) => {
5243 setLanguage ( language ) ;
5344 } ;
5445
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' ) ;
5849 return ;
5950 }
51+ editorRef . current = editor ;
52+ const doc = new Y . Doc ( ) ;
6053
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
6962 } ) ;
7063
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+ } ,
11674 } ) ;
11775 }
76+ } ) ;
11877
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' ) ;
15679
15780 providerRef . current . awareness . setLocalState ( {
15881 client : user ?. id ,
@@ -162,49 +85,89 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
16285 } ,
16386 } ) ;
16487
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+ } ) ;
168102
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`;
172128
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+ } ) ;
181151 }
182- } , 100 ) ;
152+
153+ prevClientsRef . current = newClients ;
154+ setConnectedClients ( newClients ) ;
155+ }
183156 } ) ;
184157
185- const model = editor . getModel ( ) ;
186- if ( editor && model ) {
158+ const model = editorRef . current ? .getModel ( ) ;
159+ if ( editorRef . current && model ) {
187160 bindingRef . current = new MonacoBinding (
188161 type ,
189162 model ,
190- new Set ( [ editor ] ) ,
163+ new Set ( [ editorRef . current ] ) ,
191164 providerRef . current . awareness ,
192165 ) ;
193166 }
194167 } ;
195168
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 ( ) => {
208171 if ( bindingRef . current ) {
209172 bindingRef . current . destroy ( ) ;
210173 bindingRef . current = null ;
@@ -215,47 +178,18 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
215178 providerRef . current = null ;
216179 }
217180
218- if ( docRef . current ) {
219- docRef . current . destroy ( ) ;
220- docRef . current = null ;
221- }
222-
223181 if ( editorRef . current ) {
224182 editorRef . current . dispose ( ) ;
225183 editorRef . current = null ;
226184 }
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 ) ;
254185 } ;
255186 } , [ ] ) ;
256187
257188 const handleLeaveSession = ( ) => {
258- cleanup ( true ) ;
189+ // Clear awareness state before leaving
190+ if ( providerRef . current ?. awareness ) {
191+ providerRef . current . awareness . setLocalState ( null ) ;
192+ }
259193 clearLastMatchId ( ) ;
260194 router . push ( '/' ) ;
261195 } ;
0 commit comments