@@ -29,29 +29,138 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
2929 const [ connectedClients , setConnectedClients ] = useState <
3030 Map < number , ConnectedClient >
3131 > ( new Map ( ) ) ;
32+
33+ // Refs for persistent state
3234 const providerRef = useRef < WebsocketProvider | null > ( null ) ;
3335 const bindingRef = useRef < MonacoBinding | null > ( null ) ;
3436 const editorRef = useRef < MonacoEditor . IStandaloneCodeEditor | null > ( null ) ;
37+ const docRef = useRef < Y . Doc | null > ( null ) ;
3538 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+
3643 const sockServerURI =
3744 process . env . NEXT_PUBLIC_SOCK_SERVER_URL || 'ws://localhost:4444' ;
3845 const { toast } = useToast ( ) ;
3946 const { clearLastMatchId } = useCollaborationStore ( ) ;
4047 const router = useRouter ( ) ;
4148
49+ const TOAST_DEBOUNCE = 1000 ; // Minimum time between toasts
50+
4251 const onLanguageChange = ( language : string ) => {
4352 setLanguage ( language ) ;
4453 } ;
4554
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 ) => {
47130 if ( ! matchId ) {
48- console . error ( 'Cannot mount editor : Match ID is undefined' ) ;
131+ console . error ( 'Cannot initialize : Match ID is undefined' ) ;
49132 return ;
50133 }
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' ) ;
55164
56165 providerRef . current . awareness . setLocalState ( {
57166 client : user ?. id ,
@@ -61,84 +170,52 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
61170 } ,
62171 } ) ;
63172
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+ } ) ;
99177
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+ } ) ;
106181
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 ) ;
122190 }
123-
124- prevClientsRef . current = newClients ;
125- setConnectedClients ( newClients ) ;
126- }
191+ } , 100 ) ;
127192 } ) ;
128193
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 ) {
131197 bindingRef . current = new MonacoBinding (
132198 type ,
133199 model ,
134- new Set ( [ editorRef . current ] ) ,
200+ new Set ( [ editor ] ) ,
135201 providerRef . current . awareness ,
136202 ) ;
137203 }
138204 } ;
139205
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 ) {
142219 if ( bindingRef . current ) {
143220 bindingRef . current . destroy ( ) ;
144221 bindingRef . current = null ;
@@ -149,15 +226,52 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
149226 providerRef . current = null ;
150227 }
151228
229+ if ( docRef . current ) {
230+ docRef . current . destroy ( ) ;
231+ docRef . current = null ;
232+ }
233+
152234 if ( editorRef . current ) {
153235 editorRef . current . dispose ( ) ;
154236 editorRef . current = null ;
155237 }
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 ) ;
156269 } ;
157270 } , [ ] ) ;
158271
159272 const handleLeaveSession = ( ) => {
160- clearLastMatchId ( ) ; // now users last match id will be null
273+ cleanup ( true ) ;
274+ clearLastMatchId ( ) ;
161275 router . push ( '/' ) ;
162276 } ;
163277
0 commit comments