@xtr-dev/rondevu-client 0.18.7 → 0.18.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/answerer-connection.d.ts +44 -0
- package/dist/answerer-connection.js +145 -0
- package/dist/connection-config.d.ts +21 -0
- package/dist/connection-config.js +30 -0
- package/dist/connection-events.d.ts +78 -0
- package/dist/connection-events.js +16 -0
- package/dist/connection.d.ts +148 -0
- package/dist/connection.js +481 -0
- package/dist/exponential-backoff.d.ts +30 -0
- package/dist/exponential-backoff.js +46 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +7 -0
- package/dist/message-buffer.d.ts +55 -0
- package/dist/message-buffer.js +106 -0
- package/dist/offerer-connection.d.ts +50 -0
- package/dist/offerer-connection.js +193 -0
- package/dist/rondevu.d.ts +77 -64
- package/dist/rondevu.js +185 -290
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message buffering system for storing messages during disconnections
|
|
3
|
+
*/
|
|
4
|
+
export class MessageBuffer {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
this.buffer = [];
|
|
8
|
+
this.messageIdCounter = 0;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Add a message to the buffer
|
|
12
|
+
* Returns the buffered message with metadata
|
|
13
|
+
*/
|
|
14
|
+
add(data) {
|
|
15
|
+
const message = {
|
|
16
|
+
id: `msg_${Date.now()}_${this.messageIdCounter++}`,
|
|
17
|
+
data,
|
|
18
|
+
timestamp: Date.now(),
|
|
19
|
+
attempts: 0,
|
|
20
|
+
};
|
|
21
|
+
// Check if buffer is full
|
|
22
|
+
if (this.buffer.length >= this.config.maxSize) {
|
|
23
|
+
// Remove oldest message
|
|
24
|
+
const discarded = this.buffer.shift();
|
|
25
|
+
if (discarded) {
|
|
26
|
+
return message; // Signal overflow by returning the new message
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
this.buffer.push(message);
|
|
30
|
+
return message;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get all messages in the buffer
|
|
34
|
+
*/
|
|
35
|
+
getAll() {
|
|
36
|
+
return [...this.buffer];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get messages that haven't exceeded max age
|
|
40
|
+
*/
|
|
41
|
+
getValid() {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
return this.buffer.filter((msg) => now - msg.timestamp < this.config.maxAge);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get and remove expired messages
|
|
47
|
+
*/
|
|
48
|
+
getExpired() {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const expired = [];
|
|
51
|
+
this.buffer = this.buffer.filter((msg) => {
|
|
52
|
+
if (now - msg.timestamp >= this.config.maxAge) {
|
|
53
|
+
expired.push(msg);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
58
|
+
return expired;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Remove a specific message by ID
|
|
62
|
+
*/
|
|
63
|
+
remove(messageId) {
|
|
64
|
+
const index = this.buffer.findIndex((msg) => msg.id === messageId);
|
|
65
|
+
if (index === -1)
|
|
66
|
+
return null;
|
|
67
|
+
const [removed] = this.buffer.splice(index, 1);
|
|
68
|
+
return removed;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Clear all messages from the buffer
|
|
72
|
+
*/
|
|
73
|
+
clear() {
|
|
74
|
+
const cleared = [...this.buffer];
|
|
75
|
+
this.buffer = [];
|
|
76
|
+
return cleared;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Increment attempt count for a message
|
|
80
|
+
*/
|
|
81
|
+
incrementAttempt(messageId) {
|
|
82
|
+
const message = this.buffer.find((msg) => msg.id === messageId);
|
|
83
|
+
if (!message)
|
|
84
|
+
return false;
|
|
85
|
+
message.attempts++;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get the current size of the buffer
|
|
90
|
+
*/
|
|
91
|
+
size() {
|
|
92
|
+
return this.buffer.length;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Check if buffer is empty
|
|
96
|
+
*/
|
|
97
|
+
isEmpty() {
|
|
98
|
+
return this.buffer.length === 0;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Check if buffer is full
|
|
102
|
+
*/
|
|
103
|
+
isFull() {
|
|
104
|
+
return this.buffer.length >= this.config.maxSize;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offerer-side WebRTC connection with offer creation and answer processing
|
|
3
|
+
*/
|
|
4
|
+
import { RondevuConnection } from './connection.js';
|
|
5
|
+
import { RondevuAPI } from './api.js';
|
|
6
|
+
import { ConnectionConfig } from './connection-config.js';
|
|
7
|
+
export interface OffererOptions {
|
|
8
|
+
api: RondevuAPI;
|
|
9
|
+
serviceFqn: string;
|
|
10
|
+
offerId: string;
|
|
11
|
+
rtcConfig?: RTCConfiguration;
|
|
12
|
+
config?: Partial<ConnectionConfig>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Offerer connection - creates offers and waits for answers
|
|
16
|
+
*/
|
|
17
|
+
export declare class OffererConnection extends RondevuConnection {
|
|
18
|
+
private api;
|
|
19
|
+
private serviceFqn;
|
|
20
|
+
private offerId;
|
|
21
|
+
constructor(options: OffererOptions);
|
|
22
|
+
/**
|
|
23
|
+
* Initialize the connection by creating offer
|
|
24
|
+
*/
|
|
25
|
+
initialize(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Process an answer from the answerer
|
|
28
|
+
*/
|
|
29
|
+
processAnswer(sdp: string, answererId: string): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Generate a hash fingerprint of SDP for deduplication
|
|
32
|
+
*/
|
|
33
|
+
private hashSdp;
|
|
34
|
+
/**
|
|
35
|
+
* Handle local ICE candidate generation
|
|
36
|
+
*/
|
|
37
|
+
protected onLocalIceCandidate(candidate: RTCIceCandidate): void;
|
|
38
|
+
/**
|
|
39
|
+
* Poll for remote ICE candidates
|
|
40
|
+
*/
|
|
41
|
+
protected pollIceCandidates(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Attempt to reconnect
|
|
44
|
+
*/
|
|
45
|
+
protected attemptReconnect(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Get the offer ID
|
|
48
|
+
*/
|
|
49
|
+
getOfferId(): string;
|
|
50
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offerer-side WebRTC connection with offer creation and answer processing
|
|
3
|
+
*/
|
|
4
|
+
import { RondevuConnection } from './connection.js';
|
|
5
|
+
import { ConnectionState } from './connection-events.js';
|
|
6
|
+
/**
|
|
7
|
+
* Offerer connection - creates offers and waits for answers
|
|
8
|
+
*/
|
|
9
|
+
export class OffererConnection extends RondevuConnection {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super(options.rtcConfig, options.config);
|
|
12
|
+
this.api = options.api;
|
|
13
|
+
this.serviceFqn = options.serviceFqn;
|
|
14
|
+
this.offerId = options.offerId;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Initialize the connection by creating offer
|
|
18
|
+
*/
|
|
19
|
+
async initialize() {
|
|
20
|
+
this.debug('Initializing offerer connection');
|
|
21
|
+
// Create peer connection
|
|
22
|
+
this.createPeerConnection();
|
|
23
|
+
// Create data channel BEFORE creating offer
|
|
24
|
+
// This is critical to avoid race conditions
|
|
25
|
+
if (!this.pc)
|
|
26
|
+
throw new Error('Peer connection not created');
|
|
27
|
+
this.dc = this.pc.createDataChannel('data', {
|
|
28
|
+
ordered: true,
|
|
29
|
+
maxRetransmits: 3,
|
|
30
|
+
});
|
|
31
|
+
// Setup data channel handlers IMMEDIATELY after creation
|
|
32
|
+
this.setupDataChannelHandlers(this.dc);
|
|
33
|
+
// Start connection timeout
|
|
34
|
+
this.startConnectionTimeout();
|
|
35
|
+
// Create and set local description
|
|
36
|
+
const offer = await this.pc.createOffer();
|
|
37
|
+
await this.pc.setLocalDescription(offer);
|
|
38
|
+
this.transitionTo(ConnectionState.SIGNALING, 'Offer created, waiting for answer');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Process an answer from the answerer
|
|
42
|
+
*/
|
|
43
|
+
async processAnswer(sdp, answererId) {
|
|
44
|
+
if (!this.pc) {
|
|
45
|
+
this.debug('Cannot process answer: peer connection not initialized');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Generate SDP fingerprint for deduplication
|
|
49
|
+
const fingerprint = await this.hashSdp(sdp);
|
|
50
|
+
// Check for duplicate answer
|
|
51
|
+
if (this.answerProcessed) {
|
|
52
|
+
if (this.answerSdpFingerprint === fingerprint) {
|
|
53
|
+
this.debug('Duplicate answer detected (same fingerprint), skipping');
|
|
54
|
+
this.emit('answer:duplicate', this.offerId);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
throw new Error('Received different answer after already processing one (protocol violation)');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Validate state
|
|
62
|
+
if (this.state !== ConnectionState.SIGNALING && this.state !== ConnectionState.CHECKING) {
|
|
63
|
+
this.debug(`Cannot process answer in state ${this.state}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Mark as processed BEFORE setRemoteDescription to prevent race conditions
|
|
67
|
+
this.answerProcessed = true;
|
|
68
|
+
this.answerSdpFingerprint = fingerprint;
|
|
69
|
+
try {
|
|
70
|
+
await this.pc.setRemoteDescription({
|
|
71
|
+
type: 'answer',
|
|
72
|
+
sdp,
|
|
73
|
+
});
|
|
74
|
+
this.debug(`Answer processed successfully from ${answererId}`);
|
|
75
|
+
this.emit('answer:processed', this.offerId, answererId);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// Reset flags on error so we can try again
|
|
79
|
+
this.answerProcessed = false;
|
|
80
|
+
this.answerSdpFingerprint = null;
|
|
81
|
+
this.debug('Failed to set remote description:', error);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Generate a hash fingerprint of SDP for deduplication
|
|
87
|
+
*/
|
|
88
|
+
async hashSdp(sdp) {
|
|
89
|
+
// Simple hash using built-in crypto if available
|
|
90
|
+
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
91
|
+
const encoder = new TextEncoder();
|
|
92
|
+
const data = encoder.encode(sdp);
|
|
93
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
94
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
95
|
+
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Fallback: use simple string hash
|
|
99
|
+
let hash = 0;
|
|
100
|
+
for (let i = 0; i < sdp.length; i++) {
|
|
101
|
+
const char = sdp.charCodeAt(i);
|
|
102
|
+
hash = (hash << 5) - hash + char;
|
|
103
|
+
hash = hash & hash;
|
|
104
|
+
}
|
|
105
|
+
return hash.toString(16);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Handle local ICE candidate generation
|
|
110
|
+
*/
|
|
111
|
+
onLocalIceCandidate(candidate) {
|
|
112
|
+
this.debug('Generated local ICE candidate');
|
|
113
|
+
// Send ICE candidate to server
|
|
114
|
+
this.api
|
|
115
|
+
.addOfferIceCandidates(this.serviceFqn, this.offerId, [
|
|
116
|
+
{
|
|
117
|
+
candidate: candidate.candidate,
|
|
118
|
+
sdpMLineIndex: candidate.sdpMLineIndex,
|
|
119
|
+
sdpMid: candidate.sdpMid,
|
|
120
|
+
},
|
|
121
|
+
])
|
|
122
|
+
.catch((error) => {
|
|
123
|
+
this.debug('Failed to send ICE candidate:', error);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Poll for remote ICE candidates
|
|
128
|
+
*/
|
|
129
|
+
pollIceCandidates() {
|
|
130
|
+
this.api
|
|
131
|
+
.getOfferIceCandidates(this.serviceFqn, this.offerId, this.lastIcePollTime)
|
|
132
|
+
.then((result) => {
|
|
133
|
+
if (result.candidates.length > 0) {
|
|
134
|
+
this.debug(`Received ${result.candidates.length} remote ICE candidates`);
|
|
135
|
+
for (const iceCandidate of result.candidates) {
|
|
136
|
+
if (iceCandidate.candidate && this.pc) {
|
|
137
|
+
const candidate = iceCandidate.candidate;
|
|
138
|
+
this.pc
|
|
139
|
+
.addIceCandidate(new RTCIceCandidate(candidate))
|
|
140
|
+
.then(() => {
|
|
141
|
+
this.emit('ice:candidate:remote', new RTCIceCandidate(candidate));
|
|
142
|
+
})
|
|
143
|
+
.catch((error) => {
|
|
144
|
+
this.debug('Failed to add ICE candidate:', error);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Update last poll time
|
|
148
|
+
if (iceCandidate.createdAt > this.lastIcePollTime) {
|
|
149
|
+
this.lastIcePollTime = iceCandidate.createdAt;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
.catch((error) => {
|
|
155
|
+
this.debug('Failed to poll ICE candidates:', error);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Attempt to reconnect
|
|
160
|
+
*/
|
|
161
|
+
attemptReconnect() {
|
|
162
|
+
this.debug('Attempting to reconnect');
|
|
163
|
+
// For offerer, we need to create a new offer
|
|
164
|
+
// Clean up old connection
|
|
165
|
+
if (this.pc) {
|
|
166
|
+
this.pc.close();
|
|
167
|
+
this.pc = null;
|
|
168
|
+
}
|
|
169
|
+
if (this.dc) {
|
|
170
|
+
this.dc.close();
|
|
171
|
+
this.dc = null;
|
|
172
|
+
}
|
|
173
|
+
// Reset answer processing flags
|
|
174
|
+
this.answerProcessed = false;
|
|
175
|
+
this.answerSdpFingerprint = null;
|
|
176
|
+
// Reinitialize
|
|
177
|
+
this.initialize()
|
|
178
|
+
.then(() => {
|
|
179
|
+
this.emit('reconnect:success');
|
|
180
|
+
})
|
|
181
|
+
.catch((error) => {
|
|
182
|
+
this.debug('Reconnection failed:', error);
|
|
183
|
+
this.emit('reconnect:failed', error);
|
|
184
|
+
this.scheduleReconnect();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get the offer ID
|
|
189
|
+
*/
|
|
190
|
+
getOfferId() {
|
|
191
|
+
return this.offerId;
|
|
192
|
+
}
|
|
193
|
+
}
|
package/dist/rondevu.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { RondevuAPI, Keypair, IceCandidate, BatcherOptions } from './api.js';
|
|
2
2
|
import { CryptoAdapter } from './crypto-adapter.js';
|
|
3
3
|
import { EventEmitter } from 'eventemitter3';
|
|
4
|
+
import { OffererConnection } from './offerer-connection.js';
|
|
5
|
+
import { AnswererConnection } from './answerer-connection.js';
|
|
6
|
+
import { ConnectionConfig } from './connection-config.js';
|
|
4
7
|
export type IceServerPreset = 'ipv4-turn' | 'hostname-turns' | 'google-stun' | 'relay-only';
|
|
5
8
|
export declare const ICE_SERVER_PRESETS: Record<IceServerPreset, RTCIceServer[]>;
|
|
6
9
|
export interface RondevuOptions {
|
|
@@ -32,6 +35,7 @@ export interface PublishServiceOptions {
|
|
|
32
35
|
maxOffers: number;
|
|
33
36
|
offerFactory?: OfferFactory;
|
|
34
37
|
ttl?: number;
|
|
38
|
+
connectionConfig?: Partial<ConnectionConfig>;
|
|
35
39
|
}
|
|
36
40
|
export interface ConnectionContext {
|
|
37
41
|
pc: RTCPeerConnection;
|
|
@@ -44,8 +48,8 @@ export interface ConnectToServiceOptions {
|
|
|
44
48
|
serviceFqn?: string;
|
|
45
49
|
service?: string;
|
|
46
50
|
username?: string;
|
|
47
|
-
onConnection?: (context: ConnectionContext) => void | Promise<void>;
|
|
48
51
|
rtcConfig?: RTCConfiguration;
|
|
52
|
+
connectionConfig?: Partial<ConnectionConfig>;
|
|
49
53
|
}
|
|
50
54
|
export interface ActiveOffer {
|
|
51
55
|
offerId: string;
|
|
@@ -101,14 +105,13 @@ export declare class ConnectionError extends RondevuError {
|
|
|
101
105
|
constructor(message: string, context?: Record<string, any>);
|
|
102
106
|
}
|
|
103
107
|
/**
|
|
104
|
-
* Rondevu - Complete WebRTC signaling client
|
|
108
|
+
* Rondevu - Complete WebRTC signaling client with durable connections
|
|
105
109
|
*
|
|
106
|
-
*
|
|
107
|
-
* -
|
|
108
|
-
* -
|
|
109
|
-
* -
|
|
110
|
-
* -
|
|
111
|
-
* - Keypair management
|
|
110
|
+
* v1.0.0 introduces breaking changes:
|
|
111
|
+
* - connectToService() now returns AnswererConnection instead of ConnectionContext
|
|
112
|
+
* - Automatic reconnection and message buffering built-in
|
|
113
|
+
* - Connection objects expose .send() method instead of raw DataChannel
|
|
114
|
+
* - Rich event system for connection lifecycle (connected, disconnected, reconnecting, etc.)
|
|
112
115
|
*
|
|
113
116
|
* @example
|
|
114
117
|
* ```typescript
|
|
@@ -119,39 +122,39 @@ export declare class ConnectionError extends RondevuError {
|
|
|
119
122
|
* iceServers: 'ipv4-turn' // Use preset: 'ipv4-turn', 'hostname-turns', 'google-stun', or 'relay-only'
|
|
120
123
|
* })
|
|
121
124
|
*
|
|
122
|
-
* // Or use custom ICE servers
|
|
123
|
-
* const rondevu2 = await Rondevu.connect({
|
|
124
|
-
* apiUrl: 'https://signal.example.com',
|
|
125
|
-
* username: 'bob',
|
|
126
|
-
* iceServers: [
|
|
127
|
-
* { urls: 'stun:stun.l.google.com:19302' },
|
|
128
|
-
* { urls: 'turn:turn.example.com:3478', username: 'user', credential: 'pass' }
|
|
129
|
-
* ]
|
|
130
|
-
* })
|
|
131
|
-
*
|
|
132
125
|
* // Publish a service with automatic offer management
|
|
133
126
|
* await rondevu.publishService({
|
|
134
127
|
* service: 'chat:2.0.0',
|
|
135
|
-
* maxOffers: 5
|
|
136
|
-
* offerFactory: async (pc) => {
|
|
137
|
-
* // pc is created by Rondevu with ICE handlers already attached
|
|
138
|
-
* const dc = pc.createDataChannel('chat')
|
|
139
|
-
* const offer = await pc.createOffer()
|
|
140
|
-
* await pc.setLocalDescription(offer)
|
|
141
|
-
* return { dc, offer }
|
|
142
|
-
* }
|
|
128
|
+
* maxOffers: 5 // Maintain up to 5 concurrent offers
|
|
143
129
|
* })
|
|
144
130
|
*
|
|
145
131
|
* // Start accepting connections (auto-fills offers and polls)
|
|
146
132
|
* await rondevu.startFilling()
|
|
147
133
|
*
|
|
148
|
-
* //
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
134
|
+
* // Listen for connections (v1.0.0 API)
|
|
135
|
+
* rondevu.on('connection:opened', (offerId, connection) => {
|
|
136
|
+
* connection.on('connected', () => console.log('Connected!'))
|
|
137
|
+
* connection.on('message', (data) => console.log('Received:', data))
|
|
138
|
+
* connection.send('Hello!')
|
|
139
|
+
* })
|
|
140
|
+
*
|
|
141
|
+
* // Connect to a service (v1.0.0 - returns AnswererConnection)
|
|
142
|
+
* const connection = await rondevu.connectToService({
|
|
143
|
+
* serviceFqn: 'chat:2.0.0@bob'
|
|
144
|
+
* })
|
|
145
|
+
*
|
|
146
|
+
* connection.on('connected', () => {
|
|
147
|
+
* console.log('Connected!')
|
|
148
|
+
* connection.send('Hello!')
|
|
149
|
+
* })
|
|
150
|
+
*
|
|
151
|
+
* connection.on('message', (data) => {
|
|
152
|
+
* console.log('Received:', data)
|
|
153
|
+
* })
|
|
152
154
|
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
+
* connection.on('reconnecting', (attempt) => {
|
|
156
|
+
* console.log(`Reconnecting, attempt ${attempt}`)
|
|
157
|
+
* })
|
|
155
158
|
* ```
|
|
156
159
|
*/
|
|
157
160
|
export declare class Rondevu extends EventEmitter {
|
|
@@ -172,11 +175,12 @@ export declare class Rondevu extends EventEmitter {
|
|
|
172
175
|
private maxOffers;
|
|
173
176
|
private offerFactory;
|
|
174
177
|
private ttl;
|
|
175
|
-
private
|
|
178
|
+
private activeConnections;
|
|
179
|
+
private connectionConfig?;
|
|
176
180
|
private filling;
|
|
181
|
+
private fillingSemaphore;
|
|
177
182
|
private pollingInterval;
|
|
178
183
|
private lastPollTimestamp;
|
|
179
|
-
private isPolling;
|
|
180
184
|
private constructor();
|
|
181
185
|
/**
|
|
182
186
|
* Internal debug logging - only logs if debug mode is enabled
|
|
@@ -215,26 +219,22 @@ export declare class Rondevu extends EventEmitter {
|
|
|
215
219
|
* ```typescript
|
|
216
220
|
* await rondevu.publishService({
|
|
217
221
|
* service: 'chat:2.0.0',
|
|
218
|
-
* maxOffers: 5
|
|
222
|
+
* maxOffers: 5,
|
|
223
|
+
* connectionConfig: {
|
|
224
|
+
* reconnectEnabled: true,
|
|
225
|
+
* bufferEnabled: true
|
|
226
|
+
* }
|
|
219
227
|
* })
|
|
220
228
|
* await rondevu.startFilling()
|
|
221
229
|
* ```
|
|
222
230
|
*/
|
|
223
231
|
publishService(options: PublishServiceOptions): Promise<void>;
|
|
224
232
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* Note: This is used by connectToService() where the offerId is already known.
|
|
228
|
-
* For createOffer(), we use inline ICE handling with early candidate queuing
|
|
229
|
-
* since the offerId isn't available until after the factory completes.
|
|
230
|
-
*/
|
|
231
|
-
private setupIceCandidateHandler;
|
|
232
|
-
/**
|
|
233
|
-
* Create a single offer and publish it to the server
|
|
233
|
+
* Create a single offer and publish it to the server using OffererConnection
|
|
234
234
|
*/
|
|
235
235
|
private createOffer;
|
|
236
236
|
/**
|
|
237
|
-
* Fill offers to reach maxOffers count
|
|
237
|
+
* Fill offers to reach maxOffers count with semaphore protection
|
|
238
238
|
*/
|
|
239
239
|
private fillOffers;
|
|
240
240
|
/**
|
|
@@ -259,7 +259,7 @@ export declare class Rondevu extends EventEmitter {
|
|
|
259
259
|
/**
|
|
260
260
|
* Check if an offer is currently connected
|
|
261
261
|
* @param offerId - The offer ID to check
|
|
262
|
-
* @returns True if the offer exists and
|
|
262
|
+
* @returns True if the offer exists and is connected
|
|
263
263
|
*/
|
|
264
264
|
isConnected(offerId: string): boolean;
|
|
265
265
|
/**
|
|
@@ -283,41 +283,45 @@ export declare class Rondevu extends EventEmitter {
|
|
|
283
283
|
*/
|
|
284
284
|
private resolveServiceFqn;
|
|
285
285
|
/**
|
|
286
|
-
*
|
|
287
|
-
* Returns
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Automatically connect to a service (answerer side)
|
|
292
|
-
* Handles the entire connection flow: discovery, WebRTC setup, answer exchange, ICE candidates
|
|
286
|
+
* Connect to a service (answerer side) - v1.0.0 API
|
|
287
|
+
* Returns an AnswererConnection with automatic reconnection and buffering
|
|
288
|
+
*
|
|
289
|
+
* BREAKING CHANGE: This now returns AnswererConnection instead of ConnectionContext
|
|
293
290
|
*
|
|
294
291
|
* @example
|
|
295
292
|
* ```typescript
|
|
296
293
|
* // Connect to specific user
|
|
297
294
|
* const connection = await rondevu.connectToService({
|
|
298
295
|
* serviceFqn: 'chat:2.0.0@alice',
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
* dc.addEventListener('open', () => dc.send('Hello!'))
|
|
296
|
+
* connectionConfig: {
|
|
297
|
+
* reconnectEnabled: true,
|
|
298
|
+
* bufferEnabled: true
|
|
303
299
|
* }
|
|
304
300
|
* })
|
|
305
301
|
*
|
|
302
|
+
* connection.on('connected', () => {
|
|
303
|
+
* console.log('Connected!')
|
|
304
|
+
* connection.send('Hello!')
|
|
305
|
+
* })
|
|
306
|
+
*
|
|
307
|
+
* connection.on('message', (data) => {
|
|
308
|
+
* console.log('Received:', data)
|
|
309
|
+
* })
|
|
310
|
+
*
|
|
311
|
+
* connection.on('reconnecting', (attempt) => {
|
|
312
|
+
* console.log(`Reconnecting, attempt ${attempt}`)
|
|
313
|
+
* })
|
|
314
|
+
*
|
|
306
315
|
* // Discover random service
|
|
307
316
|
* const connection = await rondevu.connectToService({
|
|
308
|
-
* service: 'chat:2.0.0'
|
|
309
|
-
* onConnection: ({ dc, peerUsername }) => {
|
|
310
|
-
* console.log('Connected to', peerUsername)
|
|
311
|
-
* }
|
|
317
|
+
* service: 'chat:2.0.0'
|
|
312
318
|
* })
|
|
313
319
|
* ```
|
|
314
320
|
*/
|
|
315
|
-
connectToService(options: ConnectToServiceOptions): Promise<
|
|
321
|
+
connectToService(options: ConnectToServiceOptions): Promise<AnswererConnection>;
|
|
316
322
|
/**
|
|
317
323
|
* Find a service - unified discovery method
|
|
318
324
|
*
|
|
319
|
-
* Replaces getService(), discoverService(), and discoverServices() with a single method.
|
|
320
|
-
*
|
|
321
325
|
* @param serviceFqn - Service identifier (e.g., 'chat:1.0.0' or 'chat:1.0.0@alice')
|
|
322
326
|
* @param options - Discovery options
|
|
323
327
|
*
|
|
@@ -399,6 +403,15 @@ export declare class Rondevu extends EventEmitter {
|
|
|
399
403
|
* Get the public key
|
|
400
404
|
*/
|
|
401
405
|
getPublicKey(): string;
|
|
406
|
+
/**
|
|
407
|
+
* Get active connections (for offerer side)
|
|
408
|
+
*/
|
|
409
|
+
getActiveConnections(): Map<string, OffererConnection>;
|
|
410
|
+
/**
|
|
411
|
+
* Get all active offers (legacy compatibility)
|
|
412
|
+
* @deprecated Use getActiveConnections() instead
|
|
413
|
+
*/
|
|
414
|
+
getActiveOffers(): ActiveOffer[];
|
|
402
415
|
/**
|
|
403
416
|
* Access to underlying API for advanced operations
|
|
404
417
|
* @deprecated Use direct methods on Rondevu instance instead
|