@xtr-dev/rondevu-client 0.18.10 → 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 (70) hide show
  1. package/README.md +92 -117
  2. package/dist/api/batcher.d.ts +83 -0
  3. package/dist/api/batcher.js +155 -0
  4. package/dist/api/client.d.ts +198 -0
  5. package/dist/api/client.js +400 -0
  6. package/dist/{answerer-connection.d.ts → connections/answerer.d.ts} +25 -8
  7. package/dist/{answerer-connection.js → connections/answerer.js} +70 -48
  8. package/dist/{connection.d.ts → connections/base.d.ts} +30 -7
  9. package/dist/{connection.js → connections/base.js} +65 -14
  10. package/dist/connections/config.d.ts +51 -0
  11. package/dist/{connection-config.js → connections/config.js} +20 -0
  12. package/dist/{connection-events.d.ts → connections/events.d.ts} +6 -6
  13. package/dist/connections/offerer.d.ts +108 -0
  14. package/dist/connections/offerer.js +306 -0
  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 +22 -0
  18. package/dist/core/index.js +22 -0
  19. package/dist/core/offer-pool.d.ts +113 -0
  20. package/dist/core/offer-pool.js +281 -0
  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 +296 -0
  30. package/dist/core/rondevu.js +472 -0
  31. package/dist/crypto/adapter.d.ts +53 -0
  32. package/dist/crypto/node.d.ts +57 -0
  33. package/dist/crypto/node.js +149 -0
  34. package/dist/crypto/web.d.ts +38 -0
  35. package/dist/crypto/web.js +129 -0
  36. package/dist/utils/async-lock.d.ts +42 -0
  37. package/dist/utils/async-lock.js +75 -0
  38. package/dist/{message-buffer.d.ts → utils/message-buffer.d.ts} +1 -1
  39. package/dist/{message-buffer.js → utils/message-buffer.js} +4 -4
  40. package/dist/webrtc/adapter.d.ts +22 -0
  41. package/dist/webrtc/adapter.js +5 -0
  42. package/dist/webrtc/browser.d.ts +12 -0
  43. package/dist/webrtc/browser.js +15 -0
  44. package/dist/webrtc/node.d.ts +32 -0
  45. package/dist/webrtc/node.js +32 -0
  46. package/package.json +20 -9
  47. package/dist/api.d.ts +0 -146
  48. package/dist/api.js +0 -279
  49. package/dist/connection-config.d.ts +0 -21
  50. package/dist/crypto-adapter.d.ts +0 -37
  51. package/dist/index.d.ts +0 -13
  52. package/dist/index.js +0 -10
  53. package/dist/node-crypto-adapter.d.ts +0 -35
  54. package/dist/node-crypto-adapter.js +0 -78
  55. package/dist/offerer-connection.d.ts +0 -54
  56. package/dist/offerer-connection.js +0 -177
  57. package/dist/rondevu-signaler.d.ts +0 -112
  58. package/dist/rondevu-signaler.js +0 -401
  59. package/dist/rondevu.d.ts +0 -407
  60. package/dist/rondevu.js +0 -847
  61. package/dist/rpc-batcher.d.ts +0 -61
  62. package/dist/rpc-batcher.js +0 -111
  63. package/dist/web-crypto-adapter.d.ts +0 -16
  64. package/dist/web-crypto-adapter.js +0 -52
  65. /package/dist/{connection-events.js → connections/events.js} +0 -0
  66. /package/dist/{types.d.ts → core/types.d.ts} +0 -0
  67. /package/dist/{types.js → core/types.js} +0 -0
  68. /package/dist/{crypto-adapter.js → crypto/adapter.js} +0 -0
  69. /package/dist/{exponential-backoff.d.ts → utils/exponential-backoff.d.ts} +0 -0
  70. /package/dist/{exponential-backoff.js → utils/exponential-backoff.js} +0 -0
@@ -0,0 +1,281 @@
1
+ import { EventEmitter } from 'eventemitter3';
2
+ import { OffererConnection } from '../connections/offerer.js';
3
+ import { AsyncLock } from '../utils/async-lock.js';
4
+ /**
5
+ * OfferPool manages a pool of WebRTC offers for published tags.
6
+ * Maintains a target number of active offers and automatically replaces
7
+ * offers that fail or get answered.
8
+ */
9
+ export class OfferPool extends EventEmitter {
10
+ constructor(options) {
11
+ super();
12
+ // State
13
+ this.activeConnections = new Map();
14
+ this.fillLock = new AsyncLock();
15
+ this.running = false;
16
+ this.api = options.api;
17
+ this.tags = options.tags;
18
+ this.ownerUsername = options.ownerUsername;
19
+ this.webrtcAdapter = options.webrtcAdapter;
20
+ this.maxOffers = options.maxOffers;
21
+ this.offerFactory = options.offerFactory;
22
+ this.ttl = options.ttl;
23
+ this.iceServers = options.iceServers;
24
+ this.iceTransportPolicy = options.iceTransportPolicy;
25
+ this.connectionConfig = options.connectionConfig;
26
+ this.debugEnabled = options.debugEnabled || false;
27
+ }
28
+ /**
29
+ * Start filling offers
30
+ * Polling is managed externally by Rondevu's PollingManager
31
+ */
32
+ async start() {
33
+ if (this.running) {
34
+ this.debug('Already running');
35
+ return;
36
+ }
37
+ this.debug('Starting offer pool');
38
+ this.running = true;
39
+ // Fill initial offers
40
+ await this.fillOffers();
41
+ }
42
+ /**
43
+ * Stop filling offers
44
+ * Closes all active connections
45
+ */
46
+ stop() {
47
+ this.debug('Stopping offer pool');
48
+ this.running = false;
49
+ // Close all active connections
50
+ for (const [offerId, connection] of this.activeConnections.entries()) {
51
+ if (connection.isRotating()) {
52
+ this.debug(`Connection ${offerId} is rotating, will close anyway`);
53
+ }
54
+ this.debug(`Closing connection ${offerId}`);
55
+ connection.close();
56
+ }
57
+ this.activeConnections.clear();
58
+ }
59
+ /**
60
+ * Get count of active offers
61
+ */
62
+ getOfferCount() {
63
+ return this.activeConnections.size;
64
+ }
65
+ /**
66
+ * Get all active connections
67
+ */
68
+ getActiveConnections() {
69
+ return this.activeConnections;
70
+ }
71
+ /**
72
+ * Check if a specific offer is connected
73
+ */
74
+ isConnected(offerId) {
75
+ const connection = this.activeConnections.get(offerId);
76
+ return connection ? connection.getState() === 'connected' : false;
77
+ }
78
+ /**
79
+ * Disconnect all active offers
80
+ */
81
+ disconnectAll() {
82
+ this.debug('Disconnecting all offers');
83
+ for (const [offerId, connection] of this.activeConnections.entries()) {
84
+ this.debug(`Closing connection ${offerId}`);
85
+ connection.close();
86
+ }
87
+ this.activeConnections.clear();
88
+ }
89
+ /**
90
+ * Fill offers to reach maxOffers count
91
+ * Uses AsyncLock to prevent concurrent fills
92
+ */
93
+ async fillOffers() {
94
+ if (!this.running)
95
+ return;
96
+ return this.fillLock.run(async () => {
97
+ const currentCount = this.activeConnections.size;
98
+ const needed = this.maxOffers - currentCount;
99
+ this.debug(`Filling offers: current=${currentCount}, needed=${needed}`);
100
+ for (let i = 0; i < needed; i++) {
101
+ try {
102
+ await this.createOffer();
103
+ }
104
+ catch (err) {
105
+ console.error('[OfferPool] Failed to create offer:', err);
106
+ }
107
+ }
108
+ });
109
+ }
110
+ /**
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
115
+ */
116
+ async createOfferAndPublish() {
117
+ const rtcConfig = {
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
+ }
134
+ };
135
+ // 2. Call the factory to create offer
136
+ let dc;
137
+ let offer;
138
+ try {
139
+ const factoryResult = await this.offerFactory(pc);
140
+ dc = factoryResult.dc;
141
+ offer = factoryResult.offer;
142
+ }
143
+ catch (err) {
144
+ pc.close();
145
+ throw err;
146
+ }
147
+ // 3. Publish to server to get offerId
148
+ const result = await this.api.publish({
149
+ tags: this.tags,
150
+ offers: [{ sdp: offer.sdp }],
151
+ ttl: this.ttl,
152
+ });
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 };
172
+ }
173
+ /**
174
+ * Create a single offer and publish it to the server
175
+ */
176
+ async createOffer() {
177
+ this.debug('Creating new offer...');
178
+ const { offerId, pc, dc } = await this.createOfferAndPublish();
179
+ // Create OffererConnection instance
180
+ const connection = new OffererConnection({
181
+ api: this.api,
182
+ ownerUsername: this.ownerUsername,
183
+ offerId,
184
+ pc,
185
+ dc,
186
+ webrtcAdapter: this.webrtcAdapter,
187
+ config: {
188
+ ...this.connectionConfig,
189
+ debug: this.debugEnabled,
190
+ },
191
+ });
192
+ // Setup connection event handlers
193
+ connection.on('connected', () => {
194
+ this.debug(`Connection established for offer ${offerId}`);
195
+ this.emit('connection:opened', offerId, connection);
196
+ });
197
+ connection.on('failed', async (error) => {
198
+ const currentOfferId = connection.getOfferId();
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}`);
207
+ try {
208
+ // Create new offer and rebind existing connection
209
+ const { newOfferId, pc, dc } = await this.createNewOfferForRotation();
210
+ // Rebind the connection to new offer
211
+ await connection.rebindToOffer(newOfferId, pc, dc);
212
+ // Update map: remove old offerId, add new offerId with same connection
213
+ this.activeConnections.delete(currentOfferId);
214
+ this.activeConnections.set(newOfferId, connection);
215
+ this.emit('connection:rotated', currentOfferId, newOfferId, connection);
216
+ this.debug(`Connection rotated: ${currentOfferId} → ${newOfferId}`);
217
+ }
218
+ catch (rotationError) {
219
+ // If rotation fails, fall back to destroying connection
220
+ this.debug(`Rotation failed for ${currentOfferId}:`, rotationError);
221
+ this.activeConnections.delete(currentOfferId);
222
+ this.emit('offer:failed', currentOfferId, error);
223
+ this.fillOffers(); // Create replacement
224
+ }
225
+ });
226
+ connection.on('closed', () => {
227
+ this.debug(`Connection closed for offer ${offerId}`);
228
+ this.activeConnections.delete(offerId);
229
+ this.fillOffers(); // Replace closed offer
230
+ });
231
+ // Store active connection
232
+ this.activeConnections.set(offerId, connection);
233
+ // Initialize the connection
234
+ await connection.initialize();
235
+ this.debug(`Offer created: ${offerId}`);
236
+ this.emit('offer:created', offerId, this.tags);
237
+ }
238
+ /**
239
+ * Handle poll:answer event from PollingManager
240
+ * Called by Rondevu when a poll:answer event is received
241
+ */
242
+ async handlePollAnswer(data) {
243
+ if (!this.running)
244
+ return;
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);
255
+ }
256
+ }
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);
270
+ }
271
+ // Silently ignore ICE candidates for offers we don't have
272
+ }
273
+ /**
274
+ * Debug logging (only if debug enabled)
275
+ */
276
+ debug(...args) {
277
+ if (this.debugEnabled) {
278
+ console.log('[OfferPool]', ...args);
279
+ }
280
+ }
281
+ }
@@ -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
+ }