Add ServiceHost, ServiceClient, and RondevuService for high-level service management

- Add RondevuService: High-level API for username claiming and service publishing with Ed25519 signatures
- Add ServiceHost: Manages offer pool for hosting services with auto-replacement
- Add ServiceClient: Connects to hosted services with automatic reconnection
- Add NoOpSignaler: Placeholder signaler for connection setup
- Integrate Ed25519 signature functionality from @noble/ed25519
- Add ESLint and Prettier configuration with 4-space indentation
- Add demo with local signaling test
- Version bump to 0.10.0

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-07 19:37:43 +01:00
parent 945d5a8792
commit 54355323d9
21 changed files with 5066 additions and 307 deletions

141
demo/README.md Normal file
View File

@@ -0,0 +1,141 @@
# Rondevu WebRTC Local Test
Simple side-by-side demo for testing `WebRTCRondevuConnection` with **local signaling** (no server required).
## Quick Start
```bash
npm run dev
```
Opens browser at `http://localhost:3000`
## How It Works
This demo uses **local in-memory signaling** to test WebRTC connections between two peers on the same page. The `LocalSignaler` class simulates a signaling server by directly exchanging ICE candidates and SDP between peers.
### Architecture
- **LocalSignaler**: Implements the `Signaler` interface with local peer-to-peer communication
- **Host (Peer A)**: Creates the offer (offerer role)
- **Client (Peer B)**: Receives the offer and creates answer (answerer role)
- **ICE Exchange**: Candidates are automatically exchanged between peers through the linked signalers
## Usage Steps
1. **Create Host** (Peer A)
- Click "1⃣ Create Host Connection" on the left side
- The host will create an offer and display it
- Status changes to "Connecting"
2. **Create Client** (Peer B)
- Click "2⃣ Create Client Connection" on the right side
- The client receives the host's offer automatically
- Creates an answer and sends it back to the host
- Both peers exchange ICE candidates
3. **Connection Established**
- Watch the status indicators turn green ("Connected")
- Activity logs show the connection progress
4. **Send Messages**
- Type a message in either peer's input field
- Click "📤 Send" or press Enter
- Messages appear in the other peer's activity log
## Features
-**No signaling server required** - Everything runs locally
-**Automatic ICE candidate exchange** - Signalers handle candidate exchange
-**Real-time activity logs** - See exactly what's happening
-**Connection state indicators** - Visual feedback for connection status
-**Bidirectional messaging** - Send messages in both directions
## Code Structure
### demo.js
```javascript
// LocalSignaler - Implements local signaling
class LocalSignaler {
addIceCandidate(candidate) // Called when local peer has a candidate
addListener(callback) // Listen for remote candidates
linkTo(remoteSignaler) // Connect two signalers together
}
// Create and link signalers
const hostSignaler = new LocalSignaler('HOST', 'CLIENT')
const clientSignaler = new LocalSignaler('CLIENT', 'HOST')
hostSignaler.linkTo(clientSignaler)
clientSignaler.linkTo(hostSignaler)
// Create connections
const hostConnection = new WebRTCRondevuConnection({
id: 'test-connection',
host: 'client-peer',
service: 'test.demo@1.0.0',
offer: null, // No offer = offerer role
context: new WebRTCContext(hostSignaler)
})
const clientConnection = new WebRTCRondevuConnection({
id: 'test-connection',
host: 'host-peer',
service: 'test.demo@1.0.0',
offer: hostConnection.connection.localDescription, // With offer = answerer role
context: new WebRTCContext(clientSignaler)
})
```
### index.html
- Side-by-side layout for Host and Client
- Status indicators (disconnected/connecting/connected)
- SDP display areas (offer/answer)
- Message input and send buttons
- Activity logs for each peer
## Debugging
Open the browser console to see detailed logs:
- `[HOST]` - Logs from the host connection
- `[CLIENT]` - Logs from the client connection
- ICE candidate exchange
- Connection state changes
- Message send/receive events
## Comparison: Local vs Remote Signaling
### Local Signaling (This Demo)
```javascript
const signaler = new LocalSignaler('HOST', 'CLIENT')
signaler.linkTo(remoteSignaler) // Direct link
```
**Pros**: No server, instant testing, no network latency
**Cons**: Only works for same-page testing
### Remote Signaling (Production)
```javascript
const api = new RondevuAPI('https://api.ronde.vu', credentials)
const signaler = new RondevuSignaler(api, offerId)
```
**Pros**: Real peer discovery, works across networks
**Cons**: Requires signaling server, network latency
## Next Steps
After testing locally, you can:
1. Switch to `RondevuSignaler` for real signaling server testing
2. Test across different browsers/devices
3. Test with STUN/TURN servers for NAT traversal
4. Implement production signaling with Rondevu API
## Files
- `index.html` - UI layout and styling
- `demo.js` - Local signaling implementation and WebRTC logic
- `README.md` - This file

304
demo/demo.js Normal file
View File

@@ -0,0 +1,304 @@
import { WebRTCRondevuConnection } from '../src/index.js'
import { WebRTCContext } from '../src/webrtc-context.js'
// Local signaling implementation for testing
class LocalSignaler {
constructor(name, remoteName) {
this.name = name
this.remoteName = remoteName
this.iceCandidates = []
this.iceListeners = []
this.remote = null
this.remoteIceCandidates = []
this.offerCallbacks = []
this.answerCallbacks = []
}
// Link two signalers together
linkTo(remoteSignaler) {
this.remote = remoteSignaler
this.remoteIceCandidates = remoteSignaler.iceCandidates
}
// Set local offer (called when offer is created)
setOffer(offer) {
console.log(`[${this.name}] Setting offer`)
// Notify remote peer about the offer
if (this.remote) {
this.remote.offerCallbacks.forEach(callback => callback(offer))
}
}
// Set local answer (called when answer is created)
setAnswer(answer) {
console.log(`[${this.name}] Setting answer`)
// Notify remote peer about the answer
if (this.remote) {
this.remote.answerCallbacks.forEach(callback => callback(answer))
}
}
// Listen for offers from remote peer
addOfferListener(callback) {
this.offerCallbacks.push(callback)
return () => {
const index = this.offerCallbacks.indexOf(callback)
if (index > -1) {
this.offerCallbacks.splice(index, 1)
}
}
}
// Listen for answers from remote peer
addAnswerListener(callback) {
this.answerCallbacks.push(callback)
return () => {
const index = this.answerCallbacks.indexOf(callback)
if (index > -1) {
this.answerCallbacks.splice(index, 1)
}
}
}
// Add local ICE candidate (called by local connection)
addIceCandidate(candidate) {
console.log(`[${this.name}] Adding ICE candidate:`, candidate.candidate)
this.iceCandidates.push(candidate)
// Immediately send to remote peer if linked
if (this.remote) {
setTimeout(() => {
this.remote.iceListeners.forEach(listener => {
console.log(`[${this.name}] Sending ICE to ${this.remoteName}`)
listener(candidate)
})
}, 10)
}
}
// Listen for remote ICE candidates
addListener(callback) {
console.log(`[${this.name}] Adding ICE listener`)
this.iceListeners.push(callback)
// Send any existing remote candidates
this.remoteIceCandidates.forEach(candidate => {
setTimeout(() => callback(candidate), 10)
})
return () => {
const index = this.iceListeners.indexOf(callback)
if (index > -1) {
this.iceListeners.splice(index, 1)
}
}
}
}
// Create signalers for host and client
const hostSignaler = new LocalSignaler('HOST', 'CLIENT')
const clientSignaler = new LocalSignaler('CLIENT', 'HOST')
// Link them together for bidirectional communication
hostSignaler.linkTo(clientSignaler)
clientSignaler.linkTo(hostSignaler)
// Store connections
let hostConnection = null
let clientConnection = null
// UI Update functions
function updateStatus(peer, state) {
const statusEl = document.getElementById(`status-${peer}`)
if (statusEl) {
statusEl.className = `status ${state}`
statusEl.textContent = state.charAt(0).toUpperCase() + state.slice(1)
}
}
function addLog(peer, message) {
const logEl = document.getElementById(`log-${peer}`)
if (logEl) {
const time = new Date().toLocaleTimeString()
logEl.innerHTML += `<div class="log-entry">[${time}] ${message}</div>`
logEl.scrollTop = logEl.scrollHeight
}
}
// Create Host (Offerer)
async function createHost() {
try {
addLog('a', 'Creating host connection (offerer)...')
const hostContext = new WebRTCContext(hostSignaler)
hostConnection = new WebRTCRondevuConnection({
id: 'test-connection',
host: 'client-peer',
service: 'test.demo@1.0.0',
offer: null,
context: hostContext,
})
// Listen for state changes
hostConnection.events.on('state-change', state => {
console.log('[HOST] State changed:', state)
updateStatus('a', state)
addLog('a', `State changed to: ${state}`)
})
// Listen for messages
hostConnection.events.on('message', message => {
console.log('[HOST] Received message:', message)
addLog('a', `📨 Received: ${message}`)
})
addLog('a', '✅ Host connection created')
updateStatus('a', 'connecting')
// Wait for host to be ready (offer created and set)
await hostConnection.ready
addLog('a', '✅ Host offer created')
// Get the offer
const offer = hostConnection.connection.localDescription
document.getElementById('offer-a').value = JSON.stringify(offer, null, 2)
addLog('a', 'Offer ready to send to client')
} catch (error) {
console.error('[HOST] Error:', error)
addLog('a', `❌ Error: ${error.message}`)
updateStatus('a', 'disconnected')
}
}
// Create Client (Answerer)
async function createClient() {
try {
addLog('b', 'Creating client connection (answerer)...')
// Get offer from host
if (!hostConnection) {
alert('Please create host first!')
return
}
const offer = hostConnection.connection.localDescription
if (!offer) {
alert('Host offer not ready yet!')
return
}
addLog('b', 'Got offer from host')
const clientContext = new WebRTCContext(clientSignaler)
clientConnection = new WebRTCRondevuConnection({
id: 'test-connection',
host: 'host-peer',
service: 'test.demo@1.0.0',
offer: offer,
context: clientContext,
})
// Listen for state changes
clientConnection.events.on('state-change', state => {
console.log('[CLIENT] State changed:', state)
updateStatus('b', state)
addLog('b', `State changed to: ${state}`)
})
// Listen for messages
clientConnection.events.on('message', message => {
console.log('[CLIENT] Received message:', message)
addLog('b', `📨 Received: ${message}`)
})
addLog('b', '✅ Client connection created')
updateStatus('b', 'connecting')
// Wait for client to be ready
await clientConnection.ready
addLog('b', '✅ Client answer created')
// Get the answer
const answer = clientConnection.connection.localDescription
document.getElementById('answer-b').value = JSON.stringify(answer, null, 2)
// Set answer on host
addLog('b', 'Setting answer on host...')
await hostConnection.connection.setRemoteDescription(answer)
addLog('b', '✅ Answer set on host')
addLog('a', '✅ Answer received from client')
} catch (error) {
console.error('[CLIENT] Error:', error)
addLog('b', `❌ Error: ${error.message}`)
updateStatus('b', 'disconnected')
}
}
// Send test message from host to client
function sendFromHost() {
if (!hostConnection) {
alert('Please create host first!')
return
}
const message = document.getElementById('message-a').value || 'Hello from Host!'
addLog('a', `📤 Sending: ${message}`)
hostConnection
.sendMessage(message)
.then(success => {
if (success) {
addLog('a', '✅ Message sent successfully')
} else {
addLog('a', '⚠️ Message queued (not connected)')
}
})
.catch(error => {
addLog('a', `❌ Error sending: ${error.message}`)
})
}
// Send test message from client to host
function sendFromClient() {
if (!clientConnection) {
alert('Please create client first!')
return
}
const message = document.getElementById('message-b').value || 'Hello from Client!'
addLog('b', `📤 Sending: ${message}`)
clientConnection
.sendMessage(message)
.then(success => {
if (success) {
addLog('b', '✅ Message sent successfully')
} else {
addLog('b', '⚠️ Message queued (not connected)')
}
})
.catch(error => {
addLog('b', `❌ Error sending: ${error.message}`)
})
}
// Attach event listeners when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
// Clear all textareas on load
document.getElementById('offer-a').value = ''
document.getElementById('answer-b').value = ''
// Make functions globally available (for console testing)
window.createHost = createHost
window.createClient = createClient
window.sendFromHost = sendFromHost
window.sendFromClient = sendFromClient
console.log('🚀 Local signaling test loaded')
console.log('Steps:')
console.log('1. Click "Create Host" (Peer A)')
console.log('2. Click "Create Client" (Peer B)')
console.log('3. Wait for connection to establish')
console.log('4. Send messages between peers')
})

280
demo/index.html Normal file
View File

@@ -0,0 +1,280 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rondevu WebRTC Local Test</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
font-size: 2.5rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}
.peers {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.peer {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.peer h2 {
color: #667eea;
margin-bottom: 20px;
font-size: 1.5rem;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
}
.status {
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
font-weight: 600;
text-align: center;
}
.status.disconnected {
background: #fee;
color: #c33;
}
.status.connecting {
background: #ffeaa7;
color: #d63031;
}
.status.connected {
background: #d4edda;
color: #155724;
}
.section {
margin-bottom: 20px;
}
.section h3 {
color: #555;
font-size: 0.9rem;
text-transform: uppercase;
margin-bottom: 8px;
letter-spacing: 0.5px;
}
textarea {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
resize: vertical;
min-height: 100px;
transition: border-color 0.3s;
}
textarea:focus {
outline: none;
border-color: #667eea;
}
input[type='text'] {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 0.95rem;
transition: border-color 0.3s;
}
input[type='text']:focus {
outline: none;
border-color: #667eea;
}
button {
background: #667eea;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
width: 100%;
margin-top: 8px;
}
button:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
button:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.log {
max-height: 300px;
overflow-y: auto;
background: #f8f9fa;
border-radius: 8px;
padding: 12px;
font-family: 'Courier New', monospace;
font-size: 0.8rem;
line-height: 1.6;
}
.log-entry {
margin-bottom: 4px;
padding: 4px;
background: white;
border-radius: 4px;
}
.message-box {
display: flex;
gap: 8px;
margin-top: 8px;
}
.message-box input {
flex: 1;
margin: 0;
}
.message-box button {
width: auto;
margin: 0;
padding: 12px 20px;
}
@media (max-width: 768px) {
.peers {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🔗 Rondevu WebRTC Local Test</h1>
<div class="peers">
<!-- Peer A (Host) -->
<div class="peer">
<h2>Peer A (Host/Offerer)</h2>
<div class="status disconnected" id="status-a">Disconnected</div>
<div class="section">
<button onclick="createHost()">1⃣ Create Host Connection</button>
</div>
<div class="section">
<h3>Local Offer (SDP)</h3>
<textarea
id="offer-a"
readonly
placeholder="Offer will appear here..."
></textarea>
</div>
<div class="section">
<h3>Send Message</h3>
<div class="message-box">
<input
type="text"
id="message-a"
placeholder="Type a message..."
onkeypress="if (event.key === 'Enter') sendFromHost()"
/>
<button onclick="sendFromHost()">📤 Send</button>
</div>
</div>
<div class="section">
<h3>Activity Log</h3>
<div class="log" id="log-a"></div>
</div>
</div>
<!-- Peer B (Client) -->
<div class="peer">
<h2>Peer B (Client/Answerer)</h2>
<div class="status disconnected" id="status-b">Disconnected</div>
<div class="section">
<button onclick="createClient()">2⃣ Create Client Connection</button>
</div>
<div class="section">
<h3>Local Answer (SDP)</h3>
<textarea
id="answer-b"
readonly
placeholder="Answer will appear here..."
></textarea>
</div>
<div class="section">
<h3>Send Message</h3>
<div class="message-box">
<input
type="text"
id="message-b"
placeholder="Type a message..."
onkeypress="if (event.key === 'Enter') sendFromClient()"
/>
<button onclick="sendFromClient()">📤 Send</button>
</div>
</div>
<div class="section">
<h3>Activity Log</h3>
<div class="log" id="log-b"></div>
</div>
</div>
</div>
</div>
<script type="module" src="/demo.js"></script>
</body>
</html>