mirror of
https://github.com/xtr-dev/rondevu-client.git
synced 2025-12-13 04:13:25 +00:00
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:
39
README.md
39
README.md
@@ -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' }]
|
||||
})
|
||||
|
||||
172
src/rondevu.ts
172
src/rondevu.ts
@@ -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
|
||||
// ============================================
|
||||
|
||||
Reference in New Issue
Block a user