@xtr-dev/rondevu-client 0.6.0 → 0.7.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.
@@ -0,0 +1,11 @@
1
+ import { PeerState } from './state.js';
2
+ import type { PeerOptions } from './types.js';
3
+ import type RondevuPeer from './index.js';
4
+ /**
5
+ * Answering an offer and sending to server
6
+ */
7
+ export declare class AnsweringState extends PeerState {
8
+ constructor(peer: RondevuPeer);
9
+ get name(): string;
10
+ answer(offerId: string, offerSdp: string, options: PeerOptions): Promise<void>;
11
+ }
@@ -0,0 +1,36 @@
1
+ import { PeerState } from './state.js';
2
+ /**
3
+ * Answering an offer and sending to server
4
+ */
5
+ export class AnsweringState extends PeerState {
6
+ constructor(peer) {
7
+ super(peer);
8
+ }
9
+ get name() { return 'answering'; }
10
+ async answer(offerId, offerSdp, options) {
11
+ try {
12
+ this.peer.role = 'answerer';
13
+ this.peer.offerId = offerId;
14
+ // Set remote description
15
+ await this.peer.pc.setRemoteDescription({
16
+ type: 'offer',
17
+ sdp: offerSdp
18
+ });
19
+ // Create answer
20
+ const answer = await this.peer.pc.createAnswer();
21
+ await this.peer.pc.setLocalDescription(answer);
22
+ // Send answer to server immediately (don't wait for ICE)
23
+ await this.peer.offersApi.answer(offerId, answer.sdp);
24
+ // Enable trickle ICE - send candidates as they arrive
25
+ this.setupIceCandidateHandler(offerId);
26
+ // Transition to exchanging ICE
27
+ const { ExchangingIceState } = await import('./exchanging-ice-state.js');
28
+ this.peer.setState(new ExchangingIceState(this.peer, offerId, options));
29
+ }
30
+ catch (error) {
31
+ const { FailedState } = await import('./failed-state.js');
32
+ this.peer.setState(new FailedState(this.peer, error));
33
+ throw error;
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,8 @@
1
+ import { PeerState } from './state.js';
2
+ /**
3
+ * Closed state - connection has been terminated
4
+ */
5
+ export declare class ClosedState extends PeerState {
6
+ get name(): string;
7
+ cleanup(): void;
8
+ }
@@ -0,0 +1,10 @@
1
+ import { PeerState } from './state.js';
2
+ /**
3
+ * Closed state - connection has been terminated
4
+ */
5
+ export class ClosedState extends PeerState {
6
+ get name() { return 'closed'; }
7
+ cleanup() {
8
+ this.peer.pc.close();
9
+ }
10
+ }
@@ -0,0 +1,8 @@
1
+ import { PeerState } from './state.js';
2
+ /**
3
+ * Connected state - peer connection is established
4
+ */
5
+ export declare class ConnectedState extends PeerState {
6
+ get name(): string;
7
+ cleanup(): void;
8
+ }
@@ -0,0 +1,11 @@
1
+ import { PeerState } from './state.js';
2
+ /**
3
+ * Connected state - peer connection is established
4
+ */
5
+ export class ConnectedState extends PeerState {
6
+ get name() { return 'connected'; }
7
+ cleanup() {
8
+ // Keep connection alive, but stop any polling
9
+ // The peer connection will handle disconnects via onconnectionstatechange
10
+ }
11
+ }
@@ -0,0 +1,12 @@
1
+ import { PeerState } from './state.js';
2
+ import type { PeerOptions } from './types.js';
3
+ import type RondevuPeer from './index.js';
4
+ /**
5
+ * Creating offer and sending to server
6
+ */
7
+ export declare class CreatingOfferState extends PeerState {
8
+ private options;
9
+ constructor(peer: RondevuPeer, options: PeerOptions);
10
+ get name(): string;
11
+ createOffer(options: PeerOptions): Promise<string>;
12
+ }
@@ -0,0 +1,43 @@
1
+ import { PeerState } from './state.js';
2
+ /**
3
+ * Creating offer and sending to server
4
+ */
5
+ export class CreatingOfferState extends PeerState {
6
+ constructor(peer, options) {
7
+ super(peer);
8
+ this.options = options;
9
+ }
10
+ get name() { return 'creating-offer'; }
11
+ async createOffer(options) {
12
+ try {
13
+ this.peer.role = 'offerer';
14
+ // Create data channel if requested
15
+ if (options.createDataChannel !== false) {
16
+ const channel = this.peer.pc.createDataChannel(options.dataChannelLabel || 'data');
17
+ this.peer.emitEvent('datachannel', channel);
18
+ }
19
+ // Create WebRTC offer
20
+ const offer = await this.peer.pc.createOffer();
21
+ await this.peer.pc.setLocalDescription(offer);
22
+ // Send offer to server immediately (don't wait for ICE)
23
+ const offers = await this.peer.offersApi.create([{
24
+ sdp: offer.sdp,
25
+ topics: options.topics,
26
+ ttl: options.ttl || 300000
27
+ }]);
28
+ const offerId = offers[0].id;
29
+ this.peer.offerId = offerId;
30
+ // Enable trickle ICE - send candidates as they arrive
31
+ this.setupIceCandidateHandler(offerId);
32
+ // Transition to waiting for answer
33
+ const { WaitingForAnswerState } = await import('./waiting-for-answer-state.js');
34
+ this.peer.setState(new WaitingForAnswerState(this.peer, offerId, options));
35
+ return offerId;
36
+ }
37
+ catch (error) {
38
+ const { FailedState } = await import('./failed-state.js');
39
+ this.peer.setState(new FailedState(this.peer, error));
40
+ throw error;
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,17 @@
1
+ import { PeerState } from './state.js';
2
+ import type { PeerOptions } from './types.js';
3
+ import type RondevuPeer from './index.js';
4
+ /**
5
+ * Exchanging ICE candidates and waiting for connection
6
+ */
7
+ export declare class ExchangingIceState extends PeerState {
8
+ private offerId;
9
+ private options;
10
+ private pollingInterval?;
11
+ private timeout?;
12
+ private lastIceTimestamp;
13
+ constructor(peer: RondevuPeer, offerId: string, options: PeerOptions);
14
+ get name(): string;
15
+ private startPolling;
16
+ cleanup(): void;
17
+ }
@@ -0,0 +1,56 @@
1
+ import { PeerState } from './state.js';
2
+ /**
3
+ * Exchanging ICE candidates and waiting for connection
4
+ */
5
+ export class ExchangingIceState extends PeerState {
6
+ constructor(peer, offerId, options) {
7
+ super(peer);
8
+ this.offerId = offerId;
9
+ this.options = options;
10
+ this.lastIceTimestamp = 0;
11
+ this.startPolling();
12
+ }
13
+ get name() { return 'exchanging-ice'; }
14
+ startPolling() {
15
+ const connectionTimeout = this.options.timeouts?.iceConnection || 30000;
16
+ this.timeout = setTimeout(async () => {
17
+ this.cleanup();
18
+ const { FailedState } = await import('./failed-state.js');
19
+ this.peer.setState(new FailedState(this.peer, new Error('ICE connection timeout')));
20
+ }, connectionTimeout);
21
+ this.pollingInterval = setInterval(async () => {
22
+ try {
23
+ const candidates = await this.peer.offersApi.getIceCandidates(this.offerId, this.lastIceTimestamp);
24
+ for (const cand of candidates) {
25
+ if (cand.candidate && cand.candidate.candidate && cand.candidate.candidate !== '') {
26
+ try {
27
+ await this.peer.pc.addIceCandidate(new RTCIceCandidate(cand.candidate));
28
+ this.lastIceTimestamp = cand.createdAt;
29
+ }
30
+ catch (err) {
31
+ console.warn('Failed to add ICE candidate:', err);
32
+ this.lastIceTimestamp = cand.createdAt;
33
+ }
34
+ }
35
+ else {
36
+ this.lastIceTimestamp = cand.createdAt;
37
+ }
38
+ }
39
+ }
40
+ catch (err) {
41
+ console.error('Error polling for ICE candidates:', err);
42
+ if (err instanceof Error && err.message.includes('not found')) {
43
+ this.cleanup();
44
+ const { FailedState } = await import('./failed-state.js');
45
+ this.peer.setState(new FailedState(this.peer, new Error('Offer expired or not found')));
46
+ }
47
+ }
48
+ }, 1000);
49
+ }
50
+ cleanup() {
51
+ if (this.pollingInterval)
52
+ clearInterval(this.pollingInterval);
53
+ if (this.timeout)
54
+ clearTimeout(this.timeout);
55
+ }
56
+ }
@@ -0,0 +1,10 @@
1
+ import { PeerState } from './state.js';
2
+ /**
3
+ * Failed state - connection attempt failed
4
+ */
5
+ export declare class FailedState extends PeerState {
6
+ private error;
7
+ constructor(peer: any, error: Error);
8
+ get name(): string;
9
+ cleanup(): void;
10
+ }
@@ -0,0 +1,16 @@
1
+ import { PeerState } from './state.js';
2
+ /**
3
+ * Failed state - connection attempt failed
4
+ */
5
+ export class FailedState extends PeerState {
6
+ constructor(peer, error) {
7
+ super(peer);
8
+ this.error = error;
9
+ peer.emitEvent('failed', error);
10
+ }
11
+ get name() { return 'failed'; }
12
+ cleanup() {
13
+ // Connection is failed, clean up resources
14
+ this.peer.pc.close();
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ import { PeerState } from './state.js';
2
+ import type { PeerOptions } from './types.js';
3
+ export declare class IdleState extends PeerState {
4
+ get name(): string;
5
+ createOffer(options: PeerOptions): Promise<string>;
6
+ answer(offerId: string, offerSdp: string, options: PeerOptions): Promise<void>;
7
+ }
@@ -0,0 +1,14 @@
1
+ import { PeerState } from './state.js';
2
+ export class IdleState extends PeerState {
3
+ get name() { return 'idle'; }
4
+ async createOffer(options) {
5
+ const { CreatingOfferState } = await import('./creating-offer-state.js');
6
+ this.peer.setState(new CreatingOfferState(this.peer, options));
7
+ return this.peer.state.createOffer(options);
8
+ }
9
+ async answer(offerId, offerSdp, options) {
10
+ const { AnsweringState } = await import('./answering-state.js');
11
+ this.peer.setState(new AnsweringState(this.peer));
12
+ return this.peer.state.answer(offerId, offerSdp, options);
13
+ }
14
+ }
@@ -1,60 +1,8 @@
1
1
  import { RondevuOffers } from '../offers.js';
2
2
  import { EventEmitter } from '../event-emitter.js';
3
- /**
4
- * Timeout configurations for different connection phases
5
- */
6
- export interface PeerTimeouts {
7
- /** Timeout for ICE gathering (default: 10000ms) */
8
- iceGathering?: number;
9
- /** Timeout for waiting for answer (default: 30000ms) */
10
- waitingForAnswer?: number;
11
- /** Timeout for creating answer (default: 10000ms) */
12
- creatingAnswer?: number;
13
- /** Timeout for ICE connection (default: 30000ms) */
14
- iceConnection?: number;
15
- }
16
- /**
17
- * Options for creating a peer connection
18
- */
19
- export interface PeerOptions {
20
- /** RTCConfiguration for the peer connection */
21
- rtcConfig?: RTCConfiguration;
22
- /** Topics to advertise this connection under */
23
- topics: string[];
24
- /** How long the offer should live (milliseconds) */
25
- ttl?: number;
26
- /** Whether to create a data channel automatically (for offerer) */
27
- createDataChannel?: boolean;
28
- /** Label for the automatically created data channel */
29
- dataChannelLabel?: string;
30
- /** Timeout configurations */
31
- timeouts?: PeerTimeouts;
32
- }
33
- /**
34
- * Events emitted by RondevuPeer
35
- */
36
- export interface PeerEvents extends Record<string, (...args: any[]) => void> {
37
- 'state': (state: string) => void;
38
- 'connected': () => void;
39
- 'disconnected': () => void;
40
- 'failed': (error: Error) => void;
41
- 'datachannel': (channel: RTCDataChannel) => void;
42
- 'track': (event: RTCTrackEvent) => void;
43
- }
44
- /**
45
- * Base class for peer connection states
46
- */
47
- declare abstract class PeerState {
48
- protected peer: RondevuPeer;
49
- constructor(peer: RondevuPeer);
50
- abstract get name(): string;
51
- createOffer(options: PeerOptions): Promise<string>;
52
- answer(offerId: string, offerSdp: string, options: PeerOptions): Promise<void>;
53
- handleAnswer(sdp: string): Promise<void>;
54
- handleIceCandidate(candidate: any): Promise<void>;
55
- cleanup(): void;
56
- close(): void;
57
- }
3
+ import type { PeerOptions, PeerEvents } from './types.js';
4
+ import { PeerState } from './state.js';
5
+ export type { PeerTimeouts, PeerOptions, PeerEvents } from './types.js';
58
6
  /**
59
7
  * High-level WebRTC peer connection manager with state-based lifecycle
60
8
  * Handles offer/answer exchange, ICE candidates, timeouts, and error recovery
@@ -65,6 +13,10 @@ export default class RondevuPeer extends EventEmitter<PeerEvents> {
65
13
  offerId?: string;
66
14
  role?: 'offerer' | 'answerer';
67
15
  private _state;
16
+ private connectionStateChangeHandler?;
17
+ private dataChannelHandler?;
18
+ private trackHandler?;
19
+ private iceCandidateErrorHandler?;
68
20
  /**
69
21
  * Current connection state name
70
22
  */
@@ -106,6 +58,5 @@ export default class RondevuPeer extends EventEmitter<PeerEvents> {
106
58
  /**
107
59
  * Close the connection and clean up
108
60
  */
109
- close(): void;
61
+ close(): Promise<void>;
110
62
  }
111
- export {};
@@ -1,287 +1,8 @@
1
1
  import { EventEmitter } from '../event-emitter.js';
2
- /**
3
- * Base class for peer connection states
4
- */
5
- class PeerState {
6
- constructor(peer) {
7
- this.peer = peer;
8
- }
9
- async createOffer(options) {
10
- throw new Error(`Cannot create offer in ${this.name} state`);
11
- }
12
- async answer(offerId, offerSdp, options) {
13
- throw new Error(`Cannot answer in ${this.name} state`);
14
- }
15
- async handleAnswer(sdp) {
16
- throw new Error(`Cannot handle answer in ${this.name} state`);
17
- }
18
- async handleIceCandidate(candidate) {
19
- // ICE candidates can arrive in multiple states, so default is to add them
20
- if (this.peer.pc.remoteDescription) {
21
- await this.peer.pc.addIceCandidate(new RTCIceCandidate(candidate));
22
- }
23
- }
24
- cleanup() {
25
- // Override in states that need cleanup
26
- }
27
- close() {
28
- this.cleanup();
29
- this.peer.setState(new ClosedState(this.peer));
30
- }
31
- }
32
- /**
33
- * Initial idle state
34
- */
35
- class IdleState extends PeerState {
36
- get name() { return 'idle'; }
37
- async createOffer(options) {
38
- this.peer.setState(new CreatingOfferState(this.peer, options));
39
- return this.peer.state.createOffer(options);
40
- }
41
- async answer(offerId, offerSdp, options) {
42
- this.peer.setState(new AnsweringState(this.peer));
43
- return this.peer.state.answer(offerId, offerSdp, options);
44
- }
45
- }
46
- /**
47
- * Creating offer and sending to server
48
- */
49
- class CreatingOfferState extends PeerState {
50
- constructor(peer, options) {
51
- super(peer);
52
- this.options = options;
53
- }
54
- get name() { return 'creating-offer'; }
55
- async createOffer(options) {
56
- try {
57
- this.peer.role = 'offerer';
58
- // Create data channel if requested
59
- if (options.createDataChannel !== false) {
60
- const channel = this.peer.pc.createDataChannel(options.dataChannelLabel || 'data');
61
- this.peer.emitEvent('datachannel', channel);
62
- }
63
- // Create WebRTC offer
64
- const offer = await this.peer.pc.createOffer();
65
- await this.peer.pc.setLocalDescription(offer);
66
- // Send offer to server immediately (don't wait for ICE)
67
- const offers = await this.peer.offersApi.create([{
68
- sdp: offer.sdp,
69
- topics: options.topics,
70
- ttl: options.ttl || 300000
71
- }]);
72
- const offerId = offers[0].id;
73
- this.peer.offerId = offerId;
74
- // Enable trickle ICE - send candidates as they arrive
75
- this.peer.pc.onicecandidate = async (event) => {
76
- if (event.candidate && offerId) {
77
- const candidateData = event.candidate.toJSON();
78
- if (candidateData.candidate && candidateData.candidate !== '') {
79
- try {
80
- await this.peer.offersApi.addIceCandidates(offerId, [candidateData]);
81
- }
82
- catch (err) {
83
- console.error('Error sending ICE candidate:', err);
84
- }
85
- }
86
- }
87
- };
88
- // Transition to waiting for answer
89
- this.peer.setState(new WaitingForAnswerState(this.peer, offerId, options));
90
- return offerId;
91
- }
92
- catch (error) {
93
- this.peer.setState(new FailedState(this.peer, error));
94
- throw error;
95
- }
96
- }
97
- }
98
- /**
99
- * Waiting for answer from another peer
100
- */
101
- class WaitingForAnswerState extends PeerState {
102
- constructor(peer, offerId, options) {
103
- super(peer);
104
- this.offerId = offerId;
105
- this.options = options;
106
- this.startPolling();
107
- }
108
- get name() { return 'waiting-for-answer'; }
109
- startPolling() {
110
- const answerTimeout = this.options.timeouts?.waitingForAnswer || 30000;
111
- this.timeout = setTimeout(() => {
112
- this.cleanup();
113
- this.peer.setState(new FailedState(this.peer, new Error('Timeout waiting for answer')));
114
- }, answerTimeout);
115
- this.pollingInterval = setInterval(async () => {
116
- try {
117
- const answers = await this.peer.offersApi.getAnswers();
118
- const myAnswer = answers.find(a => a.offerId === this.offerId);
119
- if (myAnswer) {
120
- this.cleanup();
121
- await this.handleAnswer(myAnswer.sdp);
122
- }
123
- }
124
- catch (err) {
125
- console.error('Error polling for answers:', err);
126
- if (err instanceof Error && err.message.includes('not found')) {
127
- this.cleanup();
128
- this.peer.setState(new FailedState(this.peer, new Error('Offer expired or not found')));
129
- }
130
- }
131
- }, 2000);
132
- }
133
- async handleAnswer(sdp) {
134
- try {
135
- await this.peer.pc.setRemoteDescription({
136
- type: 'answer',
137
- sdp
138
- });
139
- // Transition to exchanging ICE
140
- this.peer.setState(new ExchangingIceState(this.peer, this.offerId, this.options));
141
- }
142
- catch (error) {
143
- this.peer.setState(new FailedState(this.peer, error));
144
- }
145
- }
146
- cleanup() {
147
- if (this.pollingInterval)
148
- clearInterval(this.pollingInterval);
149
- if (this.timeout)
150
- clearTimeout(this.timeout);
151
- }
152
- }
153
- /**
154
- * Answering an offer and sending to server
155
- */
156
- class AnsweringState extends PeerState {
157
- constructor(peer) {
158
- super(peer);
159
- }
160
- get name() { return 'answering'; }
161
- async answer(offerId, offerSdp, options) {
162
- try {
163
- this.peer.role = 'answerer';
164
- this.peer.offerId = offerId;
165
- // Set remote description
166
- await this.peer.pc.setRemoteDescription({
167
- type: 'offer',
168
- sdp: offerSdp
169
- });
170
- // Create answer
171
- const answer = await this.peer.pc.createAnswer();
172
- await this.peer.pc.setLocalDescription(answer);
173
- // Send answer to server immediately (don't wait for ICE)
174
- await this.peer.offersApi.answer(offerId, answer.sdp);
175
- // Enable trickle ICE - send candidates as they arrive
176
- this.peer.pc.onicecandidate = async (event) => {
177
- if (event.candidate && offerId) {
178
- const candidateData = event.candidate.toJSON();
179
- if (candidateData.candidate && candidateData.candidate !== '') {
180
- try {
181
- await this.peer.offersApi.addIceCandidates(offerId, [candidateData]);
182
- }
183
- catch (err) {
184
- console.error('Error sending ICE candidate:', err);
185
- }
186
- }
187
- }
188
- };
189
- // Transition to exchanging ICE
190
- this.peer.setState(new ExchangingIceState(this.peer, offerId, options));
191
- }
192
- catch (error) {
193
- this.peer.setState(new FailedState(this.peer, error));
194
- throw error;
195
- }
196
- }
197
- }
198
- /**
199
- * Exchanging ICE candidates and waiting for connection
200
- */
201
- class ExchangingIceState extends PeerState {
202
- constructor(peer, offerId, options) {
203
- super(peer);
204
- this.offerId = offerId;
205
- this.options = options;
206
- this.lastIceTimestamp = 0;
207
- this.startPolling();
208
- }
209
- get name() { return 'exchanging-ice'; }
210
- startPolling() {
211
- const connectionTimeout = this.options.timeouts?.iceConnection || 30000;
212
- this.timeout = setTimeout(() => {
213
- this.cleanup();
214
- this.peer.setState(new FailedState(this.peer, new Error('ICE connection timeout')));
215
- }, connectionTimeout);
216
- this.pollingInterval = setInterval(async () => {
217
- try {
218
- const candidates = await this.peer.offersApi.getIceCandidates(this.offerId, this.lastIceTimestamp);
219
- for (const cand of candidates) {
220
- if (cand.candidate && cand.candidate.candidate && cand.candidate.candidate !== '') {
221
- try {
222
- await this.peer.pc.addIceCandidate(new RTCIceCandidate(cand.candidate));
223
- this.lastIceTimestamp = cand.createdAt;
224
- }
225
- catch (err) {
226
- console.warn('Failed to add ICE candidate:', err);
227
- this.lastIceTimestamp = cand.createdAt;
228
- }
229
- }
230
- else {
231
- this.lastIceTimestamp = cand.createdAt;
232
- }
233
- }
234
- }
235
- catch (err) {
236
- console.error('Error polling for ICE candidates:', err);
237
- if (err instanceof Error && err.message.includes('not found')) {
238
- this.cleanup();
239
- this.peer.setState(new FailedState(this.peer, new Error('Offer expired or not found')));
240
- }
241
- }
242
- }, 1000);
243
- }
244
- cleanup() {
245
- if (this.pollingInterval)
246
- clearInterval(this.pollingInterval);
247
- if (this.timeout)
248
- clearTimeout(this.timeout);
249
- }
250
- }
251
- /**
252
- * Successfully connected state
253
- */
254
- class ConnectedState extends PeerState {
255
- get name() { return 'connected'; }
256
- cleanup() {
257
- // Keep connection alive, but stop any polling
258
- // The peer connection will handle disconnects via onconnectionstatechange
259
- }
260
- }
261
- /**
262
- * Failed state
263
- */
264
- class FailedState extends PeerState {
265
- constructor(peer, error) {
266
- super(peer);
267
- this.error = error;
268
- peer.emitEvent('failed', error);
269
- }
270
- get name() { return 'failed'; }
271
- cleanup() {
272
- // Connection is failed, clean up resources
273
- this.peer.pc.close();
274
- }
275
- }
276
- /**
277
- * Closed state
278
- */
279
- class ClosedState extends PeerState {
280
- get name() { return 'closed'; }
281
- cleanup() {
282
- this.peer.pc.close();
283
- }
284
- }
2
+ import { IdleState } from './idle-state.js';
3
+ import { ConnectedState } from './connected-state.js';
4
+ import { FailedState } from './failed-state.js';
5
+ import { ClosedState } from './closed-state.js';
285
6
  /**
286
7
  * High-level WebRTC peer connection manager with state-based lifecycle
287
8
  * Handles offer/answer exchange, ICE candidates, timeouts, and error recovery
@@ -321,7 +42,7 @@ export default class RondevuPeer extends EventEmitter {
321
42
  * Set up peer connection event handlers
322
43
  */
323
44
  setupPeerConnection() {
324
- this.pc.onconnectionstatechange = () => {
45
+ this.connectionStateChangeHandler = () => {
325
46
  switch (this.pc.connectionState) {
326
47
  case 'connected':
327
48
  this.setState(new ConnectedState(this));
@@ -339,15 +60,19 @@ export default class RondevuPeer extends EventEmitter {
339
60
  break;
340
61
  }
341
62
  };
342
- this.pc.ondatachannel = (event) => {
63
+ this.pc.addEventListener('connectionstatechange', this.connectionStateChangeHandler);
64
+ this.dataChannelHandler = (event) => {
343
65
  this.emitEvent('datachannel', event.channel);
344
66
  };
345
- this.pc.ontrack = (event) => {
67
+ this.pc.addEventListener('datachannel', this.dataChannelHandler);
68
+ this.trackHandler = (event) => {
346
69
  this.emitEvent('track', event);
347
70
  };
348
- this.pc.onicecandidateerror = (event) => {
71
+ this.pc.addEventListener('track', this.trackHandler);
72
+ this.iceCandidateErrorHandler = (event) => {
349
73
  console.error('ICE candidate error:', event);
350
74
  };
75
+ this.pc.addEventListener('icecandidateerror', this.iceCandidateErrorHandler);
351
76
  }
352
77
  /**
353
78
  * Set new state and emit state change event
@@ -385,8 +110,21 @@ export default class RondevuPeer extends EventEmitter {
385
110
  /**
386
111
  * Close the connection and clean up
387
112
  */
388
- close() {
389
- this._state.close();
113
+ async close() {
114
+ // Remove RTCPeerConnection event listeners
115
+ if (this.connectionStateChangeHandler) {
116
+ this.pc.removeEventListener('connectionstatechange', this.connectionStateChangeHandler);
117
+ }
118
+ if (this.dataChannelHandler) {
119
+ this.pc.removeEventListener('datachannel', this.dataChannelHandler);
120
+ }
121
+ if (this.trackHandler) {
122
+ this.pc.removeEventListener('track', this.trackHandler);
123
+ }
124
+ if (this.iceCandidateErrorHandler) {
125
+ this.pc.removeEventListener('icecandidateerror', this.iceCandidateErrorHandler);
126
+ }
127
+ await this._state.close();
390
128
  this.removeAllListeners();
391
129
  }
392
130
  }
@@ -0,0 +1,23 @@
1
+ import type { PeerOptions } from './types.js';
2
+ import type RondevuPeer from './index.js';
3
+ /**
4
+ * Base class for peer connection states
5
+ * Implements the State pattern for managing WebRTC connection lifecycle
6
+ */
7
+ export declare abstract class PeerState {
8
+ protected peer: RondevuPeer;
9
+ protected iceCandidateHandler?: (event: RTCPeerConnectionIceEvent) => void;
10
+ constructor(peer: RondevuPeer);
11
+ abstract get name(): string;
12
+ createOffer(options: PeerOptions): Promise<string>;
13
+ answer(offerId: string, offerSdp: string, options: PeerOptions): Promise<void>;
14
+ handleAnswer(sdp: string): Promise<void>;
15
+ handleIceCandidate(candidate: any): Promise<void>;
16
+ /**
17
+ * Setup trickle ICE candidate handler
18
+ * Sends local ICE candidates to server as they are discovered
19
+ */
20
+ protected setupIceCandidateHandler(offerId: string): void;
21
+ cleanup(): void;
22
+ close(): Promise<void>;
23
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Base class for peer connection states
3
+ * Implements the State pattern for managing WebRTC connection lifecycle
4
+ */
5
+ export class PeerState {
6
+ constructor(peer) {
7
+ this.peer = peer;
8
+ }
9
+ async createOffer(options) {
10
+ throw new Error(`Cannot create offer in ${this.name} state`);
11
+ }
12
+ async answer(offerId, offerSdp, options) {
13
+ throw new Error(`Cannot answer in ${this.name} state`);
14
+ }
15
+ async handleAnswer(sdp) {
16
+ throw new Error(`Cannot handle answer in ${this.name} state`);
17
+ }
18
+ async handleIceCandidate(candidate) {
19
+ // ICE candidates can arrive in multiple states, so default is to add them
20
+ if (this.peer.pc.remoteDescription) {
21
+ await this.peer.pc.addIceCandidate(new RTCIceCandidate(candidate));
22
+ }
23
+ }
24
+ /**
25
+ * Setup trickle ICE candidate handler
26
+ * Sends local ICE candidates to server as they are discovered
27
+ */
28
+ setupIceCandidateHandler(offerId) {
29
+ this.iceCandidateHandler = async (event) => {
30
+ if (event.candidate && offerId) {
31
+ const candidateData = event.candidate.toJSON();
32
+ if (candidateData.candidate && candidateData.candidate !== '') {
33
+ try {
34
+ await this.peer.offersApi.addIceCandidates(offerId, [candidateData]);
35
+ }
36
+ catch (err) {
37
+ console.error('Error sending ICE candidate:', err);
38
+ }
39
+ }
40
+ }
41
+ };
42
+ this.peer.pc.addEventListener('icecandidate', this.iceCandidateHandler);
43
+ }
44
+ cleanup() {
45
+ // Clean up ICE candidate handler if it exists
46
+ if (this.iceCandidateHandler) {
47
+ this.peer.pc.removeEventListener('icecandidate', this.iceCandidateHandler);
48
+ }
49
+ }
50
+ async close() {
51
+ this.cleanup();
52
+ const { ClosedState } = await import('./closed-state.js');
53
+ this.peer.setState(new ClosedState(this.peer));
54
+ }
55
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Timeout configurations for different connection phases
3
+ */
4
+ export interface PeerTimeouts {
5
+ /** Timeout for ICE gathering (default: 10000ms) */
6
+ iceGathering?: number;
7
+ /** Timeout for waiting for answer (default: 30000ms) */
8
+ waitingForAnswer?: number;
9
+ /** Timeout for creating answer (default: 10000ms) */
10
+ creatingAnswer?: number;
11
+ /** Timeout for ICE connection (default: 30000ms) */
12
+ iceConnection?: number;
13
+ }
14
+ /**
15
+ * Options for creating a peer connection
16
+ */
17
+ export interface PeerOptions {
18
+ /** RTCConfiguration for the peer connection */
19
+ rtcConfig?: RTCConfiguration;
20
+ /** Topics to advertise this connection under */
21
+ topics: string[];
22
+ /** How long the offer should live (milliseconds) */
23
+ ttl?: number;
24
+ /** Whether to create a data channel automatically (for offerer) */
25
+ createDataChannel?: boolean;
26
+ /** Label for the automatically created data channel */
27
+ dataChannelLabel?: string;
28
+ /** Timeout configurations */
29
+ timeouts?: PeerTimeouts;
30
+ }
31
+ /**
32
+ * Events emitted by RondevuPeer
33
+ */
34
+ export interface PeerEvents extends Record<string, (...args: any[]) => void> {
35
+ 'state': (state: string) => void;
36
+ 'connected': () => void;
37
+ 'disconnected': () => void;
38
+ 'failed': (error: Error) => void;
39
+ 'datachannel': (channel: RTCDataChannel) => void;
40
+ 'track': (event: RTCTrackEvent) => void;
41
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { PeerState } from './state.js';
2
+ import type { PeerOptions } from './types.js';
3
+ import type RondevuPeer from './index.js';
4
+ /**
5
+ * Waiting for answer from another peer
6
+ */
7
+ export declare class WaitingForAnswerState extends PeerState {
8
+ private offerId;
9
+ private options;
10
+ private pollingInterval?;
11
+ private timeout?;
12
+ constructor(peer: RondevuPeer, offerId: string, options: PeerOptions);
13
+ get name(): string;
14
+ private startPolling;
15
+ handleAnswer(sdp: string): Promise<void>;
16
+ cleanup(): void;
17
+ }
@@ -0,0 +1,60 @@
1
+ import { PeerState } from './state.js';
2
+ /**
3
+ * Waiting for answer from another peer
4
+ */
5
+ export class WaitingForAnswerState extends PeerState {
6
+ constructor(peer, offerId, options) {
7
+ super(peer);
8
+ this.offerId = offerId;
9
+ this.options = options;
10
+ this.startPolling();
11
+ }
12
+ get name() { return 'waiting-for-answer'; }
13
+ startPolling() {
14
+ const answerTimeout = this.options.timeouts?.waitingForAnswer || 30000;
15
+ this.timeout = setTimeout(async () => {
16
+ this.cleanup();
17
+ const { FailedState } = await import('./failed-state.js');
18
+ this.peer.setState(new FailedState(this.peer, new Error('Timeout waiting for answer')));
19
+ }, answerTimeout);
20
+ this.pollingInterval = setInterval(async () => {
21
+ try {
22
+ const answers = await this.peer.offersApi.getAnswers();
23
+ const myAnswer = answers.find((a) => a.offerId === this.offerId);
24
+ if (myAnswer) {
25
+ this.cleanup();
26
+ await this.handleAnswer(myAnswer.sdp);
27
+ }
28
+ }
29
+ catch (err) {
30
+ console.error('Error polling for answers:', err);
31
+ if (err instanceof Error && err.message.includes('not found')) {
32
+ this.cleanup();
33
+ const { FailedState } = await import('./failed-state.js');
34
+ this.peer.setState(new FailedState(this.peer, new Error('Offer expired or not found')));
35
+ }
36
+ }
37
+ }, 2000);
38
+ }
39
+ async handleAnswer(sdp) {
40
+ try {
41
+ await this.peer.pc.setRemoteDescription({
42
+ type: 'answer',
43
+ sdp
44
+ });
45
+ // Transition to exchanging ICE
46
+ const { ExchangingIceState } = await import('./exchanging-ice-state.js');
47
+ this.peer.setState(new ExchangingIceState(this.peer, this.offerId, this.options));
48
+ }
49
+ catch (error) {
50
+ const { FailedState } = await import('./failed-state.js');
51
+ this.peer.setState(new FailedState(this.peer, error));
52
+ }
53
+ }
54
+ cleanup() {
55
+ if (this.pollingInterval)
56
+ clearInterval(this.pollingInterval);
57
+ if (this.timeout)
58
+ clearTimeout(this.timeout);
59
+ }
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtr-dev/rondevu-client",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "TypeScript client for Rondevu topic-based peer discovery and signaling server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",