mirror of
https://github.com/xtr-dev/rondevu-demo.git
synced 2025-12-10 10:53:22 +00:00
Compare commits
5 Commits
2550c1ac3f
...
3a42f74371
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a42f74371 | |||
| 2cbd46b27a | |||
| b3dde85cd2 | |||
| 66dc35c1a7 | |||
| 74bf2757ff |
29
CLAUDE.md
29
CLAUDE.md
@@ -6,12 +6,37 @@
|
||||
|
||||
When configuring TURN servers:
|
||||
|
||||
- ✅ **DO** include the port number in TURN URLs: `turn:server.com:3478`
|
||||
- ✅ **DO** use TURNS (secure) on port 5349 when available: `turns:server.com:5349`
|
||||
- ✅ **DO** include TURN fallback on port 3478: `turn:server.com:3478`
|
||||
- ✅ **DO** include the port number in TURN URLs (even if default)
|
||||
- ✅ **DO** test TURN connectivity before deploying: `turnutils_uclient -u user -w pass server.com 3478 -y`
|
||||
- ✅ **DO** provide both TCP and UDP transports for maximum compatibility
|
||||
- ❌ **DON'T** omit the port number (even if it's the default 3478)
|
||||
- ❌ **DON'T** omit the port number
|
||||
- ❌ **DON'T** assume TURN works without testing
|
||||
|
||||
**Current Configuration:**
|
||||
```javascript
|
||||
const RTC_CONFIG = {
|
||||
iceServers: [
|
||||
{ urls: ["stun:stun.share.fish:3478"] },
|
||||
{
|
||||
urls: [
|
||||
// TURNS (secure) - TLS/DTLS on port 5349 (preferred)
|
||||
"turns:turn.share.fish:5349?transport=tcp",
|
||||
"turns:turn.share.fish:5349?transport=udp",
|
||||
// TURN (fallback) - plain on port 3478
|
||||
"turn:turn.share.fish:3478?transport=tcp",
|
||||
"turn:turn.share.fish:3478?transport=udp",
|
||||
],
|
||||
username: "webrtcuser",
|
||||
credential: "supersecretpassword"
|
||||
}
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
WebRTC will try TURNS (secure) endpoints first, falling back to plain TURN if needed.
|
||||
|
||||
### ICE Configuration
|
||||
|
||||
**Force Relay Mode for Testing:**
|
||||
|
||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -8,7 +8,6 @@
|
||||
"name": "rondevu-demo",
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"@xtr-dev/rondevu-client": "^0.8.3",
|
||||
"@zxing/library": "^0.21.3",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.2.0",
|
||||
@@ -745,15 +744,6 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/ed25519": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-3.0.0.tgz",
|
||||
"integrity": "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.27",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||
@@ -1170,15 +1160,6 @@
|
||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xtr-dev/rondevu-client": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@xtr-dev/rondevu-client/-/rondevu-client-0.8.3.tgz",
|
||||
"integrity": "sha512-yvTZYQiOeIuaQgTZPhO3Wud9OEi7Mwxt9b4JYWlhwaReVUNajI1fpWTO8hHu8BrkiowVDabDN0qLFpwRufEvsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/ed25519": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@zxing/library": {
|
||||
"version": "0.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.3.tgz",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"deploy": "npm run build && npx wrangler pages deploy dist --project-name=rondevu-demo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xtr-dev/rondevu-client": "^0.8.3",
|
||||
"@zxing/library": "^0.21.3",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.2.0",
|
||||
|
||||
144
src/App.jsx
144
src/App.jsx
@@ -6,16 +6,20 @@ const API_URL = 'https://api.ronde.vu';
|
||||
|
||||
const RTC_CONFIG = {
|
||||
iceServers: [
|
||||
{ urls: ["stun:stun.ronde.vu:3478"] },
|
||||
{ urls: ["stun:stun.share.fish:3478"] },
|
||||
{
|
||||
urls: [
|
||||
"turn:turn.ronde.vu:3478?transport=tcp",
|
||||
"turn:turn.ronde.vu:3478?transport=udp",
|
||||
// TURNS (secure) - TLS/DTLS on port 5349
|
||||
"turns:turn.share.fish:5349?transport=tcp",
|
||||
"turns:turn.share.fish:5349?transport=udp",
|
||||
// TURN (fallback) - plain on port 3478
|
||||
"turn:turn.share.fish:3478?transport=tcp",
|
||||
"turn:turn.share.fish:3478?transport=udp",
|
||||
],
|
||||
username: "webrtcuser",
|
||||
credential: "supersecretpassword"
|
||||
}
|
||||
]
|
||||
],
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
@@ -124,6 +128,8 @@ export default function App() {
|
||||
}, [setupStep, myUsername, client]);
|
||||
|
||||
// Check online status periodically
|
||||
// Note: Online detection by attempting to query services
|
||||
// In v0.9.0 there's no direct listServices API, so we check by attempting connection
|
||||
useEffect(() => {
|
||||
if (setupStep !== 'ready' || !client) return;
|
||||
|
||||
@@ -131,8 +137,14 @@ export default function App() {
|
||||
const online = new Set();
|
||||
for (const contact of contacts) {
|
||||
try {
|
||||
const services = await client.discovery.listServices(contact);
|
||||
if (services.services.some(s => s.serviceFqn === 'chat.rondevu@1.0.0')) {
|
||||
// Try to query the service via discovery endpoint
|
||||
const response = await fetch(`${API_URL}/index/${contact}/query`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ serviceFqn: 'chat.rondevu@1.0.0' })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
online.add(contact);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -163,7 +175,7 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
// Start pooled chat service
|
||||
// Start pooled chat service with durable connections
|
||||
const startChatService = async () => {
|
||||
if (!client || !myUsername || serviceHandle) return;
|
||||
|
||||
@@ -174,21 +186,22 @@ export default function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
const handle = await client.services.exposeService({
|
||||
const service = await client.exposeService({
|
||||
username: myUsername,
|
||||
privateKey: keypair.privateKey,
|
||||
serviceFqn: 'chat.rondevu@1.0.0',
|
||||
isPublic: true,
|
||||
ttl: 300000, // 5 minutes - service stays alive while page is open
|
||||
ttl: 300000, // 5 minutes - service auto-refreshes
|
||||
ttlRefreshMargin: 0.2, // Refresh at 80% of TTL
|
||||
poolSize: 10, // Support up to 10 simultaneous connections
|
||||
rtcConfig: RTC_CONFIG,
|
||||
handler: (channel, peer, connectionId) => {
|
||||
handler: (channel, connectionId) => {
|
||||
console.log(`📡 New chat connection: ${connectionId}`);
|
||||
|
||||
// Wait for peer to identify themselves
|
||||
channel.onmessage = (event) => {
|
||||
channel.on('message', (data) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
const msg = JSON.parse(data);
|
||||
|
||||
if (msg.type === 'identify') {
|
||||
// Peer identified themselves
|
||||
@@ -203,10 +216,11 @@ export default function App() {
|
||||
}
|
||||
}));
|
||||
|
||||
// Update message handler for actual chat messages
|
||||
channel.onmessage = (e) => {
|
||||
// Remove old handler and add new one for chat messages
|
||||
channel.off('message');
|
||||
channel.on('message', (chatData) => {
|
||||
try {
|
||||
const chatMsg = JSON.parse(e.data);
|
||||
const chatMsg = JSON.parse(chatData);
|
||||
if (chatMsg.type === 'message') {
|
||||
setActiveChats(prev => ({
|
||||
...prev,
|
||||
@@ -223,7 +237,7 @@ export default function App() {
|
||||
} catch (err) {
|
||||
console.error('Failed to parse chat message:', err);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Send acknowledgment
|
||||
channel.send(JSON.stringify({
|
||||
@@ -234,9 +248,9 @@ export default function App() {
|
||||
} catch (err) {
|
||||
console.error('Failed to parse identify message:', err);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
channel.onclose = () => {
|
||||
channel.on('close', () => {
|
||||
console.log(`👋 Chat closed: ${connectionId}`);
|
||||
setActiveChats(prev => {
|
||||
const updated = { ...prev };
|
||||
@@ -247,14 +261,31 @@ export default function App() {
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
},
|
||||
onError: (error, context) => {
|
||||
console.error(`Chat service error (${context}):`, error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setServiceHandle(handle);
|
||||
// Start the service
|
||||
await service.start();
|
||||
|
||||
// Listen for service events
|
||||
service.on('connection', (connId) => {
|
||||
console.log(`🔗 New connection: ${connId}`);
|
||||
});
|
||||
|
||||
service.on('disconnection', (connId) => {
|
||||
console.log(`🔌 Disconnected: ${connId}`);
|
||||
});
|
||||
|
||||
service.on('ttl-refreshed', (expiresAt) => {
|
||||
console.log(`🔄 Service TTL refreshed, expires at: ${new Date(expiresAt)}`);
|
||||
});
|
||||
|
||||
service.on('error', (error, context) => {
|
||||
console.error(`❌ Service error (${context}):`, error);
|
||||
});
|
||||
|
||||
setServiceHandle(service);
|
||||
console.log('✅ Chat service started');
|
||||
} catch (err) {
|
||||
console.error('Error starting chat service:', err);
|
||||
@@ -291,7 +322,7 @@ export default function App() {
|
||||
toast.success(`Removed ${contact}`);
|
||||
};
|
||||
|
||||
// Start chat with contact
|
||||
// Start chat with contact using durable connection
|
||||
const handleStartChat = async (contact) => {
|
||||
if (!client || activeChats[contact]?.status === 'connected') {
|
||||
setSelectedChat(contact);
|
||||
@@ -301,18 +332,46 @@ export default function App() {
|
||||
try {
|
||||
toast.loading(`Connecting to ${contact}...`, { id: 'connecting' });
|
||||
|
||||
const { peer, channel } = await client.discovery.connect(contact, 'chat.rondevu@1.0.0', { rtcConfig: RTC_CONFIG });
|
||||
// Create durable connection
|
||||
const connection = await client.connect(contact, 'chat.rondevu@1.0.0', {
|
||||
rtcConfig: RTC_CONFIG,
|
||||
maxReconnectAttempts: 5
|
||||
});
|
||||
|
||||
// Send identification
|
||||
channel.send(JSON.stringify({
|
||||
type: 'identify',
|
||||
from: myUsername
|
||||
// Create data channel (must match service pool's channel name)
|
||||
const channel = connection.createChannel('rondevu-service');
|
||||
|
||||
// Listen for connection events
|
||||
connection.on('connected', () => {
|
||||
console.log(`✅ Connected to ${contact}`);
|
||||
});
|
||||
|
||||
connection.on('reconnecting', (attempt, max, delay) => {
|
||||
console.log(`🔄 Reconnecting to ${contact} (${attempt}/${max}) in ${delay}ms`);
|
||||
toast.loading(`Reconnecting to ${contact}...`, { id: 'reconnecting' });
|
||||
});
|
||||
|
||||
connection.on('disconnected', () => {
|
||||
console.log(`🔌 Disconnected from ${contact}`);
|
||||
setActiveChats(prev => ({
|
||||
...prev,
|
||||
[contact]: { ...prev[contact], status: 'reconnecting' }
|
||||
}));
|
||||
});
|
||||
|
||||
connection.on('failed', (error) => {
|
||||
console.error(`❌ Connection to ${contact} failed:`, error);
|
||||
toast.error(`Connection to ${contact} failed`, { id: 'connecting' });
|
||||
setActiveChats(prev => ({
|
||||
...prev,
|
||||
[contact]: { ...prev[contact], status: 'disconnected' }
|
||||
}));
|
||||
});
|
||||
|
||||
// Wait for acknowledgment
|
||||
channel.onmessage = (event) => {
|
||||
channel.on('message', (data) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
const msg = JSON.parse(data);
|
||||
|
||||
if (msg.type === 'identify_ack') {
|
||||
// Connection established
|
||||
@@ -323,7 +382,7 @@ export default function App() {
|
||||
[contact]: {
|
||||
username: contact,
|
||||
channel,
|
||||
peer,
|
||||
connection,
|
||||
messages: prev[contact]?.messages || [],
|
||||
status: 'connected'
|
||||
}
|
||||
@@ -331,9 +390,10 @@ export default function App() {
|
||||
setSelectedChat(contact);
|
||||
|
||||
// Update handler for chat messages
|
||||
channel.onmessage = (e) => {
|
||||
channel.off('message');
|
||||
channel.on('message', (chatData) => {
|
||||
try {
|
||||
const chatMsg = JSON.parse(e.data);
|
||||
const chatMsg = JSON.parse(chatData);
|
||||
if (chatMsg.type === 'message') {
|
||||
setActiveChats(prev => ({
|
||||
...prev,
|
||||
@@ -350,20 +410,28 @@ export default function App() {
|
||||
} catch (err) {
|
||||
console.error('Failed to parse message:', err);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to parse ack:', err);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
channel.onclose = () => {
|
||||
channel.on('close', () => {
|
||||
setActiveChats(prev => ({
|
||||
...prev,
|
||||
[contact]: { ...prev[contact], status: 'disconnected' }
|
||||
}));
|
||||
toast.error(`Disconnected from ${contact}`);
|
||||
};
|
||||
});
|
||||
|
||||
// Connect and send identification
|
||||
await connection.connect();
|
||||
|
||||
channel.send(JSON.stringify({
|
||||
type: 'identify',
|
||||
from: myUsername
|
||||
}));
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to connect:', err);
|
||||
|
||||
Reference in New Issue
Block a user