mirror of
https://github.com/xtr-dev/rondevu-client.git
synced 2025-12-10 10:53:24 +00:00
Implement RondevuAPI and RondevuSignaler classes
Added comprehensive API client and signaling implementation: **RondevuAPI** - Single class for all Rondevu endpoints: - Authentication: register() - Offers: createOffers(), getOffer(), answerOffer(), getAnswer(), searchOffers() - ICE Candidates: addIceCandidates(), getIceCandidates() - Services: publishService(), getService(), searchServices() - Usernames: checkUsername(), claimUsername() **RondevuSignaler** - ICE candidate exchange: - addIceCandidate() - Send local candidates to server - addListener() - Poll for remote candidates (1 second intervals) - Returns cleanup function (Binnable) to stop polling - Handles offer expiration gracefully **WebRTCRondevuConnection** - WebRTC connection wrapper: - Handles offer/answer creation - Manages ICE candidate exchange via Signaler - Type-safe event bus for state changes and messages - Queue and send message interfaces **Utilities**: - createBin() - Cleanup function collector - Binnable type - Cleanup function signature All classes use the shared RondevuAPI client for consistent error handling and authentication. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
375
src/api.ts
Normal file
375
src/api.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* Rondevu API Client - Single class for all API endpoints
|
||||
*/
|
||||
|
||||
export interface Credentials {
|
||||
peerId: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
export interface OfferRequest {
|
||||
sdp: string;
|
||||
topics?: string[];
|
||||
ttl?: number;
|
||||
secret?: string;
|
||||
}
|
||||
|
||||
export interface Offer {
|
||||
id: string;
|
||||
peerId: string;
|
||||
sdp: string;
|
||||
topics: string[];
|
||||
ttl: number;
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
answererPeerId?: string;
|
||||
}
|
||||
|
||||
export interface ServiceRequest {
|
||||
username: string;
|
||||
serviceFqn: string;
|
||||
sdp: string;
|
||||
ttl?: number;
|
||||
isPublic?: boolean;
|
||||
metadata?: Record<string, any>;
|
||||
signature: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface Service {
|
||||
serviceId: string;
|
||||
uuid: string;
|
||||
offerId: string;
|
||||
username: string;
|
||||
serviceFqn: string;
|
||||
isPublic: boolean;
|
||||
metadata?: Record<string, any>;
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
export interface IceCandidate {
|
||||
candidate: RTCIceCandidateInit;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* RondevuAPI - Complete API client for Rondevu signaling server
|
||||
*/
|
||||
export class RondevuAPI {
|
||||
constructor(
|
||||
private baseUrl: string,
|
||||
private credentials?: Credentials
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Authentication header
|
||||
*/
|
||||
private getAuthHeader(): Record<string, string> {
|
||||
if (!this.credentials) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
'Authorization': `Bearer ${this.credentials.peerId}:${this.credentials.secret}`
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Authentication
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Register a new peer and get credentials
|
||||
*/
|
||||
async register(): Promise<Credentials> {
|
||||
const response = await fetch(`${this.baseUrl}/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(`Registration failed: ${error.error || response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Offers
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Create one or more offers
|
||||
*/
|
||||
async createOffers(offers: OfferRequest[]): Promise<Offer[]> {
|
||||
const response = await fetch(`${this.baseUrl}/offers`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...this.getAuthHeader()
|
||||
},
|
||||
body: JSON.stringify({ offers })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(`Failed to create offers: ${error.error || response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get offer by ID
|
||||
*/
|
||||
async getOffer(offerId: string): Promise<Offer> {
|
||||
const response = await fetch(`${this.baseUrl}/offers/${offerId}`, {
|
||||
headers: this.getAuthHeader()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(`Failed to get offer: ${error.error || response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Answer an offer
|
||||
*/
|
||||
async answerOffer(offerId: string, sdp: string, secret?: string): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/offers/${offerId}/answer`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...this.getAuthHeader()
|
||||
},
|
||||
body: JSON.stringify({ sdp, secret })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(`Failed to answer offer: ${error.error || response.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get answer for an offer (offerer polls this)
|
||||
*/
|
||||
async getAnswer(offerId: string): Promise<{ sdp: string } | null> {
|
||||
const response = await fetch(`${this.baseUrl}/offers/${offerId}/answer`, {
|
||||
headers: this.getAuthHeader()
|
||||
});
|
||||
|
||||
if (response.status === 404) {
|
||||
return null; // No answer yet
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(`Failed to get answer: ${error.error || response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search offers by topic
|
||||
*/
|
||||
async searchOffers(topic: string): Promise<Offer[]> {
|
||||
const response = await fetch(`${this.baseUrl}/offers?topic=${encodeURIComponent(topic)}`, {
|
||||
headers: this.getAuthHeader()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(`Failed to search offers: ${error.error || response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ICE Candidates
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Add ICE candidates to an offer
|
||||
*/
|
||||
async addIceCandidates(offerId: string, candidates: RTCIceCandidateInit[]): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/offers/${offerId}/ice-candidates`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...this.getAuthHeader()
|
||||
},
|
||||
body: JSON.stringify({ candidates })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(`Failed to add ICE candidates: ${error.error || response.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ICE candidates for an offer (with polling support)
|
||||
*/
|
||||
async getIceCandidates(offerId: string, since: number = 0): Promise<IceCandidate[]> {
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}/offers/${offerId}/ice-candidates?since=${since}`,
|
||||
{ headers: this.getAuthHeader() }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(`Failed to get ICE candidates: ${error.error || response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Services
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Publish a service
|
||||
*/
|
||||
async publishService(service: ServiceRequest): Promise<Service> {
|
||||
const response = await fetch(`${this.baseUrl}/services`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...this.getAuthHeader()
|
||||
},
|
||||
body: JSON.stringify(service)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(`Failed to publish service: ${error.error || response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service by UUID
|
||||
*/
|
||||
async getService(uuid: string): Promise<Service & { offerId: string; sdp: string }> {
|
||||
const response = await fetch(`${this.baseUrl}/services/${uuid}`, {
|
||||
headers: this.getAuthHeader()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(`Failed to get service: ${error.error || response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search services by username
|
||||
*/
|
||||
async searchServicesByUsername(username: string): Promise<Service[]> {
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}/services?username=${encodeURIComponent(username)}`,
|
||||
{ headers: this.getAuthHeader() }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(`Failed to search services: ${error.error || response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search services by FQN
|
||||
*/
|
||||
async searchServicesByFqn(serviceFqn: string): Promise<Service[]> {
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}/services?serviceFqn=${encodeURIComponent(serviceFqn)}`,
|
||||
{ headers: this.getAuthHeader() }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(`Failed to search services: ${error.error || response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search services by username AND FQN
|
||||
*/
|
||||
async searchServices(username: string, serviceFqn: string): Promise<Service[]> {
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}/services?username=${encodeURIComponent(username)}&serviceFqn=${encodeURIComponent(serviceFqn)}`,
|
||||
{ headers: this.getAuthHeader() }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(`Failed to search services: ${error.error || response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Usernames
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Check if username is available
|
||||
*/
|
||||
async checkUsername(username: string): Promise<{ available: boolean; owner?: string }> {
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}/usernames/${encodeURIComponent(username)}/check`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(`Failed to check username: ${error.error || response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim a username (requires Ed25519 signature)
|
||||
*/
|
||||
async claimUsername(
|
||||
username: string,
|
||||
publicKey: string,
|
||||
signature: string,
|
||||
message: string
|
||||
): Promise<{ success: boolean; username: string }> {
|
||||
const response = await fetch(`${this.baseUrl}/usernames/${encodeURIComponent(username)}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...this.getAuthHeader()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
publicKey,
|
||||
signature,
|
||||
message
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(`Failed to claim username: ${error.error || response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
15
src/bin.ts
Normal file
15
src/bin.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
export type Binnable = () => void | Promise<void>
|
||||
|
||||
export const createBin = () => {
|
||||
const bin: Binnable[] = []
|
||||
return Object.assign(
|
||||
(...rubbish: Binnable[]) => bin.push(...rubbish),
|
||||
{
|
||||
clean: (): void => {
|
||||
bin.forEach(binnable => binnable())
|
||||
bin.length = 0
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
76
src/connection.ts
Normal file
76
src/connection.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import {ConnectionEvents, ConnectionInterface, Message, QueueMessageOptions, Signaler} from "./types";
|
||||
import {EventBus} from "./event-bus";
|
||||
import {createBin} from "./bin";
|
||||
|
||||
export class WebRTCRondevuConnection implements ConnectionInterface {
|
||||
private readonly connection: RTCPeerConnection;
|
||||
private readonly side: 'offer' | 'answer';
|
||||
public readonly expiresAt: number = 0;
|
||||
public readonly lastActive: number = 0;
|
||||
public readonly events: EventBus<ConnectionEvents> = new EventBus();
|
||||
private signaler!: Signaler; // Will be set by setSignaler()
|
||||
private readonly _ready: Promise<void>;
|
||||
private _state: ConnectionInterface['state'] = 'disconnected';
|
||||
private iceBin = createBin()
|
||||
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly host: string,
|
||||
public readonly service: string,
|
||||
offer?: RTCSessionDescriptionInit) {
|
||||
this.connection = new RTCPeerConnection();
|
||||
this.side = offer ? 'answer' : 'offer';
|
||||
const ready = offer
|
||||
? this.connection.setRemoteDescription(offer)
|
||||
.then(() => this.connection.createAnswer())
|
||||
.then(answer => this.connection.setLocalDescription(answer))
|
||||
: this.connection.createOffer()
|
||||
.then(offer => this.connection.setLocalDescription(offer));
|
||||
this._ready = ready.then(() => this.setState('connecting'))
|
||||
.then(() => this.startIceListeners())
|
||||
}
|
||||
|
||||
private setState(state: ConnectionInterface['state']) {
|
||||
this._state = state;
|
||||
this.events.emit('state-change', state);
|
||||
}
|
||||
|
||||
private startIceListeners() {
|
||||
const listener = ({candidate}: {candidate: RTCIceCandidate | null}) => {
|
||||
if (candidate) this.signaler.addIceCandidate(candidate)
|
||||
}
|
||||
this.connection.addEventListener('icecandidate', listener)
|
||||
this.iceBin(
|
||||
this.signaler.addListener((candidate: RTCIceCandidate) => this.connection.addIceCandidate(candidate)),
|
||||
() => this.connection.removeEventListener('icecandidate', listener)
|
||||
)
|
||||
}
|
||||
|
||||
private stopIceListeners() {
|
||||
this.iceBin.clean()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the signaler for ICE candidate exchange
|
||||
* Must be called before connection is ready
|
||||
*/
|
||||
setSignaler(signaler: Signaler): void {
|
||||
this.signaler = signaler;
|
||||
}
|
||||
|
||||
get state() {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
get ready(): Promise<void> {
|
||||
return this._ready;
|
||||
}
|
||||
|
||||
queueMessage(message: Message, options: QueueMessageOptions = {}): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
sendMessage(message: Message): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
@@ -87,18 +87,4 @@ export class EventBus<TEvents extends Record<string, any>> {
|
||||
this.handlers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of handlers for an event
|
||||
*/
|
||||
listenerCount<K extends keyof TEvents>(event: K): number {
|
||||
return this.handlers.get(event)?.size ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all event names that have handlers
|
||||
*/
|
||||
eventNames(): Array<keyof TEvents> {
|
||||
return Array.from(this.handlers.keys());
|
||||
}
|
||||
}
|
||||
23
src/index.ts
23
src/index.ts
@@ -5,12 +5,27 @@
|
||||
|
||||
export { ConnectionManager } from './connection-manager.js';
|
||||
export { EventBus } from './event-bus.js';
|
||||
export { RondevuAPI } from './api.js';
|
||||
export { RondevuSignaler } from './signaler.js';
|
||||
export { WebRTCRondevuConnection } from './connection.js';
|
||||
export { createBin } from './bin.js';
|
||||
|
||||
// Export types
|
||||
export type {
|
||||
ConnectionIdentity,
|
||||
ConnectionState,
|
||||
ConnectionInterface,
|
||||
Connection,
|
||||
QueueMessageOptions
|
||||
QueueMessageOptions,
|
||||
Message,
|
||||
ConnectionEvents,
|
||||
Signaler
|
||||
} from './types.js';
|
||||
|
||||
export type {
|
||||
Credentials,
|
||||
OfferRequest,
|
||||
Offer,
|
||||
ServiceRequest,
|
||||
Service,
|
||||
IceCandidate
|
||||
} from './api.js';
|
||||
|
||||
export type { Binnable } from './bin.js';
|
||||
|
||||
82
src/signaler.ts
Normal file
82
src/signaler.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import {Signaler} from "./types";
|
||||
import {Binnable} from "./bin";
|
||||
import {RondevuAPI} from "./api";
|
||||
|
||||
/**
|
||||
* RondevuSignaler - Handles ICE candidate exchange via Rondevu API
|
||||
* Uses polling to retrieve remote candidates
|
||||
*/
|
||||
export class RondevuSignaler implements Signaler {
|
||||
constructor(
|
||||
private api: RondevuAPI,
|
||||
private offerId: string
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Send local ICE candidate to signaling server
|
||||
*/
|
||||
async addIceCandidate(candidate: RTCIceCandidate): Promise<void> {
|
||||
const candidateData = candidate.toJSON();
|
||||
|
||||
// Skip empty candidates
|
||||
if (!candidateData.candidate || candidateData.candidate === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.api.addIceCandidates(this.offerId, [candidateData]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll for remote ICE candidates and call callback for each one
|
||||
* Returns cleanup function to stop polling
|
||||
*/
|
||||
addListener(callback: (candidate: RTCIceCandidate) => void): Binnable {
|
||||
let lastTimestamp = 0;
|
||||
let polling = true;
|
||||
|
||||
const poll = async () => {
|
||||
while (polling) {
|
||||
try {
|
||||
const candidates = await this.api.getIceCandidates(this.offerId, lastTimestamp);
|
||||
|
||||
// Process each candidate
|
||||
for (const item of candidates) {
|
||||
if (item.candidate && item.candidate.candidate && item.candidate.candidate !== '') {
|
||||
try {
|
||||
const rtcCandidate = new RTCIceCandidate(item.candidate);
|
||||
callback(rtcCandidate);
|
||||
lastTimestamp = item.createdAt;
|
||||
} catch (err) {
|
||||
console.warn('Failed to process ICE candidate:', err);
|
||||
lastTimestamp = item.createdAt;
|
||||
}
|
||||
} else {
|
||||
lastTimestamp = item.createdAt;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// If offer not found or 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');
|
||||
polling = false;
|
||||
break;
|
||||
}
|
||||
console.error('Error polling for ICE candidates:', err);
|
||||
}
|
||||
|
||||
// Poll every second
|
||||
if (polling) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Start polling in background
|
||||
poll();
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
polling = false;
|
||||
};
|
||||
}
|
||||
}
|
||||
36
src/types.ts
36
src/types.ts
@@ -1,24 +1,34 @@
|
||||
/**
|
||||
* Core connection types
|
||||
*/
|
||||
import {EventBus} from "./event-bus";
|
||||
import {Binnable} from "./bin";
|
||||
|
||||
export interface ConnectionIdentity {
|
||||
id: string;
|
||||
hostUsername: string;
|
||||
}
|
||||
|
||||
export interface ConnectionState {
|
||||
state: 'connected' | 'disconnected' | 'connecting';
|
||||
lastActive: number;
|
||||
}
|
||||
export type Message = string | ArrayBuffer;
|
||||
|
||||
export interface QueueMessageOptions {
|
||||
expiresAt?: number;
|
||||
}
|
||||
|
||||
export interface ConnectionInterface {
|
||||
queueMessage(message: string | ArrayBuffer, options?: QueueMessageOptions): void;
|
||||
sendMessage(message: string | ArrayBuffer): void;
|
||||
export interface ConnectionEvents {
|
||||
'state-change': ConnectionInterface['state']
|
||||
'message': Message;
|
||||
}
|
||||
|
||||
export type Connection = ConnectionIdentity & ConnectionState & ConnectionInterface;
|
||||
export interface ConnectionInterface {
|
||||
id: string;
|
||||
host: string;
|
||||
service: string;
|
||||
state: 'connected' | 'disconnected' | 'connecting';
|
||||
lastActive: number;
|
||||
expiresAt?: number;
|
||||
events: EventBus<ConnectionEvents>;
|
||||
|
||||
queueMessage(message: Message, options?: QueueMessageOptions): Promise<void>;
|
||||
sendMessage(message: Message): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface Signaler {
|
||||
addIceCandidate(candidate: RTCIceCandidate): Promise<void> | void;
|
||||
addListener(callback: (candidate: RTCIceCandidate) => void): Binnable;
|
||||
}
|
||||
Reference in New Issue
Block a user