@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.
Files changed (40) hide show
  1. package/README.md +324 -47
  2. package/dist/{api.d.ts → api/client.d.ts} +17 -8
  3. package/dist/{api.js → api/client.js} +114 -81
  4. package/dist/{answerer-connection.d.ts → connections/answerer.d.ts} +13 -5
  5. package/dist/{answerer-connection.js → connections/answerer.js} +17 -32
  6. package/dist/{connection.d.ts → connections/base.d.ts} +26 -5
  7. package/dist/{connection.js → connections/base.js} +45 -4
  8. package/dist/{offerer-connection.d.ts → connections/offerer.d.ts} +30 -5
  9. package/dist/{offerer-connection.js → connections/offerer.js} +93 -32
  10. package/dist/core/index.d.ts +22 -0
  11. package/dist/core/index.js +17 -0
  12. package/dist/core/offer-pool.d.ts +94 -0
  13. package/dist/core/offer-pool.js +267 -0
  14. package/dist/{rondevu.d.ts → core/rondevu.d.ts} +7 -28
  15. package/dist/{rondevu.js → core/rondevu.js} +32 -175
  16. package/dist/{node-crypto-adapter.d.ts → crypto/node.d.ts} +1 -1
  17. package/dist/{web-crypto-adapter.d.ts → crypto/web.d.ts} +1 -1
  18. package/dist/utils/async-lock.d.ts +42 -0
  19. package/dist/utils/async-lock.js +75 -0
  20. package/dist/{message-buffer.d.ts → utils/message-buffer.d.ts} +1 -1
  21. package/package.json +3 -3
  22. package/dist/index.d.ts +0 -22
  23. package/dist/index.js +0 -17
  24. package/dist/rondevu-signaler.d.ts +0 -112
  25. package/dist/rondevu-signaler.js +0 -401
  26. /package/dist/{rpc-batcher.d.ts → api/batcher.d.ts} +0 -0
  27. /package/dist/{rpc-batcher.js → api/batcher.js} +0 -0
  28. /package/dist/{connection-config.d.ts → connections/config.d.ts} +0 -0
  29. /package/dist/{connection-config.js → connections/config.js} +0 -0
  30. /package/dist/{connection-events.d.ts → connections/events.d.ts} +0 -0
  31. /package/dist/{connection-events.js → connections/events.js} +0 -0
  32. /package/dist/{types.d.ts → core/types.d.ts} +0 -0
  33. /package/dist/{types.js → core/types.js} +0 -0
  34. /package/dist/{crypto-adapter.d.ts → crypto/adapter.d.ts} +0 -0
  35. /package/dist/{crypto-adapter.js → crypto/adapter.js} +0 -0
  36. /package/dist/{node-crypto-adapter.js → crypto/node.js} +0 -0
  37. /package/dist/{web-crypto-adapter.js → crypto/web.js} +0 -0
  38. /package/dist/{exponential-backoff.d.ts → utils/exponential-backoff.d.ts} +0 -0
  39. /package/dist/{exponential-backoff.js → utils/exponential-backoff.js} +0 -0
  40. /package/dist/{message-buffer.js → utils/message-buffer.js} +0 -0
@@ -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