@marianmeres/webrtc 1.2.6 → 1.3.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
@@ -159,6 +159,15 @@ interface WebRtcManagerConfig {
159
159
  }
160
160
  ```
161
161
 
162
+ ### GatherIceCandidatesOptions Interface
163
+
164
+ ```typescript
165
+ interface GatherIceCandidatesOptions {
166
+ timeout?: number; // Timeout in ms (default: 10000)
167
+ onCandidate?: (candidate: RTCIceCandidate | null) => void; // Called for each candidate
168
+ }
169
+ ```
170
+
162
171
  ### Properties (Getters)
163
172
 
164
173
  | Property | Type | Description |
@@ -205,6 +214,7 @@ interface WebRtcManagerConfig {
205
214
  | setRemoteDescription | `(description: RTCSessionDescriptionInit): Promise<boolean>` | Set remote SDP |
206
215
  | addIceCandidate | `(candidate: RTCIceCandidateInit \| null): Promise<boolean>` | Add ICE candidate |
207
216
  | iceRestart | `(): Promise<boolean>` | Perform ICE restart |
217
+ | gatherIceCandidates | `(options?: GatherIceCandidatesOptions): Promise<void>` | Wait for ICE gathering to complete |
208
218
  | getLocalDescription | `(): RTCSessionDescription \| null` | Get local SDP |
209
219
  | getRemoteDescription | `(): RTCSessionDescription \| null` | Get remote SDP |
210
220
  | getStats | `(): Promise<RTCStatsReport \| null>` | Get connection statistics |
package/README.md CHANGED
@@ -95,6 +95,35 @@ await manager.setLocalDescription(offer)
95
95
  await manager.setRemoteDescription(answer)
96
96
  await manager.addIceCandidate(candidate)
97
97
  await manager.iceRestart() // Trigger ICE restart
98
+ await manager.gatherIceCandidates(options) // Wait for ICE gathering to complete
99
+ ```
100
+
101
+ ### gatherIceCandidates(options?)
102
+
103
+ Wait for ICE gathering to complete. Useful for HTTP POST signaling patterns where you need all ICE candidates bundled in the local description before sending to the server.
104
+
105
+ **Options:** `timeout` (ms, default 10000), `onCandidate` (callback for each candidate)
106
+
107
+ ```typescript
108
+ const offer = await manager.createOffer();
109
+ await manager.setLocalDescription(offer);
110
+ await manager.gatherIceCandidates({ timeout: 5000 });
111
+ // Now manager.peerConnection.localDescription has all ICE candidates bundled
112
+ ```
113
+
114
+ **Error Handling:** A timeout rejection does *not* transition the FSM to ERROR state. This is intentional - `gatherIceCandidates()` is a utility method, and a timeout means "gathering didn't complete in time", not "the connection failed". The consumer decides how to handle it:
115
+
116
+ ```typescript
117
+ try {
118
+ await manager.gatherIceCandidates({ timeout: 5000 });
119
+ } catch (e) {
120
+ if (e.message === "ICE gathering timeout") {
121
+ // Options:
122
+ // 1. Retry with longer timeout
123
+ // 2. Proceed anyway - localDescription may have partial candidates
124
+ // 3. Treat as fatal: manager.reset()
125
+ }
126
+ }
98
127
  ```
99
128
 
100
129
  ### Data Channel Methods
package/dist/types.d.ts CHANGED
@@ -156,3 +156,13 @@ export interface WebRtcEvents {
156
156
  /** Emitted when an error occurs. Payload: the Error object. */
157
157
  error: Error;
158
158
  }
159
+ /**
160
+ * Options for waiting for ICE gathering to complete.
161
+ * Used with gatherIceCandidates() for HTTP POST signaling patterns.
162
+ */
163
+ export interface GatherIceCandidatesOptions {
164
+ /** Timeout in milliseconds (default: 10000) */
165
+ timeout?: number;
166
+ /** Called for each ICE candidate as it's gathered */
167
+ onCandidate?: (candidate: RTCIceCandidate | null) => void;
168
+ }
@@ -1,4 +1,4 @@
1
- import { type WebRtcFactory, type WebRtcManagerConfig, WebRtcState, type WebRtcEvents } from "./types.js";
1
+ import { type WebRtcFactory, type WebRtcManagerConfig, WebRtcState, type WebRtcEvents, type GatherIceCandidatesOptions } from "./types.js";
2
2
  /**
3
3
  * WebRTC connection manager with FSM-based lifecycle and event-driven architecture.
4
4
  *
@@ -203,6 +203,13 @@ export declare class WebRtcManager<TContext = unknown> {
203
203
  * @returns True if successful, false otherwise.
204
204
  */
205
205
  iceRestart(): Promise<boolean>;
206
+ /**
207
+ * Wait for ICE gathering to complete.
208
+ * Use this for HTTP POST signaling patterns where you need all ICE candidates
209
+ * bundled in the local description before sending to the server.
210
+ * @param options - Optional configuration for timeout and candidate callback.
211
+ */
212
+ gatherIceCandidates(options?: GatherIceCandidatesOptions): Promise<void>;
206
213
  /**
207
214
  * Returns the current local session description.
208
215
  * @returns The local description, or null if not set.
@@ -675,6 +675,52 @@ export class WebRtcManager {
675
675
  return false;
676
676
  }
677
677
  }
678
+ /**
679
+ * Wait for ICE gathering to complete.
680
+ * Use this for HTTP POST signaling patterns where you need all ICE candidates
681
+ * bundled in the local description before sending to the server.
682
+ * @param options - Optional configuration for timeout and candidate callback.
683
+ */
684
+ gatherIceCandidates(options = {}) {
685
+ const { timeout = 10000, onCandidate } = options;
686
+ if (!this.#pc) {
687
+ return Promise.reject(new Error("Peer connection not initialized"));
688
+ }
689
+ const pc = this.#pc;
690
+ if (pc.iceGatheringState === "complete") {
691
+ this.#logDebug("ICE gathering already complete");
692
+ return Promise.resolve();
693
+ }
694
+ this.#logDebug("Waiting for ICE gathering to complete...");
695
+ return new Promise((resolve, reject) => {
696
+ const timer = setTimeout(() => {
697
+ cleanup();
698
+ reject(new Error("ICE gathering timeout"));
699
+ }, timeout);
700
+ const cleanup = () => {
701
+ clearTimeout(timer);
702
+ pc.removeEventListener("icegatheringstatechange", checkState);
703
+ pc.removeEventListener("icecandidate", handleCandidate);
704
+ };
705
+ const checkState = () => {
706
+ if (pc.iceGatheringState === "complete") {
707
+ this.#logDebug("ICE gathering complete (via state change)");
708
+ cleanup();
709
+ resolve();
710
+ }
711
+ };
712
+ const handleCandidate = (event) => {
713
+ onCandidate?.(event.candidate);
714
+ if (event.candidate === null) {
715
+ this.#logDebug("ICE gathering complete (null candidate)");
716
+ cleanup();
717
+ resolve();
718
+ }
719
+ };
720
+ pc.addEventListener("icegatheringstatechange", checkState);
721
+ pc.addEventListener("icecandidate", handleCandidate);
722
+ });
723
+ }
678
724
  /**
679
725
  * Returns the current local session description.
680
726
  * @returns The local description, or null if not set.
@@ -746,13 +792,20 @@ export class WebRtcManager {
746
792
  const state = this.#pc.connectionState;
747
793
  this.#logDebug("Connection state changed:", state);
748
794
  if (state === "connected") {
749
- // Connection successful - reset reconnect attempts and clear any pending timeout
750
- this.#reconnectAttempts = 0;
751
- if (this.#fullReconnectTimeoutTimer !== null) {
752
- clearTimeout(this.#fullReconnectTimeoutTimer);
753
- this.#fullReconnectTimeoutTimer = null;
795
+ // Only dispatch if in CONNECTING state (FSM can handle CONNECTED event)
796
+ // This guards against late connection success after user has disconnected
797
+ if (this.state === WebRtcState.CONNECTING) {
798
+ // Connection successful - reset reconnect attempts and clear any pending timeout
799
+ this.#reconnectAttempts = 0;
800
+ if (this.#fullReconnectTimeoutTimer !== null) {
801
+ clearTimeout(this.#fullReconnectTimeoutTimer);
802
+ this.#fullReconnectTimeoutTimer = null;
803
+ }
804
+ this.#dispatch(WebRtcFsmEvent.CONNECTED);
805
+ }
806
+ else {
807
+ this.#logDebug(`Ignoring late connection success (current state: ${this.state})`);
754
808
  }
755
- this.#dispatch(WebRtcFsmEvent.CONNECTED);
756
809
  }
757
810
  else if (state === "failed") {
758
811
  // Connection failed - attempt reconnection if enabled
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/webrtc",
3
- "version": "1.2.6",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "main": "dist/mod.js",
6
6
  "types": "dist/mod.d.ts",
@@ -13,7 +13,7 @@
13
13
  "author": "Marian Meres",
14
14
  "license": "MIT",
15
15
  "dependencies": {
16
- "@marianmeres/fsm": "^2.11.1",
16
+ "@marianmeres/fsm": "^2.14.0",
17
17
  "@marianmeres/pubsub": "^2.4.4"
18
18
  },
19
19
  "repository": {