mirror of
https://github.com/xtr-dev/rondevu-client.git
synced 2025-12-18 14:43:24 +00:00
Restructure flat src/ directory (17 files) into organized folders:
Structure:
- src/core/ - Main API (rondevu.ts, offer-pool.ts, types.ts, index.ts)
- src/connections/ - WebRTC connections (base.ts, offerer.ts, answerer.ts, config.ts, events.ts)
- src/api/ - HTTP layer (client.ts, batcher.ts)
- src/crypto/ - Crypto adapters (adapter.ts, node.ts, web.ts)
- src/utils/ - Utilities (async-lock.ts, exponential-backoff.ts, message-buffer.ts)
Changes:
- Move all 17 files to appropriate feature folders
- Update all import paths to reflect new structure
- Update package.json main/types to point to dist/core/index.js
- Preserve git history with git mv
Benefits:
- Clear separation of concerns
- Easier navigation and maintenance
- Better scalability for future features
- Logical grouping of related files
🤖 Generated with Claude Code
169 lines
4.8 KiB
TypeScript
169 lines
4.8 KiB
TypeScript
/**
|
|
* Answerer-side WebRTC connection with answer creation and offer processing
|
|
*/
|
|
|
|
import { RondevuConnection } from './base.js'
|
|
import { ConnectionState } from './events.js'
|
|
import { RondevuAPI } from '../api/client.js'
|
|
import { ConnectionConfig } from './config.js'
|
|
|
|
export interface AnswererOptions {
|
|
api: RondevuAPI
|
|
serviceFqn: string
|
|
offerId: string
|
|
offerSdp: string
|
|
rtcConfig?: RTCConfiguration
|
|
config?: Partial<ConnectionConfig>
|
|
}
|
|
|
|
/**
|
|
* Answerer connection - processes offers and creates answers
|
|
*/
|
|
export class AnswererConnection extends RondevuConnection {
|
|
private api: RondevuAPI
|
|
private serviceFqn: string
|
|
private offerId: string
|
|
private offerSdp: string
|
|
|
|
constructor(options: AnswererOptions) {
|
|
super(options.rtcConfig, options.config)
|
|
this.api = options.api
|
|
this.serviceFqn = options.serviceFqn
|
|
this.offerId = options.offerId
|
|
this.offerSdp = options.offerSdp
|
|
}
|
|
|
|
/**
|
|
* Initialize the connection by processing offer and creating answer
|
|
*/
|
|
async initialize(): Promise<void> {
|
|
this.debug('Initializing answerer connection')
|
|
|
|
// Create peer connection
|
|
this.createPeerConnection()
|
|
if (!this.pc) throw new Error('Peer connection not created')
|
|
|
|
// Setup ondatachannel handler BEFORE setting remote description
|
|
// This is critical to avoid race conditions
|
|
this.pc.ondatachannel = (event) => {
|
|
this.debug('Received data channel')
|
|
this.dc = event.channel
|
|
this.setupDataChannelHandlers(this.dc)
|
|
}
|
|
|
|
// Start connection timeout
|
|
this.startConnectionTimeout()
|
|
|
|
// Set remote description (offer)
|
|
await this.pc.setRemoteDescription({
|
|
type: 'offer',
|
|
sdp: this.offerSdp,
|
|
})
|
|
|
|
this.transitionTo(ConnectionState.SIGNALING, 'Offer received, creating answer')
|
|
|
|
// Create and set local description (answer)
|
|
const answer = await this.pc.createAnswer()
|
|
await this.pc.setLocalDescription(answer)
|
|
|
|
this.debug('Answer created, sending to server')
|
|
|
|
// Send answer to server
|
|
await this.api.answerOffer(this.serviceFqn, this.offerId, answer.sdp!)
|
|
|
|
this.debug('Answer sent successfully')
|
|
}
|
|
|
|
/**
|
|
* Handle local ICE candidate generation
|
|
*/
|
|
protected onLocalIceCandidate(candidate: RTCIceCandidate): void {
|
|
this.debug('Generated local ICE candidate')
|
|
|
|
// For answerer, we add ICE candidates to the offer
|
|
// The server will make them available for the offerer to poll
|
|
this.api
|
|
.addOfferIceCandidates(this.serviceFqn, this.offerId, [
|
|
{
|
|
candidate: candidate.candidate,
|
|
sdpMLineIndex: candidate.sdpMLineIndex,
|
|
sdpMid: candidate.sdpMid,
|
|
},
|
|
])
|
|
.catch((error) => {
|
|
this.debug('Failed to send ICE candidate:', error)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Get the API instance
|
|
*/
|
|
protected getApi(): any {
|
|
return this.api
|
|
}
|
|
|
|
/**
|
|
* Get the service FQN
|
|
*/
|
|
protected getServiceFqn(): string {
|
|
return this.serviceFqn
|
|
}
|
|
|
|
/**
|
|
* Answerers accept ICE candidates from offerers only
|
|
*/
|
|
protected getIceCandidateRole(): 'offerer' | null {
|
|
return 'offerer'
|
|
}
|
|
|
|
/**
|
|
* Attempt to reconnect
|
|
*/
|
|
protected attemptReconnect(): void {
|
|
this.debug('Attempting to reconnect')
|
|
|
|
// For answerer, we need to fetch a new offer and create a new answer
|
|
// Clean up old connection
|
|
if (this.pc) {
|
|
this.pc.close()
|
|
this.pc = null
|
|
}
|
|
if (this.dc) {
|
|
this.dc.close()
|
|
this.dc = null
|
|
}
|
|
|
|
// Fetch new offer from service
|
|
this.api
|
|
.getService(this.serviceFqn)
|
|
.then((service) => {
|
|
if (!service || !service.offers || service.offers.length === 0) {
|
|
throw new Error('No offers available for reconnection')
|
|
}
|
|
|
|
// Pick a random offer
|
|
const offer = service.offers[Math.floor(Math.random() * service.offers.length)]
|
|
this.offerId = offer.offerId
|
|
this.offerSdp = offer.sdp
|
|
|
|
// Reinitialize with new offer
|
|
return this.initialize()
|
|
})
|
|
.then(() => {
|
|
this.emit('reconnect:success')
|
|
})
|
|
.catch((error) => {
|
|
this.debug('Reconnection failed:', error)
|
|
this.emit('reconnect:failed', error as Error)
|
|
this.scheduleReconnect()
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Get the offer ID we're answering
|
|
*/
|
|
getOfferId(): string {
|
|
return this.offerId
|
|
}
|
|
}
|