@xtr-dev/rondevu-client 0.10.1 → 0.10.2
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/README.md +326 -533
- package/dist/api.d.ts +6 -6
- package/dist/api.js +28 -25
- package/dist/durable-connection.d.ts +120 -0
- package/dist/durable-connection.js +244 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -2
- package/dist/quick-start.d.ts +29 -0
- package/dist/quick-start.js +44 -0
- package/dist/rondevu-context.d.ts +10 -0
- package/dist/rondevu-context.js +20 -0
- package/dist/rondevu-service.d.ts +4 -0
- package/dist/rondevu-service.js +9 -3
- package/dist/rondevu-signaler.d.ts +110 -0
- package/dist/rondevu-signaler.js +361 -0
- package/dist/service-client.d.ts +30 -47
- package/dist/service-client.js +95 -123
- package/dist/service-host.d.ts +28 -64
- package/dist/service-host.js +81 -147
- package/dist/types.d.ts +1 -3
- package/dist/types.js +5 -1
- package/dist/webrtc-context.d.ts +1 -3
- package/dist/webrtc-context.js +1 -2
- package/package.json +1 -1
package/dist/rondevu-service.js
CHANGED
|
@@ -48,7 +48,7 @@ export class RondevuService {
|
|
|
48
48
|
// Register with API if no credentials provided
|
|
49
49
|
if (!this.api['credentials']) {
|
|
50
50
|
const credentials = await this.api.register();
|
|
51
|
-
this.api.credentials
|
|
51
|
+
this.api.setCredentials(credentials);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
@@ -70,7 +70,7 @@ export class RondevuService {
|
|
|
70
70
|
throw new Error(`Username "${this.username}" is already claimed by another user`);
|
|
71
71
|
}
|
|
72
72
|
// Generate signature for username claim
|
|
73
|
-
const message = `claim
|
|
73
|
+
const message = `claim:${this.username}:${Date.now()}`;
|
|
74
74
|
const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey);
|
|
75
75
|
// Claim the username
|
|
76
76
|
await this.api.claimUsername(this.username, this.keypair.publicKey, signature, message);
|
|
@@ -88,7 +88,7 @@ export class RondevuService {
|
|
|
88
88
|
}
|
|
89
89
|
const { serviceFqn, sdp, ttl, isPublic, metadata } = options;
|
|
90
90
|
// Generate signature for service publication
|
|
91
|
-
const message = `publish
|
|
91
|
+
const message = `publish:${this.username}:${serviceFqn}:${Date.now()}`;
|
|
92
92
|
const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey);
|
|
93
93
|
// Create service request
|
|
94
94
|
const serviceRequest = {
|
|
@@ -110,6 +110,12 @@ export class RondevuService {
|
|
|
110
110
|
getKeypair() {
|
|
111
111
|
return this.keypair;
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Get the username
|
|
115
|
+
*/
|
|
116
|
+
getUsername() {
|
|
117
|
+
return this.username;
|
|
118
|
+
}
|
|
113
119
|
/**
|
|
114
120
|
* Get the public key
|
|
115
121
|
*/
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Signaler } from './types.js';
|
|
2
|
+
import { RondevuService } from './rondevu-service.js';
|
|
3
|
+
import { Binnable } from './bin.js';
|
|
4
|
+
export interface PollingConfig {
|
|
5
|
+
initialInterval?: number;
|
|
6
|
+
maxInterval?: number;
|
|
7
|
+
backoffMultiplier?: number;
|
|
8
|
+
maxRetries?: number;
|
|
9
|
+
jitter?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* RondevuSignaler - Handles WebRTC signaling via Rondevu service
|
|
13
|
+
*
|
|
14
|
+
* Manages offer/answer exchange and ICE candidate polling for establishing
|
|
15
|
+
* WebRTC connections through the Rondevu signaling server.
|
|
16
|
+
*
|
|
17
|
+
* Supports configurable polling with exponential backoff and jitter to reduce
|
|
18
|
+
* server load and prevent thundering herd issues.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const signaler = new RondevuSignaler(
|
|
23
|
+
* rondevuService,
|
|
24
|
+
* 'chat.app@1.0.0',
|
|
25
|
+
* 'peer-username',
|
|
26
|
+
* { initialInterval: 500, maxInterval: 5000, jitter: true }
|
|
27
|
+
* )
|
|
28
|
+
*
|
|
29
|
+
* // For offerer:
|
|
30
|
+
* await signaler.setOffer(offer)
|
|
31
|
+
* signaler.addAnswerListener(answer => {
|
|
32
|
+
* // Handle remote answer
|
|
33
|
+
* })
|
|
34
|
+
*
|
|
35
|
+
* // For answerer:
|
|
36
|
+
* signaler.addOfferListener(offer => {
|
|
37
|
+
* // Handle remote offer
|
|
38
|
+
* })
|
|
39
|
+
* await signaler.setAnswer(answer)
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare class RondevuSignaler implements Signaler {
|
|
43
|
+
private readonly rondevu;
|
|
44
|
+
private readonly service;
|
|
45
|
+
private readonly host?;
|
|
46
|
+
private offerId;
|
|
47
|
+
private serviceUuid;
|
|
48
|
+
private offerListeners;
|
|
49
|
+
private answerListeners;
|
|
50
|
+
private iceListeners;
|
|
51
|
+
private answerPollingTimeout;
|
|
52
|
+
private icePollingTimeout;
|
|
53
|
+
private lastIceTimestamp;
|
|
54
|
+
private isPolling;
|
|
55
|
+
private pollingConfig;
|
|
56
|
+
constructor(rondevu: RondevuService, service: string, host?: string | undefined, pollingConfig?: PollingConfig);
|
|
57
|
+
/**
|
|
58
|
+
* Publish an offer as a service
|
|
59
|
+
* Used by the offerer to make their offer available
|
|
60
|
+
*/
|
|
61
|
+
setOffer(offer: RTCSessionDescriptionInit): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Send an answer to the offerer
|
|
64
|
+
* Used by the answerer to respond to an offer
|
|
65
|
+
*/
|
|
66
|
+
setAnswer(answer: RTCSessionDescriptionInit): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Listen for incoming offers
|
|
69
|
+
* Used by the answerer to receive offers from the offerer
|
|
70
|
+
*/
|
|
71
|
+
addOfferListener(callback: (offer: RTCSessionDescriptionInit) => void): Binnable;
|
|
72
|
+
/**
|
|
73
|
+
* Listen for incoming answers
|
|
74
|
+
* Used by the offerer to receive the answer from the answerer
|
|
75
|
+
*/
|
|
76
|
+
addAnswerListener(callback: (answer: RTCSessionDescriptionInit) => void): Binnable;
|
|
77
|
+
/**
|
|
78
|
+
* Send an ICE candidate to the remote peer
|
|
79
|
+
*/
|
|
80
|
+
addIceCandidate(candidate: RTCIceCandidate): Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Listen for ICE candidates from the remote peer
|
|
83
|
+
*/
|
|
84
|
+
addListener(callback: (candidate: RTCIceCandidate) => void): Binnable;
|
|
85
|
+
/**
|
|
86
|
+
* Search for an offer from the host
|
|
87
|
+
* Used by the answerer to find the offerer's service
|
|
88
|
+
*/
|
|
89
|
+
private searchForOffer;
|
|
90
|
+
/**
|
|
91
|
+
* Start polling for answer (offerer side) with exponential backoff
|
|
92
|
+
*/
|
|
93
|
+
private startAnswerPolling;
|
|
94
|
+
/**
|
|
95
|
+
* Stop polling for answer
|
|
96
|
+
*/
|
|
97
|
+
private stopAnswerPolling;
|
|
98
|
+
/**
|
|
99
|
+
* Start polling for ICE candidates with adaptive backoff
|
|
100
|
+
*/
|
|
101
|
+
private startIcePolling;
|
|
102
|
+
/**
|
|
103
|
+
* Stop polling for ICE candidates
|
|
104
|
+
*/
|
|
105
|
+
private stopIcePolling;
|
|
106
|
+
/**
|
|
107
|
+
* Stop all polling and cleanup
|
|
108
|
+
*/
|
|
109
|
+
dispose(): void;
|
|
110
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RondevuSignaler - Handles WebRTC signaling via Rondevu service
|
|
3
|
+
*
|
|
4
|
+
* Manages offer/answer exchange and ICE candidate polling for establishing
|
|
5
|
+
* WebRTC connections through the Rondevu signaling server.
|
|
6
|
+
*
|
|
7
|
+
* Supports configurable polling with exponential backoff and jitter to reduce
|
|
8
|
+
* server load and prevent thundering herd issues.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const signaler = new RondevuSignaler(
|
|
13
|
+
* rondevuService,
|
|
14
|
+
* 'chat.app@1.0.0',
|
|
15
|
+
* 'peer-username',
|
|
16
|
+
* { initialInterval: 500, maxInterval: 5000, jitter: true }
|
|
17
|
+
* )
|
|
18
|
+
*
|
|
19
|
+
* // For offerer:
|
|
20
|
+
* await signaler.setOffer(offer)
|
|
21
|
+
* signaler.addAnswerListener(answer => {
|
|
22
|
+
* // Handle remote answer
|
|
23
|
+
* })
|
|
24
|
+
*
|
|
25
|
+
* // For answerer:
|
|
26
|
+
* signaler.addOfferListener(offer => {
|
|
27
|
+
* // Handle remote offer
|
|
28
|
+
* })
|
|
29
|
+
* await signaler.setAnswer(answer)
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export class RondevuSignaler {
|
|
33
|
+
constructor(rondevu, service, host, pollingConfig) {
|
|
34
|
+
this.rondevu = rondevu;
|
|
35
|
+
this.service = service;
|
|
36
|
+
this.host = host;
|
|
37
|
+
this.offerId = null;
|
|
38
|
+
this.serviceUuid = null;
|
|
39
|
+
this.offerListeners = [];
|
|
40
|
+
this.answerListeners = [];
|
|
41
|
+
this.iceListeners = [];
|
|
42
|
+
this.answerPollingTimeout = null;
|
|
43
|
+
this.icePollingTimeout = null;
|
|
44
|
+
this.lastIceTimestamp = 0;
|
|
45
|
+
this.isPolling = false;
|
|
46
|
+
this.pollingConfig = {
|
|
47
|
+
initialInterval: pollingConfig?.initialInterval ?? 500,
|
|
48
|
+
maxInterval: pollingConfig?.maxInterval ?? 5000,
|
|
49
|
+
backoffMultiplier: pollingConfig?.backoffMultiplier ?? 1.5,
|
|
50
|
+
maxRetries: pollingConfig?.maxRetries ?? 50,
|
|
51
|
+
jitter: pollingConfig?.jitter ?? true
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Publish an offer as a service
|
|
56
|
+
* Used by the offerer to make their offer available
|
|
57
|
+
*/
|
|
58
|
+
async setOffer(offer) {
|
|
59
|
+
if (!offer.sdp) {
|
|
60
|
+
throw new Error('Offer SDP is required');
|
|
61
|
+
}
|
|
62
|
+
// Publish service with the offer SDP
|
|
63
|
+
const publishedService = await this.rondevu.publishService({
|
|
64
|
+
serviceFqn: this.service,
|
|
65
|
+
sdp: offer.sdp,
|
|
66
|
+
ttl: 300000, // 5 minutes
|
|
67
|
+
isPublic: true,
|
|
68
|
+
});
|
|
69
|
+
this.offerId = publishedService.offerId;
|
|
70
|
+
this.serviceUuid = publishedService.uuid;
|
|
71
|
+
// Start polling for answer
|
|
72
|
+
this.startAnswerPolling();
|
|
73
|
+
// Start polling for ICE candidates
|
|
74
|
+
this.startIcePolling();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Send an answer to the offerer
|
|
78
|
+
* Used by the answerer to respond to an offer
|
|
79
|
+
*/
|
|
80
|
+
async setAnswer(answer) {
|
|
81
|
+
if (!answer.sdp) {
|
|
82
|
+
throw new Error('Answer SDP is required');
|
|
83
|
+
}
|
|
84
|
+
if (!this.offerId) {
|
|
85
|
+
throw new Error('No offer ID available. Must receive offer first.');
|
|
86
|
+
}
|
|
87
|
+
// Send answer to the offer
|
|
88
|
+
await this.rondevu.getAPI().answerOffer(this.offerId, answer.sdp);
|
|
89
|
+
// Start polling for ICE candidates
|
|
90
|
+
this.startIcePolling();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Listen for incoming offers
|
|
94
|
+
* Used by the answerer to receive offers from the offerer
|
|
95
|
+
*/
|
|
96
|
+
addOfferListener(callback) {
|
|
97
|
+
this.offerListeners.push(callback);
|
|
98
|
+
// If we have a host, start searching for their service
|
|
99
|
+
if (this.host && !this.isPolling) {
|
|
100
|
+
this.searchForOffer();
|
|
101
|
+
}
|
|
102
|
+
// Return cleanup function
|
|
103
|
+
return () => {
|
|
104
|
+
const index = this.offerListeners.indexOf(callback);
|
|
105
|
+
if (index > -1) {
|
|
106
|
+
this.offerListeners.splice(index, 1);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Listen for incoming answers
|
|
112
|
+
* Used by the offerer to receive the answer from the answerer
|
|
113
|
+
*/
|
|
114
|
+
addAnswerListener(callback) {
|
|
115
|
+
this.answerListeners.push(callback);
|
|
116
|
+
// Return cleanup function
|
|
117
|
+
return () => {
|
|
118
|
+
const index = this.answerListeners.indexOf(callback);
|
|
119
|
+
if (index > -1) {
|
|
120
|
+
this.answerListeners.splice(index, 1);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Send an ICE candidate to the remote peer
|
|
126
|
+
*/
|
|
127
|
+
async addIceCandidate(candidate) {
|
|
128
|
+
if (!this.offerId) {
|
|
129
|
+
console.warn('Cannot send ICE candidate: no offer ID');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const candidateData = candidate.toJSON();
|
|
133
|
+
// Skip empty candidates
|
|
134
|
+
if (!candidateData.candidate || candidateData.candidate === '') {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
await this.rondevu.getAPI().addIceCandidates(this.offerId, [candidateData]);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
console.error('Failed to send ICE candidate:', err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Listen for ICE candidates from the remote peer
|
|
146
|
+
*/
|
|
147
|
+
addListener(callback) {
|
|
148
|
+
this.iceListeners.push(callback);
|
|
149
|
+
// Return cleanup function
|
|
150
|
+
return () => {
|
|
151
|
+
const index = this.iceListeners.indexOf(callback);
|
|
152
|
+
if (index > -1) {
|
|
153
|
+
this.iceListeners.splice(index, 1);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Search for an offer from the host
|
|
159
|
+
* Used by the answerer to find the offerer's service
|
|
160
|
+
*/
|
|
161
|
+
async searchForOffer() {
|
|
162
|
+
if (!this.host) {
|
|
163
|
+
throw new Error('No host specified for offer search');
|
|
164
|
+
}
|
|
165
|
+
this.isPolling = true;
|
|
166
|
+
try {
|
|
167
|
+
// Search for services by username and service FQN
|
|
168
|
+
const services = await this.rondevu.getAPI().searchServices(this.host, this.service);
|
|
169
|
+
if (services.length === 0) {
|
|
170
|
+
console.warn(`No services found for ${this.host}/${this.service}`);
|
|
171
|
+
this.isPolling = false;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Get the first available service (already has full details from searchServices)
|
|
175
|
+
const service = services[0];
|
|
176
|
+
this.offerId = service.offerId;
|
|
177
|
+
this.serviceUuid = service.uuid;
|
|
178
|
+
// Notify offer listeners
|
|
179
|
+
const offer = {
|
|
180
|
+
type: 'offer',
|
|
181
|
+
sdp: service.sdp,
|
|
182
|
+
};
|
|
183
|
+
this.offerListeners.forEach(listener => {
|
|
184
|
+
try {
|
|
185
|
+
listener(offer);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
console.error('Offer listener error:', err);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
console.error('Failed to search for offer:', err);
|
|
194
|
+
this.isPolling = false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Start polling for answer (offerer side) with exponential backoff
|
|
199
|
+
*/
|
|
200
|
+
startAnswerPolling() {
|
|
201
|
+
if (this.answerPollingTimeout || !this.offerId) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
let interval = this.pollingConfig.initialInterval;
|
|
205
|
+
let retries = 0;
|
|
206
|
+
const poll = async () => {
|
|
207
|
+
if (!this.offerId) {
|
|
208
|
+
this.stopAnswerPolling();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const answer = await this.rondevu.getAPI().getAnswer(this.offerId);
|
|
213
|
+
if (answer && answer.sdp) {
|
|
214
|
+
// Got answer - notify listeners and stop polling
|
|
215
|
+
const answerDesc = {
|
|
216
|
+
type: 'answer',
|
|
217
|
+
sdp: answer.sdp,
|
|
218
|
+
};
|
|
219
|
+
this.answerListeners.forEach(listener => {
|
|
220
|
+
try {
|
|
221
|
+
listener(answerDesc);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
console.error('Answer listener error:', err);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
// Stop polling once we get the answer
|
|
228
|
+
this.stopAnswerPolling();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// No answer yet - exponential backoff
|
|
232
|
+
retries++;
|
|
233
|
+
if (retries > this.pollingConfig.maxRetries) {
|
|
234
|
+
console.warn('Max retries reached for answer polling');
|
|
235
|
+
this.stopAnswerPolling();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
interval = Math.min(interval * this.pollingConfig.backoffMultiplier, this.pollingConfig.maxInterval);
|
|
239
|
+
// Add jitter to prevent thundering herd
|
|
240
|
+
const finalInterval = this.pollingConfig.jitter
|
|
241
|
+
? interval + Math.random() * 100
|
|
242
|
+
: interval;
|
|
243
|
+
this.answerPollingTimeout = setTimeout(poll, finalInterval);
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
// 404 is expected when answer isn't available yet
|
|
247
|
+
if (err instanceof Error && !err.message?.includes('404')) {
|
|
248
|
+
console.error('Error polling for answer:', err);
|
|
249
|
+
}
|
|
250
|
+
// Retry with backoff
|
|
251
|
+
const finalInterval = this.pollingConfig.jitter
|
|
252
|
+
? interval + Math.random() * 100
|
|
253
|
+
: interval;
|
|
254
|
+
this.answerPollingTimeout = setTimeout(poll, finalInterval);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
poll(); // Start immediately
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Stop polling for answer
|
|
261
|
+
*/
|
|
262
|
+
stopAnswerPolling() {
|
|
263
|
+
if (this.answerPollingTimeout) {
|
|
264
|
+
clearTimeout(this.answerPollingTimeout);
|
|
265
|
+
this.answerPollingTimeout = null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Start polling for ICE candidates with adaptive backoff
|
|
270
|
+
*/
|
|
271
|
+
startIcePolling() {
|
|
272
|
+
if (this.icePollingTimeout || !this.offerId) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
let interval = this.pollingConfig.initialInterval;
|
|
276
|
+
const poll = async () => {
|
|
277
|
+
if (!this.offerId) {
|
|
278
|
+
this.stopIcePolling();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const candidates = await this.rondevu
|
|
283
|
+
.getAPI()
|
|
284
|
+
.getIceCandidates(this.offerId, this.lastIceTimestamp);
|
|
285
|
+
let foundCandidates = false;
|
|
286
|
+
for (const item of candidates) {
|
|
287
|
+
if (item.candidate && item.candidate.candidate && item.candidate.candidate !== '') {
|
|
288
|
+
foundCandidates = true;
|
|
289
|
+
try {
|
|
290
|
+
const rtcCandidate = new RTCIceCandidate(item.candidate);
|
|
291
|
+
this.iceListeners.forEach(listener => {
|
|
292
|
+
try {
|
|
293
|
+
listener(rtcCandidate);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
console.error('ICE listener error:', err);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
this.lastIceTimestamp = item.createdAt;
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
console.warn('Failed to process ICE candidate:', err);
|
|
303
|
+
this.lastIceTimestamp = item.createdAt;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
this.lastIceTimestamp = item.createdAt;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// If candidates found, reset interval to initial value
|
|
311
|
+
// Otherwise, increase interval with backoff
|
|
312
|
+
if (foundCandidates) {
|
|
313
|
+
interval = this.pollingConfig.initialInterval;
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
interval = Math.min(interval * this.pollingConfig.backoffMultiplier, this.pollingConfig.maxInterval);
|
|
317
|
+
}
|
|
318
|
+
// Add jitter
|
|
319
|
+
const finalInterval = this.pollingConfig.jitter
|
|
320
|
+
? interval + Math.random() * 100
|
|
321
|
+
: interval;
|
|
322
|
+
this.icePollingTimeout = setTimeout(poll, finalInterval);
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
// 404/410 means offer expired, stop polling
|
|
326
|
+
if (err instanceof Error && (err.message?.includes('404') || err.message?.includes('410'))) {
|
|
327
|
+
console.warn('Offer not found or expired, stopping ICE polling');
|
|
328
|
+
this.stopIcePolling();
|
|
329
|
+
}
|
|
330
|
+
else if (err instanceof Error && !err.message?.includes('404')) {
|
|
331
|
+
console.error('Error polling for ICE candidates:', err);
|
|
332
|
+
// Continue polling despite errors
|
|
333
|
+
const finalInterval = this.pollingConfig.jitter
|
|
334
|
+
? interval + Math.random() * 100
|
|
335
|
+
: interval;
|
|
336
|
+
this.icePollingTimeout = setTimeout(poll, finalInterval);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
poll(); // Start immediately
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Stop polling for ICE candidates
|
|
344
|
+
*/
|
|
345
|
+
stopIcePolling() {
|
|
346
|
+
if (this.icePollingTimeout) {
|
|
347
|
+
clearTimeout(this.icePollingTimeout);
|
|
348
|
+
this.icePollingTimeout = null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Stop all polling and cleanup
|
|
353
|
+
*/
|
|
354
|
+
dispose() {
|
|
355
|
+
this.stopAnswerPolling();
|
|
356
|
+
this.stopIcePolling();
|
|
357
|
+
this.offerListeners = [];
|
|
358
|
+
this.answerListeners = [];
|
|
359
|
+
this.iceListeners = [];
|
|
360
|
+
}
|
|
361
|
+
}
|
package/dist/service-client.d.ts
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
|
-
import { WebRTCRondevuConnection } from './connection.js';
|
|
2
1
|
import { RondevuService } from './rondevu-service.js';
|
|
2
|
+
import { RTCDurableConnection } from './durable-connection.js';
|
|
3
3
|
import { EventBus } from './event-bus.js';
|
|
4
|
-
import { ConnectionInterface } from './types.js';
|
|
5
4
|
export interface ServiceClientOptions {
|
|
6
5
|
username: string;
|
|
7
6
|
serviceFqn: string;
|
|
8
7
|
rondevuService: RondevuService;
|
|
9
8
|
autoReconnect?: boolean;
|
|
10
|
-
reconnectDelay?: number;
|
|
11
9
|
maxReconnectAttempts?: number;
|
|
12
10
|
rtcConfiguration?: RTCConfiguration;
|
|
13
11
|
}
|
|
14
12
|
export interface ServiceClientEvents {
|
|
15
|
-
connected:
|
|
16
|
-
disconnected:
|
|
17
|
-
reason: string;
|
|
18
|
-
};
|
|
13
|
+
connected: RTCDurableConnection;
|
|
14
|
+
disconnected: void;
|
|
19
15
|
reconnecting: {
|
|
20
16
|
attempt: number;
|
|
21
17
|
maxAttempts: number;
|
|
@@ -23,72 +19,59 @@ export interface ServiceClientEvents {
|
|
|
23
19
|
error: Error;
|
|
24
20
|
}
|
|
25
21
|
/**
|
|
26
|
-
* ServiceClient -
|
|
22
|
+
* ServiceClient - High-level wrapper for connecting to a WebRTC service
|
|
27
23
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
24
|
+
* Simplifies client connection by handling:
|
|
25
|
+
* - Service discovery
|
|
26
|
+
* - Offer/answer exchange
|
|
27
|
+
* - ICE candidate polling
|
|
28
|
+
* - Automatic reconnection
|
|
30
29
|
*
|
|
31
30
|
* @example
|
|
32
31
|
* ```typescript
|
|
33
|
-
* const
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* const client = new ServiceClient({
|
|
33
|
+
* username: 'host-user',
|
|
34
|
+
* serviceFqn: 'chat.app@1.0.0',
|
|
35
|
+
* rondevuService: myService
|
|
36
36
|
* })
|
|
37
37
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* username: 'host-user',
|
|
42
|
-
* serviceFqn: 'chat.app@1.0.0',
|
|
43
|
-
* rondevuService,
|
|
44
|
-
* autoReconnect: true,
|
|
38
|
+
* client.events.on('connected', conn => {
|
|
39
|
+
* conn.events.on('message', msg => console.log('Received:', msg))
|
|
40
|
+
* conn.sendMessage('Hello from client!')
|
|
45
41
|
* })
|
|
46
42
|
*
|
|
47
43
|
* await client.connect()
|
|
48
|
-
*
|
|
49
|
-
* client.events.on('connected', (conn) => {
|
|
50
|
-
* console.log('Connected to service')
|
|
51
|
-
* conn.sendMessage('Hello!')
|
|
52
|
-
* })
|
|
53
44
|
* ```
|
|
54
45
|
*/
|
|
55
46
|
export declare class ServiceClient {
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
private
|
|
60
|
-
private readonly reconnectDelay;
|
|
61
|
-
private readonly maxReconnectAttempts;
|
|
62
|
-
private readonly rtcConfiguration?;
|
|
47
|
+
private options;
|
|
48
|
+
events: EventBus<ServiceClientEvents>;
|
|
49
|
+
private signaler;
|
|
50
|
+
private webrtcContext;
|
|
63
51
|
private connection;
|
|
52
|
+
private autoReconnect;
|
|
53
|
+
private maxReconnectAttempts;
|
|
64
54
|
private reconnectAttempts;
|
|
65
|
-
private reconnectTimeout;
|
|
66
|
-
private readonly bin;
|
|
67
55
|
private isConnecting;
|
|
68
|
-
readonly events: EventBus<ServiceClientEvents>;
|
|
69
56
|
constructor(options: ServiceClientOptions);
|
|
70
57
|
/**
|
|
71
58
|
* Connect to the service
|
|
72
59
|
*/
|
|
73
|
-
connect(): Promise<
|
|
60
|
+
connect(): Promise<RTCDurableConnection>;
|
|
74
61
|
/**
|
|
75
62
|
* Disconnect from the service
|
|
76
63
|
*/
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Get the current connection
|
|
80
|
-
*/
|
|
81
|
-
getConnection(): WebRTCRondevuConnection | null;
|
|
64
|
+
dispose(): void;
|
|
82
65
|
/**
|
|
83
|
-
*
|
|
66
|
+
* @deprecated Use dispose() instead
|
|
84
67
|
*/
|
|
85
|
-
|
|
68
|
+
disconnect(): void;
|
|
86
69
|
/**
|
|
87
|
-
*
|
|
70
|
+
* Attempt to reconnect
|
|
88
71
|
*/
|
|
89
|
-
private
|
|
72
|
+
private attemptReconnect;
|
|
90
73
|
/**
|
|
91
|
-
*
|
|
74
|
+
* Get the current connection
|
|
92
75
|
*/
|
|
93
|
-
|
|
76
|
+
getConnection(): RTCDurableConnection | null;
|
|
94
77
|
}
|