@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
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Offerer-side WebRTC connection with offer creation and answer processing
|
|
3
3
|
*/
|
|
4
|
-
import { RondevuConnection } from './
|
|
5
|
-
import { ConnectionState } from './
|
|
4
|
+
import { RondevuConnection } from './base.js';
|
|
5
|
+
import { ConnectionState } from './events.js';
|
|
6
|
+
import { AsyncLock } from '../utils/async-lock.js';
|
|
6
7
|
/**
|
|
7
8
|
* Offerer connection - manages already-created offers and waits for answers
|
|
8
9
|
*/
|
|
9
10
|
export class OffererConnection extends RondevuConnection {
|
|
10
11
|
constructor(options) {
|
|
11
|
-
|
|
12
|
+
// Force reconnectEnabled: false for offerer connections (offers are ephemeral)
|
|
13
|
+
super(undefined, {
|
|
14
|
+
...options.config,
|
|
15
|
+
reconnectEnabled: false
|
|
16
|
+
});
|
|
17
|
+
// Rotation tracking
|
|
18
|
+
this.rotationLock = new AsyncLock();
|
|
19
|
+
this.rotating = false;
|
|
20
|
+
this.rotationAttempts = 0;
|
|
12
21
|
this.api = options.api;
|
|
13
22
|
this.serviceFqn = options.serviceFqn;
|
|
14
23
|
this.offerId = options.offerId;
|
|
@@ -82,6 +91,71 @@ export class OffererConnection extends RondevuConnection {
|
|
|
82
91
|
throw error;
|
|
83
92
|
}
|
|
84
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Rebind this connection to a new offer (when previous offer failed)
|
|
96
|
+
* Keeps the same connection object alive but with new underlying WebRTC
|
|
97
|
+
*/
|
|
98
|
+
async rebindToOffer(newOfferId, newPc, newDc) {
|
|
99
|
+
return this.rotationLock.run(async () => {
|
|
100
|
+
if (this.rotating) {
|
|
101
|
+
throw new Error('Rotation already in progress');
|
|
102
|
+
}
|
|
103
|
+
this.rotating = true;
|
|
104
|
+
try {
|
|
105
|
+
this.rotationAttempts++;
|
|
106
|
+
if (this.rotationAttempts > OffererConnection.MAX_ROTATION_ATTEMPTS) {
|
|
107
|
+
throw new Error('Max rotation attempts exceeded');
|
|
108
|
+
}
|
|
109
|
+
this.debug(`Rebinding connection from ${this.offerId} to ${newOfferId}`);
|
|
110
|
+
// 1. Clean up old peer connection
|
|
111
|
+
if (this.pc) {
|
|
112
|
+
this.pc.close();
|
|
113
|
+
}
|
|
114
|
+
if (this.dc && this.dc !== newDc) {
|
|
115
|
+
this.dc.close();
|
|
116
|
+
}
|
|
117
|
+
// 2. Update to new offer
|
|
118
|
+
this.offerId = newOfferId;
|
|
119
|
+
this.pc = newPc;
|
|
120
|
+
this.dc = newDc || null;
|
|
121
|
+
// 3. Reset answer processing flags
|
|
122
|
+
this.answerProcessed = false;
|
|
123
|
+
this.answerSdpFingerprint = null;
|
|
124
|
+
// 4. Setup event handlers for new peer connection
|
|
125
|
+
this.pc.onicecandidate = (event) => this.handleIceCandidate(event);
|
|
126
|
+
this.pc.oniceconnectionstatechange = () => this.handleIceConnectionStateChange();
|
|
127
|
+
this.pc.onconnectionstatechange = () => this.handleConnectionStateChange();
|
|
128
|
+
this.pc.onicegatheringstatechange = () => this.handleIceGatheringStateChange();
|
|
129
|
+
// 5. Setup data channel handlers if we have one
|
|
130
|
+
if (this.dc) {
|
|
131
|
+
this.setupDataChannelHandlers(this.dc);
|
|
132
|
+
}
|
|
133
|
+
// 6. Restart connection timeout
|
|
134
|
+
this.startConnectionTimeout();
|
|
135
|
+
// 7. Transition to SIGNALING state (waiting for answer)
|
|
136
|
+
this.transitionTo(ConnectionState.SIGNALING, 'Offer rotated, waiting for answer');
|
|
137
|
+
// Note: Message buffer is NOT cleared - it persists!
|
|
138
|
+
this.debug(`Rebind complete. Buffer has ${this.messageBuffer?.size() ?? 0} messages`);
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
this.rotating = false;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check if connection is currently rotating
|
|
147
|
+
*/
|
|
148
|
+
isRotating() {
|
|
149
|
+
return this.rotating;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Override onConnected to reset rotation attempts
|
|
153
|
+
*/
|
|
154
|
+
onConnected() {
|
|
155
|
+
super.onConnected();
|
|
156
|
+
this.rotationAttempts = 0;
|
|
157
|
+
this.debug('Connection established, rotation attempts reset');
|
|
158
|
+
}
|
|
85
159
|
/**
|
|
86
160
|
* Generate a hash fingerprint of SDP for deduplication
|
|
87
161
|
*/
|
|
@@ -124,36 +198,22 @@ export class OffererConnection extends RondevuConnection {
|
|
|
124
198
|
});
|
|
125
199
|
}
|
|
126
200
|
/**
|
|
127
|
-
*
|
|
201
|
+
* Get the API instance
|
|
128
202
|
*/
|
|
129
|
-
|
|
130
|
-
this.api
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
});
|
|
203
|
+
getApi() {
|
|
204
|
+
return this.api;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get the service FQN
|
|
208
|
+
*/
|
|
209
|
+
getServiceFqn() {
|
|
210
|
+
return this.serviceFqn;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Offerers accept all ICE candidates (no filtering)
|
|
214
|
+
*/
|
|
215
|
+
getIceCandidateRole() {
|
|
216
|
+
return null;
|
|
157
217
|
}
|
|
158
218
|
/**
|
|
159
219
|
* Attempt to reconnect
|
|
@@ -175,3 +235,4 @@ export class OffererConnection extends RondevuConnection {
|
|
|
175
235
|
return this.offerId;
|
|
176
236
|
}
|
|
177
237
|
}
|
|
238
|
+
OffererConnection.MAX_ROTATION_ATTEMPTS = 5;
|
|
@@ -0,0 +1,22 @@
|
|
|
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/client.js';
|
|
7
|
+
export { RpcBatcher } from '../api/batcher.js';
|
|
8
|
+
export { RondevuConnection } from '../connections/base.js';
|
|
9
|
+
export { OffererConnection } from '../connections/offerer.js';
|
|
10
|
+
export { AnswererConnection } from '../connections/answerer.js';
|
|
11
|
+
export { ExponentialBackoff } from '../utils/exponential-backoff.js';
|
|
12
|
+
export { MessageBuffer } from '../utils/message-buffer.js';
|
|
13
|
+
export { WebCryptoAdapter } from '../crypto/web.js';
|
|
14
|
+
export { NodeCryptoAdapter } from '../crypto/node.js';
|
|
15
|
+
export type { Signaler, Binnable, } from './types.js';
|
|
16
|
+
export type { Keypair, OfferRequest, ServiceRequest, Service, ServiceOffer, IceCandidate, } from '../api/client.js';
|
|
17
|
+
export type { RondevuOptions, PublishServiceOptions, ConnectToServiceOptions, ConnectionContext, OfferContext, OfferFactory, ActiveOffer, FindServiceOptions, ServiceResult, PaginatedServiceResult } from './rondevu.js';
|
|
18
|
+
export type { CryptoAdapter } from '../crypto/adapter.js';
|
|
19
|
+
export type { ConnectionConfig, } from '../connections/config.js';
|
|
20
|
+
export type { ConnectionState, BufferedMessage, ReconnectInfo, StateChangeInfo, ConnectionEventMap, ConnectionEventName, ConnectionEventArgs, } from '../connections/events.js';
|
|
21
|
+
export type { OffererOptions, } from '../connections/offerer.js';
|
|
22
|
+
export type { AnswererOptions, } from '../connections/answerer.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
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/client.js';
|
|
7
|
+
export { RpcBatcher } from '../api/batcher.js';
|
|
8
|
+
// Export connection classes
|
|
9
|
+
export { RondevuConnection } from '../connections/base.js';
|
|
10
|
+
export { OffererConnection } from '../connections/offerer.js';
|
|
11
|
+
export { AnswererConnection } from '../connections/answerer.js';
|
|
12
|
+
// Export utilities
|
|
13
|
+
export { ExponentialBackoff } from '../utils/exponential-backoff.js';
|
|
14
|
+
export { MessageBuffer } from '../utils/message-buffer.js';
|
|
15
|
+
// Export crypto adapters
|
|
16
|
+
export { WebCryptoAdapter } from '../crypto/web.js';
|
|
17
|
+
export { NodeCryptoAdapter } from '../crypto/node.js';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { EventEmitter } from 'eventemitter3';
|
|
2
|
+
import { RondevuAPI } from '../api/client.js';
|
|
3
|
+
import { OffererConnection } from '../connections/offerer.js';
|
|
4
|
+
import { ConnectionConfig } from '../connections/config.js';
|
|
5
|
+
export type OfferFactory = (pc: RTCPeerConnection) => Promise<{
|
|
6
|
+
dc?: RTCDataChannel;
|
|
7
|
+
offer: RTCSessionDescriptionInit;
|
|
8
|
+
}>;
|
|
9
|
+
export interface OfferPoolOptions {
|
|
10
|
+
api: RondevuAPI;
|
|
11
|
+
serviceFqn: string;
|
|
12
|
+
maxOffers: number;
|
|
13
|
+
offerFactory: OfferFactory;
|
|
14
|
+
ttl: number;
|
|
15
|
+
iceServers: RTCIceServer[];
|
|
16
|
+
connectionConfig?: Partial<ConnectionConfig>;
|
|
17
|
+
debugEnabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
interface OfferPoolEvents {
|
|
20
|
+
'connection:opened': (offerId: string, connection: OffererConnection) => void;
|
|
21
|
+
'offer:created': (offerId: string, serviceFqn: string) => void;
|
|
22
|
+
'offer:failed': (offerId: string, error: Error) => void;
|
|
23
|
+
'connection:rotated': (oldOfferId: string, newOfferId: string, connection: OffererConnection) => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* OfferPool manages a pool of WebRTC offers for a published service.
|
|
27
|
+
* Maintains a target number of active offers and automatically replaces
|
|
28
|
+
* offers that fail or get answered.
|
|
29
|
+
*/
|
|
30
|
+
export declare class OfferPool extends EventEmitter<OfferPoolEvents> {
|
|
31
|
+
private readonly api;
|
|
32
|
+
private readonly serviceFqn;
|
|
33
|
+
private readonly maxOffers;
|
|
34
|
+
private readonly offerFactory;
|
|
35
|
+
private readonly ttl;
|
|
36
|
+
private readonly iceServers;
|
|
37
|
+
private readonly connectionConfig?;
|
|
38
|
+
private readonly debugEnabled;
|
|
39
|
+
private readonly activeConnections;
|
|
40
|
+
private readonly fillLock;
|
|
41
|
+
private running;
|
|
42
|
+
private pollingInterval;
|
|
43
|
+
private lastPollTimestamp;
|
|
44
|
+
private static readonly POLLING_INTERVAL_MS;
|
|
45
|
+
constructor(options: OfferPoolOptions);
|
|
46
|
+
/**
|
|
47
|
+
* Start filling offers and polling for answers
|
|
48
|
+
*/
|
|
49
|
+
start(): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Stop filling offers and polling
|
|
52
|
+
* Closes all active connections
|
|
53
|
+
*/
|
|
54
|
+
stop(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Get count of active offers
|
|
57
|
+
*/
|
|
58
|
+
getOfferCount(): number;
|
|
59
|
+
/**
|
|
60
|
+
* Get all active connections
|
|
61
|
+
*/
|
|
62
|
+
getActiveConnections(): Map<string, OffererConnection>;
|
|
63
|
+
/**
|
|
64
|
+
* Check if a specific offer is connected
|
|
65
|
+
*/
|
|
66
|
+
isConnected(offerId: string): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Disconnect all active offers
|
|
69
|
+
*/
|
|
70
|
+
disconnectAll(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Fill offers to reach maxOffers count
|
|
73
|
+
* Uses AsyncLock to prevent concurrent fills
|
|
74
|
+
*/
|
|
75
|
+
private fillOffers;
|
|
76
|
+
/**
|
|
77
|
+
* Create a new offer for rotation (reuses existing creation logic)
|
|
78
|
+
* Similar to createOffer() but only creates the offer, doesn't create connection
|
|
79
|
+
*/
|
|
80
|
+
private createNewOfferForRotation;
|
|
81
|
+
/**
|
|
82
|
+
* Create a single offer and publish it to the server
|
|
83
|
+
*/
|
|
84
|
+
private createOffer;
|
|
85
|
+
/**
|
|
86
|
+
* Poll for answers and delegate to OffererConnections
|
|
87
|
+
*/
|
|
88
|
+
private pollInternal;
|
|
89
|
+
/**
|
|
90
|
+
* Debug logging (only if debug enabled)
|
|
91
|
+
*/
|
|
92
|
+
private debug;
|
|
93
|
+
}
|
|
94
|
+
export {};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { EventEmitter } from 'eventemitter3';
|
|
2
|
+
import { OffererConnection } from '../connections/offerer.js';
|
|
3
|
+
import { AsyncLock } from '../utils/async-lock.js';
|
|
4
|
+
/**
|
|
5
|
+
* OfferPool manages a pool of WebRTC offers for a published service.
|
|
6
|
+
* Maintains a target number of active offers and automatically replaces
|
|
7
|
+
* offers that fail or get answered.
|
|
8
|
+
*/
|
|
9
|
+
export class OfferPool extends EventEmitter {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super();
|
|
12
|
+
// State
|
|
13
|
+
this.activeConnections = new Map();
|
|
14
|
+
this.fillLock = new AsyncLock();
|
|
15
|
+
this.running = false;
|
|
16
|
+
this.pollingInterval = null;
|
|
17
|
+
this.lastPollTimestamp = 0;
|
|
18
|
+
this.api = options.api;
|
|
19
|
+
this.serviceFqn = options.serviceFqn;
|
|
20
|
+
this.maxOffers = options.maxOffers;
|
|
21
|
+
this.offerFactory = options.offerFactory;
|
|
22
|
+
this.ttl = options.ttl;
|
|
23
|
+
this.iceServers = options.iceServers;
|
|
24
|
+
this.connectionConfig = options.connectionConfig;
|
|
25
|
+
this.debugEnabled = options.debugEnabled || false;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Start filling offers and polling for answers
|
|
29
|
+
*/
|
|
30
|
+
async start() {
|
|
31
|
+
if (this.running) {
|
|
32
|
+
this.debug('Already running');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
this.debug('Starting offer pool');
|
|
36
|
+
this.running = true;
|
|
37
|
+
// Fill initial offers
|
|
38
|
+
await this.fillOffers();
|
|
39
|
+
// Start polling for answers
|
|
40
|
+
this.pollingInterval = setInterval(() => {
|
|
41
|
+
this.pollInternal();
|
|
42
|
+
}, OfferPool.POLLING_INTERVAL_MS);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Stop filling offers and polling
|
|
46
|
+
* Closes all active connections
|
|
47
|
+
*/
|
|
48
|
+
stop() {
|
|
49
|
+
this.debug('Stopping offer pool');
|
|
50
|
+
this.running = false;
|
|
51
|
+
// Stop polling
|
|
52
|
+
if (this.pollingInterval) {
|
|
53
|
+
clearInterval(this.pollingInterval);
|
|
54
|
+
this.pollingInterval = null;
|
|
55
|
+
}
|
|
56
|
+
// Close all active connections
|
|
57
|
+
for (const [offerId, connection] of this.activeConnections.entries()) {
|
|
58
|
+
if (connection.isRotating()) {
|
|
59
|
+
this.debug(`Connection ${offerId} is rotating, will close anyway`);
|
|
60
|
+
}
|
|
61
|
+
this.debug(`Closing connection ${offerId}`);
|
|
62
|
+
connection.close();
|
|
63
|
+
}
|
|
64
|
+
this.activeConnections.clear();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get count of active offers
|
|
68
|
+
*/
|
|
69
|
+
getOfferCount() {
|
|
70
|
+
return this.activeConnections.size;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get all active connections
|
|
74
|
+
*/
|
|
75
|
+
getActiveConnections() {
|
|
76
|
+
return this.activeConnections;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if a specific offer is connected
|
|
80
|
+
*/
|
|
81
|
+
isConnected(offerId) {
|
|
82
|
+
const connection = this.activeConnections.get(offerId);
|
|
83
|
+
return connection ? connection.getState() === 'connected' : false;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Disconnect all active offers
|
|
87
|
+
*/
|
|
88
|
+
disconnectAll() {
|
|
89
|
+
this.debug('Disconnecting all offers');
|
|
90
|
+
for (const [offerId, connection] of this.activeConnections.entries()) {
|
|
91
|
+
this.debug(`Closing connection ${offerId}`);
|
|
92
|
+
connection.close();
|
|
93
|
+
}
|
|
94
|
+
this.activeConnections.clear();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Fill offers to reach maxOffers count
|
|
98
|
+
* Uses AsyncLock to prevent concurrent fills
|
|
99
|
+
*/
|
|
100
|
+
async fillOffers() {
|
|
101
|
+
if (!this.running)
|
|
102
|
+
return;
|
|
103
|
+
return this.fillLock.run(async () => {
|
|
104
|
+
const currentCount = this.activeConnections.size;
|
|
105
|
+
const needed = this.maxOffers - currentCount;
|
|
106
|
+
this.debug(`Filling offers: current=${currentCount}, needed=${needed}`);
|
|
107
|
+
for (let i = 0; i < needed; i++) {
|
|
108
|
+
try {
|
|
109
|
+
await this.createOffer();
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
console.error('[OfferPool] Failed to create offer:', err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Create a new offer for rotation (reuses existing creation logic)
|
|
119
|
+
* Similar to createOffer() but only creates the offer, doesn't create connection
|
|
120
|
+
*/
|
|
121
|
+
async createNewOfferForRotation() {
|
|
122
|
+
const rtcConfig = {
|
|
123
|
+
iceServers: this.iceServers
|
|
124
|
+
};
|
|
125
|
+
this.debug('Creating new offer for rotation...');
|
|
126
|
+
// 1. Create RTCPeerConnection
|
|
127
|
+
const pc = new RTCPeerConnection(rtcConfig);
|
|
128
|
+
// 2. Call the factory to create offer
|
|
129
|
+
let dc;
|
|
130
|
+
let offer;
|
|
131
|
+
try {
|
|
132
|
+
const factoryResult = await this.offerFactory(pc);
|
|
133
|
+
dc = factoryResult.dc;
|
|
134
|
+
offer = factoryResult.offer;
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
pc.close();
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
// 3. Publish to server to get offerId
|
|
141
|
+
const result = await this.api.publishService({
|
|
142
|
+
serviceFqn: this.serviceFqn,
|
|
143
|
+
offers: [{ sdp: offer.sdp }],
|
|
144
|
+
ttl: this.ttl,
|
|
145
|
+
});
|
|
146
|
+
const newOfferId = result.offers[0].offerId;
|
|
147
|
+
this.debug(`New offer created for rotation: ${newOfferId}`);
|
|
148
|
+
return { newOfferId, pc, dc };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Create a single offer and publish it to the server
|
|
152
|
+
*/
|
|
153
|
+
async createOffer() {
|
|
154
|
+
const rtcConfig = {
|
|
155
|
+
iceServers: this.iceServers
|
|
156
|
+
};
|
|
157
|
+
this.debug('Creating new offer...');
|
|
158
|
+
// 1. Create RTCPeerConnection
|
|
159
|
+
const pc = new RTCPeerConnection(rtcConfig);
|
|
160
|
+
// 2. Call the factory to create offer
|
|
161
|
+
let dc;
|
|
162
|
+
let offer;
|
|
163
|
+
try {
|
|
164
|
+
const factoryResult = await this.offerFactory(pc);
|
|
165
|
+
dc = factoryResult.dc;
|
|
166
|
+
offer = factoryResult.offer;
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
pc.close();
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
// 3. Publish to server to get offerId
|
|
173
|
+
const result = await this.api.publishService({
|
|
174
|
+
serviceFqn: this.serviceFqn,
|
|
175
|
+
offers: [{ sdp: offer.sdp }],
|
|
176
|
+
ttl: this.ttl,
|
|
177
|
+
});
|
|
178
|
+
const offerId = result.offers[0].offerId;
|
|
179
|
+
// 4. Create OffererConnection instance
|
|
180
|
+
const connection = new OffererConnection({
|
|
181
|
+
api: this.api,
|
|
182
|
+
serviceFqn: this.serviceFqn,
|
|
183
|
+
offerId,
|
|
184
|
+
pc,
|
|
185
|
+
dc,
|
|
186
|
+
config: {
|
|
187
|
+
...this.connectionConfig,
|
|
188
|
+
debug: this.debugEnabled,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
// Setup connection event handlers
|
|
192
|
+
connection.on('connected', () => {
|
|
193
|
+
this.debug(`Connection established for offer ${offerId}`);
|
|
194
|
+
this.emit('connection:opened', offerId, connection);
|
|
195
|
+
});
|
|
196
|
+
connection.on('failed', async (error) => {
|
|
197
|
+
const currentOfferId = connection.getOfferId();
|
|
198
|
+
this.debug(`Connection failed for offer ${currentOfferId}, rotating...`);
|
|
199
|
+
try {
|
|
200
|
+
// Create new offer and rebind existing connection
|
|
201
|
+
const { newOfferId, pc, dc } = await this.createNewOfferForRotation();
|
|
202
|
+
// Rebind the connection to new offer
|
|
203
|
+
await connection.rebindToOffer(newOfferId, pc, dc);
|
|
204
|
+
// Update map: remove old offerId, add new offerId with same connection
|
|
205
|
+
this.activeConnections.delete(currentOfferId);
|
|
206
|
+
this.activeConnections.set(newOfferId, connection);
|
|
207
|
+
this.emit('connection:rotated', currentOfferId, newOfferId, connection);
|
|
208
|
+
this.debug(`Connection rotated: ${currentOfferId} → ${newOfferId}`);
|
|
209
|
+
}
|
|
210
|
+
catch (rotationError) {
|
|
211
|
+
// If rotation fails, fall back to destroying connection
|
|
212
|
+
this.debug(`Rotation failed for ${currentOfferId}:`, rotationError);
|
|
213
|
+
this.activeConnections.delete(currentOfferId);
|
|
214
|
+
this.emit('offer:failed', currentOfferId, error);
|
|
215
|
+
this.fillOffers(); // Create replacement
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
connection.on('closed', () => {
|
|
219
|
+
this.debug(`Connection closed for offer ${offerId}`);
|
|
220
|
+
this.activeConnections.delete(offerId);
|
|
221
|
+
this.fillOffers(); // Replace closed offer
|
|
222
|
+
});
|
|
223
|
+
// Store active connection
|
|
224
|
+
this.activeConnections.set(offerId, connection);
|
|
225
|
+
// Initialize the connection
|
|
226
|
+
await connection.initialize();
|
|
227
|
+
this.debug(`Offer created: ${offerId}`);
|
|
228
|
+
this.emit('offer:created', offerId, this.serviceFqn);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Poll for answers and delegate to OffererConnections
|
|
232
|
+
*/
|
|
233
|
+
async pollInternal() {
|
|
234
|
+
if (!this.running)
|
|
235
|
+
return;
|
|
236
|
+
try {
|
|
237
|
+
const result = await this.api.poll(this.lastPollTimestamp);
|
|
238
|
+
// Process answers - delegate to OffererConnections
|
|
239
|
+
for (const answer of result.answers) {
|
|
240
|
+
const connection = this.activeConnections.get(answer.offerId);
|
|
241
|
+
if (connection) {
|
|
242
|
+
try {
|
|
243
|
+
await connection.processAnswer(answer.sdp, answer.answererId);
|
|
244
|
+
this.lastPollTimestamp = Math.max(this.lastPollTimestamp, answer.answeredAt);
|
|
245
|
+
// Create replacement offer
|
|
246
|
+
this.fillOffers();
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
this.debug(`Failed to process answer for offer ${answer.offerId}:`, err);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
console.error('[OfferPool] Polling error:', err);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Debug logging (only if debug enabled)
|
|
260
|
+
*/
|
|
261
|
+
debug(...args) {
|
|
262
|
+
if (this.debugEnabled) {
|
|
263
|
+
console.log('[OfferPool]', ...args);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
OfferPool.POLLING_INTERVAL_MS = 1000;
|