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:
|
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** 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
|
- ✅ **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
|
- ❌ **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
|
### ICE Configuration
|
||||||
|
|
||||||
**Force Relay Mode for Testing:**
|
**Force Relay Mode for Testing:**
|
||||||
|
|||||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -8,7 +8,6 @@
|
|||||||
"name": "rondevu-demo",
|
"name": "rondevu-demo",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xtr-dev/rondevu-client": "^0.8.3",
|
|
||||||
"@zxing/library": "^0.21.3",
|
"@zxing/library": "^0.21.3",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -745,15 +744,6 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.27",
|
"version": "1.0.0-beta.27",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
"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"
|
"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": {
|
"node_modules/@zxing/library": {
|
||||||
"version": "0.21.3",
|
"version": "0.21.3",
|
||||||
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.3.tgz",
|
"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"
|
"deploy": "npm run build && npx wrangler pages deploy dist --project-name=rondevu-demo"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xtr-dev/rondevu-client": "^0.8.3",
|
|
||||||
"@zxing/library": "^0.21.3",
|
"@zxing/library": "^0.21.3",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^18.2.0",
|
"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 = {
|
const RTC_CONFIG = {
|
||||||
iceServers: [
|
iceServers: [
|
||||||
{ urls: ["stun:stun.ronde.vu:3478"] },
|
{ urls: ["stun:stun.share.fish:3478"] },
|
||||||
{
|
{
|
||||||
urls: [
|
urls: [
|
||||||
"turn:turn.ronde.vu:3478?transport=tcp",
|
// TURNS (secure) - TLS/DTLS on port 5349
|
||||||
"turn:turn.ronde.vu:3478?transport=udp",
|
"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",
|
username: "webrtcuser",
|
||||||
credential: "supersecretpassword"
|
credential: "supersecretpassword"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
@@ -124,6 +128,8 @@ export default function App() {
|
|||||||
}, [setupStep, myUsername, client]);
|
}, [setupStep, myUsername, client]);
|
||||||
|
|
||||||
// Check online status periodically
|
// 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(() => {
|
useEffect(() => {
|
||||||
if (setupStep !== 'ready' || !client) return;
|
if (setupStep !== 'ready' || !client) return;
|
||||||
|
|
||||||
@@ -131,8 +137,14 @@ export default function App() {
|
|||||||
const online = new Set();
|
const online = new Set();
|
||||||
for (const contact of contacts) {
|
for (const contact of contacts) {
|
||||||
try {
|
try {
|
||||||
const services = await client.discovery.listServices(contact);
|
// Try to query the service via discovery endpoint
|
||||||
if (services.services.some(s => s.serviceFqn === 'chat.rondevu@1.0.0')) {
|
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);
|
online.add(contact);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -163,7 +175,7 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start pooled chat service
|
// Start pooled chat service with durable connections
|
||||||
const startChatService = async () => {
|
const startChatService = async () => {
|
||||||
if (!client || !myUsername || serviceHandle) return;
|
if (!client || !myUsername || serviceHandle) return;
|
||||||
|
|
||||||
@@ -174,21 +186,22 @@ export default function App() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handle = await client.services.exposeService({
|
const service = await client.exposeService({
|
||||||
username: myUsername,
|
username: myUsername,
|
||||||
privateKey: keypair.privateKey,
|
privateKey: keypair.privateKey,
|
||||||
serviceFqn: 'chat.rondevu@1.0.0',
|
serviceFqn: 'chat.rondevu@1.0.0',
|
||||||
isPublic: true,
|
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
|
poolSize: 10, // Support up to 10 simultaneous connections
|
||||||
rtcConfig: RTC_CONFIG,
|
rtcConfig: RTC_CONFIG,
|
||||||
handler: (channel, peer, connectionId) => {
|
handler: (channel, connectionId) => {
|
||||||
console.log(`📡 New chat connection: ${connectionId}`);
|
console.log(`📡 New chat connection: ${connectionId}`);
|
||||||
|
|
||||||
// Wait for peer to identify themselves
|
// Wait for peer to identify themselves
|
||||||
channel.onmessage = (event) => {
|
channel.on('message', (data) => {
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(event.data);
|
const msg = JSON.parse(data);
|
||||||
|
|
||||||
if (msg.type === 'identify') {
|
if (msg.type === 'identify') {
|
||||||
// Peer identified themselves
|
// Peer identified themselves
|
||||||
@@ -203,10 +216,11 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Update message handler for actual chat messages
|
// Remove old handler and add new one for chat messages
|
||||||
channel.onmessage = (e) => {
|
channel.off('message');
|
||||||
|
channel.on('message', (chatData) => {
|
||||||
try {
|
try {
|
||||||
const chatMsg = JSON.parse(e.data);
|
const chatMsg = JSON.parse(chatData);
|
||||||
if (chatMsg.type === 'message') {
|
if (chatMsg.type === 'message') {
|
||||||
setActiveChats(prev => ({
|
setActiveChats(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -223,7 +237,7 @@ export default function App() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to parse chat message:', err);
|
console.error('Failed to parse chat message:', err);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// Send acknowledgment
|
// Send acknowledgment
|
||||||
channel.send(JSON.stringify({
|
channel.send(JSON.stringify({
|
||||||
@@ -234,9 +248,9 @@ export default function App() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to parse identify message:', err);
|
console.error('Failed to parse identify message:', err);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
channel.onclose = () => {
|
channel.on('close', () => {
|
||||||
console.log(`👋 Chat closed: ${connectionId}`);
|
console.log(`👋 Chat closed: ${connectionId}`);
|
||||||
setActiveChats(prev => {
|
setActiveChats(prev => {
|
||||||
const updated = { ...prev };
|
const updated = { ...prev };
|
||||||
@@ -247,14 +261,31 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
return updated;
|
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');
|
console.log('✅ Chat service started');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error starting chat service:', err);
|
console.error('Error starting chat service:', err);
|
||||||
@@ -291,7 +322,7 @@ export default function App() {
|
|||||||
toast.success(`Removed ${contact}`);
|
toast.success(`Removed ${contact}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start chat with contact
|
// Start chat with contact using durable connection
|
||||||
const handleStartChat = async (contact) => {
|
const handleStartChat = async (contact) => {
|
||||||
if (!client || activeChats[contact]?.status === 'connected') {
|
if (!client || activeChats[contact]?.status === 'connected') {
|
||||||
setSelectedChat(contact);
|
setSelectedChat(contact);
|
||||||
@@ -301,18 +332,46 @@ export default function App() {
|
|||||||
try {
|
try {
|
||||||
toast.loading(`Connecting to ${contact}...`, { id: 'connecting' });
|
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
|
// Create data channel (must match service pool's channel name)
|
||||||
channel.send(JSON.stringify({
|
const channel = connection.createChannel('rondevu-service');
|
||||||
type: 'identify',
|
|
||||||
from: myUsername
|
// 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
|
// Wait for acknowledgment
|
||||||
channel.onmessage = (event) => {
|
channel.on('message', (data) => {
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(event.data);
|
const msg = JSON.parse(data);
|
||||||
|
|
||||||
if (msg.type === 'identify_ack') {
|
if (msg.type === 'identify_ack') {
|
||||||
// Connection established
|
// Connection established
|
||||||
@@ -323,7 +382,7 @@ export default function App() {
|
|||||||
[contact]: {
|
[contact]: {
|
||||||
username: contact,
|
username: contact,
|
||||||
channel,
|
channel,
|
||||||
peer,
|
connection,
|
||||||
messages: prev[contact]?.messages || [],
|
messages: prev[contact]?.messages || [],
|
||||||
status: 'connected'
|
status: 'connected'
|
||||||
}
|
}
|
||||||
@@ -331,9 +390,10 @@ export default function App() {
|
|||||||
setSelectedChat(contact);
|
setSelectedChat(contact);
|
||||||
|
|
||||||
// Update handler for chat messages
|
// Update handler for chat messages
|
||||||
channel.onmessage = (e) => {
|
channel.off('message');
|
||||||
|
channel.on('message', (chatData) => {
|
||||||
try {
|
try {
|
||||||
const chatMsg = JSON.parse(e.data);
|
const chatMsg = JSON.parse(chatData);
|
||||||
if (chatMsg.type === 'message') {
|
if (chatMsg.type === 'message') {
|
||||||
setActiveChats(prev => ({
|
setActiveChats(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -350,20 +410,28 @@ export default function App() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to parse message:', err);
|
console.error('Failed to parse message:', err);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to parse ack:', err);
|
console.error('Failed to parse ack:', err);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
channel.onclose = () => {
|
channel.on('close', () => {
|
||||||
setActiveChats(prev => ({
|
setActiveChats(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[contact]: { ...prev[contact], status: 'disconnected' }
|
[contact]: { ...prev[contact], status: 'disconnected' }
|
||||||
}));
|
}));
|
||||||
toast.error(`Disconnected from ${contact}`);
|
toast.error(`Disconnected from ${contact}`);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
// Connect and send identification
|
||||||
|
await connection.connect();
|
||||||
|
|
||||||
|
channel.send(JSON.stringify({
|
||||||
|
type: 'identify',
|
||||||
|
from: myUsername
|
||||||
|
}));
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to connect:', err);
|
console.error('Failed to connect:', err);
|
||||||
|
|||||||
Reference in New Issue
Block a user