@marianmeres/webrtc 1.1.1 → 1.2.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.
package/AGENTS.md CHANGED
@@ -114,9 +114,11 @@ ERROR --RESET--> IDLE
114
114
  ### Constructor
115
115
 
116
116
  ```typescript
117
- new WebRtcManager(factory: WebRtcFactory, config?: WebRtcManagerConfig)
117
+ new WebRtcManager<TContext = unknown>(factory: WebRtcFactory, config?: WebRtcManagerConfig)
118
118
  ```
119
119
 
120
+ **Type Parameter:** `TContext` - Optional type for the `context` property (default: `unknown`)
121
+
120
122
  ### Logger Interface
121
123
 
122
124
  Console-compatible logger interface for custom logging implementations.
@@ -166,6 +168,7 @@ interface WebRtcManagerConfig {
166
168
  | remoteStream | MediaStream \| null | Remote audio stream |
167
169
  | dataChannels | ReadonlyMap<string, RTCDataChannel> | Active data channels |
168
170
  | peerConnection | RTCPeerConnection \| null | Underlying connection |
171
+ | context | TContext \| null | User-defined context for arbitrary data |
169
172
 
170
173
  ### Lifecycle Methods
171
174
 
package/API.md CHANGED
@@ -31,9 +31,15 @@ The main class for managing WebRTC connections.
31
31
  ### Constructor
32
32
 
33
33
  ```typescript
34
- new WebRtcManager(factory: WebRtcFactory, config?: WebRtcManagerConfig)
34
+ new WebRtcManager<TContext = unknown>(factory: WebRtcFactory, config?: WebRtcManagerConfig)
35
35
  ```
36
36
 
37
+ **Type Parameters:**
38
+
39
+ | Name | Default | Description |
40
+ |------|---------|-------------|
41
+ | TContext | `unknown` | Type for the `context` property |
42
+
37
43
  **Parameters:**
38
44
 
39
45
  | Name | Type | Required | Description |
@@ -123,6 +129,33 @@ Returns the underlying RTCPeerConnection, or `null` if not initialized.
123
129
 
124
130
  ---
125
131
 
132
+ #### context
133
+
134
+ ```typescript
135
+ context: TContext | null
136
+ ```
137
+
138
+ User-defined context object for storing arbitrary data associated with this manager. The class accepts an optional generic type parameter for type-safe context access.
139
+
140
+ **Type Parameter:** `TContext` - The type of the context object (default: `unknown`)
141
+
142
+ **Default:** `null`
143
+
144
+ **Example:**
145
+
146
+ ```typescript
147
+ // With type parameter for full type safety:
148
+ const manager = new WebRtcManager<{ audioStream: MediaStream; sessionId: string }>(factory);
149
+ manager.context = { audioStream: myStream, sessionId: '123' };
150
+ manager.context.audioStream; // typed as MediaStream
151
+
152
+ // Without type parameter (backwards compatible):
153
+ const manager = new WebRtcManager(factory);
154
+ manager.context = { anything: 'goes' };
155
+ ```
156
+
157
+ ---
158
+
126
159
  ### Lifecycle Methods
127
160
 
128
161
  #### initialize()
@@ -759,6 +792,9 @@ interface WebRtcManagerConfig {
759
792
  /** Initial reconnection delay in ms (doubles each attempt). Default: 1000 */
760
793
  reconnectDelay?: number;
761
794
 
795
+ /** Timeout in ms for full reconnection to reach connected state. Default: 30000 */
796
+ fullReconnectTimeout?: number;
797
+
762
798
  /** Callback to control whether reconnection should proceed */
763
799
  shouldReconnect?: (context: {
764
800
  attempt: number;
@@ -942,19 +978,24 @@ When `autoReconnect: true`:
942
978
  2. **Attempts 3+:** Full reconnection (new peer connection)
943
979
  3. **Backoff:** `reconnectDelay * 2^(attempt-1)` milliseconds
944
980
 
945
- For "full" strategy reconnections, listen for the `reconnecting` event and re-perform signaling:
981
+ #### Full Reconnection and Signaling
982
+
983
+ **Important:** For "full" strategy reconnections, the manager creates a new peer connection but **cannot automatically complete the signaling handshake**. You must listen for the `reconnecting` event and re-perform signaling when `strategy === 'full'`:
946
984
 
947
985
  ```typescript
948
- manager.on('reconnecting', ({ attempt, strategy }) => {
986
+ manager.on('reconnecting', async ({ attempt, strategy }) => {
949
987
  if (strategy === 'full') {
950
988
  // Re-do offer/answer exchange
951
989
  const offer = await manager.createOffer();
952
990
  await manager.setLocalDescription(offer);
953
991
  sendToRemote(offer);
954
992
  }
993
+ // For 'ice-restart', the manager handles it automatically
955
994
  });
956
995
  ```
957
996
 
997
+ If the connection doesn't reach `CONNECTED` state within `fullReconnectTimeout` (default: 30 seconds), it's treated as a failed attempt and the next reconnection attempt begins. When all attempts are exhausted, `EVENT_RECONNECT_FAILED` is emitted.
998
+
958
999
  ### Conditional Reconnection
959
1000
 
960
1001
  Use the `shouldReconnect` callback to suppress reconnection when the peer disconnected intentionally:
package/README.md CHANGED
@@ -38,9 +38,11 @@ The manager doesn't handle the signaling transport layer - you're responsible fo
38
38
  ### Constructor
39
39
 
40
40
  ```typescript
41
- const manager = new WebRtcManager(factory, config);
41
+ const manager = new WebRtcManager<TContext>(factory, config);
42
42
  ```
43
43
 
44
+ - `TContext`: Optional type parameter for the `context` property (default: `unknown`)
45
+
44
46
  - `factory`: Object implementing `WebRtcFactory` interface (provides `createPeerConnection`, `getUserMedia`, `enumerateDevices`)
45
47
  - `config`: Optional configuration object
46
48
 
@@ -51,6 +53,7 @@ const manager = new WebRtcManager(factory, config);
51
53
  - `autoReconnect`: Enable automatic reconnection (default: false)
52
54
  - `maxReconnectAttempts`: Max reconnection attempts (default: 5)
53
55
  - `reconnectDelay`: Initial reconnection delay in ms (default: 1000)
56
+ - `fullReconnectTimeout`: Timeout in ms for full reconnection to succeed (default: 30000)
54
57
  - `shouldReconnect`: Callback to control whether reconnection should proceed (see below)
55
58
  - `debug`: Enable debug logging (default: false)
56
59
  - `logger`: Custom logger instance implementing `Logger` interface (default: console)
@@ -63,6 +66,7 @@ manager.localStream // MediaStream | null
63
66
  manager.remoteStream // MediaStream | null
64
67
  manager.dataChannels // ReadonlyMap<string, RTCDataChannel>
65
68
  manager.peerConnection // RTCPeerConnection | null
69
+ manager.context // TContext | null - user-defined data
66
70
  ```
67
71
 
68
72
  ### Lifecycle Methods
@@ -161,6 +165,39 @@ The callback receives:
161
165
  - `maxAttempts`: Configured maximum attempts
162
166
  - `strategy`: `"ice-restart"` (attempts 1-2) or `"full"` (attempts 3+)
163
167
 
168
+ ### Reconnection Strategies
169
+
170
+ The manager uses two reconnection strategies with exponential backoff:
171
+
172
+ 1. **ICE Restart** (attempts 1-2): Lightweight reconnection that keeps the existing peer connection and restarts ICE negotiation. Works when the network path changed but the remote peer is still available.
173
+
174
+ 2. **Full Reconnection** (attempts 3+): Creates a completely new peer connection. This is necessary when ICE restart fails, but **requires consumer action** to complete the signaling handshake.
175
+
176
+ #### Handling Full Reconnection
177
+
178
+ When a full reconnection is triggered, the manager will:
179
+ 1. Clean up the old peer connection
180
+ 2. Create a new peer connection
181
+ 3. Emit `EVENT_RECONNECTING` with `strategy: 'full'`
182
+
183
+ **Important:** The manager cannot automatically complete the signaling handshake for full reconnections. You must listen for the `reconnecting` event and re-establish signaling when the strategy is `'full'`:
184
+
185
+ ```typescript
186
+ manager.on(WebRtcManager.EVENT_RECONNECTING, async ({ attempt, strategy }) => {
187
+ console.log(`Reconnecting (attempt ${attempt}, strategy: ${strategy})`);
188
+
189
+ if (strategy === 'full') {
190
+ // Re-do the signaling handshake
191
+ const offer = await manager.createOffer();
192
+ await manager.setLocalDescription(offer);
193
+ signalingChannel.send({ type: 'offer', offer });
194
+ }
195
+ // For 'ice-restart', the manager handles it automatically
196
+ });
197
+ ```
198
+
199
+ If the full reconnection doesn't reach `CONNECTED` state within `fullReconnectTimeout` (default: 30 seconds), it's treated as a failed attempt and the next reconnection attempt begins (or `EVENT_RECONNECT_FAILED` is emitted if max attempts reached).
200
+
164
201
  ## Examples
165
202
 
166
203
  ### Basic Usage (Vanilla JavaScript)
@@ -422,24 +459,31 @@ Mock-based tests for the manager's logic and state transitions:
422
459
  deno task test
423
460
  ```
424
461
 
425
- ### Browser Integration Tests
462
+ ### Interactive Example
426
463
 
427
- Real peer-to-peer connection tests running in a browser environment:
464
+ The `example/` directory contains a working demo of two peers communicating via WebRTC data channels:
428
465
 
429
466
  ```bash
430
- deno task test:browser
467
+ # Build the example bundle
468
+ deno task example:build
469
+
470
+ # Serve the example directory
471
+ cd example && deno run -A jsr:@std/http/file-server
431
472
  ```
432
473
 
433
- This builds the test bundle and starts a local server. Open the provided URL in your browser to run the tests interactively.
474
+ Then open `http://localhost:8000/` in your browser.
434
475
 
435
- The browser tests verify:
436
- - Actual P2P connections between peers
437
- - Data channel message exchange
438
- - ICE candidate exchange
439
- - Connection state transitions
440
- - Resource cleanup
476
+ **Structure:**
477
+ - `index.html` - Parent page with two side-by-side iframes, acts as signaling relay via `postMessage`
478
+ - `peer1.html` - The "offerer" peer (click "Connect" to initiate)
479
+ - `peer2.html` - The "answerer" peer (waits for connection)
441
480
 
442
- See [tests/browser/README.md](tests/browser/README.md) for more details.
481
+ This example demonstrates:
482
+ - P2P connection establishment without a signaling server (uses `postMessage` between iframes)
483
+ - SDP offer/answer exchange
484
+ - ICE candidate exchange
485
+ - Data channel creation and message passing
486
+ - State change monitoring
443
487
 
444
488
  ## License
445
489
 
package/dist/types.d.ts CHANGED
@@ -26,6 +26,8 @@ export interface WebRtcManagerConfig {
26
26
  maxReconnectAttempts?: number;
27
27
  /** Initial reconnection delay in ms. Doubles with each attempt. Defaults to 1000. */
28
28
  reconnectDelay?: number;
29
+ /** Timeout in ms for full reconnection strategy to reach connected state. Defaults to 30000. */
30
+ fullReconnectTimeout?: number;
29
31
  /**
30
32
  * Callback to determine whether reconnection should be attempted.
31
33
  * Called before each reconnection attempt when autoReconnect is enabled.
@@ -25,7 +25,7 @@ import { type WebRtcFactory, type WebRtcManagerConfig, WebRtcState, type WebRtcE
25
25
  * await manager.setLocalDescription(offer);
26
26
  * ```
27
27
  */
28
- export declare class WebRtcManager {
28
+ export declare class WebRtcManager<TContext = unknown> {
29
29
  #private;
30
30
  /** Event emitted when connection state changes. Payload: {@link WebRtcState} */
31
31
  static readonly EVENT_STATE_CHANGE = "state_change";
@@ -51,6 +51,24 @@ export declare class WebRtcManager {
51
51
  static readonly EVENT_MICROPHONE_FAILED = "microphone_failed";
52
52
  /** Event emitted when an error occurs. Payload: `Error` */
53
53
  static readonly EVENT_ERROR = "error";
54
+ /**
55
+ * User-defined context object for storing arbitrary data associated with this manager.
56
+ * Useful for attaching application-specific state (e.g., audio streams, metadata)
57
+ * without modifying the manager internals.
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * // With type parameter for full type safety:
62
+ * const manager = new WebRtcManager<{ audioStream: MediaStream; sessionId: string }>(factory);
63
+ * manager.context = { audioStream: myStream, sessionId: '123' };
64
+ * manager.context.audioStream; // typed as MediaStream
65
+ *
66
+ * // Without type parameter (backwards compatible):
67
+ * const manager = new WebRtcManager(factory);
68
+ * manager.context = { anything: 'goes' };
69
+ * ```
70
+ */
71
+ context: TContext | null;
54
72
  /**
55
73
  * Creates a new WebRtcManager instance.
56
74
  * @param factory - Factory object providing WebRTC primitives (peer connection, media, devices).
@@ -88,7 +88,26 @@ export class WebRtcManager {
88
88
  #remoteStream = null;
89
89
  #dataChannels = new Map();
90
90
  #reconnectAttempts = 0;
91
+ /**
92
+ * User-defined context object for storing arbitrary data associated with this manager.
93
+ * Useful for attaching application-specific state (e.g., audio streams, metadata)
94
+ * without modifying the manager internals.
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * // With type parameter for full type safety:
99
+ * const manager = new WebRtcManager<{ audioStream: MediaStream; sessionId: string }>(factory);
100
+ * manager.context = { audioStream: myStream, sessionId: '123' };
101
+ * manager.context.audioStream; // typed as MediaStream
102
+ *
103
+ * // Without type parameter (backwards compatible):
104
+ * const manager = new WebRtcManager(factory);
105
+ * manager.context = { anything: 'goes' };
106
+ * ```
107
+ */
108
+ context = null;
91
109
  #reconnectTimer = null;
110
+ #fullReconnectTimeoutTimer = null;
92
111
  #deviceChangeHandler = null;
93
112
  /**
94
113
  * Creates a new WebRtcManager instance.
@@ -705,8 +724,12 @@ export class WebRtcManager {
705
724
  const state = this.#pc.connectionState;
706
725
  this.#debug("Connection state changed:", state);
707
726
  if (state === "connected") {
708
- // Connection successful - reset reconnect attempts
727
+ // Connection successful - reset reconnect attempts and clear any pending timeout
709
728
  this.#reconnectAttempts = 0;
729
+ if (this.#fullReconnectTimeoutTimer !== null) {
730
+ clearTimeout(this.#fullReconnectTimeoutTimer);
731
+ this.#fullReconnectTimeoutTimer = null;
732
+ }
710
733
  this.#dispatch(WebRtcFsmEvent.CONNECTED);
711
734
  }
712
735
  else if (state === "failed") {
@@ -747,6 +770,11 @@ export class WebRtcManager {
747
770
  clearTimeout(this.#reconnectTimer);
748
771
  this.#reconnectTimer = null;
749
772
  }
773
+ // Clear any pending full reconnection timeout
774
+ if (this.#fullReconnectTimeoutTimer !== null) {
775
+ clearTimeout(this.#fullReconnectTimeoutTimer);
776
+ this.#fullReconnectTimeoutTimer = null;
777
+ }
750
778
  // Remove device change listener
751
779
  if (this.#deviceChangeHandler) {
752
780
  navigator.mediaDevices.removeEventListener("devicechange", this.#deviceChangeHandler);
@@ -857,7 +885,17 @@ export class WebRtcManager {
857
885
  this.#cleanup();
858
886
  this.#dispatch(WebRtcFsmEvent.RESET);
859
887
  await this.connect();
860
- // If successful, onconnectionstatechange will reset attempts
888
+ // Start timeout for full reconnection - if connection doesn't succeed
889
+ // within the timeout, treat it as a failure
890
+ const timeout = this.#config.fullReconnectTimeout ?? 30000;
891
+ this.#fullReconnectTimeoutTimer = setTimeout(() => {
892
+ this.#fullReconnectTimeoutTimer = null;
893
+ // Only trigger failure if still not connected
894
+ if (this.state !== WebRtcState.CONNECTED) {
895
+ this.#debug("Full reconnection timeout reached, connection not established");
896
+ this.#handleConnectionFailure();
897
+ }
898
+ }, timeout);
861
899
  }
862
900
  catch (e) {
863
901
  this.#logger.error("[WebRtcManager] Reconnection failed:", e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/webrtc",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "main": "dist/mod.js",
6
6
  "types": "dist/mod.d.ts",