@xtr-dev/rondevu-client 0.18.10 → 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 (41) 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} +77 -85
  15. package/dist/core/rondevu.js +600 -0
  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 +4 -4
  22. package/dist/index.d.ts +0 -13
  23. package/dist/index.js +0 -10
  24. package/dist/rondevu-signaler.d.ts +0 -112
  25. package/dist/rondevu-signaler.js +0 -401
  26. package/dist/rondevu.js +0 -847
  27. /package/dist/{rpc-batcher.d.ts → api/batcher.d.ts} +0 -0
  28. /package/dist/{rpc-batcher.js → api/batcher.js} +0 -0
  29. /package/dist/{connection-config.d.ts → connections/config.d.ts} +0 -0
  30. /package/dist/{connection-config.js → connections/config.js} +0 -0
  31. /package/dist/{connection-events.d.ts → connections/events.d.ts} +0 -0
  32. /package/dist/{connection-events.js → connections/events.js} +0 -0
  33. /package/dist/{types.d.ts → core/types.d.ts} +0 -0
  34. /package/dist/{types.js → core/types.js} +0 -0
  35. /package/dist/{crypto-adapter.d.ts → crypto/adapter.d.ts} +0 -0
  36. /package/dist/{crypto-adapter.js → crypto/adapter.js} +0 -0
  37. /package/dist/{node-crypto-adapter.js → crypto/node.js} +0 -0
  38. /package/dist/{web-crypto-adapter.js → crypto/web.js} +0 -0
  39. /package/dist/{exponential-backoff.d.ts → utils/exponential-backoff.d.ts} +0 -0
  40. /package/dist/{exponential-backoff.js → utils/exponential-backoff.js} +0 -0
  41. /package/dist/{message-buffer.js → utils/message-buffer.js} +0 -0
@@ -0,0 +1,75 @@
1
+ /**
2
+ * AsyncLock provides a mutual exclusion primitive for asynchronous operations.
3
+ * Ensures only one async operation can proceed at a time while queuing others.
4
+ */
5
+ export class AsyncLock {
6
+ constructor() {
7
+ this.locked = false;
8
+ this.queue = [];
9
+ }
10
+ /**
11
+ * Acquire the lock. If already locked, waits until released.
12
+ * @returns Promise that resolves when lock is acquired
13
+ */
14
+ async acquire() {
15
+ if (!this.locked) {
16
+ this.locked = true;
17
+ return;
18
+ }
19
+ // Lock is held, wait in queue
20
+ return new Promise(resolve => {
21
+ this.queue.push(resolve);
22
+ });
23
+ }
24
+ /**
25
+ * Release the lock. If others are waiting, grants lock to next in queue.
26
+ */
27
+ release() {
28
+ const next = this.queue.shift();
29
+ if (next) {
30
+ // Grant lock to next waiter
31
+ next();
32
+ }
33
+ else {
34
+ // No waiters, mark as unlocked
35
+ this.locked = false;
36
+ }
37
+ }
38
+ /**
39
+ * Run a function with the lock acquired, automatically releasing after.
40
+ * This is the recommended way to use AsyncLock to prevent forgetting to release.
41
+ *
42
+ * @param fn - Async function to run with lock held
43
+ * @returns Promise resolving to the function's return value
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * const lock = new AsyncLock()
48
+ * const result = await lock.run(async () => {
49
+ * // Critical section - only one caller at a time
50
+ * return await doSomething()
51
+ * })
52
+ * ```
53
+ */
54
+ async run(fn) {
55
+ await this.acquire();
56
+ try {
57
+ return await fn();
58
+ }
59
+ finally {
60
+ this.release();
61
+ }
62
+ }
63
+ /**
64
+ * Check if lock is currently held
65
+ */
66
+ isLocked() {
67
+ return this.locked;
68
+ }
69
+ /**
70
+ * Get number of operations waiting for the lock
71
+ */
72
+ getQueueLength() {
73
+ return this.queue.length;
74
+ }
75
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Message buffering system for storing messages during disconnections
3
3
  */
4
- import { BufferedMessage } from './connection-events.js';
4
+ import { BufferedMessage } from '../connections/events.js';
5
5
  export interface MessageBufferConfig {
6
6
  maxSize: number;
7
7
  maxAge: number;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@xtr-dev/rondevu-client",
3
- "version": "0.18.10",
4
- "description": "TypeScript client for Rondevu WebRTC signaling with username-based discovery",
3
+ "version": "0.20.1",
4
+ "description": "TypeScript client for Rondevu with durable WebRTC connections, automatic reconnection, and message queuing",
5
5
  "type": "module",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
6
+ "main": "dist/core/index.js",
7
+ "types": "dist/core/index.d.ts",
8
8
  "scripts": {
9
9
  "build": "tsc",
10
10
  "typecheck": "tsc --noEmit",
package/dist/index.d.ts DELETED
@@ -1,13 +0,0 @@
1
- /**
2
- * @xtr-dev/rondevu-client
3
- * WebRTC peer signaling client
4
- */
5
- export { Rondevu, RondevuError, NetworkError, ValidationError, ConnectionError } from './rondevu.js';
6
- export { RondevuAPI } from './api.js';
7
- export { RpcBatcher } from './rpc-batcher.js';
8
- export { WebCryptoAdapter } from './web-crypto-adapter.js';
9
- export { NodeCryptoAdapter } from './node-crypto-adapter.js';
10
- export type { Signaler, Binnable, } from './types.js';
11
- export type { Keypair, OfferRequest, ServiceRequest, Service, ServiceOffer, IceCandidate, } from './api.js';
12
- export type { RondevuOptions, PublishServiceOptions, ConnectToServiceOptions, ConnectionContext, OfferContext, OfferFactory, ActiveOffer, FindServiceOptions, ServiceResult, PaginatedServiceResult } from './rondevu.js';
13
- export type { CryptoAdapter } from './crypto-adapter.js';
package/dist/index.js DELETED
@@ -1,10 +0,0 @@
1
- /**
2
- * @xtr-dev/rondevu-client
3
- * WebRTC peer signaling client
4
- */
5
- export { Rondevu, RondevuError, NetworkError, ValidationError, ConnectionError } from './rondevu.js';
6
- export { RondevuAPI } from './api.js';
7
- export { RpcBatcher } from './rpc-batcher.js';
8
- // Export crypto adapters
9
- export { WebCryptoAdapter } from './web-crypto-adapter.js';
10
- export { NodeCryptoAdapter } from './node-crypto-adapter.js';
@@ -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
- }