Skip to content

Commit 29099c7

Browse files
authored
Merge pull request #188 from CS3219-AY2425S1/fix/cleanup_features
Longer resync interval
2 parents 569e99f + b0f812e commit 29099c7

File tree

1 file changed

+124
-93
lines changed

1 file changed

+124
-93
lines changed

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

Lines changed: 124 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
3333
const bindingRef = useRef<MonacoBinding | null>(null);
3434
const editorRef = useRef<MonacoEditor.IStandaloneCodeEditor | null>(null);
3535
const prevClientsRef = useRef<Map<number, ConnectedClient>>(new Map());
36+
const connectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
37+
const awarenessUpdateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
38+
3639
const sockServerURI =
3740
process.env.NEXT_PUBLIC_SOCK_SERVER_URL || 'ws://localhost:4444';
3841
const { toast } = useToast();
@@ -43,118 +46,126 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
4346
setLanguage(language);
4447
};
4548

46-
const handleEditorMount = (editor: MonacoEditor.IStandaloneCodeEditor) => {
47-
if (!matchId) {
48-
console.error('Cannot mount editor: Match ID is undefined');
49-
return;
49+
const updateLocalAwareness = () => {
50+
if (providerRef.current?.awareness && user) {
51+
providerRef.current.awareness.setLocalState({
52+
client: user.id,
53+
user: {
54+
name: user.username,
55+
color: stringToColor(user.id || ''),
56+
},
57+
});
5058
}
51-
editorRef.current = editor;
52-
const doc = new Y.Doc();
59+
};
5360

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
62-
});
61+
const handleAwarenessUpdate = () => {
62+
const states = providerRef.current?.awareness.getStates();
63+
if (!states) return;
6364

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-
},
74-
});
75-
}
76-
});
65+
// Clear any pending awareness update
66+
if (awarenessUpdateTimeoutRef.current) {
67+
clearTimeout(awarenessUpdateTimeoutRef.current);
68+
}
7769

78-
const type = doc.getText('monaco');
70+
// Debounce awareness updates
71+
awarenessUpdateTimeoutRef.current = setTimeout(() => {
72+
const newClients = new Map<number, ConnectedClient>();
7973

80-
providerRef.current.awareness.setLocalState({
81-
client: user?.id,
82-
user: {
83-
name: user?.username,
84-
color: stringToColor(user?.id || ''),
85-
},
86-
});
74+
states.forEach((value) => {
75+
const state = value as AwarenessState;
76+
if (state.client) {
77+
newClients.set(state.client, {
78+
id: state.client,
79+
user: state.user,
80+
});
81+
}
82+
});
8783

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-
});
84+
// Only process changes if the client list has actually changed
85+
const currentClientIds = Array.from(prevClientsRef.current.keys()).sort();
86+
const newClientIds = Array.from(newClients.keys()).sort();
10287

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`;
88+
if (JSON.stringify(currentClientIds) !== JSON.stringify(newClientIds)) {
89+
// Handle new connections
90+
const newConnections = newClientIds.filter(
91+
(id) => !currentClientIds.includes(id) && id.toString() !== user?.id,
92+
);
12893

94+
if (newConnections.length > 0) {
95+
const newUsers = newConnections
96+
.map((id) => newClients.get(id)?.user.name)
97+
.filter(Boolean);
98+
99+
if (newUsers.length > 0) {
129100
toast({
130101
title: 'User Connected!',
131-
description,
102+
description:
103+
newUsers.length === 1
104+
? `${newUsers[0]} joined the session`
105+
: `${newUsers.slice(0, -1).join(', ')} and ${newUsers[newUsers.length - 1]} joined the session`,
132106
variant: 'success',
133107
});
134108
}
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-
});
151109
}
152110

111+
// Handle disconnections
112+
const disconnections = currentClientIds.filter(
113+
(id) => !newClientIds.includes(id) && id.toString() !== user?.id,
114+
);
115+
116+
disconnections.forEach((id) => {
117+
const disconnectedUser = prevClientsRef.current.get(id);
118+
if (disconnectedUser) {
119+
toast({
120+
title: 'User Disconnected',
121+
description: `${disconnectedUser.user.name} left the session`,
122+
variant: 'warning',
123+
});
124+
}
125+
});
126+
153127
prevClientsRef.current = newClients;
154128
setConnectedClients(newClients);
155129
}
130+
}, 1000); // Debounce for 1 second
131+
};
132+
133+
const handleEditorMount = (editor: MonacoEditor.IStandaloneCodeEditor) => {
134+
if (!matchId) {
135+
console.error('Cannot mount editor: Match ID is undefined');
136+
return;
137+
}
138+
139+
editorRef.current = editor;
140+
const doc = new Y.Doc();
141+
142+
providerRef.current = new WebsocketProvider(sockServerURI, matchId, doc, {
143+
connect: true,
144+
params: { keepalive: 'true' },
145+
WebSocketPolyfill: WebSocket,
146+
resyncInterval: 5000,
147+
maxBackoffTime: 2500,
148+
disableBc: true, // Disable broadcast channel to prevent duplicate events
149+
});
150+
151+
providerRef.current.on('status', ({ status }: { status: string }) => {
152+
if (status === 'connected') {
153+
// Clear any pending connection timeout
154+
if (connectionTimeoutRef.current) {
155+
clearTimeout(connectionTimeoutRef.current);
156+
}
157+
158+
// Update awareness state
159+
updateLocalAwareness();
160+
}
156161
});
157162

163+
const type = doc.getText('monaco');
164+
updateLocalAwareness();
165+
166+
// Set up awareness change handler
167+
providerRef.current.awareness.on('change', handleAwarenessUpdate);
168+
158169
const model = editorRef.current?.getModel();
159170
if (editorRef.current && model) {
160171
bindingRef.current = new MonacoBinding(
@@ -164,16 +175,37 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
164175
providerRef.current.awareness,
165176
);
166177
}
178+
179+
// Set up periodic awareness state refresh
180+
const refreshInterval = setInterval(() => {
181+
if (providerRef.current?.wsconnected) {
182+
updateLocalAwareness();
183+
}
184+
}, 30000); // Refresh every 30 seconds
185+
186+
return () => {
187+
clearInterval(refreshInterval);
188+
};
167189
};
168190

169191
useEffect(() => {
170192
return () => {
193+
// Clear all timeouts
194+
if (connectionTimeoutRef.current) {
195+
clearTimeout(connectionTimeoutRef.current);
196+
}
197+
if (awarenessUpdateTimeoutRef.current) {
198+
clearTimeout(awarenessUpdateTimeoutRef.current);
199+
}
200+
201+
// Clean up provider and binding
171202
if (bindingRef.current) {
172203
bindingRef.current.destroy();
173204
bindingRef.current = null;
174205
}
175206

176207
if (providerRef.current) {
208+
providerRef.current.disconnect();
177209
providerRef.current.destroy();
178210
providerRef.current = null;
179211
}
@@ -186,7 +218,6 @@ const CollaborationEditor = ({ matchId }: CollaborationEditorProps) => {
186218
}, []);
187219

188220
const handleLeaveSession = () => {
189-
// Clear awareness state before leaving
190221
if (providerRef.current?.awareness) {
191222
providerRef.current.awareness.setLocalState(null);
192223
}

0 commit comments

Comments
 (0)