Skip to content

Commit 39e7dbe

Browse files
committed
Add periodic pining to keepalive
1 parent 875990e commit 39e7dbe

File tree

1 file changed

+99
-165
lines changed

1 file changed

+99
-165
lines changed

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

Lines changed: 99 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -22,137 +22,60 @@ import AudioSharing from './AudioSharing';
2222
interface CollaborationEditorProps {
2323
matchId: string | null;
2424
}
25-
type AwarenessStates = Map<number, AwarenessState>;
2625

2726
const 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

Comments
 (0)