mirror of
https://github.com/xtr-dev/rondevu-client.git
synced 2025-12-13 04:13:25 +00:00
Remove all legacy and backward compatibility support
- Remove serviceFqn, offers array from PublishServiceOptions - Make service and maxOffers required fields - Simplify publishService() to only support automatic mode - Remove RondevuSignaler class completely - Update exports to include new types (ConnectionContext, ConnectToServiceOptions) - Update test-connect.js to use connectToService() - Remove all "manual mode" and "legacy" references from documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
85
README.md
85
README.md
@@ -38,14 +38,14 @@ npm install @xtr-dev/rondevu-client
|
||||
```typescript
|
||||
import { Rondevu } from '@xtr-dev/rondevu-client'
|
||||
|
||||
// 1. Connect to Rondevu with ICE server preset (generates keypair, username auto-claimed on first request)
|
||||
// 1. Connect to Rondevu (generates keypair, username auto-claimed on first request)
|
||||
const rondevu = await Rondevu.connect({
|
||||
apiUrl: 'https://api.ronde.vu',
|
||||
username: 'alice', // Or omit for anonymous username
|
||||
iceServers: 'ipv4-turn' // Use preset: 'ipv4-turn', 'hostname-turns', 'google-stun', or 'relay-only'
|
||||
})
|
||||
|
||||
// 2. Publish service with custom offer factory for event handling
|
||||
// 2. Publish service
|
||||
await rondevu.publishService({
|
||||
service: 'chat:1.0.0',
|
||||
maxOffers: 5, // Maintain up to 5 concurrent offers
|
||||
@@ -70,7 +70,7 @@ await rondevu.publishService({
|
||||
ttl: 300000
|
||||
})
|
||||
|
||||
// 3. Start accepting connections (auto-fills offers and polls)
|
||||
// 3. Start accepting connections
|
||||
await rondevu.startFilling()
|
||||
|
||||
// 4. Stop when done
|
||||
@@ -79,8 +79,6 @@ await rondevu.startFilling()
|
||||
|
||||
### Connecting to a Service (Answerer)
|
||||
|
||||
**Automatic mode (recommended):**
|
||||
|
||||
```typescript
|
||||
import { Rondevu } from '@xtr-dev/rondevu-client'
|
||||
|
||||
@@ -91,7 +89,7 @@ const rondevu = await Rondevu.connect({
|
||||
iceServers: 'ipv4-turn'
|
||||
})
|
||||
|
||||
// 2. Connect to service (automatic setup)
|
||||
// 2. Connect to service
|
||||
const connection = await rondevu.connectToService({
|
||||
serviceFqn: 'chat:1.0.0@alice',
|
||||
onConnection: ({ dc, peerUsername }) => {
|
||||
@@ -112,81 +110,6 @@ 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
|
||||
const pc = new RTCPeerConnection({
|
||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
||||
})
|
||||
|
||||
// 4. Set remote offer and create answer
|
||||
await pc.setRemoteDescription({ type: 'offer', sdp: serviceData.sdp })
|
||||
|
||||
const answer = await pc.createAnswer()
|
||||
await pc.setLocalDescription(answer)
|
||||
|
||||
// 5. Send answer
|
||||
await rondevu.postOfferAnswer(
|
||||
serviceData.serviceFqn,
|
||||
serviceData.offerId,
|
||||
answer.sdp
|
||||
)
|
||||
|
||||
// 6. Send ICE candidates
|
||||
pc.onicecandidate = (event) => {
|
||||
if (event.candidate) {
|
||||
rondevu.addOfferIceCandidates(
|
||||
serviceData.serviceFqn,
|
||||
serviceData.offerId,
|
||||
[event.candidate.toJSON()]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Poll for ICE candidates
|
||||
let lastIceTimestamp = 0
|
||||
const iceInterval = setInterval(async () => {
|
||||
const result = await rondevu.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
|
||||
}
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
// 8. Handle data channel
|
||||
pc.ondatachannel = (event) => {
|
||||
const dc = event.channel
|
||||
|
||||
dc.onmessage = (event) => {
|
||||
console.log('Received:', event.data)
|
||||
}
|
||||
|
||||
dc.onopen = () => {
|
||||
dc.send('Hello from Bob!')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Rondevu Class
|
||||
|
||||
12
src/index.ts
12
src/index.ts
@@ -5,7 +5,6 @@
|
||||
|
||||
export { Rondevu } from './rondevu.js'
|
||||
export { RondevuAPI } from './api.js'
|
||||
export { RondevuSignaler } from './rondevu-signaler.js'
|
||||
export { RpcBatcher } from './rpc-batcher.js'
|
||||
|
||||
// Export crypto adapters
|
||||
@@ -27,9 +26,14 @@ export type {
|
||||
IceCandidate,
|
||||
} from './api.js'
|
||||
|
||||
export type { RondevuOptions, PublishServiceOptions } from './rondevu.js'
|
||||
|
||||
export type { PollingConfig } from './rondevu-signaler.js'
|
||||
export type {
|
||||
RondevuOptions,
|
||||
PublishServiceOptions,
|
||||
ConnectToServiceOptions,
|
||||
ConnectionContext,
|
||||
OfferContext,
|
||||
OfferFactory
|
||||
} from './rondevu.js'
|
||||
|
||||
export type { CryptoAdapter } from './crypto-adapter.js'
|
||||
|
||||
|
||||
@@ -1,478 +0,0 @@
|
||||
import { Signaler, Binnable } from './types.js'
|
||||
import { Rondevu } from './rondevu.js'
|
||||
|
||||
export interface PollingConfig {
|
||||
initialInterval?: number // Default: 500ms
|
||||
maxInterval?: number // Default: 5000ms
|
||||
backoffMultiplier?: number // Default: 1.5
|
||||
maxRetries?: number // Default: 50 (50 seconds max)
|
||||
jitter?: boolean // Default: true
|
||||
}
|
||||
|
||||
/**
|
||||
* RondevuSignaler - Handles WebRTC signaling via Rondevu service
|
||||
*
|
||||
* Manages offer/answer exchange and ICE candidate polling for establishing
|
||||
* WebRTC connections through the Rondevu signaling server.
|
||||
*
|
||||
* Supports configurable polling with exponential backoff and jitter to reduce
|
||||
* server load and prevent thundering herd issues.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const signaler = new RondevuSignaler(
|
||||
* rondevuService,
|
||||
* 'chat.app@1.0.0',
|
||||
* 'peer-username',
|
||||
* { initialInterval: 500, maxInterval: 5000, jitter: true }
|
||||
* )
|
||||
*
|
||||
* // For offerer:
|
||||
* await signaler.setOffer(offer)
|
||||
* signaler.addAnswerListener(answer => {
|
||||
* // Handle remote answer
|
||||
* })
|
||||
*
|
||||
* // For answerer:
|
||||
* signaler.addOfferListener(offer => {
|
||||
* // Handle remote offer
|
||||
* })
|
||||
* await signaler.setAnswer(answer)
|
||||
* ```
|
||||
*/
|
||||
export class RondevuSignaler implements Signaler {
|
||||
private offerId: string | null = null
|
||||
private serviceFqn: string | null = null
|
||||
private offerListeners: Array<(offer: RTCSessionDescriptionInit) => void> = []
|
||||
private answerListeners: Array<(answer: RTCSessionDescriptionInit) => void> = []
|
||||
private iceListeners: Array<(candidate: RTCIceCandidate) => void> = []
|
||||
private pollingTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
private icePollingTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
private lastPollTimestamp = 0
|
||||
private isPolling = false
|
||||
private isOfferer = false
|
||||
private pollingConfig: Required<PollingConfig>
|
||||
|
||||
constructor(
|
||||
private readonly rondevu: Rondevu,
|
||||
private readonly service: string,
|
||||
private readonly host?: string,
|
||||
pollingConfig?: PollingConfig
|
||||
) {
|
||||
this.pollingConfig = {
|
||||
initialInterval: pollingConfig?.initialInterval ?? 500,
|
||||
maxInterval: pollingConfig?.maxInterval ?? 5000,
|
||||
backoffMultiplier: pollingConfig?.backoffMultiplier ?? 1.5,
|
||||
maxRetries: pollingConfig?.maxRetries ?? 50,
|
||||
jitter: pollingConfig?.jitter ?? true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an offer as a service
|
||||
* Used by the offerer to make their offer available
|
||||
*/
|
||||
async setOffer(offer: RTCSessionDescriptionInit): Promise<void> {
|
||||
if (!offer.sdp) {
|
||||
throw new Error('Offer SDP is required')
|
||||
}
|
||||
|
||||
// Publish service with the offer SDP
|
||||
const publishedService = await this.rondevu.publishService({
|
||||
serviceFqn: this.service,
|
||||
offers: [{ sdp: offer.sdp }],
|
||||
ttl: 300000, // 5 minutes
|
||||
})
|
||||
|
||||
// Get the first offer from the published service
|
||||
if (!publishedService.offers || publishedService.offers.length === 0) {
|
||||
throw new Error('No offers returned from service publication')
|
||||
}
|
||||
|
||||
this.offerId = publishedService.offers[0].offerId
|
||||
this.serviceFqn = publishedService.serviceFqn
|
||||
this.isOfferer = true
|
||||
|
||||
// Start combined polling for answers and ICE candidates
|
||||
this.startPolling()
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an answer to the offerer
|
||||
* Used by the answerer to respond to an offer
|
||||
*/
|
||||
async setAnswer(answer: RTCSessionDescriptionInit): Promise<void> {
|
||||
if (!answer.sdp) {
|
||||
throw new Error('Answer SDP is required')
|
||||
}
|
||||
|
||||
if (!this.serviceFqn || !this.offerId) {
|
||||
throw new Error('No service FQN or offer ID available. Must receive offer first.')
|
||||
}
|
||||
|
||||
// Send answer to the service
|
||||
await this.rondevu.getAPIPublic().answerOffer(this.serviceFqn, this.offerId, answer.sdp)
|
||||
this.isOfferer = false
|
||||
|
||||
// Start polling for ICE candidates (answerer uses separate endpoint)
|
||||
this.startIcePolling()
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for incoming offers
|
||||
* Used by the answerer to receive offers from the offerer
|
||||
*/
|
||||
addOfferListener(callback: (offer: RTCSessionDescriptionInit) => void): Binnable {
|
||||
this.offerListeners.push(callback)
|
||||
|
||||
// If we have a host, start searching for their service
|
||||
if (this.host && !this.isPolling) {
|
||||
this.searchForOffer()
|
||||
}
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
const index = this.offerListeners.indexOf(callback)
|
||||
if (index > -1) {
|
||||
this.offerListeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for incoming answers
|
||||
* Used by the offerer to receive the answer from the answerer
|
||||
*/
|
||||
addAnswerListener(callback: (answer: RTCSessionDescriptionInit) => void): Binnable {
|
||||
this.answerListeners.push(callback)
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
const index = this.answerListeners.indexOf(callback)
|
||||
if (index > -1) {
|
||||
this.answerListeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an ICE candidate to the remote peer
|
||||
*/
|
||||
async addIceCandidate(candidate: RTCIceCandidate): Promise<void> {
|
||||
if (!this.serviceFqn || !this.offerId) {
|
||||
console.warn('Cannot send ICE candidate: no service FQN or offer ID')
|
||||
return
|
||||
}
|
||||
|
||||
const candidateData = candidate.toJSON()
|
||||
|
||||
// Skip empty candidates
|
||||
if (!candidateData.candidate || candidateData.candidate === '') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.rondevu.getAPIPublic().addOfferIceCandidates(
|
||||
this.serviceFqn,
|
||||
this.offerId,
|
||||
[candidateData]
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('Failed to send ICE candidate:', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for ICE candidates from the remote peer
|
||||
*/
|
||||
addListener(callback: (candidate: RTCIceCandidate) => void): Binnable {
|
||||
this.iceListeners.push(callback)
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
const index = this.iceListeners.indexOf(callback)
|
||||
if (index > -1) {
|
||||
this.iceListeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an offer from the host
|
||||
* Used by the answerer to find the offerer's service
|
||||
*/
|
||||
private async searchForOffer(): Promise<void> {
|
||||
if (!this.host) {
|
||||
throw new Error('No host specified for offer search')
|
||||
}
|
||||
|
||||
this.isPolling = true
|
||||
|
||||
try {
|
||||
// Get service by FQN (service should include @username)
|
||||
const serviceFqn = `${this.service}@${this.host}`
|
||||
const serviceData = await this.rondevu.getAPIPublic().getService(serviceFqn)
|
||||
|
||||
if (!serviceData) {
|
||||
console.warn(`No service found for ${serviceFqn}`)
|
||||
this.isPolling = false
|
||||
return
|
||||
}
|
||||
|
||||
// Store service details
|
||||
this.offerId = serviceData.offerId
|
||||
this.serviceFqn = serviceData.serviceFqn
|
||||
|
||||
// Notify offer listeners
|
||||
const offer: RTCSessionDescriptionInit = {
|
||||
type: 'offer',
|
||||
sdp: serviceData.sdp,
|
||||
}
|
||||
|
||||
this.offerListeners.forEach(listener => {
|
||||
try {
|
||||
listener(offer)
|
||||
} catch (err) {
|
||||
console.error('Offer listener error:', err)
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Failed to search for offer:', err)
|
||||
this.isPolling = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start combined polling for answers and ICE candidates (offerer side)
|
||||
* Uses poll() for efficient batch polling
|
||||
*/
|
||||
private startPolling(): void {
|
||||
if (this.pollingTimeout || !this.isOfferer) {
|
||||
return
|
||||
}
|
||||
|
||||
let interval = this.pollingConfig.initialInterval
|
||||
let retries = 0
|
||||
let answerReceived = false
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const result = await this.rondevu.poll(this.lastPollTimestamp)
|
||||
|
||||
let foundActivity = false
|
||||
|
||||
// Process answers
|
||||
if (result.answers.length > 0 && !answerReceived) {
|
||||
foundActivity = true
|
||||
|
||||
// Find answer for our offerId
|
||||
const answer = result.answers.find(a => a.offerId === this.offerId)
|
||||
|
||||
if (answer && answer.sdp) {
|
||||
answerReceived = true
|
||||
|
||||
const answerDesc: RTCSessionDescriptionInit = {
|
||||
type: 'answer',
|
||||
sdp: answer.sdp,
|
||||
}
|
||||
|
||||
this.answerListeners.forEach(listener => {
|
||||
try {
|
||||
listener(answerDesc)
|
||||
} catch (err) {
|
||||
console.error('Answer listener error:', err)
|
||||
}
|
||||
})
|
||||
|
||||
this.lastPollTimestamp = Math.max(this.lastPollTimestamp, answer.answeredAt)
|
||||
}
|
||||
}
|
||||
|
||||
// Process ICE candidates for our offer
|
||||
if (this.offerId && result.iceCandidates[this.offerId]) {
|
||||
const candidates = result.iceCandidates[this.offerId]
|
||||
|
||||
// Filter for answerer candidates (offerer receives answerer's candidates)
|
||||
const answererCandidates = candidates.filter(c => c.role === 'answerer')
|
||||
|
||||
if (answererCandidates.length > 0) {
|
||||
foundActivity = true
|
||||
|
||||
for (const item of answererCandidates) {
|
||||
if (item.candidate && item.candidate.candidate && item.candidate.candidate !== '') {
|
||||
try {
|
||||
const rtcCandidate = new RTCIceCandidate(item.candidate)
|
||||
|
||||
this.iceListeners.forEach(listener => {
|
||||
try {
|
||||
listener(rtcCandidate)
|
||||
} catch (err) {
|
||||
console.error('ICE listener error:', err)
|
||||
}
|
||||
})
|
||||
|
||||
this.lastPollTimestamp = Math.max(this.lastPollTimestamp, item.createdAt)
|
||||
} catch (err) {
|
||||
console.warn('Failed to process ICE candidate:', err)
|
||||
this.lastPollTimestamp = Math.max(this.lastPollTimestamp, item.createdAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust interval based on activity
|
||||
if (foundActivity) {
|
||||
interval = this.pollingConfig.initialInterval
|
||||
retries = 0
|
||||
} else {
|
||||
retries++
|
||||
if (retries > this.pollingConfig.maxRetries) {
|
||||
console.warn('Max retries reached for polling')
|
||||
this.stopPolling()
|
||||
return
|
||||
}
|
||||
|
||||
interval = Math.min(
|
||||
interval * this.pollingConfig.backoffMultiplier,
|
||||
this.pollingConfig.maxInterval
|
||||
)
|
||||
}
|
||||
|
||||
// Add jitter to prevent thundering herd
|
||||
const finalInterval = this.pollingConfig.jitter
|
||||
? interval + Math.random() * 100
|
||||
: interval
|
||||
|
||||
this.pollingTimeout = setTimeout(poll, finalInterval)
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error polling offers:', err)
|
||||
|
||||
// Retry with backoff
|
||||
const finalInterval = this.pollingConfig.jitter
|
||||
? interval + Math.random() * 100
|
||||
: interval
|
||||
this.pollingTimeout = setTimeout(poll, finalInterval)
|
||||
}
|
||||
}
|
||||
|
||||
poll() // Start immediately
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop combined polling
|
||||
*/
|
||||
private stopPolling(): void {
|
||||
if (this.pollingTimeout) {
|
||||
clearTimeout(this.pollingTimeout)
|
||||
this.pollingTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start polling for ICE candidates (answerer side only)
|
||||
* Answerers use the separate endpoint since they don't have offers to poll
|
||||
*/
|
||||
private startIcePolling(): void {
|
||||
if (this.icePollingTimeout || !this.serviceFqn || !this.offerId || this.isOfferer) {
|
||||
return
|
||||
}
|
||||
|
||||
let interval = this.pollingConfig.initialInterval
|
||||
|
||||
const poll = async () => {
|
||||
if (!this.serviceFqn || !this.offerId) {
|
||||
this.stopIcePolling()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.rondevu
|
||||
.getAPIPublic()
|
||||
.getOfferIceCandidates(this.serviceFqn, this.offerId, this.lastPollTimestamp)
|
||||
|
||||
let foundCandidates = false
|
||||
|
||||
for (const item of result.candidates) {
|
||||
if (item.candidate && item.candidate.candidate && item.candidate.candidate !== '') {
|
||||
foundCandidates = true
|
||||
try {
|
||||
const rtcCandidate = new RTCIceCandidate(item.candidate)
|
||||
|
||||
this.iceListeners.forEach(listener => {
|
||||
try {
|
||||
listener(rtcCandidate)
|
||||
} catch (err) {
|
||||
console.error('ICE listener error:', err)
|
||||
}
|
||||
})
|
||||
|
||||
this.lastPollTimestamp = item.createdAt
|
||||
} catch (err) {
|
||||
console.warn('Failed to process ICE candidate:', err)
|
||||
this.lastPollTimestamp = item.createdAt
|
||||
}
|
||||
} else {
|
||||
this.lastPollTimestamp = item.createdAt
|
||||
}
|
||||
}
|
||||
|
||||
// If candidates found, reset interval to initial value
|
||||
// Otherwise, increase interval with backoff
|
||||
if (foundCandidates) {
|
||||
interval = this.pollingConfig.initialInterval
|
||||
} else {
|
||||
interval = Math.min(
|
||||
interval * this.pollingConfig.backoffMultiplier,
|
||||
this.pollingConfig.maxInterval
|
||||
)
|
||||
}
|
||||
|
||||
// Add jitter
|
||||
const finalInterval = this.pollingConfig.jitter
|
||||
? interval + Math.random() * 100
|
||||
: interval
|
||||
|
||||
this.icePollingTimeout = setTimeout(poll, finalInterval)
|
||||
|
||||
} catch (err) {
|
||||
// 404/410 means offer expired, stop polling
|
||||
if (err instanceof Error && (err.message?.includes('404') || err.message?.includes('410'))) {
|
||||
console.warn('Offer not found or expired, stopping ICE polling')
|
||||
this.stopIcePolling()
|
||||
} else if (err instanceof Error && !err.message?.includes('404')) {
|
||||
console.error('Error polling for ICE candidates:', err)
|
||||
// Continue polling despite errors
|
||||
const finalInterval = this.pollingConfig.jitter
|
||||
? interval + Math.random() * 100
|
||||
: interval
|
||||
this.icePollingTimeout = setTimeout(poll, finalInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
poll() // Start immediately
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop polling for ICE candidates
|
||||
*/
|
||||
private stopIcePolling(): void {
|
||||
if (this.icePollingTimeout) {
|
||||
clearTimeout(this.icePollingTimeout)
|
||||
this.icePollingTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all polling and cleanup
|
||||
*/
|
||||
dispose(): void {
|
||||
this.stopPolling()
|
||||
this.stopIcePolling()
|
||||
this.offerListeners = []
|
||||
this.answerListeners = []
|
||||
this.iceListeners = []
|
||||
}
|
||||
}
|
||||
@@ -67,12 +67,10 @@ export interface OfferContext {
|
||||
export type OfferFactory = (rtcConfig: RTCConfiguration) => Promise<OfferContext>
|
||||
|
||||
export interface PublishServiceOptions {
|
||||
service?: string // Service name and version (e.g., "chat:2.0.0") - username will be auto-appended
|
||||
serviceFqn?: string // Full service FQN (legacy, use 'service' instead)
|
||||
maxOffers?: number // Maximum number of concurrent offers to maintain (automatic mode)
|
||||
offers?: Array<{ sdp: string }> // Manual offers array (legacy mode)
|
||||
service: string // Service name and version (e.g., "chat:2.0.0") - username will be auto-appended
|
||||
maxOffers: number // Maximum number of concurrent offers to maintain
|
||||
offerFactory?: OfferFactory // Optional: custom offer creation (defaults to simple data channel)
|
||||
ttl?: number // Time-to-live for offers in milliseconds
|
||||
ttl?: number // Time-to-live for offers in milliseconds (default: 300000)
|
||||
}
|
||||
|
||||
export interface ConnectionContext {
|
||||
@@ -312,18 +310,10 @@ export class Rondevu {
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a service
|
||||
*
|
||||
* Two modes:
|
||||
* 1. Automatic offer management (recommended):
|
||||
* Pass maxOffers and optionally offerFactory
|
||||
* Publish a service with automatic offer management
|
||||
* Call startFilling() to begin accepting connections
|
||||
*
|
||||
* 2. Manual mode (legacy):
|
||||
* Pass offers array with pre-created SDP offers
|
||||
* Returns published service data
|
||||
*
|
||||
* @example Automatic mode:
|
||||
* @example
|
||||
* ```typescript
|
||||
* await rondevu.publishService({
|
||||
* service: 'chat:2.0.0',
|
||||
@@ -331,50 +321,17 @@ export class Rondevu {
|
||||
* })
|
||||
* await rondevu.startFilling()
|
||||
* ```
|
||||
*
|
||||
* @example Manual mode (legacy):
|
||||
* ```typescript
|
||||
* const published = await rondevu.publishService({
|
||||
* serviceFqn: 'chat:2.0.0@alice',
|
||||
* offers: [{ sdp: offerSdp }]
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
async publishService(options: PublishServiceOptions): Promise<any> {
|
||||
const { service, serviceFqn, maxOffers, offers, offerFactory, ttl } = options
|
||||
async publishService(options: PublishServiceOptions): Promise<void> {
|
||||
const { service, maxOffers, offerFactory, ttl } = options
|
||||
|
||||
// Manual mode (legacy) - publish pre-created offers
|
||||
if (offers && offers.length > 0) {
|
||||
const fqn = serviceFqn || `${service}@${this.username}`
|
||||
const result = await this.api.publishService({
|
||||
serviceFqn: fqn,
|
||||
offers,
|
||||
ttl: ttl || 300000,
|
||||
signature: '',
|
||||
message: '',
|
||||
})
|
||||
this.usernameClaimed = true
|
||||
return result
|
||||
}
|
||||
|
||||
// Automatic mode - store configuration for startFilling()
|
||||
if (maxOffers !== undefined) {
|
||||
const svc = service || serviceFqn?.split('@')[0]
|
||||
if (!svc) {
|
||||
throw new Error('Either service or serviceFqn must be provided')
|
||||
}
|
||||
|
||||
this.currentService = svc
|
||||
this.currentService = service
|
||||
this.maxOffers = maxOffers
|
||||
this.offerFactory = offerFactory || this.defaultOfferFactory.bind(this)
|
||||
this.ttl = ttl || 300000
|
||||
|
||||
console.log(`[Rondevu] Publishing service: ${svc} with maxOffers: ${maxOffers}`)
|
||||
console.log(`[Rondevu] Publishing service: ${service} with maxOffers: ${maxOffers}`)
|
||||
this.usernameClaimed = true
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error('Either maxOffers (automatic mode) or offers array (manual mode) must be provided')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user