@xtr-dev/rondevu-client 0.18.10 → 0.21.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 (70) hide show
  1. package/README.md +92 -117
  2. package/dist/api/batcher.d.ts +83 -0
  3. package/dist/api/batcher.js +155 -0
  4. package/dist/api/client.d.ts +198 -0
  5. package/dist/api/client.js +400 -0
  6. package/dist/{answerer-connection.d.ts → connections/answerer.d.ts} +25 -8
  7. package/dist/{answerer-connection.js → connections/answerer.js} +70 -48
  8. package/dist/{connection.d.ts → connections/base.d.ts} +30 -7
  9. package/dist/{connection.js → connections/base.js} +65 -14
  10. package/dist/connections/config.d.ts +51 -0
  11. package/dist/{connection-config.js → connections/config.js} +20 -0
  12. package/dist/{connection-events.d.ts → connections/events.d.ts} +6 -6
  13. package/dist/connections/offerer.d.ts +108 -0
  14. package/dist/connections/offerer.js +306 -0
  15. package/dist/core/ice-config.d.ts +35 -0
  16. package/dist/core/ice-config.js +111 -0
  17. package/dist/core/index.d.ts +22 -0
  18. package/dist/core/index.js +22 -0
  19. package/dist/core/offer-pool.d.ts +113 -0
  20. package/dist/core/offer-pool.js +281 -0
  21. package/dist/core/peer.d.ts +155 -0
  22. package/dist/core/peer.js +252 -0
  23. package/dist/core/polling-manager.d.ts +71 -0
  24. package/dist/core/polling-manager.js +122 -0
  25. package/dist/core/rondevu-errors.d.ts +59 -0
  26. package/dist/core/rondevu-errors.js +75 -0
  27. package/dist/core/rondevu-types.d.ts +125 -0
  28. package/dist/core/rondevu-types.js +6 -0
  29. package/dist/core/rondevu.d.ts +296 -0
  30. package/dist/core/rondevu.js +472 -0
  31. package/dist/crypto/adapter.d.ts +53 -0
  32. package/dist/crypto/node.d.ts +57 -0
  33. package/dist/crypto/node.js +149 -0
  34. package/dist/crypto/web.d.ts +38 -0
  35. package/dist/crypto/web.js +129 -0
  36. package/dist/utils/async-lock.d.ts +42 -0
  37. package/dist/utils/async-lock.js +75 -0
  38. package/dist/{message-buffer.d.ts → utils/message-buffer.d.ts} +1 -1
  39. package/dist/{message-buffer.js → utils/message-buffer.js} +4 -4
  40. package/dist/webrtc/adapter.d.ts +22 -0
  41. package/dist/webrtc/adapter.js +5 -0
  42. package/dist/webrtc/browser.d.ts +12 -0
  43. package/dist/webrtc/browser.js +15 -0
  44. package/dist/webrtc/node.d.ts +32 -0
  45. package/dist/webrtc/node.js +32 -0
  46. package/package.json +20 -9
  47. package/dist/api.d.ts +0 -146
  48. package/dist/api.js +0 -279
  49. package/dist/connection-config.d.ts +0 -21
  50. package/dist/crypto-adapter.d.ts +0 -37
  51. package/dist/index.d.ts +0 -13
  52. package/dist/index.js +0 -10
  53. package/dist/node-crypto-adapter.d.ts +0 -35
  54. package/dist/node-crypto-adapter.js +0 -78
  55. package/dist/offerer-connection.d.ts +0 -54
  56. package/dist/offerer-connection.js +0 -177
  57. package/dist/rondevu-signaler.d.ts +0 -112
  58. package/dist/rondevu-signaler.js +0 -401
  59. package/dist/rondevu.d.ts +0 -407
  60. package/dist/rondevu.js +0 -847
  61. package/dist/rpc-batcher.d.ts +0 -61
  62. package/dist/rpc-batcher.js +0 -111
  63. package/dist/web-crypto-adapter.d.ts +0 -16
  64. package/dist/web-crypto-adapter.js +0 -52
  65. /package/dist/{connection-events.js → connections/events.js} +0 -0
  66. /package/dist/{types.d.ts → core/types.d.ts} +0 -0
  67. /package/dist/{types.js → core/types.js} +0 -0
  68. /package/dist/{crypto-adapter.js → crypto/adapter.js} +0 -0
  69. /package/dist/{exponential-backoff.d.ts → utils/exponential-backoff.d.ts} +0 -0
  70. /package/dist/{exponential-backoff.js → utils/exponential-backoff.js} +0 -0
@@ -1,177 +0,0 @@
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 - manages already-created offers and waits for answers
8
- */
9
- export class OffererConnection extends RondevuConnection {
10
- constructor(options) {
11
- super(undefined, options.config); // rtcConfig not needed, PC already created
12
- this.api = options.api;
13
- this.serviceFqn = options.serviceFqn;
14
- this.offerId = options.offerId;
15
- // Use the already-created peer connection and data channel
16
- this.pc = options.pc;
17
- this.dc = options.dc || null;
18
- }
19
- /**
20
- * Initialize the connection - setup handlers for already-created offer
21
- */
22
- async initialize() {
23
- this.debug('Initializing offerer connection');
24
- if (!this.pc)
25
- throw new Error('Peer connection not provided');
26
- // Setup peer connection event handlers
27
- this.pc.onicecandidate = (event) => this.handleIceCandidate(event);
28
- this.pc.oniceconnectionstatechange = () => this.handleIceConnectionStateChange();
29
- this.pc.onconnectionstatechange = () => this.handleConnectionStateChange();
30
- this.pc.onicegatheringstatechange = () => this.handleIceGatheringStateChange();
31
- // Setup data channel handlers if we have one
32
- if (this.dc) {
33
- this.setupDataChannelHandlers(this.dc);
34
- }
35
- // Start connection timeout
36
- this.startConnectionTimeout();
37
- // Transition to signaling state (offer already created and published)
38
- this.transitionTo(ConnectionState.SIGNALING, 'Offer published, 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
- * Note: For offerer connections, reconnection is handled by the Rondevu instance
162
- * creating a new offer via fillOffers(). This method is a no-op.
163
- */
164
- attemptReconnect() {
165
- this.debug('Reconnection not applicable for offerer - new offer will be created by Rondevu instance');
166
- // Offerer reconnection is handled externally by Rondevu.fillOffers()
167
- // which creates entirely new offers. We don't reconnect the same offer.
168
- // Just emit failure and let the parent handle it.
169
- this.emit('reconnect:failed', new Error('Offerer reconnection handled by parent'));
170
- }
171
- /**
172
- * Get the offer ID
173
- */
174
- getOfferId() {
175
- return this.offerId;
176
- }
177
- }
@@ -1,112 +0,0 @@
1
- import { Signaler, Binnable } from './types.js';
2
- import { Rondevu } from './rondevu.js';
3
- export interface PollingConfig {
4
- initialInterval?: number;
5
- maxInterval?: number;
6
- backoffMultiplier?: number;
7
- maxRetries?: number;
8
- jitter?: boolean;
9
- }
10
- /**
11
- * RondevuSignaler - Handles WebRTC signaling via Rondevu service
12
- *
13
- * Manages offer/answer exchange and ICE candidate polling for establishing
14
- * WebRTC connections through the Rondevu signaling server.
15
- *
16
- * Supports configurable polling with exponential backoff and jitter to reduce
17
- * server load and prevent thundering herd issues.
18
- *
19
- * @example
20
- * ```typescript
21
- * const signaler = new RondevuSignaler(
22
- * rondevuService,
23
- * 'chat.app@1.0.0',
24
- * 'peer-username',
25
- * { initialInterval: 500, maxInterval: 5000, jitter: true }
26
- * )
27
- *
28
- * // For offerer:
29
- * await signaler.setOffer(offer)
30
- * signaler.addAnswerListener(answer => {
31
- * // Handle remote answer
32
- * })
33
- *
34
- * // For answerer:
35
- * signaler.addOfferListener(offer => {
36
- * // Handle remote offer
37
- * })
38
- * await signaler.setAnswer(answer)
39
- * ```
40
- */
41
- export declare class RondevuSignaler implements Signaler {
42
- private readonly rondevu;
43
- private readonly service;
44
- private readonly host?;
45
- private offerId;
46
- private serviceFqn;
47
- private offerListeners;
48
- private answerListeners;
49
- private iceListeners;
50
- private pollingTimeout;
51
- private icePollingTimeout;
52
- private lastPollTimestamp;
53
- private isPolling;
54
- private isOfferer;
55
- private pollingConfig;
56
- constructor(rondevu: Rondevu, 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 combined polling for answers and ICE candidates (offerer side)
92
- * Uses poll() for efficient batch polling
93
- */
94
- private startPolling;
95
- /**
96
- * Stop combined polling
97
- */
98
- private stopPolling;
99
- /**
100
- * Start polling for ICE candidates (answerer side only)
101
- * Answerers use the separate endpoint since they don't have offers to poll
102
- */
103
- private startIcePolling;
104
- /**
105
- * Stop polling for ICE candidates
106
- */
107
- private stopIcePolling;
108
- /**
109
- * Stop all polling and cleanup
110
- */
111
- dispose(): void;
112
- }
@@ -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
- }