@xtr-dev/rondevu-client 0.18.7 → 0.18.9

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.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Answerer-side WebRTC connection with answer creation and offer processing
3
+ */
4
+ import { RondevuConnection } from './connection.js';
5
+ import { RondevuAPI } from './api.js';
6
+ import { ConnectionConfig } from './connection-config.js';
7
+ export interface AnswererOptions {
8
+ api: RondevuAPI;
9
+ serviceFqn: string;
10
+ offerId: string;
11
+ offerSdp: string;
12
+ rtcConfig?: RTCConfiguration;
13
+ config?: Partial<ConnectionConfig>;
14
+ }
15
+ /**
16
+ * Answerer connection - processes offers and creates answers
17
+ */
18
+ export declare class AnswererConnection extends RondevuConnection {
19
+ private api;
20
+ private serviceFqn;
21
+ private offerId;
22
+ private offerSdp;
23
+ constructor(options: AnswererOptions);
24
+ /**
25
+ * Initialize the connection by processing offer and creating answer
26
+ */
27
+ initialize(): Promise<void>;
28
+ /**
29
+ * Handle local ICE candidate generation
30
+ */
31
+ protected onLocalIceCandidate(candidate: RTCIceCandidate): void;
32
+ /**
33
+ * Poll for remote ICE candidates (from offerer)
34
+ */
35
+ protected pollIceCandidates(): void;
36
+ /**
37
+ * Attempt to reconnect
38
+ */
39
+ protected attemptReconnect(): void;
40
+ /**
41
+ * Get the offer ID we're answering
42
+ */
43
+ getOfferId(): string;
44
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Answerer-side WebRTC connection with answer creation and offer processing
3
+ */
4
+ import { RondevuConnection } from './connection.js';
5
+ import { ConnectionState } from './connection-events.js';
6
+ /**
7
+ * Answerer connection - processes offers and creates answers
8
+ */
9
+ export class AnswererConnection extends RondevuConnection {
10
+ constructor(options) {
11
+ super(options.rtcConfig, options.config);
12
+ this.api = options.api;
13
+ this.serviceFqn = options.serviceFqn;
14
+ this.offerId = options.offerId;
15
+ this.offerSdp = options.offerSdp;
16
+ }
17
+ /**
18
+ * Initialize the connection by processing offer and creating answer
19
+ */
20
+ async initialize() {
21
+ this.debug('Initializing answerer connection');
22
+ // Create peer connection
23
+ this.createPeerConnection();
24
+ if (!this.pc)
25
+ throw new Error('Peer connection not created');
26
+ // Setup ondatachannel handler BEFORE setting remote description
27
+ // This is critical to avoid race conditions
28
+ this.pc.ondatachannel = (event) => {
29
+ this.debug('Received data channel');
30
+ this.dc = event.channel;
31
+ this.setupDataChannelHandlers(this.dc);
32
+ };
33
+ // Start connection timeout
34
+ this.startConnectionTimeout();
35
+ // Set remote description (offer)
36
+ await this.pc.setRemoteDescription({
37
+ type: 'offer',
38
+ sdp: this.offerSdp,
39
+ });
40
+ this.transitionTo(ConnectionState.SIGNALING, 'Offer received, creating answer');
41
+ // Create and set local description (answer)
42
+ const answer = await this.pc.createAnswer();
43
+ await this.pc.setLocalDescription(answer);
44
+ this.debug('Answer created, sending to server');
45
+ // Send answer to server
46
+ await this.api.answerOffer(this.serviceFqn, this.offerId, answer.sdp);
47
+ this.debug('Answer sent successfully');
48
+ }
49
+ /**
50
+ * Handle local ICE candidate generation
51
+ */
52
+ onLocalIceCandidate(candidate) {
53
+ this.debug('Generated local ICE candidate');
54
+ // For answerer, we add ICE candidates to the offer
55
+ // The server will make them available for the offerer to poll
56
+ this.api
57
+ .addOfferIceCandidates(this.serviceFqn, this.offerId, [
58
+ {
59
+ candidate: candidate.candidate,
60
+ sdpMLineIndex: candidate.sdpMLineIndex,
61
+ sdpMid: candidate.sdpMid,
62
+ },
63
+ ])
64
+ .catch((error) => {
65
+ this.debug('Failed to send ICE candidate:', error);
66
+ });
67
+ }
68
+ /**
69
+ * Poll for remote ICE candidates (from offerer)
70
+ */
71
+ pollIceCandidates() {
72
+ this.api
73
+ .getOfferIceCandidates(this.serviceFqn, this.offerId, this.lastIcePollTime)
74
+ .then((result) => {
75
+ if (result.candidates.length > 0) {
76
+ this.debug(`Received ${result.candidates.length} remote ICE candidates`);
77
+ for (const iceCandidate of result.candidates) {
78
+ // Only process ICE candidates from the offerer
79
+ if (iceCandidate.role === 'offerer' && iceCandidate.candidate && this.pc) {
80
+ const candidate = iceCandidate.candidate;
81
+ this.pc
82
+ .addIceCandidate(new RTCIceCandidate(candidate))
83
+ .then(() => {
84
+ this.emit('ice:candidate:remote', new RTCIceCandidate(candidate));
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
+ });
100
+ }
101
+ /**
102
+ * Attempt to reconnect
103
+ */
104
+ attemptReconnect() {
105
+ this.debug('Attempting to reconnect');
106
+ // For answerer, we need to fetch a new offer and create a new answer
107
+ // Clean up old connection
108
+ if (this.pc) {
109
+ this.pc.close();
110
+ this.pc = null;
111
+ }
112
+ if (this.dc) {
113
+ this.dc.close();
114
+ this.dc = null;
115
+ }
116
+ // Fetch new offer from service
117
+ this.api
118
+ .getService(this.serviceFqn)
119
+ .then((service) => {
120
+ if (!service || !service.offers || service.offers.length === 0) {
121
+ throw new Error('No offers available for reconnection');
122
+ }
123
+ // Pick a random offer
124
+ const offer = service.offers[Math.floor(Math.random() * service.offers.length)];
125
+ this.offerId = offer.offerId;
126
+ this.offerSdp = offer.sdp;
127
+ // Reinitialize with new offer
128
+ return this.initialize();
129
+ })
130
+ .then(() => {
131
+ this.emit('reconnect:success');
132
+ })
133
+ .catch((error) => {
134
+ this.debug('Reconnection failed:', error);
135
+ this.emit('reconnect:failed', error);
136
+ this.scheduleReconnect();
137
+ });
138
+ }
139
+ /**
140
+ * Get the offer ID we're answering
141
+ */
142
+ getOfferId() {
143
+ return this.offerId;
144
+ }
145
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Connection configuration interfaces and defaults
3
+ */
4
+ export interface ConnectionConfig {
5
+ connectionTimeout: number;
6
+ iceGatheringTimeout: number;
7
+ reconnectEnabled: boolean;
8
+ maxReconnectAttempts: number;
9
+ reconnectBackoffBase: number;
10
+ reconnectBackoffMax: number;
11
+ reconnectJitter: number;
12
+ bufferEnabled: boolean;
13
+ maxBufferSize: number;
14
+ maxBufferAge: number;
15
+ preserveBufferOnClose: boolean;
16
+ icePollingInterval: number;
17
+ icePollingTimeout: number;
18
+ debug: boolean;
19
+ }
20
+ export declare const DEFAULT_CONNECTION_CONFIG: ConnectionConfig;
21
+ export declare function mergeConnectionConfig(userConfig?: Partial<ConnectionConfig>): ConnectionConfig;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Connection configuration interfaces and defaults
3
+ */
4
+ export const DEFAULT_CONNECTION_CONFIG = {
5
+ // Timeouts
6
+ connectionTimeout: 30000, // 30 seconds
7
+ iceGatheringTimeout: 10000, // 10 seconds
8
+ // Reconnection
9
+ reconnectEnabled: true,
10
+ maxReconnectAttempts: 5, // 5 attempts before giving up
11
+ reconnectBackoffBase: 1000, // Start with 1 second
12
+ reconnectBackoffMax: 30000, // Cap at 30 seconds
13
+ reconnectJitter: 0.1, // 10% jitter
14
+ // Message buffering
15
+ bufferEnabled: true,
16
+ maxBufferSize: 100, // 100 messages
17
+ maxBufferAge: 60000, // 1 minute
18
+ preserveBufferOnClose: false, // Clear buffer on close
19
+ // ICE polling
20
+ icePollingInterval: 500, // Poll every 500ms
21
+ icePollingTimeout: 30000, // Stop polling after 30s
22
+ // Debug
23
+ debug: false,
24
+ };
25
+ export function mergeConnectionConfig(userConfig) {
26
+ return {
27
+ ...DEFAULT_CONNECTION_CONFIG,
28
+ ...userConfig,
29
+ };
30
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * TypeScript event type definitions for RondevuConnection
3
+ */
4
+ export declare enum ConnectionState {
5
+ INITIALIZING = "initializing",// Creating peer connection
6
+ GATHERING = "gathering",// ICE gathering in progress
7
+ SIGNALING = "signaling",// Exchanging offer/answer
8
+ CHECKING = "checking",// ICE connectivity checks
9
+ CONNECTING = "connecting",// ICE connection attempts
10
+ CONNECTED = "connected",// Data channel open, working
11
+ DISCONNECTED = "disconnected",// Temporarily disconnected
12
+ RECONNECTING = "reconnecting",// Attempting reconnection
13
+ FAILED = "failed",// Connection failed
14
+ CLOSED = "closed"
15
+ }
16
+ export interface BufferedMessage {
17
+ id: string;
18
+ data: string | ArrayBuffer | Blob;
19
+ timestamp: number;
20
+ attempts: number;
21
+ }
22
+ export interface ReconnectInfo {
23
+ attempt: number;
24
+ delay: number;
25
+ maxAttempts: number;
26
+ }
27
+ export interface StateChangeInfo {
28
+ oldState: ConnectionState;
29
+ newState: ConnectionState;
30
+ reason?: string;
31
+ }
32
+ /**
33
+ * Event map for RondevuConnection
34
+ * Maps event names to their payload types
35
+ */
36
+ export interface ConnectionEventMap {
37
+ 'state:changed': [StateChangeInfo];
38
+ 'connecting': [];
39
+ 'connected': [];
40
+ 'disconnected': [reason?: string];
41
+ 'failed': [error: Error];
42
+ 'closed': [reason?: string];
43
+ 'reconnect:scheduled': [ReconnectInfo];
44
+ 'reconnect:attempting': [attempt: number];
45
+ 'reconnect:success': [];
46
+ 'reconnect:failed': [error: Error];
47
+ 'reconnect:exhausted': [attempts: number];
48
+ 'message': [data: string | ArrayBuffer | Blob];
49
+ 'message:sent': [data: string | ArrayBuffer | Blob, buffered: boolean];
50
+ 'message:buffered': [data: string | ArrayBuffer | Blob];
51
+ 'message:replayed': [message: BufferedMessage];
52
+ 'message:buffer:overflow': [discardedMessage: BufferedMessage];
53
+ 'message:buffer:expired': [message: BufferedMessage];
54
+ 'ice:candidate:local': [candidate: RTCIceCandidate | null];
55
+ 'ice:candidate:remote': [candidate: RTCIceCandidate | null];
56
+ 'ice:connection:state': [state: RTCIceConnectionState];
57
+ 'ice:gathering:state': [state: RTCIceGatheringState];
58
+ 'ice:polling:started': [];
59
+ 'ice:polling:stopped': [];
60
+ 'answer:processed': [offerId: string, answererId: string];
61
+ 'answer:duplicate': [offerId: string];
62
+ 'datachannel:open': [];
63
+ 'datachannel:close': [];
64
+ 'datachannel:error': [error: Event];
65
+ 'cleanup:started': [];
66
+ 'cleanup:complete': [];
67
+ 'connection:state': [state: RTCPeerConnectionState];
68
+ 'connection:timeout': [];
69
+ 'ice:gathering:timeout': [];
70
+ }
71
+ /**
72
+ * Helper type to extract event names from the event map
73
+ */
74
+ export type ConnectionEventName = keyof ConnectionEventMap;
75
+ /**
76
+ * Helper type to extract event arguments for a specific event
77
+ */
78
+ export type ConnectionEventArgs<T extends ConnectionEventName> = ConnectionEventMap[T];
@@ -0,0 +1,16 @@
1
+ /**
2
+ * TypeScript event type definitions for RondevuConnection
3
+ */
4
+ export var ConnectionState;
5
+ (function (ConnectionState) {
6
+ ConnectionState["INITIALIZING"] = "initializing";
7
+ ConnectionState["GATHERING"] = "gathering";
8
+ ConnectionState["SIGNALING"] = "signaling";
9
+ ConnectionState["CHECKING"] = "checking";
10
+ ConnectionState["CONNECTING"] = "connecting";
11
+ ConnectionState["CONNECTED"] = "connected";
12
+ ConnectionState["DISCONNECTED"] = "disconnected";
13
+ ConnectionState["RECONNECTING"] = "reconnecting";
14
+ ConnectionState["FAILED"] = "failed";
15
+ ConnectionState["CLOSED"] = "closed";
16
+ })(ConnectionState || (ConnectionState = {}));
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Base connection class with state machine, reconnection, and message buffering
3
+ */
4
+ import { EventEmitter } from 'eventemitter3';
5
+ import { ConnectionConfig } from './connection-config.js';
6
+ import { ConnectionState, ConnectionEventMap } 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 declare abstract class RondevuConnection extends EventEmitter<ConnectionEventMap> {
13
+ protected rtcConfig?: RTCConfiguration | undefined;
14
+ protected pc: RTCPeerConnection | null;
15
+ protected dc: RTCDataChannel | null;
16
+ protected state: ConnectionState;
17
+ protected config: ConnectionConfig;
18
+ protected messageBuffer: MessageBuffer | null;
19
+ protected backoff: ExponentialBackoff | null;
20
+ protected reconnectTimeout: ReturnType<typeof setTimeout> | null;
21
+ protected reconnectAttempts: number;
22
+ protected connectionTimeout: ReturnType<typeof setTimeout> | null;
23
+ protected iceGatheringTimeout: ReturnType<typeof setTimeout> | null;
24
+ protected icePollingInterval: ReturnType<typeof setInterval> | null;
25
+ protected lastIcePollTime: number;
26
+ protected answerProcessed: boolean;
27
+ protected answerSdpFingerprint: string | null;
28
+ constructor(rtcConfig?: RTCConfiguration | undefined, userConfig?: Partial<ConnectionConfig>);
29
+ /**
30
+ * Transition to a new state and emit events
31
+ */
32
+ protected transitionTo(newState: ConnectionState, reason?: string): void;
33
+ /**
34
+ * Create and configure RTCPeerConnection
35
+ */
36
+ protected createPeerConnection(): RTCPeerConnection;
37
+ /**
38
+ * Setup data channel event handlers
39
+ */
40
+ protected setupDataChannelHandlers(dc: RTCDataChannel): void;
41
+ /**
42
+ * Handle local ICE candidate generation
43
+ */
44
+ protected handleIceCandidate(event: RTCPeerConnectionIceEvent): void;
45
+ /**
46
+ * Handle ICE connection state changes (primary state driver)
47
+ */
48
+ protected handleIceConnectionStateChange(): void;
49
+ /**
50
+ * Handle connection state changes (backup validation)
51
+ */
52
+ protected handleConnectionStateChange(): void;
53
+ /**
54
+ * Handle ICE gathering state changes
55
+ */
56
+ protected handleIceGatheringStateChange(): void;
57
+ /**
58
+ * Handle data channel open event
59
+ */
60
+ protected handleDataChannelOpen(): void;
61
+ /**
62
+ * Handle data channel close event
63
+ */
64
+ protected handleDataChannelClose(): void;
65
+ /**
66
+ * Handle data channel error event
67
+ */
68
+ protected handleDataChannelError(error: Event): void;
69
+ /**
70
+ * Handle incoming message
71
+ */
72
+ protected handleMessage(event: MessageEvent): void;
73
+ /**
74
+ * Called when connection is successfully established
75
+ */
76
+ protected onConnected(): void;
77
+ /**
78
+ * Start ICE candidate polling
79
+ */
80
+ protected startIcePolling(): void;
81
+ /**
82
+ * Stop ICE candidate polling
83
+ */
84
+ protected stopIcePolling(): void;
85
+ /**
86
+ * Start connection timeout
87
+ */
88
+ protected startConnectionTimeout(): void;
89
+ /**
90
+ * Clear connection timeout
91
+ */
92
+ protected clearConnectionTimeout(): void;
93
+ /**
94
+ * Start ICE gathering timeout
95
+ */
96
+ protected startIceGatheringTimeout(): void;
97
+ /**
98
+ * Clear ICE gathering timeout
99
+ */
100
+ protected clearIceGatheringTimeout(): void;
101
+ /**
102
+ * Schedule reconnection attempt
103
+ */
104
+ protected scheduleReconnect(): void;
105
+ /**
106
+ * Cancel scheduled reconnection
107
+ */
108
+ protected cancelReconnect(): void;
109
+ /**
110
+ * Send a message directly (bypasses buffer)
111
+ */
112
+ protected sendDirect(data: string | ArrayBuffer | Blob): void;
113
+ /**
114
+ * Send a message with automatic buffering
115
+ */
116
+ send(data: string | ArrayBuffer | Blob): void;
117
+ /**
118
+ * Buffer a message for later delivery
119
+ */
120
+ protected bufferMessage(data: string | ArrayBuffer | Blob): void;
121
+ /**
122
+ * Get current connection state
123
+ */
124
+ getState(): ConnectionState;
125
+ /**
126
+ * Get the data channel
127
+ */
128
+ getDataChannel(): RTCDataChannel | null;
129
+ /**
130
+ * Get the peer connection
131
+ */
132
+ getPeerConnection(): RTCPeerConnection | null;
133
+ /**
134
+ * Close the connection
135
+ */
136
+ close(): void;
137
+ /**
138
+ * Complete cleanup of all resources
139
+ */
140
+ protected cleanup(): void;
141
+ /**
142
+ * Debug logging helper
143
+ */
144
+ protected debug(...args: any[]): void;
145
+ protected abstract onLocalIceCandidate(candidate: RTCIceCandidate): void;
146
+ protected abstract pollIceCandidates(): void;
147
+ protected abstract attemptReconnect(): void;
148
+ }