@xtr-dev/rondevu-client 0.18.6 → 0.18.8
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/dist/answerer-connection.d.ts +44 -0
- package/dist/answerer-connection.js +145 -0
- package/dist/connection-config.d.ts +21 -0
- package/dist/connection-config.js +30 -0
- package/dist/connection-events.d.ts +78 -0
- package/dist/connection-events.js +16 -0
- package/dist/connection.d.ts +148 -0
- package/dist/connection.js +481 -0
- package/dist/exponential-backoff.d.ts +30 -0
- package/dist/exponential-backoff.js +46 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +7 -0
- package/dist/message-buffer.d.ts +55 -0
- package/dist/message-buffer.js +106 -0
- package/dist/offerer-connection.d.ts +50 -0
- package/dist/offerer-connection.js +193 -0
- package/dist/rondevu.d.ts +77 -64
- package/dist/rondevu.js +180 -298
- package/package.json +1 -1
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base connection class with state machine, reconnection, and message buffering
|
|
3
|
+
*/
|
|
4
|
+
import { EventEmitter } from 'eventemitter3';
|
|
5
|
+
import { mergeConnectionConfig } from './connection-config.js';
|
|
6
|
+
import { ConnectionState, } from './connection-events.js';
|
|
7
|
+
import { ExponentialBackoff } from './exponential-backoff.js';
|
|
8
|
+
import { MessageBuffer } from './message-buffer.js';
|
|
9
|
+
/**
|
|
10
|
+
* Abstract base class for WebRTC connections with durability features
|
|
11
|
+
*/
|
|
12
|
+
export class RondevuConnection extends EventEmitter {
|
|
13
|
+
constructor(rtcConfig, userConfig) {
|
|
14
|
+
super();
|
|
15
|
+
this.rtcConfig = rtcConfig;
|
|
16
|
+
this.pc = null;
|
|
17
|
+
this.dc = null;
|
|
18
|
+
this.state = ConnectionState.INITIALIZING;
|
|
19
|
+
// Message buffering
|
|
20
|
+
this.messageBuffer = null;
|
|
21
|
+
// Reconnection
|
|
22
|
+
this.backoff = null;
|
|
23
|
+
this.reconnectTimeout = null;
|
|
24
|
+
this.reconnectAttempts = 0;
|
|
25
|
+
// Timeouts
|
|
26
|
+
this.connectionTimeout = null;
|
|
27
|
+
this.iceGatheringTimeout = null;
|
|
28
|
+
// ICE polling
|
|
29
|
+
this.icePollingInterval = null;
|
|
30
|
+
this.lastIcePollTime = 0;
|
|
31
|
+
// Answer fingerprinting (for offerer)
|
|
32
|
+
this.answerProcessed = false;
|
|
33
|
+
this.answerSdpFingerprint = null;
|
|
34
|
+
this.config = mergeConnectionConfig(userConfig);
|
|
35
|
+
// Initialize message buffer if enabled
|
|
36
|
+
if (this.config.bufferEnabled) {
|
|
37
|
+
this.messageBuffer = new MessageBuffer({
|
|
38
|
+
maxSize: this.config.maxBufferSize,
|
|
39
|
+
maxAge: this.config.maxBufferAge,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// Initialize backoff if reconnection enabled
|
|
43
|
+
if (this.config.reconnectEnabled) {
|
|
44
|
+
this.backoff = new ExponentialBackoff({
|
|
45
|
+
base: this.config.reconnectBackoffBase,
|
|
46
|
+
max: this.config.reconnectBackoffMax,
|
|
47
|
+
jitter: this.config.reconnectJitter,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Transition to a new state and emit events
|
|
53
|
+
*/
|
|
54
|
+
transitionTo(newState, reason) {
|
|
55
|
+
if (this.state === newState)
|
|
56
|
+
return;
|
|
57
|
+
const oldState = this.state;
|
|
58
|
+
this.state = newState;
|
|
59
|
+
this.debug(`State transition: ${oldState} → ${newState}${reason ? ` (${reason})` : ''}`);
|
|
60
|
+
this.emit('state:changed', { oldState, newState, reason });
|
|
61
|
+
// Emit specific lifecycle events
|
|
62
|
+
switch (newState) {
|
|
63
|
+
case ConnectionState.CONNECTING:
|
|
64
|
+
this.emit('connecting');
|
|
65
|
+
break;
|
|
66
|
+
case ConnectionState.CONNECTED:
|
|
67
|
+
this.emit('connected');
|
|
68
|
+
break;
|
|
69
|
+
case ConnectionState.DISCONNECTED:
|
|
70
|
+
this.emit('disconnected', reason);
|
|
71
|
+
break;
|
|
72
|
+
case ConnectionState.FAILED:
|
|
73
|
+
this.emit('failed', new Error(reason || 'Connection failed'));
|
|
74
|
+
break;
|
|
75
|
+
case ConnectionState.CLOSED:
|
|
76
|
+
this.emit('closed', reason);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Create and configure RTCPeerConnection
|
|
82
|
+
*/
|
|
83
|
+
createPeerConnection() {
|
|
84
|
+
this.pc = new RTCPeerConnection(this.rtcConfig);
|
|
85
|
+
// Setup event handlers BEFORE any signaling
|
|
86
|
+
this.pc.onicecandidate = (event) => this.handleIceCandidate(event);
|
|
87
|
+
this.pc.oniceconnectionstatechange = () => this.handleIceConnectionStateChange();
|
|
88
|
+
this.pc.onconnectionstatechange = () => this.handleConnectionStateChange();
|
|
89
|
+
this.pc.onicegatheringstatechange = () => this.handleIceGatheringStateChange();
|
|
90
|
+
return this.pc;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Setup data channel event handlers
|
|
94
|
+
*/
|
|
95
|
+
setupDataChannelHandlers(dc) {
|
|
96
|
+
dc.onopen = () => this.handleDataChannelOpen();
|
|
97
|
+
dc.onclose = () => this.handleDataChannelClose();
|
|
98
|
+
dc.onerror = (error) => this.handleDataChannelError(error);
|
|
99
|
+
dc.onmessage = (event) => this.handleMessage(event);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Handle local ICE candidate generation
|
|
103
|
+
*/
|
|
104
|
+
handleIceCandidate(event) {
|
|
105
|
+
this.emit('ice:candidate:local', event.candidate);
|
|
106
|
+
if (event.candidate) {
|
|
107
|
+
this.onLocalIceCandidate(event.candidate);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Handle ICE connection state changes (primary state driver)
|
|
112
|
+
*/
|
|
113
|
+
handleIceConnectionStateChange() {
|
|
114
|
+
if (!this.pc)
|
|
115
|
+
return;
|
|
116
|
+
const iceState = this.pc.iceConnectionState;
|
|
117
|
+
this.emit('ice:connection:state', iceState);
|
|
118
|
+
this.debug(`ICE connection state: ${iceState}`);
|
|
119
|
+
switch (iceState) {
|
|
120
|
+
case 'checking':
|
|
121
|
+
if (this.state === ConnectionState.SIGNALING) {
|
|
122
|
+
this.transitionTo(ConnectionState.CHECKING, 'ICE checking started');
|
|
123
|
+
}
|
|
124
|
+
this.startIcePolling();
|
|
125
|
+
break;
|
|
126
|
+
case 'connected':
|
|
127
|
+
case 'completed':
|
|
128
|
+
this.stopIcePolling();
|
|
129
|
+
// Wait for data channel to open before transitioning to CONNECTED
|
|
130
|
+
if (this.dc?.readyState === 'open') {
|
|
131
|
+
this.transitionTo(ConnectionState.CONNECTED, 'ICE connected and data channel open');
|
|
132
|
+
this.onConnected();
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
case 'disconnected':
|
|
136
|
+
if (this.state === ConnectionState.CONNECTED) {
|
|
137
|
+
this.transitionTo(ConnectionState.DISCONNECTED, 'ICE disconnected');
|
|
138
|
+
this.scheduleReconnect();
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
case 'failed':
|
|
142
|
+
this.stopIcePolling();
|
|
143
|
+
this.transitionTo(ConnectionState.FAILED, 'ICE connection failed');
|
|
144
|
+
this.scheduleReconnect();
|
|
145
|
+
break;
|
|
146
|
+
case 'closed':
|
|
147
|
+
this.stopIcePolling();
|
|
148
|
+
this.transitionTo(ConnectionState.CLOSED, 'ICE connection closed');
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Handle connection state changes (backup validation)
|
|
154
|
+
*/
|
|
155
|
+
handleConnectionStateChange() {
|
|
156
|
+
if (!this.pc)
|
|
157
|
+
return;
|
|
158
|
+
const connState = this.pc.connectionState;
|
|
159
|
+
this.emit('connection:state', connState);
|
|
160
|
+
this.debug(`Connection state: ${connState}`);
|
|
161
|
+
// Connection state provides backup validation
|
|
162
|
+
if (connState === 'failed' && this.state !== ConnectionState.FAILED) {
|
|
163
|
+
this.transitionTo(ConnectionState.FAILED, 'PeerConnection failed');
|
|
164
|
+
this.scheduleReconnect();
|
|
165
|
+
}
|
|
166
|
+
else if (connState === 'closed' && this.state !== ConnectionState.CLOSED) {
|
|
167
|
+
this.transitionTo(ConnectionState.CLOSED, 'PeerConnection closed');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Handle ICE gathering state changes
|
|
172
|
+
*/
|
|
173
|
+
handleIceGatheringStateChange() {
|
|
174
|
+
if (!this.pc)
|
|
175
|
+
return;
|
|
176
|
+
const gatheringState = this.pc.iceGatheringState;
|
|
177
|
+
this.emit('ice:gathering:state', gatheringState);
|
|
178
|
+
this.debug(`ICE gathering state: ${gatheringState}`);
|
|
179
|
+
if (gatheringState === 'gathering' && this.state === ConnectionState.INITIALIZING) {
|
|
180
|
+
this.transitionTo(ConnectionState.GATHERING, 'ICE gathering started');
|
|
181
|
+
this.startIceGatheringTimeout();
|
|
182
|
+
}
|
|
183
|
+
else if (gatheringState === 'complete') {
|
|
184
|
+
this.clearIceGatheringTimeout();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Handle data channel open event
|
|
189
|
+
*/
|
|
190
|
+
handleDataChannelOpen() {
|
|
191
|
+
this.debug('Data channel opened');
|
|
192
|
+
this.emit('datachannel:open');
|
|
193
|
+
// Only transition to CONNECTED if ICE is also connected
|
|
194
|
+
if (this.pc && (this.pc.iceConnectionState === 'connected' || this.pc.iceConnectionState === 'completed')) {
|
|
195
|
+
this.transitionTo(ConnectionState.CONNECTED, 'Data channel opened and ICE connected');
|
|
196
|
+
this.onConnected();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Handle data channel close event
|
|
201
|
+
*/
|
|
202
|
+
handleDataChannelClose() {
|
|
203
|
+
this.debug('Data channel closed');
|
|
204
|
+
this.emit('datachannel:close');
|
|
205
|
+
if (this.state === ConnectionState.CONNECTED) {
|
|
206
|
+
this.transitionTo(ConnectionState.DISCONNECTED, 'Data channel closed');
|
|
207
|
+
this.scheduleReconnect();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Handle data channel error event
|
|
212
|
+
*/
|
|
213
|
+
handleDataChannelError(error) {
|
|
214
|
+
this.debug('Data channel error:', error);
|
|
215
|
+
this.emit('datachannel:error', error);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Handle incoming message
|
|
219
|
+
*/
|
|
220
|
+
handleMessage(event) {
|
|
221
|
+
this.emit('message', event.data);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Called when connection is successfully established
|
|
225
|
+
*/
|
|
226
|
+
onConnected() {
|
|
227
|
+
this.clearConnectionTimeout();
|
|
228
|
+
this.reconnectAttempts = 0;
|
|
229
|
+
this.backoff?.reset();
|
|
230
|
+
// Replay buffered messages
|
|
231
|
+
if (this.messageBuffer && !this.messageBuffer.isEmpty()) {
|
|
232
|
+
const messages = this.messageBuffer.getValid();
|
|
233
|
+
this.debug(`Replaying ${messages.length} buffered messages`);
|
|
234
|
+
for (const message of messages) {
|
|
235
|
+
try {
|
|
236
|
+
this.sendDirect(message.data);
|
|
237
|
+
this.emit('message:replayed', message);
|
|
238
|
+
this.messageBuffer.remove(message.id);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
this.debug('Failed to replay message:', error);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Remove expired messages
|
|
245
|
+
const expired = this.messageBuffer.getExpired();
|
|
246
|
+
for (const msg of expired) {
|
|
247
|
+
this.emit('message:buffer:expired', msg);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Start ICE candidate polling
|
|
253
|
+
*/
|
|
254
|
+
startIcePolling() {
|
|
255
|
+
if (this.icePollingInterval)
|
|
256
|
+
return;
|
|
257
|
+
this.debug('Starting ICE polling');
|
|
258
|
+
this.emit('ice:polling:started');
|
|
259
|
+
this.lastIcePollTime = Date.now();
|
|
260
|
+
this.icePollingInterval = setInterval(() => {
|
|
261
|
+
const elapsed = Date.now() - this.lastIcePollTime;
|
|
262
|
+
if (elapsed > this.config.icePollingTimeout) {
|
|
263
|
+
this.debug('ICE polling timeout');
|
|
264
|
+
this.stopIcePolling();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
this.pollIceCandidates();
|
|
268
|
+
}, this.config.icePollingInterval);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Stop ICE candidate polling
|
|
272
|
+
*/
|
|
273
|
+
stopIcePolling() {
|
|
274
|
+
if (!this.icePollingInterval)
|
|
275
|
+
return;
|
|
276
|
+
this.debug('Stopping ICE polling');
|
|
277
|
+
clearInterval(this.icePollingInterval);
|
|
278
|
+
this.icePollingInterval = null;
|
|
279
|
+
this.emit('ice:polling:stopped');
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Start connection timeout
|
|
283
|
+
*/
|
|
284
|
+
startConnectionTimeout() {
|
|
285
|
+
this.clearConnectionTimeout();
|
|
286
|
+
this.connectionTimeout = setTimeout(() => {
|
|
287
|
+
if (this.state !== ConnectionState.CONNECTED) {
|
|
288
|
+
this.debug('Connection timeout');
|
|
289
|
+
this.emit('connection:timeout');
|
|
290
|
+
this.transitionTo(ConnectionState.FAILED, 'Connection timeout');
|
|
291
|
+
this.scheduleReconnect();
|
|
292
|
+
}
|
|
293
|
+
}, this.config.connectionTimeout);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Clear connection timeout
|
|
297
|
+
*/
|
|
298
|
+
clearConnectionTimeout() {
|
|
299
|
+
if (this.connectionTimeout) {
|
|
300
|
+
clearTimeout(this.connectionTimeout);
|
|
301
|
+
this.connectionTimeout = null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Start ICE gathering timeout
|
|
306
|
+
*/
|
|
307
|
+
startIceGatheringTimeout() {
|
|
308
|
+
this.clearIceGatheringTimeout();
|
|
309
|
+
this.iceGatheringTimeout = setTimeout(() => {
|
|
310
|
+
if (this.pc && this.pc.iceGatheringState !== 'complete') {
|
|
311
|
+
this.debug('ICE gathering timeout');
|
|
312
|
+
this.emit('ice:gathering:timeout');
|
|
313
|
+
}
|
|
314
|
+
}, this.config.iceGatheringTimeout);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Clear ICE gathering timeout
|
|
318
|
+
*/
|
|
319
|
+
clearIceGatheringTimeout() {
|
|
320
|
+
if (this.iceGatheringTimeout) {
|
|
321
|
+
clearTimeout(this.iceGatheringTimeout);
|
|
322
|
+
this.iceGatheringTimeout = null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Schedule reconnection attempt
|
|
327
|
+
*/
|
|
328
|
+
scheduleReconnect() {
|
|
329
|
+
if (!this.config.reconnectEnabled || !this.backoff)
|
|
330
|
+
return;
|
|
331
|
+
// Check if we've exceeded max attempts
|
|
332
|
+
if (this.config.maxReconnectAttempts > 0 && this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
333
|
+
this.debug('Max reconnection attempts reached');
|
|
334
|
+
this.emit('reconnect:exhausted', this.reconnectAttempts);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const delay = this.backoff.next();
|
|
338
|
+
this.reconnectAttempts++;
|
|
339
|
+
this.debug(`Scheduling reconnection attempt ${this.reconnectAttempts} in ${delay}ms`);
|
|
340
|
+
this.emit('reconnect:scheduled', {
|
|
341
|
+
attempt: this.reconnectAttempts,
|
|
342
|
+
delay,
|
|
343
|
+
maxAttempts: this.config.maxReconnectAttempts,
|
|
344
|
+
});
|
|
345
|
+
this.transitionTo(ConnectionState.RECONNECTING, `Attempt ${this.reconnectAttempts}`);
|
|
346
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
347
|
+
this.emit('reconnect:attempting', this.reconnectAttempts);
|
|
348
|
+
this.attemptReconnect();
|
|
349
|
+
}, delay);
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Cancel scheduled reconnection
|
|
353
|
+
*/
|
|
354
|
+
cancelReconnect() {
|
|
355
|
+
if (this.reconnectTimeout) {
|
|
356
|
+
clearTimeout(this.reconnectTimeout);
|
|
357
|
+
this.reconnectTimeout = null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Send a message directly (bypasses buffer)
|
|
362
|
+
*/
|
|
363
|
+
sendDirect(data) {
|
|
364
|
+
if (!this.dc || this.dc.readyState !== 'open') {
|
|
365
|
+
throw new Error('Data channel is not open');
|
|
366
|
+
}
|
|
367
|
+
// Handle different data types explicitly
|
|
368
|
+
this.dc.send(data);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Send a message with automatic buffering
|
|
372
|
+
*/
|
|
373
|
+
send(data) {
|
|
374
|
+
if (this.state === ConnectionState.CONNECTED && this.dc?.readyState === 'open') {
|
|
375
|
+
// Send directly
|
|
376
|
+
try {
|
|
377
|
+
this.sendDirect(data);
|
|
378
|
+
this.emit('message:sent', data, false);
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
this.debug('Failed to send message:', error);
|
|
382
|
+
this.bufferMessage(data);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
// Buffer for later
|
|
387
|
+
this.bufferMessage(data);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Buffer a message for later delivery
|
|
392
|
+
*/
|
|
393
|
+
bufferMessage(data) {
|
|
394
|
+
if (!this.messageBuffer) {
|
|
395
|
+
this.debug('Message buffering disabled, message dropped');
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (this.messageBuffer.isFull()) {
|
|
399
|
+
const oldest = this.messageBuffer.getAll()[0];
|
|
400
|
+
this.emit('message:buffer:overflow', oldest);
|
|
401
|
+
}
|
|
402
|
+
const message = this.messageBuffer.add(data);
|
|
403
|
+
this.emit('message:buffered', data);
|
|
404
|
+
this.emit('message:sent', data, true);
|
|
405
|
+
this.debug(`Message buffered (${this.messageBuffer.size()}/${this.config.maxBufferSize})`);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Get current connection state
|
|
409
|
+
*/
|
|
410
|
+
getState() {
|
|
411
|
+
return this.state;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Get the data channel
|
|
415
|
+
*/
|
|
416
|
+
getDataChannel() {
|
|
417
|
+
return this.dc;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Get the peer connection
|
|
421
|
+
*/
|
|
422
|
+
getPeerConnection() {
|
|
423
|
+
return this.pc;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Close the connection
|
|
427
|
+
*/
|
|
428
|
+
close() {
|
|
429
|
+
this.debug('Closing connection');
|
|
430
|
+
this.transitionTo(ConnectionState.CLOSED, 'User requested close');
|
|
431
|
+
this.cleanup();
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Complete cleanup of all resources
|
|
435
|
+
*/
|
|
436
|
+
cleanup() {
|
|
437
|
+
this.debug('Cleaning up connection');
|
|
438
|
+
this.emit('cleanup:started');
|
|
439
|
+
// Clear all timeouts
|
|
440
|
+
this.clearConnectionTimeout();
|
|
441
|
+
this.clearIceGatheringTimeout();
|
|
442
|
+
this.cancelReconnect();
|
|
443
|
+
// Stop ICE polling
|
|
444
|
+
this.stopIcePolling();
|
|
445
|
+
// Close data channel
|
|
446
|
+
if (this.dc) {
|
|
447
|
+
this.dc.onopen = null;
|
|
448
|
+
this.dc.onclose = null;
|
|
449
|
+
this.dc.onerror = null;
|
|
450
|
+
this.dc.onmessage = null;
|
|
451
|
+
if (this.dc.readyState !== 'closed') {
|
|
452
|
+
this.dc.close();
|
|
453
|
+
}
|
|
454
|
+
this.dc = null;
|
|
455
|
+
}
|
|
456
|
+
// Close peer connection
|
|
457
|
+
if (this.pc) {
|
|
458
|
+
this.pc.onicecandidate = null;
|
|
459
|
+
this.pc.oniceconnectionstatechange = null;
|
|
460
|
+
this.pc.onconnectionstatechange = null;
|
|
461
|
+
this.pc.onicegatheringstatechange = null;
|
|
462
|
+
if (this.pc.connectionState !== 'closed') {
|
|
463
|
+
this.pc.close();
|
|
464
|
+
}
|
|
465
|
+
this.pc = null;
|
|
466
|
+
}
|
|
467
|
+
// Clear message buffer if not preserving
|
|
468
|
+
if (this.messageBuffer && !this.config.preserveBufferOnClose) {
|
|
469
|
+
this.messageBuffer.clear();
|
|
470
|
+
}
|
|
471
|
+
this.emit('cleanup:complete');
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Debug logging helper
|
|
475
|
+
*/
|
|
476
|
+
debug(...args) {
|
|
477
|
+
if (this.config.debug) {
|
|
478
|
+
console.log('[RondevuConnection]', ...args);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exponential backoff utility for connection reconnection
|
|
3
|
+
*/
|
|
4
|
+
export interface BackoffConfig {
|
|
5
|
+
base: number;
|
|
6
|
+
max: number;
|
|
7
|
+
jitter: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class ExponentialBackoff {
|
|
10
|
+
private config;
|
|
11
|
+
private attempt;
|
|
12
|
+
constructor(config: BackoffConfig);
|
|
13
|
+
/**
|
|
14
|
+
* Calculate the next delay based on the current attempt number
|
|
15
|
+
* Formula: min(base * 2^attempt, max) with jitter
|
|
16
|
+
*/
|
|
17
|
+
next(): number;
|
|
18
|
+
/**
|
|
19
|
+
* Get the current attempt number
|
|
20
|
+
*/
|
|
21
|
+
getAttempt(): number;
|
|
22
|
+
/**
|
|
23
|
+
* Reset the backoff state
|
|
24
|
+
*/
|
|
25
|
+
reset(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Peek at what the next delay would be without incrementing
|
|
28
|
+
*/
|
|
29
|
+
peek(): number;
|
|
30
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exponential backoff utility for connection reconnection
|
|
3
|
+
*/
|
|
4
|
+
export class ExponentialBackoff {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
this.attempt = 0;
|
|
8
|
+
if (config.jitter < 0 || config.jitter > 1) {
|
|
9
|
+
throw new Error('Jitter must be between 0 and 1');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Calculate the next delay based on the current attempt number
|
|
14
|
+
* Formula: min(base * 2^attempt, max) with jitter
|
|
15
|
+
*/
|
|
16
|
+
next() {
|
|
17
|
+
const exponentialDelay = this.config.base * Math.pow(2, this.attempt);
|
|
18
|
+
const cappedDelay = Math.min(exponentialDelay, this.config.max);
|
|
19
|
+
// Add jitter: delay ± (jitter * delay)
|
|
20
|
+
const jitterAmount = cappedDelay * this.config.jitter;
|
|
21
|
+
const jitter = (Math.random() * 2 - 1) * jitterAmount; // Random value between -jitterAmount and +jitterAmount
|
|
22
|
+
const finalDelay = Math.max(0, cappedDelay + jitter);
|
|
23
|
+
this.attempt++;
|
|
24
|
+
return Math.round(finalDelay);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get the current attempt number
|
|
28
|
+
*/
|
|
29
|
+
getAttempt() {
|
|
30
|
+
return this.attempt;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Reset the backoff state
|
|
34
|
+
*/
|
|
35
|
+
reset() {
|
|
36
|
+
this.attempt = 0;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Peek at what the next delay would be without incrementing
|
|
40
|
+
*/
|
|
41
|
+
peek() {
|
|
42
|
+
const exponentialDelay = this.config.base * Math.pow(2, this.attempt);
|
|
43
|
+
const cappedDelay = Math.min(exponentialDelay, this.config.max);
|
|
44
|
+
return cappedDelay;
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,9 +5,18 @@
|
|
|
5
5
|
export { Rondevu, RondevuError, NetworkError, ValidationError, ConnectionError } from './rondevu.js';
|
|
6
6
|
export { RondevuAPI } from './api.js';
|
|
7
7
|
export { RpcBatcher } from './rpc-batcher.js';
|
|
8
|
+
export { RondevuConnection } from './connection.js';
|
|
9
|
+
export { OffererConnection } from './offerer-connection.js';
|
|
10
|
+
export { AnswererConnection } from './answerer-connection.js';
|
|
11
|
+
export { ExponentialBackoff } from './exponential-backoff.js';
|
|
12
|
+
export { MessageBuffer } from './message-buffer.js';
|
|
8
13
|
export { WebCryptoAdapter } from './web-crypto-adapter.js';
|
|
9
14
|
export { NodeCryptoAdapter } from './node-crypto-adapter.js';
|
|
10
15
|
export type { Signaler, Binnable, } from './types.js';
|
|
11
16
|
export type { Keypair, OfferRequest, ServiceRequest, Service, ServiceOffer, IceCandidate, } from './api.js';
|
|
12
17
|
export type { RondevuOptions, PublishServiceOptions, ConnectToServiceOptions, ConnectionContext, OfferContext, OfferFactory, ActiveOffer, FindServiceOptions, ServiceResult, PaginatedServiceResult } from './rondevu.js';
|
|
13
18
|
export type { CryptoAdapter } from './crypto-adapter.js';
|
|
19
|
+
export type { ConnectionConfig, } from './connection-config.js';
|
|
20
|
+
export type { ConnectionState, BufferedMessage, ReconnectInfo, StateChangeInfo, ConnectionEventMap, ConnectionEventName, ConnectionEventArgs, } from './connection-events.js';
|
|
21
|
+
export type { OffererOptions, } from './offerer-connection.js';
|
|
22
|
+
export type { AnswererOptions, } from './answerer-connection.js';
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
export { Rondevu, RondevuError, NetworkError, ValidationError, ConnectionError } from './rondevu.js';
|
|
6
6
|
export { RondevuAPI } from './api.js';
|
|
7
7
|
export { RpcBatcher } from './rpc-batcher.js';
|
|
8
|
+
// Export connection classes
|
|
9
|
+
export { RondevuConnection } from './connection.js';
|
|
10
|
+
export { OffererConnection } from './offerer-connection.js';
|
|
11
|
+
export { AnswererConnection } from './answerer-connection.js';
|
|
12
|
+
// Export utilities
|
|
13
|
+
export { ExponentialBackoff } from './exponential-backoff.js';
|
|
14
|
+
export { MessageBuffer } from './message-buffer.js';
|
|
8
15
|
// Export crypto adapters
|
|
9
16
|
export { WebCryptoAdapter } from './web-crypto-adapter.js';
|
|
10
17
|
export { NodeCryptoAdapter } from './node-crypto-adapter.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message buffering system for storing messages during disconnections
|
|
3
|
+
*/
|
|
4
|
+
import { BufferedMessage } from './connection-events.js';
|
|
5
|
+
export interface MessageBufferConfig {
|
|
6
|
+
maxSize: number;
|
|
7
|
+
maxAge: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class MessageBuffer {
|
|
10
|
+
private config;
|
|
11
|
+
private buffer;
|
|
12
|
+
private messageIdCounter;
|
|
13
|
+
constructor(config: MessageBufferConfig);
|
|
14
|
+
/**
|
|
15
|
+
* Add a message to the buffer
|
|
16
|
+
* Returns the buffered message with metadata
|
|
17
|
+
*/
|
|
18
|
+
add(data: string | ArrayBuffer | Blob): BufferedMessage;
|
|
19
|
+
/**
|
|
20
|
+
* Get all messages in the buffer
|
|
21
|
+
*/
|
|
22
|
+
getAll(): BufferedMessage[];
|
|
23
|
+
/**
|
|
24
|
+
* Get messages that haven't exceeded max age
|
|
25
|
+
*/
|
|
26
|
+
getValid(): BufferedMessage[];
|
|
27
|
+
/**
|
|
28
|
+
* Get and remove expired messages
|
|
29
|
+
*/
|
|
30
|
+
getExpired(): BufferedMessage[];
|
|
31
|
+
/**
|
|
32
|
+
* Remove a specific message by ID
|
|
33
|
+
*/
|
|
34
|
+
remove(messageId: string): BufferedMessage | null;
|
|
35
|
+
/**
|
|
36
|
+
* Clear all messages from the buffer
|
|
37
|
+
*/
|
|
38
|
+
clear(): BufferedMessage[];
|
|
39
|
+
/**
|
|
40
|
+
* Increment attempt count for a message
|
|
41
|
+
*/
|
|
42
|
+
incrementAttempt(messageId: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Get the current size of the buffer
|
|
45
|
+
*/
|
|
46
|
+
size(): number;
|
|
47
|
+
/**
|
|
48
|
+
* Check if buffer is empty
|
|
49
|
+
*/
|
|
50
|
+
isEmpty(): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Check if buffer is full
|
|
53
|
+
*/
|
|
54
|
+
isFull(): boolean;
|
|
55
|
+
}
|