@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.
- 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} +77 -85
- package/dist/core/rondevu.js +600 -0
- 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 +4 -4
- package/dist/index.d.ts +0 -13
- package/dist/index.js +0 -10
- package/dist/rondevu-signaler.d.ts +0 -112
- package/dist/rondevu-signaler.js +0 -401
- package/dist/rondevu.js +0 -847
- /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
|
@@ -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 '
|
|
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.
|
|
4
|
-
"description": "TypeScript client for Rondevu WebRTC
|
|
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
|
-
}
|
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
|
-
}
|