@xtr-dev/rondevu-client 0.18.9 → 0.20.1
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 +324 -47
- package/dist/{api.d.ts → api/client.d.ts} +17 -8
- package/dist/{api.js → api/client.js} +114 -81
- package/dist/{answerer-connection.d.ts → connections/answerer.d.ts} +13 -5
- package/dist/{answerer-connection.js → connections/answerer.js} +17 -32
- package/dist/{connection.d.ts → connections/base.d.ts} +26 -5
- package/dist/{connection.js → connections/base.js} +45 -4
- package/dist/{offerer-connection.d.ts → connections/offerer.d.ts} +30 -5
- package/dist/{offerer-connection.js → connections/offerer.js} +93 -32
- package/dist/core/index.d.ts +22 -0
- package/dist/core/index.js +17 -0
- package/dist/core/offer-pool.d.ts +94 -0
- package/dist/core/offer-pool.js +267 -0
- package/dist/{rondevu.d.ts → core/rondevu.d.ts} +7 -28
- package/dist/{rondevu.js → core/rondevu.js} +32 -175
- package/dist/{node-crypto-adapter.d.ts → crypto/node.d.ts} +1 -1
- package/dist/{web-crypto-adapter.d.ts → crypto/web.d.ts} +1 -1
- package/dist/utils/async-lock.d.ts +42 -0
- package/dist/utils/async-lock.js +75 -0
- package/dist/{message-buffer.d.ts → utils/message-buffer.d.ts} +1 -1
- package/package.json +3 -3
- package/dist/index.d.ts +0 -22
- package/dist/index.js +0 -17
- package/dist/rondevu-signaler.d.ts +0 -112
- package/dist/rondevu-signaler.js +0 -401
- /package/dist/{rpc-batcher.d.ts → api/batcher.d.ts} +0 -0
- /package/dist/{rpc-batcher.js → api/batcher.js} +0 -0
- /package/dist/{connection-config.d.ts → connections/config.d.ts} +0 -0
- /package/dist/{connection-config.js → connections/config.js} +0 -0
- /package/dist/{connection-events.d.ts → connections/events.d.ts} +0 -0
- /package/dist/{connection-events.js → connections/events.js} +0 -0
- /package/dist/{types.d.ts → core/types.d.ts} +0 -0
- /package/dist/{types.js → core/types.js} +0 -0
- /package/dist/{crypto-adapter.d.ts → crypto/adapter.d.ts} +0 -0
- /package/dist/{crypto-adapter.js → crypto/adapter.js} +0 -0
- /package/dist/{node-crypto-adapter.js → crypto/node.js} +0 -0
- /package/dist/{web-crypto-adapter.js → crypto/web.js} +0 -0
- /package/dist/{exponential-backoff.d.ts → utils/exponential-backoff.d.ts} +0 -0
- /package/dist/{exponential-backoff.js → utils/exponential-backoff.js} +0 -0
- /package/dist/{message-buffer.js → utils/message-buffer.js} +0 -0
package/dist/rondevu-signaler.js
DELETED
|
@@ -1,401 +0,0 @@
|
|
|
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.serviceFqn = null;
|
|
39
|
-
this.offerListeners = [];
|
|
40
|
-
this.answerListeners = [];
|
|
41
|
-
this.iceListeners = [];
|
|
42
|
-
this.pollingTimeout = null;
|
|
43
|
-
this.icePollingTimeout = null;
|
|
44
|
-
this.lastPollTimestamp = 0;
|
|
45
|
-
this.isPolling = false;
|
|
46
|
-
this.isOfferer = false;
|
|
47
|
-
this.pollingConfig = {
|
|
48
|
-
initialInterval: pollingConfig?.initialInterval ?? 500,
|
|
49
|
-
maxInterval: pollingConfig?.maxInterval ?? 5000,
|
|
50
|
-
backoffMultiplier: pollingConfig?.backoffMultiplier ?? 1.5,
|
|
51
|
-
maxRetries: pollingConfig?.maxRetries ?? 50,
|
|
52
|
-
jitter: pollingConfig?.jitter ?? true
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Publish an offer as a service
|
|
57
|
-
* Used by the offerer to make their offer available
|
|
58
|
-
*/
|
|
59
|
-
async setOffer(offer) {
|
|
60
|
-
if (!offer.sdp) {
|
|
61
|
-
throw new Error('Offer SDP is required');
|
|
62
|
-
}
|
|
63
|
-
// Publish service with the offer SDP
|
|
64
|
-
const publishedService = await this.rondevu.publishService({
|
|
65
|
-
serviceFqn: this.service,
|
|
66
|
-
offers: [{ sdp: offer.sdp }],
|
|
67
|
-
ttl: 300000, // 5 minutes
|
|
68
|
-
});
|
|
69
|
-
// Get the first offer from the published service
|
|
70
|
-
if (!publishedService.offers || publishedService.offers.length === 0) {
|
|
71
|
-
throw new Error('No offers returned from service publication');
|
|
72
|
-
}
|
|
73
|
-
this.offerId = publishedService.offers[0].offerId;
|
|
74
|
-
this.serviceFqn = publishedService.serviceFqn;
|
|
75
|
-
this.isOfferer = true;
|
|
76
|
-
// Start combined polling for answers and ICE candidates
|
|
77
|
-
this.startPolling();
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Send an answer to the offerer
|
|
81
|
-
* Used by the answerer to respond to an offer
|
|
82
|
-
*/
|
|
83
|
-
async setAnswer(answer) {
|
|
84
|
-
if (!answer.sdp) {
|
|
85
|
-
throw new Error('Answer SDP is required');
|
|
86
|
-
}
|
|
87
|
-
if (!this.serviceFqn || !this.offerId) {
|
|
88
|
-
throw new Error('No service FQN or offer ID available. Must receive offer first.');
|
|
89
|
-
}
|
|
90
|
-
// Send answer to the service
|
|
91
|
-
await this.rondevu.getAPIPublic().answerOffer(this.serviceFqn, this.offerId, answer.sdp);
|
|
92
|
-
this.isOfferer = false;
|
|
93
|
-
// Start polling for ICE candidates (answerer uses separate endpoint)
|
|
94
|
-
this.startIcePolling();
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Listen for incoming offers
|
|
98
|
-
* Used by the answerer to receive offers from the offerer
|
|
99
|
-
*/
|
|
100
|
-
addOfferListener(callback) {
|
|
101
|
-
this.offerListeners.push(callback);
|
|
102
|
-
// If we have a host, start searching for their service
|
|
103
|
-
if (this.host && !this.isPolling) {
|
|
104
|
-
this.searchForOffer();
|
|
105
|
-
}
|
|
106
|
-
// Return cleanup function
|
|
107
|
-
return () => {
|
|
108
|
-
const index = this.offerListeners.indexOf(callback);
|
|
109
|
-
if (index > -1) {
|
|
110
|
-
this.offerListeners.splice(index, 1);
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Listen for incoming answers
|
|
116
|
-
* Used by the offerer to receive the answer from the answerer
|
|
117
|
-
*/
|
|
118
|
-
addAnswerListener(callback) {
|
|
119
|
-
this.answerListeners.push(callback);
|
|
120
|
-
// Return cleanup function
|
|
121
|
-
return () => {
|
|
122
|
-
const index = this.answerListeners.indexOf(callback);
|
|
123
|
-
if (index > -1) {
|
|
124
|
-
this.answerListeners.splice(index, 1);
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Send an ICE candidate to the remote peer
|
|
130
|
-
*/
|
|
131
|
-
async addIceCandidate(candidate) {
|
|
132
|
-
if (!this.serviceFqn || !this.offerId) {
|
|
133
|
-
console.warn('Cannot send ICE candidate: no service FQN or offer ID');
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const candidateData = candidate.toJSON();
|
|
137
|
-
// Skip empty candidates
|
|
138
|
-
if (!candidateData.candidate || candidateData.candidate === '') {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
try {
|
|
142
|
-
await this.rondevu.getAPIPublic().addOfferIceCandidates(this.serviceFqn, this.offerId, [candidateData]);
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
console.error('Failed to send ICE candidate:', err);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Listen for ICE candidates from the remote peer
|
|
150
|
-
*/
|
|
151
|
-
addListener(callback) {
|
|
152
|
-
this.iceListeners.push(callback);
|
|
153
|
-
// Return cleanup function
|
|
154
|
-
return () => {
|
|
155
|
-
const index = this.iceListeners.indexOf(callback);
|
|
156
|
-
if (index > -1) {
|
|
157
|
-
this.iceListeners.splice(index, 1);
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Search for an offer from the host
|
|
163
|
-
* Used by the answerer to find the offerer's service
|
|
164
|
-
*/
|
|
165
|
-
async searchForOffer() {
|
|
166
|
-
if (!this.host) {
|
|
167
|
-
throw new Error('No host specified for offer search');
|
|
168
|
-
}
|
|
169
|
-
this.isPolling = true;
|
|
170
|
-
try {
|
|
171
|
-
// Get service by FQN (service should include @username)
|
|
172
|
-
const serviceFqn = `${this.service}@${this.host}`;
|
|
173
|
-
const serviceData = await this.rondevu.getAPIPublic().getService(serviceFqn);
|
|
174
|
-
if (!serviceData) {
|
|
175
|
-
console.warn(`No service found for ${serviceFqn}`);
|
|
176
|
-
this.isPolling = false;
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
// Store service details
|
|
180
|
-
this.offerId = serviceData.offerId;
|
|
181
|
-
this.serviceFqn = serviceData.serviceFqn;
|
|
182
|
-
// Notify offer listeners
|
|
183
|
-
const offer = {
|
|
184
|
-
type: 'offer',
|
|
185
|
-
sdp: serviceData.sdp,
|
|
186
|
-
};
|
|
187
|
-
this.offerListeners.forEach(listener => {
|
|
188
|
-
try {
|
|
189
|
-
listener(offer);
|
|
190
|
-
}
|
|
191
|
-
catch (err) {
|
|
192
|
-
console.error('Offer listener error:', err);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
catch (err) {
|
|
197
|
-
console.error('Failed to search for offer:', err);
|
|
198
|
-
this.isPolling = false;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Start combined polling for answers and ICE candidates (offerer side)
|
|
203
|
-
* Uses poll() for efficient batch polling
|
|
204
|
-
*/
|
|
205
|
-
startPolling() {
|
|
206
|
-
if (this.pollingTimeout || !this.isOfferer) {
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
let interval = this.pollingConfig.initialInterval;
|
|
210
|
-
let retries = 0;
|
|
211
|
-
let answerReceived = false;
|
|
212
|
-
const poll = async () => {
|
|
213
|
-
try {
|
|
214
|
-
const result = await this.rondevu.poll(this.lastPollTimestamp);
|
|
215
|
-
let foundActivity = false;
|
|
216
|
-
// Process answers
|
|
217
|
-
if (result.answers.length > 0 && !answerReceived) {
|
|
218
|
-
foundActivity = true;
|
|
219
|
-
// Find answer for our offerId
|
|
220
|
-
const answer = result.answers.find(a => a.offerId === this.offerId);
|
|
221
|
-
if (answer && answer.sdp) {
|
|
222
|
-
answerReceived = true;
|
|
223
|
-
const answerDesc = {
|
|
224
|
-
type: 'answer',
|
|
225
|
-
sdp: answer.sdp,
|
|
226
|
-
};
|
|
227
|
-
this.answerListeners.forEach(listener => {
|
|
228
|
-
try {
|
|
229
|
-
listener(answerDesc);
|
|
230
|
-
}
|
|
231
|
-
catch (err) {
|
|
232
|
-
console.error('Answer listener error:', err);
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
this.lastPollTimestamp = Math.max(this.lastPollTimestamp, answer.answeredAt);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
// Process ICE candidates for our offer
|
|
239
|
-
if (this.offerId && result.iceCandidates[this.offerId]) {
|
|
240
|
-
const candidates = result.iceCandidates[this.offerId];
|
|
241
|
-
// Filter for answerer candidates (offerer receives answerer's candidates)
|
|
242
|
-
const answererCandidates = candidates.filter(c => c.role === 'answerer');
|
|
243
|
-
if (answererCandidates.length > 0) {
|
|
244
|
-
foundActivity = true;
|
|
245
|
-
for (const item of answererCandidates) {
|
|
246
|
-
if (item.candidate && item.candidate.candidate && item.candidate.candidate !== '') {
|
|
247
|
-
try {
|
|
248
|
-
const rtcCandidate = new RTCIceCandidate(item.candidate);
|
|
249
|
-
this.iceListeners.forEach(listener => {
|
|
250
|
-
try {
|
|
251
|
-
listener(rtcCandidate);
|
|
252
|
-
}
|
|
253
|
-
catch (err) {
|
|
254
|
-
console.error('ICE listener error:', err);
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
this.lastPollTimestamp = Math.max(this.lastPollTimestamp, item.createdAt);
|
|
258
|
-
}
|
|
259
|
-
catch (err) {
|
|
260
|
-
console.warn('Failed to process ICE candidate:', err);
|
|
261
|
-
this.lastPollTimestamp = Math.max(this.lastPollTimestamp, item.createdAt);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
// Adjust interval based on activity
|
|
268
|
-
if (foundActivity) {
|
|
269
|
-
interval = this.pollingConfig.initialInterval;
|
|
270
|
-
retries = 0;
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
retries++;
|
|
274
|
-
if (retries > this.pollingConfig.maxRetries) {
|
|
275
|
-
console.warn('Max retries reached for polling');
|
|
276
|
-
this.stopPolling();
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
interval = Math.min(interval * this.pollingConfig.backoffMultiplier, this.pollingConfig.maxInterval);
|
|
280
|
-
}
|
|
281
|
-
// Add jitter to prevent thundering herd
|
|
282
|
-
const finalInterval = this.pollingConfig.jitter
|
|
283
|
-
? interval + Math.random() * 100
|
|
284
|
-
: interval;
|
|
285
|
-
this.pollingTimeout = setTimeout(poll, finalInterval);
|
|
286
|
-
}
|
|
287
|
-
catch (err) {
|
|
288
|
-
console.error('Error polling offers:', err);
|
|
289
|
-
// Retry with backoff
|
|
290
|
-
const finalInterval = this.pollingConfig.jitter
|
|
291
|
-
? interval + Math.random() * 100
|
|
292
|
-
: interval;
|
|
293
|
-
this.pollingTimeout = setTimeout(poll, finalInterval);
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
poll(); // Start immediately
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* Stop combined polling
|
|
300
|
-
*/
|
|
301
|
-
stopPolling() {
|
|
302
|
-
if (this.pollingTimeout) {
|
|
303
|
-
clearTimeout(this.pollingTimeout);
|
|
304
|
-
this.pollingTimeout = null;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Start polling for ICE candidates (answerer side only)
|
|
309
|
-
* Answerers use the separate endpoint since they don't have offers to poll
|
|
310
|
-
*/
|
|
311
|
-
startIcePolling() {
|
|
312
|
-
if (this.icePollingTimeout || !this.serviceFqn || !this.offerId || this.isOfferer) {
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
let interval = this.pollingConfig.initialInterval;
|
|
316
|
-
const poll = async () => {
|
|
317
|
-
if (!this.serviceFqn || !this.offerId) {
|
|
318
|
-
this.stopIcePolling();
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
try {
|
|
322
|
-
const result = await this.rondevu
|
|
323
|
-
.getAPIPublic()
|
|
324
|
-
.getOfferIceCandidates(this.serviceFqn, this.offerId, this.lastPollTimestamp);
|
|
325
|
-
let foundCandidates = false;
|
|
326
|
-
for (const item of result.candidates) {
|
|
327
|
-
if (item.candidate && item.candidate.candidate && item.candidate.candidate !== '') {
|
|
328
|
-
foundCandidates = true;
|
|
329
|
-
try {
|
|
330
|
-
const rtcCandidate = new RTCIceCandidate(item.candidate);
|
|
331
|
-
this.iceListeners.forEach(listener => {
|
|
332
|
-
try {
|
|
333
|
-
listener(rtcCandidate);
|
|
334
|
-
}
|
|
335
|
-
catch (err) {
|
|
336
|
-
console.error('ICE listener error:', err);
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
this.lastPollTimestamp = item.createdAt;
|
|
340
|
-
}
|
|
341
|
-
catch (err) {
|
|
342
|
-
console.warn('Failed to process ICE candidate:', err);
|
|
343
|
-
this.lastPollTimestamp = item.createdAt;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
this.lastPollTimestamp = item.createdAt;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
// If candidates found, reset interval to initial value
|
|
351
|
-
// Otherwise, increase interval with backoff
|
|
352
|
-
if (foundCandidates) {
|
|
353
|
-
interval = this.pollingConfig.initialInterval;
|
|
354
|
-
}
|
|
355
|
-
else {
|
|
356
|
-
interval = Math.min(interval * this.pollingConfig.backoffMultiplier, this.pollingConfig.maxInterval);
|
|
357
|
-
}
|
|
358
|
-
// Add jitter
|
|
359
|
-
const finalInterval = this.pollingConfig.jitter
|
|
360
|
-
? interval + Math.random() * 100
|
|
361
|
-
: interval;
|
|
362
|
-
this.icePollingTimeout = setTimeout(poll, finalInterval);
|
|
363
|
-
}
|
|
364
|
-
catch (err) {
|
|
365
|
-
// 404/410 means offer expired, stop polling
|
|
366
|
-
if (err instanceof Error && (err.message?.includes('404') || err.message?.includes('410'))) {
|
|
367
|
-
console.warn('Offer not found or expired, stopping ICE polling');
|
|
368
|
-
this.stopIcePolling();
|
|
369
|
-
}
|
|
370
|
-
else if (err instanceof Error && !err.message?.includes('404')) {
|
|
371
|
-
console.error('Error polling for ICE candidates:', err);
|
|
372
|
-
// Continue polling despite errors
|
|
373
|
-
const finalInterval = this.pollingConfig.jitter
|
|
374
|
-
? interval + Math.random() * 100
|
|
375
|
-
: interval;
|
|
376
|
-
this.icePollingTimeout = setTimeout(poll, finalInterval);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
};
|
|
380
|
-
poll(); // Start immediately
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* Stop polling for ICE candidates
|
|
384
|
-
*/
|
|
385
|
-
stopIcePolling() {
|
|
386
|
-
if (this.icePollingTimeout) {
|
|
387
|
-
clearTimeout(this.icePollingTimeout);
|
|
388
|
-
this.icePollingTimeout = null;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Stop all polling and cleanup
|
|
393
|
-
*/
|
|
394
|
-
dispose() {
|
|
395
|
-
this.stopPolling();
|
|
396
|
-
this.stopIcePolling();
|
|
397
|
-
this.offerListeners = [];
|
|
398
|
-
this.answerListeners = [];
|
|
399
|
-
this.iceListeners = [];
|
|
400
|
-
}
|
|
401
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|