@xtr-dev/rondevu-client 0.18.9 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +324 -47
- package/dist/{api.d.ts → api/client.d.ts} +17 -8
- package/dist/{api.js → api/client.js} +114 -81
- package/dist/{answerer-connection.d.ts → connections/answerer.d.ts} +13 -5
- package/dist/{answerer-connection.js → connections/answerer.js} +17 -32
- package/dist/{connection.d.ts → connections/base.d.ts} +26 -5
- package/dist/{connection.js → connections/base.js} +45 -4
- package/dist/{offerer-connection.d.ts → connections/offerer.d.ts} +30 -5
- package/dist/{offerer-connection.js → connections/offerer.js} +93 -32
- package/dist/core/index.d.ts +22 -0
- package/dist/core/index.js +17 -0
- package/dist/core/offer-pool.d.ts +94 -0
- package/dist/core/offer-pool.js +267 -0
- package/dist/{rondevu.d.ts → core/rondevu.d.ts} +7 -28
- package/dist/{rondevu.js → core/rondevu.js} +32 -175
- package/dist/{node-crypto-adapter.d.ts → crypto/node.d.ts} +1 -1
- package/dist/{web-crypto-adapter.d.ts → crypto/web.d.ts} +1 -1
- package/dist/utils/async-lock.d.ts +42 -0
- package/dist/utils/async-lock.js +75 -0
- package/dist/{message-buffer.d.ts → utils/message-buffer.d.ts} +1 -1
- package/package.json +3 -3
- package/dist/index.d.ts +0 -22
- package/dist/index.js +0 -17
- package/dist/rondevu-signaler.d.ts +0 -112
- package/dist/rondevu-signaler.js +0 -401
- /package/dist/{rpc-batcher.d.ts → api/batcher.d.ts} +0 -0
- /package/dist/{rpc-batcher.js → api/batcher.js} +0 -0
- /package/dist/{connection-config.d.ts → connections/config.d.ts} +0 -0
- /package/dist/{connection-config.js → connections/config.js} +0 -0
- /package/dist/{connection-events.d.ts → connections/events.d.ts} +0 -0
- /package/dist/{connection-events.js → connections/events.js} +0 -0
- /package/dist/{types.d.ts → core/types.d.ts} +0 -0
- /package/dist/{types.js → core/types.js} +0 -0
- /package/dist/{crypto-adapter.d.ts → crypto/adapter.d.ts} +0 -0
- /package/dist/{crypto-adapter.js → crypto/adapter.js} +0 -0
- /package/dist/{node-crypto-adapter.js → crypto/node.js} +0 -0
- /package/dist/{web-crypto-adapter.js → crypto/web.js} +0 -0
- /package/dist/{exponential-backoff.d.ts → utils/exponential-backoff.d.ts} +0 -0
- /package/dist/{exponential-backoff.js → utils/exponential-backoff.js} +0 -0
- /package/dist/{message-buffer.js → utils/message-buffer.js} +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rondevu API Client - RPC interface
|
|
3
3
|
*/
|
|
4
|
-
import { WebCryptoAdapter } from '
|
|
5
|
-
import { RpcBatcher } from './
|
|
4
|
+
import { WebCryptoAdapter } from '../crypto/web.js';
|
|
5
|
+
import { RpcBatcher } from './batcher.js';
|
|
6
6
|
/**
|
|
7
7
|
* RondevuAPI - RPC-based API client for Rondevu signaling server
|
|
8
8
|
*/
|
|
@@ -20,34 +20,90 @@ export class RondevuAPI {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* Create canonical JSON string with sorted keys for deterministic signing
|
|
24
24
|
*/
|
|
25
|
-
|
|
25
|
+
canonicalJSON(obj) {
|
|
26
|
+
if (obj === null || obj === undefined) {
|
|
27
|
+
return JSON.stringify(obj);
|
|
28
|
+
}
|
|
29
|
+
if (typeof obj !== 'object') {
|
|
30
|
+
return JSON.stringify(obj);
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(obj)) {
|
|
33
|
+
return '[' + obj.map(item => this.canonicalJSON(item)).join(',') + ']';
|
|
34
|
+
}
|
|
35
|
+
const sortedKeys = Object.keys(obj).sort();
|
|
36
|
+
const pairs = sortedKeys.map(key => {
|
|
37
|
+
return JSON.stringify(key) + ':' + this.canonicalJSON(obj[key]);
|
|
38
|
+
});
|
|
39
|
+
return '{' + pairs.join(',') + '}';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Generate authentication headers for RPC request
|
|
43
|
+
* Signs the payload (method + params + timestamp + username)
|
|
44
|
+
*/
|
|
45
|
+
async generateAuthHeaders(request, includePublicKey = false) {
|
|
26
46
|
const timestamp = Date.now();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
47
|
+
// Create payload with timestamp and username for signing: { method, params, timestamp, username }
|
|
48
|
+
const payload = { ...request, timestamp, username: this.username };
|
|
49
|
+
// Create canonical JSON representation for signing
|
|
50
|
+
const canonical = this.canonicalJSON(payload);
|
|
51
|
+
// Sign the canonical representation
|
|
52
|
+
const signature = await this.crypto.signMessage(canonical, this.keypair.privateKey);
|
|
53
|
+
const headers = {
|
|
54
|
+
'X-Signature': signature,
|
|
55
|
+
'X-Timestamp': timestamp.toString(),
|
|
56
|
+
'X-Username': this.username,
|
|
57
|
+
};
|
|
58
|
+
if (includePublicKey) {
|
|
59
|
+
headers['X-Public-Key'] = this.keypair.publicKey;
|
|
60
|
+
}
|
|
61
|
+
return headers;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Generate authentication fields embedded in request body (for batch requests)
|
|
65
|
+
* Signs the payload (method + params + timestamp + username)
|
|
66
|
+
*/
|
|
67
|
+
async generateAuthForRequest(request, includePublicKey = false) {
|
|
68
|
+
const timestamp = Date.now();
|
|
69
|
+
// Create payload with timestamp and username for signing: { method, params, timestamp, username }
|
|
70
|
+
const payload = { ...request, timestamp, username: this.username };
|
|
71
|
+
// Create canonical JSON representation for signing
|
|
72
|
+
const canonical = this.canonicalJSON(payload);
|
|
73
|
+
// Sign the canonical representation
|
|
74
|
+
const signature = await this.crypto.signMessage(canonical, this.keypair.privateKey);
|
|
75
|
+
const authRequest = {
|
|
76
|
+
...request,
|
|
77
|
+
signature,
|
|
78
|
+
timestamp,
|
|
79
|
+
username: this.username,
|
|
80
|
+
};
|
|
81
|
+
if (includePublicKey) {
|
|
82
|
+
authRequest.publicKey = this.keypair.publicKey;
|
|
83
|
+
}
|
|
84
|
+
return authRequest;
|
|
32
85
|
}
|
|
33
86
|
/**
|
|
34
87
|
* Execute RPC call with optional batching
|
|
35
88
|
*/
|
|
36
|
-
async rpc(request) {
|
|
89
|
+
async rpc(request, authHeaders) {
|
|
37
90
|
// Use batcher if enabled
|
|
38
91
|
if (this.batcher) {
|
|
39
92
|
return await this.batcher.add(request);
|
|
40
93
|
}
|
|
41
94
|
// Direct call without batching
|
|
42
|
-
return await this.rpcDirect(request);
|
|
95
|
+
return await this.rpcDirect(request, authHeaders);
|
|
43
96
|
}
|
|
44
97
|
/**
|
|
45
98
|
* Execute single RPC call directly (bypasses batcher)
|
|
46
99
|
*/
|
|
47
|
-
async rpcDirect(request) {
|
|
100
|
+
async rpcDirect(request, authHeaders) {
|
|
48
101
|
const response = await fetch(`${this.baseUrl}/rpc`, {
|
|
49
102
|
method: 'POST',
|
|
50
|
-
headers: {
|
|
103
|
+
headers: {
|
|
104
|
+
'Content-Type': 'application/json',
|
|
105
|
+
...authHeaders,
|
|
106
|
+
},
|
|
51
107
|
body: JSON.stringify(request),
|
|
52
108
|
});
|
|
53
109
|
if (!response.ok) {
|
|
@@ -61,30 +117,25 @@ export class RondevuAPI {
|
|
|
61
117
|
}
|
|
62
118
|
/**
|
|
63
119
|
* Execute batch RPC calls directly (bypasses batcher)
|
|
120
|
+
* Each request in the batch has its own embedded authentication (signature, timestamp, username, publicKey)
|
|
64
121
|
*/
|
|
65
122
|
async rpcBatchDirect(requests) {
|
|
123
|
+
// Add auth to each request in the batch
|
|
124
|
+
const requestsWithAuth = await Promise.all(requests.map(req => this.generateAuthForRequest(req, true)));
|
|
66
125
|
const response = await fetch(`${this.baseUrl}/rpc`, {
|
|
67
126
|
method: 'POST',
|
|
68
127
|
headers: { 'Content-Type': 'application/json' },
|
|
69
|
-
body: JSON.stringify(
|
|
128
|
+
body: JSON.stringify(requestsWithAuth),
|
|
70
129
|
});
|
|
71
130
|
if (!response.ok) {
|
|
72
131
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
73
132
|
}
|
|
74
133
|
const results = await response.json();
|
|
75
|
-
// Validate response is an array
|
|
76
134
|
if (!Array.isArray(results)) {
|
|
77
|
-
console.error('Invalid RPC batch response:', results);
|
|
78
135
|
throw new Error('Server returned invalid batch response (not an array)');
|
|
79
136
|
}
|
|
80
|
-
//
|
|
81
|
-
if (results.length !== requests.length) {
|
|
82
|
-
console.error(`Response length mismatch: expected ${requests.length}, got ${results.length}`);
|
|
83
|
-
}
|
|
137
|
+
// Map results, throwing error for any failed request
|
|
84
138
|
return results.map((result, i) => {
|
|
85
|
-
if (!result || typeof result !== 'object') {
|
|
86
|
-
throw new Error(`Invalid response at index ${i}`);
|
|
87
|
-
}
|
|
88
139
|
if (!result.success) {
|
|
89
140
|
throw new Error(result.error || `RPC call ${i} failed`);
|
|
90
141
|
}
|
|
@@ -125,26 +176,24 @@ export class RondevuAPI {
|
|
|
125
176
|
* Check if a username is available
|
|
126
177
|
*/
|
|
127
178
|
async isUsernameAvailable(username) {
|
|
128
|
-
const
|
|
129
|
-
const result = await this.rpc({
|
|
179
|
+
const request = {
|
|
130
180
|
method: 'getUser',
|
|
131
|
-
message: auth.message,
|
|
132
|
-
signature: auth.signature,
|
|
133
181
|
params: { username },
|
|
134
|
-
}
|
|
182
|
+
};
|
|
183
|
+
const authHeaders = await this.generateAuthHeaders(request, false);
|
|
184
|
+
const result = await this.rpc(request, authHeaders);
|
|
135
185
|
return result.available;
|
|
136
186
|
}
|
|
137
187
|
/**
|
|
138
188
|
* Check if current username is claimed
|
|
139
189
|
*/
|
|
140
190
|
async isUsernameClaimed() {
|
|
141
|
-
const
|
|
142
|
-
const result = await this.rpc({
|
|
191
|
+
const request = {
|
|
143
192
|
method: 'getUser',
|
|
144
|
-
message: auth.message,
|
|
145
|
-
signature: auth.signature,
|
|
146
193
|
params: { username: this.username },
|
|
147
|
-
}
|
|
194
|
+
};
|
|
195
|
+
const authHeaders = await this.generateAuthHeaders(request, false);
|
|
196
|
+
const result = await this.rpc(request, authHeaders);
|
|
148
197
|
return !result.available;
|
|
149
198
|
}
|
|
150
199
|
// ============================================
|
|
@@ -154,47 +203,41 @@ export class RondevuAPI {
|
|
|
154
203
|
* Publish a service
|
|
155
204
|
*/
|
|
156
205
|
async publishService(service) {
|
|
157
|
-
const
|
|
158
|
-
return await this.rpc({
|
|
206
|
+
const request = {
|
|
159
207
|
method: 'publishService',
|
|
160
|
-
message: auth.message,
|
|
161
|
-
signature: auth.signature,
|
|
162
|
-
publicKey: this.keypair.publicKey,
|
|
163
208
|
params: {
|
|
164
209
|
serviceFqn: service.serviceFqn,
|
|
165
210
|
offers: service.offers,
|
|
166
211
|
ttl: service.ttl,
|
|
167
212
|
},
|
|
168
|
-
}
|
|
213
|
+
};
|
|
214
|
+
const authHeaders = await this.generateAuthHeaders(request, true);
|
|
215
|
+
return await this.rpc(request, authHeaders);
|
|
169
216
|
}
|
|
170
217
|
/**
|
|
171
218
|
* Get service by FQN (direct lookup, random, or paginated)
|
|
172
219
|
*/
|
|
173
220
|
async getService(serviceFqn, options) {
|
|
174
|
-
const
|
|
175
|
-
return await this.rpc({
|
|
221
|
+
const request = {
|
|
176
222
|
method: 'getService',
|
|
177
|
-
message: auth.message,
|
|
178
|
-
signature: auth.signature,
|
|
179
|
-
publicKey: this.keypair.publicKey,
|
|
180
223
|
params: {
|
|
181
224
|
serviceFqn,
|
|
182
225
|
...options,
|
|
183
226
|
},
|
|
184
|
-
}
|
|
227
|
+
};
|
|
228
|
+
const authHeaders = await this.generateAuthHeaders(request, true);
|
|
229
|
+
return await this.rpc(request, authHeaders);
|
|
185
230
|
}
|
|
186
231
|
/**
|
|
187
232
|
* Delete a service
|
|
188
233
|
*/
|
|
189
234
|
async deleteService(serviceFqn) {
|
|
190
|
-
const
|
|
191
|
-
await this.rpc({
|
|
235
|
+
const request = {
|
|
192
236
|
method: 'deleteService',
|
|
193
|
-
message: auth.message,
|
|
194
|
-
signature: auth.signature,
|
|
195
|
-
publicKey: this.keypair.publicKey,
|
|
196
237
|
params: { serviceFqn },
|
|
197
|
-
}
|
|
238
|
+
};
|
|
239
|
+
const authHeaders = await this.generateAuthHeaders(request, true);
|
|
240
|
+
await this.rpc(request, authHeaders);
|
|
198
241
|
}
|
|
199
242
|
// ============================================
|
|
200
243
|
// WebRTC Signaling
|
|
@@ -203,28 +246,24 @@ export class RondevuAPI {
|
|
|
203
246
|
* Answer an offer
|
|
204
247
|
*/
|
|
205
248
|
async answerOffer(serviceFqn, offerId, sdp) {
|
|
206
|
-
const
|
|
207
|
-
await this.rpc({
|
|
249
|
+
const request = {
|
|
208
250
|
method: 'answerOffer',
|
|
209
|
-
message: auth.message,
|
|
210
|
-
signature: auth.signature,
|
|
211
|
-
publicKey: this.keypair.publicKey,
|
|
212
251
|
params: { serviceFqn, offerId, sdp },
|
|
213
|
-
}
|
|
252
|
+
};
|
|
253
|
+
const authHeaders = await this.generateAuthHeaders(request, true);
|
|
254
|
+
await this.rpc(request, authHeaders);
|
|
214
255
|
}
|
|
215
256
|
/**
|
|
216
257
|
* Get answer for a specific offer (offerer polls this)
|
|
217
258
|
*/
|
|
218
259
|
async getOfferAnswer(serviceFqn, offerId) {
|
|
219
260
|
try {
|
|
220
|
-
const
|
|
221
|
-
return await this.rpc({
|
|
261
|
+
const request = {
|
|
222
262
|
method: 'getOfferAnswer',
|
|
223
|
-
message: auth.message,
|
|
224
|
-
signature: auth.signature,
|
|
225
|
-
publicKey: this.keypair.publicKey,
|
|
226
263
|
params: { serviceFqn, offerId },
|
|
227
|
-
}
|
|
264
|
+
};
|
|
265
|
+
const authHeaders = await this.generateAuthHeaders(request, true);
|
|
266
|
+
return await this.rpc(request, authHeaders);
|
|
228
267
|
}
|
|
229
268
|
catch (err) {
|
|
230
269
|
if (err.message.includes('not yet answered')) {
|
|
@@ -237,40 +276,34 @@ export class RondevuAPI {
|
|
|
237
276
|
* Combined polling for answers and ICE candidates
|
|
238
277
|
*/
|
|
239
278
|
async poll(since) {
|
|
240
|
-
const
|
|
241
|
-
return await this.rpc({
|
|
279
|
+
const request = {
|
|
242
280
|
method: 'poll',
|
|
243
|
-
message: auth.message,
|
|
244
|
-
signature: auth.signature,
|
|
245
|
-
publicKey: this.keypair.publicKey,
|
|
246
281
|
params: { since },
|
|
247
|
-
}
|
|
282
|
+
};
|
|
283
|
+
const authHeaders = await this.generateAuthHeaders(request, true);
|
|
284
|
+
return await this.rpc(request, authHeaders);
|
|
248
285
|
}
|
|
249
286
|
/**
|
|
250
287
|
* Add ICE candidates to a specific offer
|
|
251
288
|
*/
|
|
252
289
|
async addOfferIceCandidates(serviceFqn, offerId, candidates) {
|
|
253
|
-
const
|
|
254
|
-
return await this.rpc({
|
|
290
|
+
const request = {
|
|
255
291
|
method: 'addIceCandidates',
|
|
256
|
-
message: auth.message,
|
|
257
|
-
signature: auth.signature,
|
|
258
|
-
publicKey: this.keypair.publicKey,
|
|
259
292
|
params: { serviceFqn, offerId, candidates },
|
|
260
|
-
}
|
|
293
|
+
};
|
|
294
|
+
const authHeaders = await this.generateAuthHeaders(request, true);
|
|
295
|
+
return await this.rpc(request, authHeaders);
|
|
261
296
|
}
|
|
262
297
|
/**
|
|
263
298
|
* Get ICE candidates for a specific offer
|
|
264
299
|
*/
|
|
265
300
|
async getOfferIceCandidates(serviceFqn, offerId, since = 0) {
|
|
266
|
-
const
|
|
267
|
-
const result = await this.rpc({
|
|
301
|
+
const request = {
|
|
268
302
|
method: 'getIceCandidates',
|
|
269
|
-
message: auth.message,
|
|
270
|
-
signature: auth.signature,
|
|
271
|
-
publicKey: this.keypair.publicKey,
|
|
272
303
|
params: { serviceFqn, offerId, since },
|
|
273
|
-
}
|
|
304
|
+
};
|
|
305
|
+
const authHeaders = await this.generateAuthHeaders(request, true);
|
|
306
|
+
const result = await this.rpc(request, authHeaders);
|
|
274
307
|
return {
|
|
275
308
|
candidates: result.candidates || [],
|
|
276
309
|
offerId: result.offerId,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Answerer-side WebRTC connection with answer creation and offer processing
|
|
3
3
|
*/
|
|
4
|
-
import { RondevuConnection } from './
|
|
5
|
-
import { RondevuAPI } from '
|
|
6
|
-
import { ConnectionConfig } from './
|
|
4
|
+
import { RondevuConnection } from './base.js';
|
|
5
|
+
import { RondevuAPI } from '../api/client.js';
|
|
6
|
+
import { ConnectionConfig } from './config.js';
|
|
7
7
|
export interface AnswererOptions {
|
|
8
8
|
api: RondevuAPI;
|
|
9
9
|
serviceFqn: string;
|
|
@@ -30,9 +30,17 @@ export declare class AnswererConnection extends RondevuConnection {
|
|
|
30
30
|
*/
|
|
31
31
|
protected onLocalIceCandidate(candidate: RTCIceCandidate): void;
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
33
|
+
* Get the API instance
|
|
34
34
|
*/
|
|
35
|
-
protected
|
|
35
|
+
protected getApi(): any;
|
|
36
|
+
/**
|
|
37
|
+
* Get the service FQN
|
|
38
|
+
*/
|
|
39
|
+
protected getServiceFqn(): string;
|
|
40
|
+
/**
|
|
41
|
+
* Answerers accept ICE candidates from offerers only
|
|
42
|
+
*/
|
|
43
|
+
protected getIceCandidateRole(): 'offerer' | null;
|
|
36
44
|
/**
|
|
37
45
|
* Attempt to reconnect
|
|
38
46
|
*/
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Answerer-side WebRTC connection with answer creation and offer processing
|
|
3
3
|
*/
|
|
4
|
-
import { RondevuConnection } from './
|
|
5
|
-
import { ConnectionState } from './
|
|
4
|
+
import { RondevuConnection } from './base.js';
|
|
5
|
+
import { ConnectionState } from './events.js';
|
|
6
6
|
/**
|
|
7
7
|
* Answerer connection - processes offers and creates answers
|
|
8
8
|
*/
|
|
@@ -66,37 +66,22 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
69
|
+
* Get the API instance
|
|
70
70
|
*/
|
|
71
|
-
|
|
72
|
-
this.api
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
})
|
|
86
|
-
.catch((error) => {
|
|
87
|
-
this.debug('Failed to add ICE candidate:', error);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
// Update last poll time
|
|
91
|
-
if (iceCandidate.createdAt > this.lastIcePollTime) {
|
|
92
|
-
this.lastIcePollTime = iceCandidate.createdAt;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
})
|
|
97
|
-
.catch((error) => {
|
|
98
|
-
this.debug('Failed to poll ICE candidates:', error);
|
|
99
|
-
});
|
|
71
|
+
getApi() {
|
|
72
|
+
return this.api;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get the service FQN
|
|
76
|
+
*/
|
|
77
|
+
getServiceFqn() {
|
|
78
|
+
return this.serviceFqn;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Answerers accept ICE candidates from offerers only
|
|
82
|
+
*/
|
|
83
|
+
getIceCandidateRole() {
|
|
84
|
+
return 'offerer';
|
|
100
85
|
}
|
|
101
86
|
/**
|
|
102
87
|
* Attempt to reconnect
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Base connection class with state machine, reconnection, and message buffering
|
|
3
3
|
*/
|
|
4
4
|
import { EventEmitter } from 'eventemitter3';
|
|
5
|
-
import { ConnectionConfig } from './
|
|
6
|
-
import { ConnectionState, ConnectionEventMap } from './
|
|
7
|
-
import { ExponentialBackoff } from '
|
|
8
|
-
import { MessageBuffer } from '
|
|
5
|
+
import { ConnectionConfig } from './config.js';
|
|
6
|
+
import { ConnectionState, ConnectionEventMap } from './events.js';
|
|
7
|
+
import { ExponentialBackoff } from '../utils/exponential-backoff.js';
|
|
8
|
+
import { MessageBuffer } from '../utils/message-buffer.js';
|
|
9
9
|
/**
|
|
10
10
|
* Abstract base class for WebRTC connections with durability features
|
|
11
11
|
*/
|
|
@@ -82,6 +82,28 @@ export declare abstract class RondevuConnection extends EventEmitter<ConnectionE
|
|
|
82
82
|
* Stop ICE candidate polling
|
|
83
83
|
*/
|
|
84
84
|
protected stopIcePolling(): void;
|
|
85
|
+
/**
|
|
86
|
+
* Get the API instance - subclasses must provide
|
|
87
|
+
*/
|
|
88
|
+
protected abstract getApi(): any;
|
|
89
|
+
/**
|
|
90
|
+
* Get the service FQN - subclasses must provide
|
|
91
|
+
*/
|
|
92
|
+
protected abstract getServiceFqn(): string;
|
|
93
|
+
/**
|
|
94
|
+
* Get the offer ID - subclasses must provide
|
|
95
|
+
*/
|
|
96
|
+
protected abstract getOfferId(): string;
|
|
97
|
+
/**
|
|
98
|
+
* Get the ICE candidate role this connection should accept.
|
|
99
|
+
* Returns null for no filtering (offerer), or specific role (answerer accepts 'offerer').
|
|
100
|
+
*/
|
|
101
|
+
protected abstract getIceCandidateRole(): 'offerer' | null;
|
|
102
|
+
/**
|
|
103
|
+
* Poll for remote ICE candidates (consolidated implementation)
|
|
104
|
+
* Subclasses implement getIceCandidateRole() to specify filtering
|
|
105
|
+
*/
|
|
106
|
+
protected pollIceCandidates(): void;
|
|
85
107
|
/**
|
|
86
108
|
* Start connection timeout
|
|
87
109
|
*/
|
|
@@ -143,6 +165,5 @@ export declare abstract class RondevuConnection extends EventEmitter<ConnectionE
|
|
|
143
165
|
*/
|
|
144
166
|
protected debug(...args: any[]): void;
|
|
145
167
|
protected abstract onLocalIceCandidate(candidate: RTCIceCandidate): void;
|
|
146
|
-
protected abstract pollIceCandidates(): void;
|
|
147
168
|
protected abstract attemptReconnect(): void;
|
|
148
169
|
}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Base connection class with state machine, reconnection, and message buffering
|
|
3
3
|
*/
|
|
4
4
|
import { EventEmitter } from 'eventemitter3';
|
|
5
|
-
import { mergeConnectionConfig } from './
|
|
6
|
-
import { ConnectionState, } from './
|
|
7
|
-
import { ExponentialBackoff } from '
|
|
8
|
-
import { MessageBuffer } from '
|
|
5
|
+
import { mergeConnectionConfig } from './config.js';
|
|
6
|
+
import { ConnectionState, } from './events.js';
|
|
7
|
+
import { ExponentialBackoff } from '../utils/exponential-backoff.js';
|
|
8
|
+
import { MessageBuffer } from '../utils/message-buffer.js';
|
|
9
9
|
/**
|
|
10
10
|
* Abstract base class for WebRTC connections with durability features
|
|
11
11
|
*/
|
|
@@ -278,6 +278,47 @@ export class RondevuConnection extends EventEmitter {
|
|
|
278
278
|
this.icePollingInterval = null;
|
|
279
279
|
this.emit('ice:polling:stopped');
|
|
280
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Poll for remote ICE candidates (consolidated implementation)
|
|
283
|
+
* Subclasses implement getIceCandidateRole() to specify filtering
|
|
284
|
+
*/
|
|
285
|
+
pollIceCandidates() {
|
|
286
|
+
const acceptRole = this.getIceCandidateRole();
|
|
287
|
+
const api = this.getApi();
|
|
288
|
+
const serviceFqn = this.getServiceFqn();
|
|
289
|
+
const offerId = this.getOfferId();
|
|
290
|
+
api
|
|
291
|
+
.getOfferIceCandidates(serviceFqn, offerId, this.lastIcePollTime)
|
|
292
|
+
.then((result) => {
|
|
293
|
+
if (result.candidates.length > 0) {
|
|
294
|
+
this.debug(`Received ${result.candidates.length} remote ICE candidates`);
|
|
295
|
+
for (const iceCandidate of result.candidates) {
|
|
296
|
+
// Filter by role if specified (answerer only filters for 'offerer')
|
|
297
|
+
if (acceptRole !== null && iceCandidate.role !== acceptRole) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (iceCandidate.candidate && this.pc) {
|
|
301
|
+
const candidate = iceCandidate.candidate;
|
|
302
|
+
this.pc
|
|
303
|
+
.addIceCandidate(new RTCIceCandidate(candidate))
|
|
304
|
+
.then(() => {
|
|
305
|
+
this.emit('ice:candidate:remote', new RTCIceCandidate(candidate));
|
|
306
|
+
})
|
|
307
|
+
.catch((error) => {
|
|
308
|
+
this.debug('Failed to add ICE candidate:', error);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
// Update last poll time
|
|
312
|
+
if (iceCandidate.createdAt > this.lastIcePollTime) {
|
|
313
|
+
this.lastIcePollTime = iceCandidate.createdAt;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
.catch((error) => {
|
|
319
|
+
this.debug('Failed to poll ICE candidates:', error);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
281
322
|
/**
|
|
282
323
|
* Start connection timeout
|
|
283
324
|
*/
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Offerer-side WebRTC connection with offer creation and answer processing
|
|
3
3
|
*/
|
|
4
|
-
import { RondevuConnection } from './
|
|
5
|
-
import { RondevuAPI } from '
|
|
6
|
-
import { ConnectionConfig } from './
|
|
4
|
+
import { RondevuConnection } from './base.js';
|
|
5
|
+
import { RondevuAPI } from '../api/client.js';
|
|
6
|
+
import { ConnectionConfig } from './config.js';
|
|
7
7
|
export interface OffererOptions {
|
|
8
8
|
api: RondevuAPI;
|
|
9
9
|
serviceFqn: string;
|
|
@@ -19,6 +19,10 @@ export declare class OffererConnection extends RondevuConnection {
|
|
|
19
19
|
private api;
|
|
20
20
|
private serviceFqn;
|
|
21
21
|
private offerId;
|
|
22
|
+
private rotationLock;
|
|
23
|
+
private rotating;
|
|
24
|
+
private rotationAttempts;
|
|
25
|
+
private static readonly MAX_ROTATION_ATTEMPTS;
|
|
22
26
|
constructor(options: OffererOptions);
|
|
23
27
|
/**
|
|
24
28
|
* Initialize the connection - setup handlers for already-created offer
|
|
@@ -28,6 +32,19 @@ export declare class OffererConnection extends RondevuConnection {
|
|
|
28
32
|
* Process an answer from the answerer
|
|
29
33
|
*/
|
|
30
34
|
processAnswer(sdp: string, answererId: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Rebind this connection to a new offer (when previous offer failed)
|
|
37
|
+
* Keeps the same connection object alive but with new underlying WebRTC
|
|
38
|
+
*/
|
|
39
|
+
rebindToOffer(newOfferId: string, newPc: RTCPeerConnection, newDc?: RTCDataChannel): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Check if connection is currently rotating
|
|
42
|
+
*/
|
|
43
|
+
isRotating(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Override onConnected to reset rotation attempts
|
|
46
|
+
*/
|
|
47
|
+
protected onConnected(): void;
|
|
31
48
|
/**
|
|
32
49
|
* Generate a hash fingerprint of SDP for deduplication
|
|
33
50
|
*/
|
|
@@ -37,9 +54,17 @@ export declare class OffererConnection extends RondevuConnection {
|
|
|
37
54
|
*/
|
|
38
55
|
protected onLocalIceCandidate(candidate: RTCIceCandidate): void;
|
|
39
56
|
/**
|
|
40
|
-
*
|
|
57
|
+
* Get the API instance
|
|
58
|
+
*/
|
|
59
|
+
protected getApi(): any;
|
|
60
|
+
/**
|
|
61
|
+
* Get the service FQN
|
|
62
|
+
*/
|
|
63
|
+
protected getServiceFqn(): string;
|
|
64
|
+
/**
|
|
65
|
+
* Offerers accept all ICE candidates (no filtering)
|
|
41
66
|
*/
|
|
42
|
-
protected
|
|
67
|
+
protected getIceCandidateRole(): 'offerer' | null;
|
|
43
68
|
/**
|
|
44
69
|
* Attempt to reconnect
|
|
45
70
|
*
|