Skip to content

Commit 22c7ee8

Browse files
committed
Fix doc Sharing cleanup
1 parent af19462 commit 22c7ee8

File tree

1 file changed

+186
-72
lines changed

1 file changed

+186
-72
lines changed

peerprep-fe/src/app/collaboration/components/CollaborationEditor.tsx

Lines changed: 186 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)