Add connectToService() for automatic answering side setup

- Add ConnectToServiceOptions and ConnectionContext interfaces
- Implement connectToService() method that handles entire answering flow
- Automatically discovers/gets service, creates RTCPeerConnection, exchanges answer and ICE candidates
- Supports both direct lookup (serviceFqn) and discovery (service)
- Returns connection context with pc, dc, serviceFqn, offerId, and peerUsername
- Update README with automatic mode examples

🤖 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-12 22:26:44 +01:00
parent e5c82b75b1
commit ec19ce50db
2 changed files with 209 additions and 2 deletions

View File

@@ -79,6 +79,8 @@ await rondevu.startFilling()
### Connecting to a Service (Answerer)
**Automatic mode (recommended):**
```typescript
import { Rondevu } from '@xtr-dev/rondevu-client'
@@ -86,13 +88,46 @@ import { Rondevu } from '@xtr-dev/rondevu-client'
const rondevu = await Rondevu.connect({
apiUrl: 'https://api.ronde.vu',
username: 'bob',
iceServers: 'ipv4-turn' // Use same preset as offerer
iceServers: 'ipv4-turn'
})
// 2. Connect to service (automatic setup)
const connection = await rondevu.connectToService({
serviceFqn: 'chat:1.0.0@alice',
onConnection: ({ dc, peerUsername }) => {
console.log('Connected to', peerUsername)
dc.addEventListener('message', (e) => {
console.log('Received:', e.data)
})
dc.addEventListener('open', () => {
dc.send('Hello from Bob!')
})
}
})
// Access connection
connection.dc.send('Another message')
connection.pc.close() // Close when done
```
**Manual mode (legacy):**
```typescript
import { Rondevu } from '@xtr-dev/rondevu-client'
// 1. Connect to Rondevu
const rondevu = await Rondevu.connect({
apiUrl: 'https://api.ronde.vu',
username: 'bob',
iceServers: 'ipv4-turn'
})
// 2. Get service offer
const serviceData = await rondevu.getService('chat:1.0.0@alice')
// 3. Create peer connection (use custom ICE servers if needed)
// 3. Create peer connection
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
})

View File

@@ -75,6 +75,22 @@ export interface PublishServiceOptions {
ttl?: number // Time-to-live for offers in milliseconds
}
export interface ConnectionContext {
pc: RTCPeerConnection
dc: RTCDataChannel
serviceFqn: string
offerId: string
peerUsername: string
}
export interface ConnectToServiceOptions {
serviceFqn?: string // Full FQN like 'chat:2.0.0@alice'
service?: string // Service without username (for discovery)
username?: string // Target username (combined with service)
onConnection?: (context: ConnectionContext) => void | Promise<void> // Called when data channel opens
rtcConfig?: RTCConfiguration // Optional: override default ICE servers
}
interface ActiveOffer {
offerId: string
serviceFqn: string
@@ -547,6 +563,162 @@ export class Rondevu {
this.activeOffers.clear()
}
/**
* Automatically connect to a service (answerer side)
* Handles the entire connection flow: discovery, WebRTC setup, answer exchange, ICE candidates
*
* @example
* ```typescript
* // Connect to specific user
* const connection = await rondevu.connectToService({
* serviceFqn: 'chat:2.0.0@alice',
* onConnection: ({ dc, peerUsername }) => {
* console.log('Connected to', peerUsername)
* dc.addEventListener('message', (e) => console.log(e.data))
* dc.addEventListener('open', () => dc.send('Hello!'))
* }
* })
*
* // Discover random service
* const connection = await rondevu.connectToService({
* service: 'chat:2.0.0',
* onConnection: ({ dc, peerUsername }) => {
* console.log('Connected to', peerUsername)
* }
* })
* ```
*/
async connectToService(options: ConnectToServiceOptions): Promise<ConnectionContext> {
const { serviceFqn, service, username, onConnection, rtcConfig } = options
// Determine the full service FQN
let fqn: string
if (serviceFqn) {
fqn = serviceFqn
} else if (service && username) {
fqn = `${service}@${username}`
} else if (service) {
// Discovery mode - get random service
console.log(`[Rondevu] Discovering service: ${service}`)
const discovered = await this.discoverService(service)
fqn = discovered.serviceFqn
} else {
throw new Error('Either serviceFqn or service must be provided')
}
console.log(`[Rondevu] Connecting to service: ${fqn}`)
// 1. Get service offer
const serviceData = await this.api.getService(fqn)
console.log(`[Rondevu] Found service from @${serviceData.username}`)
// 2. Create RTCPeerConnection
const rtcConfiguration = rtcConfig || {
iceServers: this.iceServers
}
const pc = new RTCPeerConnection(rtcConfiguration)
// 3. Set up data channel handler (answerer receives it from offerer)
let dc: RTCDataChannel | null = null
const dataChannelPromise = new Promise<RTCDataChannel>((resolve) => {
pc.ondatachannel = (event) => {
console.log('[Rondevu] Data channel received from offerer')
dc = event.channel
resolve(dc)
}
})
// 4. Set up ICE candidate exchange
pc.onicecandidate = async (event) => {
if (event.candidate) {
try {
await this.api.addOfferIceCandidates(
serviceData.serviceFqn,
serviceData.offerId,
[event.candidate.toJSON()]
)
} catch (err) {
console.error('[Rondevu] Failed to send ICE candidate:', err)
}
}
}
// 5. Poll for remote ICE candidates
let lastIceTimestamp = 0
const icePollInterval = setInterval(async () => {
try {
const result = await this.api.getOfferIceCandidates(
serviceData.serviceFqn,
serviceData.offerId,
lastIceTimestamp
)
for (const item of result.candidates) {
if (item.candidate) {
await pc.addIceCandidate(new RTCIceCandidate(item.candidate))
lastIceTimestamp = item.createdAt
}
}
} catch (err) {
console.error('[Rondevu] Failed to poll ICE candidates:', err)
}
}, 1000)
// 6. Set remote description
await pc.setRemoteDescription({
type: 'offer',
sdp: serviceData.sdp
})
// 7. Create and send answer
const answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
await this.api.answerOffer(
serviceData.serviceFqn,
serviceData.offerId,
answer.sdp!
)
// 8. Wait for data channel to be established
dc = await dataChannelPromise
// Create connection context
const context: ConnectionContext = {
pc,
dc,
serviceFqn: serviceData.serviceFqn,
offerId: serviceData.offerId,
peerUsername: serviceData.username
}
// 9. Set up connection state monitoring
pc.onconnectionstatechange = () => {
console.log(`[Rondevu] Connection state: ${pc.connectionState}`)
if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
clearInterval(icePollInterval)
}
}
// 10. Wait for data channel to open and call onConnection
if (dc.readyState === 'open') {
console.log('[Rondevu] Data channel already open')
if (onConnection) {
await onConnection(context)
}
} else {
await new Promise<void>((resolve) => {
dc!.addEventListener('open', async () => {
console.log('[Rondevu] Data channel opened')
if (onConnection) {
await onConnection(context)
}
resolve()
})
})
}
return context
}
// ============================================
// Service Discovery
// ============================================