@xtr-dev/rondevu-client 0.20.1 → 0.21.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.
Files changed (43) hide show
  1. package/README.md +83 -385
  2. package/dist/api/batcher.d.ts +60 -38
  3. package/dist/api/batcher.js +121 -77
  4. package/dist/api/client.d.ts +104 -61
  5. package/dist/api/client.js +273 -185
  6. package/dist/connections/answerer.d.ts +15 -6
  7. package/dist/connections/answerer.js +56 -19
  8. package/dist/connections/base.d.ts +6 -4
  9. package/dist/connections/base.js +26 -16
  10. package/dist/connections/config.d.ts +30 -0
  11. package/dist/connections/config.js +20 -0
  12. package/dist/connections/events.d.ts +6 -6
  13. package/dist/connections/offerer.d.ts +37 -8
  14. package/dist/connections/offerer.js +92 -24
  15. package/dist/core/ice-config.d.ts +35 -0
  16. package/dist/core/ice-config.js +111 -0
  17. package/dist/core/index.d.ts +18 -18
  18. package/dist/core/index.js +18 -13
  19. package/dist/core/offer-pool.d.ts +30 -11
  20. package/dist/core/offer-pool.js +90 -76
  21. package/dist/core/peer.d.ts +155 -0
  22. package/dist/core/peer.js +252 -0
  23. package/dist/core/polling-manager.d.ts +71 -0
  24. package/dist/core/polling-manager.js +122 -0
  25. package/dist/core/rondevu-errors.d.ts +59 -0
  26. package/dist/core/rondevu-errors.js +75 -0
  27. package/dist/core/rondevu-types.d.ts +125 -0
  28. package/dist/core/rondevu-types.js +6 -0
  29. package/dist/core/rondevu.d.ts +106 -209
  30. package/dist/core/rondevu.js +221 -349
  31. package/dist/crypto/adapter.d.ts +25 -9
  32. package/dist/crypto/node.d.ts +27 -5
  33. package/dist/crypto/node.js +96 -25
  34. package/dist/crypto/web.d.ts +26 -4
  35. package/dist/crypto/web.js +102 -25
  36. package/dist/utils/message-buffer.js +4 -4
  37. package/dist/webrtc/adapter.d.ts +22 -0
  38. package/dist/webrtc/adapter.js +5 -0
  39. package/dist/webrtc/browser.d.ts +12 -0
  40. package/dist/webrtc/browser.js +15 -0
  41. package/dist/webrtc/node.d.ts +32 -0
  42. package/dist/webrtc/node.js +32 -0
  43. package/package.json +17 -6
@@ -2,7 +2,7 @@ import { EventEmitter } from 'eventemitter3';
2
2
  import { OffererConnection } from '../connections/offerer.js';
3
3
  import { AsyncLock } from '../utils/async-lock.js';
4
4
  /**
5
- * OfferPool manages a pool of WebRTC offers for a published service.
5
+ * OfferPool manages a pool of WebRTC offers for published tags.
6
6
  * Maintains a target number of active offers and automatically replaces
7
7
  * offers that fail or get answered.
8
8
  */
@@ -13,19 +13,21 @@ export class OfferPool extends EventEmitter {
13
13
  this.activeConnections = new Map();
14
14
  this.fillLock = new AsyncLock();
15
15
  this.running = false;
16
- this.pollingInterval = null;
17
- this.lastPollTimestamp = 0;
18
16
  this.api = options.api;
19
- this.serviceFqn = options.serviceFqn;
17
+ this.tags = options.tags;
18
+ this.ownerUsername = options.ownerUsername;
19
+ this.webrtcAdapter = options.webrtcAdapter;
20
20
  this.maxOffers = options.maxOffers;
21
21
  this.offerFactory = options.offerFactory;
22
22
  this.ttl = options.ttl;
23
23
  this.iceServers = options.iceServers;
24
+ this.iceTransportPolicy = options.iceTransportPolicy;
24
25
  this.connectionConfig = options.connectionConfig;
25
26
  this.debugEnabled = options.debugEnabled || false;
26
27
  }
27
28
  /**
28
- * Start filling offers and polling for answers
29
+ * Start filling offers
30
+ * Polling is managed externally by Rondevu's PollingManager
29
31
  */
30
32
  async start() {
31
33
  if (this.running) {
@@ -36,23 +38,14 @@ export class OfferPool extends EventEmitter {
36
38
  this.running = true;
37
39
  // Fill initial offers
38
40
  await this.fillOffers();
39
- // Start polling for answers
40
- this.pollingInterval = setInterval(() => {
41
- this.pollInternal();
42
- }, OfferPool.POLLING_INTERVAL_MS);
43
41
  }
44
42
  /**
45
- * Stop filling offers and polling
43
+ * Stop filling offers
46
44
  * Closes all active connections
47
45
  */
48
46
  stop() {
49
47
  this.debug('Stopping offer pool');
50
48
  this.running = false;
51
- // Stop polling
52
- if (this.pollingInterval) {
53
- clearInterval(this.pollingInterval);
54
- this.pollingInterval = null;
55
- }
56
49
  // Close all active connections
57
50
  for (const [offerId, connection] of this.activeConnections.entries()) {
58
51
  if (connection.isRotating()) {
@@ -115,16 +108,30 @@ export class OfferPool extends EventEmitter {
115
108
  });
116
109
  }
117
110
  /**
118
- * Create a new offer for rotation (reuses existing creation logic)
119
- * Similar to createOffer() but only creates the offer, doesn't create connection
111
+ * Create and publish an offer to the server.
112
+ * Shared logic used by both createOffer() and createNewOfferForRotation().
113
+ *
114
+ * @returns The offer ID, RTCPeerConnection, and optional data channel
120
115
  */
121
- async createNewOfferForRotation() {
116
+ async createOfferAndPublish() {
122
117
  const rtcConfig = {
123
- iceServers: this.iceServers
118
+ iceServers: this.iceServers,
119
+ iceTransportPolicy: this.iceTransportPolicy,
120
+ };
121
+ // 1. Create RTCPeerConnection using adapter
122
+ const pc = this.webrtcAdapter.createPeerConnection(rtcConfig);
123
+ // Collect ICE candidates during offer creation
124
+ // We need to set this up BEFORE setLocalDescription is called
125
+ const collectedCandidates = [];
126
+ pc.onicecandidate = event => {
127
+ if (event.candidate) {
128
+ collectedCandidates.push({
129
+ candidate: event.candidate.candidate,
130
+ sdpMLineIndex: event.candidate.sdpMLineIndex,
131
+ sdpMid: event.candidate.sdpMid,
132
+ });
133
+ }
124
134
  };
125
- this.debug('Creating new offer for rotation...');
126
- // 1. Create RTCPeerConnection
127
- const pc = new RTCPeerConnection(rtcConfig);
128
135
  // 2. Call the factory to create offer
129
136
  let dc;
130
137
  let offer;
@@ -138,51 +145,45 @@ export class OfferPool extends EventEmitter {
138
145
  throw err;
139
146
  }
140
147
  // 3. Publish to server to get offerId
141
- const result = await this.api.publishService({
142
- serviceFqn: this.serviceFqn,
148
+ const result = await this.api.publish({
149
+ tags: this.tags,
143
150
  offers: [{ sdp: offer.sdp }],
144
151
  ttl: this.ttl,
145
152
  });
146
- const newOfferId = result.offers[0].offerId;
147
- this.debug(`New offer created for rotation: ${newOfferId}`);
148
- return { newOfferId, pc, dc };
153
+ const offerId = result.offers[0].offerId;
154
+ // 4. Send any ICE candidates we've already collected
155
+ if (collectedCandidates.length > 0) {
156
+ this.debug(`Sending ${collectedCandidates.length} early ICE candidates for offer ${offerId}`);
157
+ this.api.addOfferIceCandidates(offerId, collectedCandidates).catch(err => {
158
+ this.debug('Failed to send early ICE candidates:', err);
159
+ });
160
+ }
161
+ return { offerId, pc, dc };
162
+ }
163
+ /**
164
+ * Create a new offer for rotation (reuses existing creation logic)
165
+ * Similar to createOffer() but only creates the offer, doesn't create connection
166
+ */
167
+ async createNewOfferForRotation() {
168
+ this.debug('Creating new offer for rotation...');
169
+ const { offerId, pc, dc } = await this.createOfferAndPublish();
170
+ this.debug(`New offer created for rotation: ${offerId}`);
171
+ return { newOfferId: offerId, pc, dc };
149
172
  }
150
173
  /**
151
174
  * Create a single offer and publish it to the server
152
175
  */
153
176
  async createOffer() {
154
- const rtcConfig = {
155
- iceServers: this.iceServers
156
- };
157
177
  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
178
+ const { offerId, pc, dc } = await this.createOfferAndPublish();
179
+ // Create OffererConnection instance
180
180
  const connection = new OffererConnection({
181
181
  api: this.api,
182
- serviceFqn: this.serviceFqn,
182
+ ownerUsername: this.ownerUsername,
183
183
  offerId,
184
184
  pc,
185
185
  dc,
186
+ webrtcAdapter: this.webrtcAdapter,
186
187
  config: {
187
188
  ...this.connectionConfig,
188
189
  debug: this.debugEnabled,
@@ -195,7 +196,14 @@ export class OfferPool extends EventEmitter {
195
196
  });
196
197
  connection.on('failed', async (error) => {
197
198
  const currentOfferId = connection.getOfferId();
198
- this.debug(`Connection failed for offer ${currentOfferId}, rotating...`);
199
+ this.debug(`Connection failed for offer ${currentOfferId}`);
200
+ // Double-check connection state before rotating
201
+ // (polling events may have already recovered the connection)
202
+ if (connection.getState() !== 'failed') {
203
+ this.debug(`Connection ${currentOfferId} recovered, skipping rotation`);
204
+ return;
205
+ }
206
+ this.debug(`Proceeding with rotation for offer ${currentOfferId}`);
199
207
  try {
200
208
  // Create new offer and rebind existing connection
201
209
  const { newOfferId, pc, dc } = await this.createNewOfferForRotation();
@@ -225,35 +233,42 @@ export class OfferPool extends EventEmitter {
225
233
  // Initialize the connection
226
234
  await connection.initialize();
227
235
  this.debug(`Offer created: ${offerId}`);
228
- this.emit('offer:created', offerId, this.serviceFqn);
236
+ this.emit('offer:created', offerId, this.tags);
229
237
  }
230
238
  /**
231
- * Poll for answers and delegate to OffererConnections
239
+ * Handle poll:answer event from PollingManager
240
+ * Called by Rondevu when a poll:answer event is received
232
241
  */
233
- async pollInternal() {
242
+ async handlePollAnswer(data) {
234
243
  if (!this.running)
235
244
  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
- }
245
+ const connection = this.activeConnections.get(data.offerId);
246
+ if (connection) {
247
+ this.debug(`Processing answer for offer ${data.offerId}`);
248
+ try {
249
+ await connection.processAnswer(data.sdp, data.answererId);
250
+ // Create replacement offer
251
+ this.fillOffers();
252
+ }
253
+ catch (err) {
254
+ this.debug(`Failed to process answer for offer ${data.offerId}:`, err);
252
255
  }
253
256
  }
254
- catch (err) {
255
- console.error('[OfferPool] Polling error:', err);
257
+ // Silently ignore answers for offers we don't have - they may be for other connections
258
+ }
259
+ /**
260
+ * Handle poll:ice event from PollingManager
261
+ * Called by Rondevu when a poll:ice event is received
262
+ */
263
+ handlePollIce(data) {
264
+ if (!this.running)
265
+ return;
266
+ const connection = this.activeConnections.get(data.offerId);
267
+ if (connection) {
268
+ this.debug(`Processing ${data.candidates.length} ICE candidates for offer ${data.offerId}`);
269
+ connection.handleRemoteIceCandidates(data.candidates);
256
270
  }
271
+ // Silently ignore ICE candidates for offers we don't have
257
272
  }
258
273
  /**
259
274
  * Debug logging (only if debug enabled)
@@ -264,4 +279,3 @@ export class OfferPool extends EventEmitter {
264
279
  }
265
280
  }
266
281
  }
267
- OfferPool.POLLING_INTERVAL_MS = 1000;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Peer - Clean DX wrapper for peer-to-peer connections
3
+ *
4
+ * Provides a simple interface for connecting to a peer by tags/username,
5
+ * with automatic reconnection and message buffering.
6
+ */
7
+ import { EventEmitter } from 'eventemitter3';
8
+ import { RondevuAPI } from '../api/client.js';
9
+ import { AnswererConnection } from '../connections/answerer.js';
10
+ import { ConnectionConfig } from '../connections/config.js';
11
+ /**
12
+ * Simplified peer state (maps from ConnectionState)
13
+ */
14
+ export type PeerState = 'connecting' | 'connected' | 'disconnected' | 'reconnecting' | 'failed' | 'closed';
15
+ /**
16
+ * Event map for Peer
17
+ */
18
+ export interface PeerEventMap {
19
+ /** Emitted when connection state changes */
20
+ state: [state: PeerState, previousState: PeerState];
21
+ /** Emitted when connection is established */
22
+ open: [];
23
+ /** Emitted when connection is closed */
24
+ close: [reason?: string];
25
+ /** Emitted when a message is received */
26
+ message: [data: string | ArrayBuffer | Blob];
27
+ /** Emitted when an error occurs */
28
+ error: [error: Error];
29
+ /** Emitted when reconnection is attempted */
30
+ reconnecting: [attempt: number, maxAttempts: number];
31
+ }
32
+ export type PeerEventName = keyof PeerEventMap;
33
+ /**
34
+ * Options for creating a Peer connection
35
+ */
36
+ export interface PeerOptions {
37
+ /** Tags to match for peer discovery */
38
+ tags: string[];
39
+ /** Optional: connect to specific username */
40
+ username?: string;
41
+ /** Optional: custom RTC configuration */
42
+ rtcConfig?: RTCConfiguration;
43
+ /** Optional: connection behavior configuration */
44
+ config?: Partial<ConnectionConfig>;
45
+ }
46
+ /**
47
+ * Internal options passed from Rondevu
48
+ */
49
+ export interface PeerInternalOptions extends PeerOptions {
50
+ api: RondevuAPI;
51
+ iceServers: RTCIceServer[];
52
+ iceTransportPolicy?: RTCIceTransportPolicy;
53
+ debug?: boolean;
54
+ }
55
+ /**
56
+ * Peer - A clean interface for peer-to-peer connections
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const peer = await rondevu.peer({
61
+ * username: 'alice',
62
+ * tags: ['chat']
63
+ * })
64
+ *
65
+ * peer.on('open', () => {
66
+ * console.log('Connected to', peer.peerUsername)
67
+ * peer.send('Hello!')
68
+ * })
69
+ *
70
+ * peer.on('message', (data) => {
71
+ * console.log('Received:', data)
72
+ * })
73
+ *
74
+ * peer.on('state', (state) => {
75
+ * console.log('Connection state:', state)
76
+ * })
77
+ *
78
+ * // Access underlying WebRTC objects
79
+ * const pc = peer.peerConnection // RTCPeerConnection
80
+ * const dc = peer.dataChannel // RTCDataChannel
81
+ * ```
82
+ */
83
+ export declare class Peer extends EventEmitter<PeerEventMap> {
84
+ private connection;
85
+ private api;
86
+ private tags;
87
+ private targetUsername?;
88
+ private iceServers;
89
+ private iceTransportPolicy?;
90
+ private connectionConfig?;
91
+ private debugEnabled;
92
+ private _state;
93
+ private _peerUsername;
94
+ private _offerId;
95
+ constructor(options: PeerInternalOptions);
96
+ /**
97
+ * Initialize the peer connection (called internally by Rondevu.peer())
98
+ */
99
+ initialize(): Promise<void>;
100
+ /**
101
+ * Setup event handlers to forward from AnswererConnection
102
+ */
103
+ private setupEventHandlers;
104
+ /**
105
+ * Map internal ConnectionState to simplified PeerState
106
+ */
107
+ private mapState;
108
+ /**
109
+ * Current connection state
110
+ */
111
+ get state(): PeerState;
112
+ /**
113
+ * Username of the connected peer
114
+ */
115
+ get peerUsername(): string;
116
+ /**
117
+ * The offer ID being used for this connection
118
+ */
119
+ get offerId(): string;
120
+ /**
121
+ * Tags used for discovery
122
+ */
123
+ get peerTags(): string[];
124
+ /**
125
+ * The underlying RTCPeerConnection (null if not connected)
126
+ */
127
+ get peerConnection(): RTCPeerConnection | null;
128
+ /**
129
+ * The underlying RTCDataChannel (null if not connected)
130
+ */
131
+ get dataChannel(): RTCDataChannel | null;
132
+ /**
133
+ * Whether the peer is currently connected
134
+ */
135
+ get isConnected(): boolean;
136
+ /**
137
+ * Send a message to the peer
138
+ * Messages are buffered if not connected (when buffering is enabled)
139
+ *
140
+ * @param data - String, ArrayBuffer, or Blob to send
141
+ */
142
+ send(data: string | ArrayBuffer | Blob): void;
143
+ /**
144
+ * Close the peer connection
145
+ */
146
+ close(): void;
147
+ /**
148
+ * Get the underlying AnswererConnection for advanced use cases
149
+ */
150
+ getConnection(): AnswererConnection | null;
151
+ /**
152
+ * Debug logging
153
+ */
154
+ private debug;
155
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Peer - Clean DX wrapper for peer-to-peer connections
3
+ *
4
+ * Provides a simple interface for connecting to a peer by tags/username,
5
+ * with automatic reconnection and message buffering.
6
+ */
7
+ import { EventEmitter } from 'eventemitter3';
8
+ import { AnswererConnection } from '../connections/answerer.js';
9
+ import { ConnectionState } from '../connections/events.js';
10
+ /**
11
+ * Peer - A clean interface for peer-to-peer connections
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const peer = await rondevu.peer({
16
+ * username: 'alice',
17
+ * tags: ['chat']
18
+ * })
19
+ *
20
+ * peer.on('open', () => {
21
+ * console.log('Connected to', peer.peerUsername)
22
+ * peer.send('Hello!')
23
+ * })
24
+ *
25
+ * peer.on('message', (data) => {
26
+ * console.log('Received:', data)
27
+ * })
28
+ *
29
+ * peer.on('state', (state) => {
30
+ * console.log('Connection state:', state)
31
+ * })
32
+ *
33
+ * // Access underlying WebRTC objects
34
+ * const pc = peer.peerConnection // RTCPeerConnection
35
+ * const dc = peer.dataChannel // RTCDataChannel
36
+ * ```
37
+ */
38
+ export class Peer extends EventEmitter {
39
+ constructor(options) {
40
+ super();
41
+ this.connection = null;
42
+ this._state = 'connecting';
43
+ this._peerUsername = '';
44
+ this._offerId = '';
45
+ this.api = options.api;
46
+ this.tags = options.tags;
47
+ this.targetUsername = options.username;
48
+ this.iceServers = options.iceServers;
49
+ this.iceTransportPolicy = options.iceTransportPolicy;
50
+ this.connectionConfig = options.config;
51
+ this.debugEnabled = options.debug || false;
52
+ }
53
+ /**
54
+ * Initialize the peer connection (called internally by Rondevu.peer())
55
+ */
56
+ async initialize() {
57
+ this.debug('Initializing peer connection');
58
+ this.debug(`Tags: ${this.tags.join(', ')}${this.targetUsername ? `, username: ${this.targetUsername}` : ''}`);
59
+ // Discover offers
60
+ const result = (await this.api.discover({
61
+ tags: this.tags,
62
+ limit: 100,
63
+ }));
64
+ if (!result.offers || result.offers.length === 0) {
65
+ throw new Error(`No peers found for tags: ${this.tags.join(', ')}`);
66
+ }
67
+ // Filter by username if specified
68
+ let availableOffers = result.offers;
69
+ if (this.targetUsername) {
70
+ availableOffers = result.offers.filter((o) => o.username === this.targetUsername);
71
+ if (availableOffers.length === 0) {
72
+ throw new Error(`No peers found for tags: ${this.tags.join(', ')} from @${this.targetUsername}`);
73
+ }
74
+ }
75
+ // Pick a random offer
76
+ const offer = availableOffers[Math.floor(Math.random() * availableOffers.length)];
77
+ this._peerUsername = offer.username;
78
+ this._offerId = offer.offerId;
79
+ this.debug(`Selected offer ${offer.offerId} from @${offer.username}`);
80
+ // Create the underlying AnswererConnection
81
+ this.connection = new AnswererConnection({
82
+ api: this.api,
83
+ ownerUsername: offer.username,
84
+ tags: offer.tags,
85
+ offerId: offer.offerId,
86
+ offerSdp: offer.sdp,
87
+ rtcConfig: {
88
+ iceServers: this.iceServers,
89
+ iceTransportPolicy: this.iceTransportPolicy,
90
+ },
91
+ config: {
92
+ ...this.connectionConfig,
93
+ debug: this.debugEnabled,
94
+ },
95
+ });
96
+ // Wire up events
97
+ this.setupEventHandlers();
98
+ // Start connection
99
+ await this.connection.initialize();
100
+ }
101
+ /**
102
+ * Setup event handlers to forward from AnswererConnection
103
+ */
104
+ setupEventHandlers() {
105
+ if (!this.connection)
106
+ return;
107
+ // Map ConnectionState to PeerState
108
+ this.connection.on('state:changed', ({ oldState, newState, reason }) => {
109
+ const mappedOld = this.mapState(oldState);
110
+ const mappedNew = this.mapState(newState);
111
+ if (mappedOld !== mappedNew) {
112
+ this._state = mappedNew;
113
+ this.emit('state', mappedNew, mappedOld);
114
+ }
115
+ });
116
+ // Forward connection events
117
+ this.connection.on('connected', () => {
118
+ this._state = 'connected';
119
+ this.emit('open');
120
+ });
121
+ this.connection.on('closed', reason => {
122
+ this._state = 'closed';
123
+ this.emit('close', reason);
124
+ });
125
+ this.connection.on('failed', error => {
126
+ this._state = 'failed';
127
+ this.emit('error', error);
128
+ });
129
+ // Forward message events
130
+ this.connection.on('message', data => {
131
+ this.emit('message', data);
132
+ });
133
+ // Forward reconnection events
134
+ this.connection.on('reconnect:scheduled', info => {
135
+ this._state = 'reconnecting';
136
+ this.emit('reconnecting', info.attempt, info.maxAttempts);
137
+ });
138
+ this.connection.on('reconnect:success', () => {
139
+ // State will be updated by 'connected' event
140
+ });
141
+ this.connection.on('reconnect:failed', error => {
142
+ this.emit('error', error);
143
+ });
144
+ }
145
+ /**
146
+ * Map internal ConnectionState to simplified PeerState
147
+ */
148
+ mapState(state) {
149
+ switch (state) {
150
+ case ConnectionState.INITIALIZING:
151
+ case ConnectionState.GATHERING:
152
+ case ConnectionState.SIGNALING:
153
+ case ConnectionState.CHECKING:
154
+ case ConnectionState.CONNECTING:
155
+ return 'connecting';
156
+ case ConnectionState.CONNECTED:
157
+ return 'connected';
158
+ case ConnectionState.DISCONNECTED:
159
+ return 'disconnected';
160
+ case ConnectionState.RECONNECTING:
161
+ return 'reconnecting';
162
+ case ConnectionState.FAILED:
163
+ return 'failed';
164
+ case ConnectionState.CLOSED:
165
+ return 'closed';
166
+ default:
167
+ return 'connecting';
168
+ }
169
+ }
170
+ // ========================================
171
+ // Public Properties
172
+ // ========================================
173
+ /**
174
+ * Current connection state
175
+ */
176
+ get state() {
177
+ return this._state;
178
+ }
179
+ /**
180
+ * Username of the connected peer
181
+ */
182
+ get peerUsername() {
183
+ return this._peerUsername;
184
+ }
185
+ /**
186
+ * The offer ID being used for this connection
187
+ */
188
+ get offerId() {
189
+ return this._offerId;
190
+ }
191
+ /**
192
+ * Tags used for discovery
193
+ */
194
+ get peerTags() {
195
+ return this.tags;
196
+ }
197
+ /**
198
+ * The underlying RTCPeerConnection (null if not connected)
199
+ */
200
+ get peerConnection() {
201
+ return this.connection?.getPeerConnection() ?? null;
202
+ }
203
+ /**
204
+ * The underlying RTCDataChannel (null if not connected)
205
+ */
206
+ get dataChannel() {
207
+ return this.connection?.getDataChannel() ?? null;
208
+ }
209
+ /**
210
+ * Whether the peer is currently connected
211
+ */
212
+ get isConnected() {
213
+ return this._state === 'connected';
214
+ }
215
+ // ========================================
216
+ // Public Methods
217
+ // ========================================
218
+ /**
219
+ * Send a message to the peer
220
+ * Messages are buffered if not connected (when buffering is enabled)
221
+ *
222
+ * @param data - String, ArrayBuffer, or Blob to send
223
+ */
224
+ send(data) {
225
+ if (!this.connection) {
226
+ throw new Error('Peer not initialized');
227
+ }
228
+ this.connection.send(data);
229
+ }
230
+ /**
231
+ * Close the peer connection
232
+ */
233
+ close() {
234
+ this.debug('Closing peer connection');
235
+ this.connection?.close();
236
+ this._state = 'closed';
237
+ }
238
+ /**
239
+ * Get the underlying AnswererConnection for advanced use cases
240
+ */
241
+ getConnection() {
242
+ return this.connection;
243
+ }
244
+ /**
245
+ * Debug logging
246
+ */
247
+ debug(...args) {
248
+ if (this.debugEnabled) {
249
+ console.log('[Peer]', ...args);
250
+ }
251
+ }
252
+ }