@marianmeres/webrtc 1.0.2 → 1.1.0

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
@@ -4,7 +4,7 @@
4
4
 
5
5
  ```yaml
6
6
  name: "@marianmeres/webrtc"
7
- version: "0.0.2"
7
+ version: "1.0.2"
8
8
  license: MIT
9
9
  author: Marian Meres
10
10
  repository: https://github.com/marianmeres/webrtc
@@ -27,12 +27,12 @@ A lightweight, framework-agnostic WebRTC manager providing:
27
27
 
28
28
  ```yaml
29
29
  production:
30
- - "@marianmeres/fsm": "^2.3.0"
31
- - "@marianmeres/pubsub": "^2.4.0"
30
+ - "@marianmeres/fsm": "^2.11.0"
31
+ - "@marianmeres/pubsub": "^2.4.4"
32
32
  development:
33
- - "@std/assert": testing
34
- - "@std/fs": file operations
35
- - "@std/path": path utilities
33
+ - "@std/assert": "^1.0.16"
34
+ - "@std/fs": "^1.0.20"
35
+ - "@std/path": "^1.1.3"
36
36
  ```
37
37
 
38
38
  ## File Structure
@@ -123,10 +123,10 @@ Console-compatible logger interface for custom logging implementations.
123
123
 
124
124
  ```typescript
125
125
  interface Logger {
126
- debug: (...args: any[]) => string;
127
- log: (...args: any[]) => string;
128
- warn: (...args: any[]) => string;
129
- error: (...args: any[]) => string;
126
+ debug: (...args: any[]) => any;
127
+ log: (...args: any[]) => any;
128
+ warn: (...args: any[]) => any;
129
+ error: (...args: any[]) => any;
130
130
  }
131
131
  ```
132
132
 
package/API.md CHANGED
@@ -687,10 +687,10 @@ Console-compatible logger interface for custom logging implementations.
687
687
 
688
688
  ```typescript
689
689
  interface Logger {
690
- debug: (...args: any[]) => string;
691
- log: (...args: any[]) => string;
692
- warn: (...args: any[]) => string;
693
- error: (...args: any[]) => string;
690
+ debug: (...args: any[]) => any;
691
+ log: (...args: any[]) => any;
692
+ warn: (...args: any[]) => any;
693
+ error: (...args: any[]) => any;
694
694
  }
695
695
  ```
696
696
 
@@ -759,6 +759,13 @@ interface WebRtcManagerConfig {
759
759
  /** Initial reconnection delay in ms (doubles each attempt). Default: 1000 */
760
760
  reconnectDelay?: number;
761
761
 
762
+ /** Callback to control whether reconnection should proceed */
763
+ shouldReconnect?: (context: {
764
+ attempt: number;
765
+ maxAttempts: number;
766
+ strategy: "ice-restart" | "full";
767
+ }) => boolean;
768
+
762
769
  /** Enable debug logging. Default: false */
763
770
  debug?: boolean;
764
771
 
@@ -947,3 +954,31 @@ manager.on('reconnecting', ({ attempt, strategy }) => {
947
954
  }
948
955
  });
949
956
  ```
957
+
958
+ ### Conditional Reconnection
959
+
960
+ Use the `shouldReconnect` callback to suppress reconnection when the peer disconnected intentionally:
961
+
962
+ ```typescript
963
+ let peerLeftIntentionally = false;
964
+
965
+ const manager = new WebRtcManager(factory, {
966
+ autoReconnect: true,
967
+ shouldReconnect: ({ attempt, maxAttempts, strategy }) => {
968
+ // Return false to suppress reconnection
969
+ return !peerLeftIntentionally;
970
+ },
971
+ });
972
+
973
+ // Track intentional disconnects via data channel
974
+ manager.on('data_channel_message', ({ data }) => {
975
+ if (JSON.parse(data).type === 'bye') {
976
+ peerLeftIntentionally = true;
977
+ }
978
+ });
979
+ ```
980
+
981
+ The callback receives:
982
+ - `attempt`: Current attempt number (1-based)
983
+ - `maxAttempts`: Configured maximum attempts
984
+ - `strategy`: `"ice-restart"` or `"full"`
package/README.md CHANGED
@@ -51,6 +51,7 @@ const manager = new WebRtcManager(factory, config);
51
51
  - `autoReconnect`: Enable automatic reconnection (default: false)
52
52
  - `maxReconnectAttempts`: Max reconnection attempts (default: 5)
53
53
  - `reconnectDelay`: Initial reconnection delay in ms (default: 1000)
54
+ - `shouldReconnect`: Callback to control whether reconnection should proceed (see below)
54
55
  - `debug`: Enable debug logging (default: false)
55
56
  - `logger`: Custom logger instance implementing `Logger` interface (default: console)
56
57
 
@@ -129,6 +130,37 @@ const unsub = manager.subscribe((state) => {
129
130
  - `EVENT_MICROPHONE_FAILED`
130
131
  - `EVENT_ERROR`
131
132
 
133
+ ### Controlling Reconnection
134
+
135
+ When `autoReconnect` is enabled, you can use the `shouldReconnect` callback to conditionally suppress reconnection attempts. This is useful when the remote peer disconnected intentionally (e.g., left the call) rather than due to network failure.
136
+
137
+ ```typescript
138
+ let peerLeftIntentionally = false;
139
+
140
+ const manager = new WebRtcManager(factory, {
141
+ autoReconnect: true,
142
+ shouldReconnect: ({ attempt, maxAttempts, strategy }) => {
143
+ if (peerLeftIntentionally) {
144
+ return false; // Don't reconnect
145
+ }
146
+ return true;
147
+ },
148
+ });
149
+
150
+ // Listen for "goodbye" message from peer before they disconnect
151
+ manager.on(WebRtcManager.EVENT_DATA_CHANNEL_MESSAGE, ({ data }) => {
152
+ const msg = JSON.parse(data);
153
+ if (msg.type === 'bye') {
154
+ peerLeftIntentionally = true;
155
+ }
156
+ });
157
+ ```
158
+
159
+ The callback receives:
160
+ - `attempt`: Current reconnection attempt (1-based)
161
+ - `maxAttempts`: Configured maximum attempts
162
+ - `strategy`: `"ice-restart"` (attempts 1-2) or `"full"` (attempts 3+)
163
+
132
164
  ## Examples
133
165
 
134
166
  ### Basic Usage (Vanilla JavaScript)
package/dist/types.d.ts CHANGED
@@ -1,14 +1,18 @@
1
1
  /**
2
- * Console-compatible logger interface.
2
+ * Console-compatible logger interface (see @marianmeres/clog ).
3
3
  * Each method accepts variadic arguments and returns a string representation of the first argument.
4
4
  * This enables patterns like `throw new Error(logger.error("msg"))`.
5
5
  */
6
6
  export interface Logger {
7
- debug: (...args: any[]) => string;
8
- log: (...args: any[]) => string;
9
- warn: (...args: any[]) => string;
10
- error: (...args: any[]) => string;
7
+ debug: (...args: any[]) => any;
8
+ log: (...args: any[]) => any;
9
+ warn: (...args: any[]) => any;
10
+ error: (...args: any[]) => any;
11
11
  }
12
+ /**
13
+ * Configuration options for WebRtcManager.
14
+ * All options are optional with sensible defaults.
15
+ */
12
16
  export interface WebRtcManagerConfig {
13
17
  /** Initial peer configuration (ICE servers, etc.) */
14
18
  peerConfig?: RTCConfiguration;
@@ -22,44 +26,109 @@ export interface WebRtcManagerConfig {
22
26
  maxReconnectAttempts?: number;
23
27
  /** Initial reconnection delay in ms. Doubles with each attempt. Defaults to 1000. */
24
28
  reconnectDelay?: number;
29
+ /**
30
+ * Callback to determine whether reconnection should be attempted.
31
+ * Called before each reconnection attempt when autoReconnect is enabled.
32
+ * Return false to suppress reconnection (e.g., when peer disconnected intentionally).
33
+ * If not provided, reconnection proceeds automatically up to maxReconnectAttempts.
34
+ */
35
+ shouldReconnect?: (context: {
36
+ /** Current reconnection attempt number (1-based) */
37
+ attempt: number;
38
+ /** Maximum configured reconnection attempts */
39
+ maxAttempts: number;
40
+ /** Strategy that will be used: "ice-restart" for first attempts, "full" for later */
41
+ strategy: "ice-restart" | "full";
42
+ }) => boolean;
25
43
  /** Enable debug logging. Defaults to false. */
26
44
  debug?: boolean;
27
45
  /** Custom logger instance. If not provided, falls back to console. */
28
46
  logger?: Logger;
29
47
  }
48
+ /**
49
+ * Factory interface for creating WebRTC primitives.
50
+ * Allows dependency injection of browser APIs for testing and flexibility.
51
+ */
30
52
  export interface WebRtcFactory {
53
+ /**
54
+ * Creates a new RTCPeerConnection instance.
55
+ * @param config - Optional RTCConfiguration for ICE servers, certificates, etc.
56
+ * @returns A new RTCPeerConnection instance.
57
+ */
31
58
  createPeerConnection(config?: RTCConfiguration): RTCPeerConnection;
59
+ /**
60
+ * Requests access to user media (microphone/camera).
61
+ * @param constraints - Media constraints specifying audio/video requirements.
62
+ * @returns Promise resolving to a MediaStream.
63
+ */
32
64
  getUserMedia(constraints: MediaStreamConstraints): Promise<MediaStream>;
65
+ /**
66
+ * Enumerates all available media input and output devices.
67
+ * @returns Promise resolving to an array of MediaDeviceInfo objects.
68
+ */
33
69
  enumerateDevices(): Promise<MediaDeviceInfo[]>;
34
70
  }
71
+ /**
72
+ * Possible states of the WebRTC connection lifecycle.
73
+ * The manager transitions between these states based on connection events.
74
+ */
35
75
  export declare enum WebRtcState {
76
+ /** Initial state, no resources allocated. */
36
77
  IDLE = "IDLE",
78
+ /** Creating peer connection and setting up media tracks. */
37
79
  INITIALIZING = "INITIALIZING",
80
+ /** Performing SDP offer/answer exchange. */
38
81
  CONNECTING = "CONNECTING",
82
+ /** Connection established, communication active. */
39
83
  CONNECTED = "CONNECTED",
84
+ /** Automatic reconnection in progress. */
40
85
  RECONNECTING = "RECONNECTING",
86
+ /** Connection closed, can be restarted. */
41
87
  DISCONNECTED = "DISCONNECTED",
88
+ /** Error state, requires reset() to recover. */
42
89
  ERROR = "ERROR"
43
90
  }
91
+ /**
92
+ * Internal FSM events that trigger state transitions.
93
+ * These events are dispatched internally by the manager methods.
94
+ */
44
95
  export declare enum WebRtcFsmEvent {
96
+ /** Triggers transition from IDLE to INITIALIZING. */
45
97
  INIT = "initialize",
98
+ /** Triggers transition to CONNECTING state. */
46
99
  CONNECT = "connect",
100
+ /** Signals successful connection establishment. */
47
101
  CONNECTED = "connected",
102
+ /** Triggers transition to RECONNECTING state. */
48
103
  RECONNECTING = "reconnecting",
104
+ /** Triggers transition to DISCONNECTED state. */
49
105
  DISCONNECT = "disconnect",
106
+ /** Triggers transition to ERROR state. */
50
107
  ERROR = "error",
108
+ /** Resets the manager to IDLE state. */
51
109
  RESET = "reset"
52
110
  }
111
+ /**
112
+ * Type definitions for all WebRTC manager events and their payloads.
113
+ * Use with the `on()` method to subscribe to specific events.
114
+ */
53
115
  export interface WebRtcEvents {
116
+ /** Emitted when connection state changes. Payload: the new WebRtcState. */
54
117
  state_change: WebRtcState;
118
+ /** Emitted when local media stream changes. Payload: MediaStream or null if disabled. */
55
119
  local_stream: MediaStream | null;
120
+ /** Emitted when remote media stream is received. Payload: MediaStream or null. */
56
121
  remote_stream: MediaStream | null;
122
+ /** Emitted when a data channel opens. Payload: the RTCDataChannel. */
57
123
  data_channel_open: RTCDataChannel;
124
+ /** Emitted when a data channel receives a message. Payload: channel and data. */
58
125
  data_channel_message: {
59
126
  channel: RTCDataChannel;
60
127
  data: any;
61
128
  };
129
+ /** Emitted when a data channel closes. Payload: the RTCDataChannel. */
62
130
  data_channel_close: RTCDataChannel;
131
+ /** Emitted when an ICE candidate is generated. Payload: RTCIceCandidate or null. */
63
132
  ice_candidate: RTCIceCandidate | null;
64
133
  /**
65
134
  * Emitted when reconnection is being attempted.
@@ -71,13 +140,17 @@ export interface WebRtcEvents {
71
140
  attempt: number;
72
141
  strategy: "ice-restart" | "full";
73
142
  };
143
+ /** Emitted when all reconnection attempts have failed. Payload: total attempts. */
74
144
  reconnect_failed: {
75
145
  attempts: number;
76
146
  };
147
+ /** Emitted when audio devices change. Payload: array of available audio inputs. */
77
148
  device_changed: MediaDeviceInfo[];
149
+ /** Emitted when microphone access or switching fails. */
78
150
  microphone_failed: {
79
151
  error?: any;
80
152
  reason?: string;
81
153
  };
154
+ /** Emitted when an error occurs. Payload: the Error object. */
82
155
  error: Error;
83
156
  }
package/dist/types.js CHANGED
@@ -1,20 +1,42 @@
1
+ /**
2
+ * Possible states of the WebRTC connection lifecycle.
3
+ * The manager transitions between these states based on connection events.
4
+ */
1
5
  export var WebRtcState;
2
6
  (function (WebRtcState) {
7
+ /** Initial state, no resources allocated. */
3
8
  WebRtcState["IDLE"] = "IDLE";
9
+ /** Creating peer connection and setting up media tracks. */
4
10
  WebRtcState["INITIALIZING"] = "INITIALIZING";
11
+ /** Performing SDP offer/answer exchange. */
5
12
  WebRtcState["CONNECTING"] = "CONNECTING";
13
+ /** Connection established, communication active. */
6
14
  WebRtcState["CONNECTED"] = "CONNECTED";
15
+ /** Automatic reconnection in progress. */
7
16
  WebRtcState["RECONNECTING"] = "RECONNECTING";
17
+ /** Connection closed, can be restarted. */
8
18
  WebRtcState["DISCONNECTED"] = "DISCONNECTED";
19
+ /** Error state, requires reset() to recover. */
9
20
  WebRtcState["ERROR"] = "ERROR";
10
21
  })(WebRtcState || (WebRtcState = {}));
22
+ /**
23
+ * Internal FSM events that trigger state transitions.
24
+ * These events are dispatched internally by the manager methods.
25
+ */
11
26
  export var WebRtcFsmEvent;
12
27
  (function (WebRtcFsmEvent) {
28
+ /** Triggers transition from IDLE to INITIALIZING. */
13
29
  WebRtcFsmEvent["INIT"] = "initialize";
30
+ /** Triggers transition to CONNECTING state. */
14
31
  WebRtcFsmEvent["CONNECT"] = "connect";
32
+ /** Signals successful connection establishment. */
15
33
  WebRtcFsmEvent["CONNECTED"] = "connected";
34
+ /** Triggers transition to RECONNECTING state. */
16
35
  WebRtcFsmEvent["RECONNECTING"] = "reconnecting";
36
+ /** Triggers transition to DISCONNECTED state. */
17
37
  WebRtcFsmEvent["DISCONNECT"] = "disconnect";
38
+ /** Triggers transition to ERROR state. */
18
39
  WebRtcFsmEvent["ERROR"] = "error";
40
+ /** Resets the manager to IDLE state. */
19
41
  WebRtcFsmEvent["RESET"] = "reset";
20
42
  })(WebRtcFsmEvent || (WebRtcFsmEvent = {}));
@@ -2,22 +2,26 @@ import { FSM } from "@marianmeres/fsm";
2
2
  import { PubSub } from "@marianmeres/pubsub";
3
3
  import { WebRtcState, WebRtcFsmEvent, } from "./types.js";
4
4
  /**
5
- * Default console-based logger that wraps console methods to satisfy the Logger interface.
5
+ * Default console-based logger that wraps console methods.
6
6
  * Returns string representation of the first argument for chaining.
7
7
  */
8
8
  const createDefaultLogger = () => ({
9
+ // deno-lint-ignore no-explicit-any
9
10
  debug: (...args) => {
10
11
  console.debug(...args);
11
12
  return String(args[0] ?? "");
12
13
  },
14
+ // deno-lint-ignore no-explicit-any
13
15
  log: (...args) => {
14
16
  console.log(...args);
15
17
  return String(args[0] ?? "");
16
18
  },
19
+ // deno-lint-ignore no-explicit-any
17
20
  warn: (...args) => {
18
21
  console.warn(...args);
19
22
  return String(args[0] ?? "");
20
23
  },
24
+ // deno-lint-ignore no-explicit-any
21
25
  error: (...args) => {
22
26
  console.error(...args);
23
27
  return String(args[0] ?? "");
@@ -173,6 +177,7 @@ export class WebRtcManager {
173
177
  * @param handler - Callback function that receives the event data.
174
178
  * @returns Unsubscribe function to remove the event listener.
175
179
  */
180
+ // deno-lint-ignore no-explicit-any
176
181
  on(event, handler) {
177
182
  return this.#pubsub.subscribe(event, handler);
178
183
  }
@@ -387,7 +392,9 @@ export class WebRtcManager {
387
392
  }
388
393
  catch (e) {
389
394
  this.#logger.error("[WebRtcManager] Failed to get user media:", e);
390
- this.#pubsub.publish(WebRtcManager.EVENT_MICROPHONE_FAILED, { error: e });
395
+ this.#pubsub.publish(WebRtcManager.EVENT_MICROPHONE_FAILED, {
396
+ error: e,
397
+ });
391
398
  return false;
392
399
  }
393
400
  }
@@ -678,11 +685,13 @@ export class WebRtcManager {
678
685
  this.#pubsub.publish(WebRtcManager.EVENT_STATE_CHANGE, newState);
679
686
  }
680
687
  }
688
+ // deno-lint-ignore no-explicit-any
681
689
  #debug(...args) {
682
690
  if (this.#config.debug) {
683
691
  this.#logger.debug("[WebRtcManager]", ...args);
684
692
  }
685
693
  }
694
+ // deno-lint-ignore no-explicit-any
686
695
  #error(error) {
687
696
  this.#logger.error("[WebRtcManager]", error);
688
697
  this.#dispatch(WebRtcFsmEvent.ERROR);
@@ -705,7 +714,12 @@ export class WebRtcManager {
705
714
  this.#handleConnectionFailure();
706
715
  }
707
716
  else if (state === "disconnected" || state === "closed") {
708
- this.#dispatch(WebRtcFsmEvent.DISCONNECT);
717
+ // Only dispatch if not already in a terminal state
718
+ if (this.state !== WebRtcState.DISCONNECTED &&
719
+ this.state !== WebRtcState.ERROR &&
720
+ this.state !== WebRtcState.IDLE) {
721
+ this.#dispatch(WebRtcFsmEvent.DISCONNECT);
722
+ }
709
723
  }
710
724
  };
711
725
  this.#pc.ontrack = (event) => {
@@ -766,7 +780,12 @@ export class WebRtcManager {
766
780
  }
767
781
  #handleConnectionFailure() {
768
782
  this.#debug("Handling connection failure");
769
- this.#dispatch(WebRtcFsmEvent.DISCONNECT);
783
+ // Only dispatch DISCONNECT if not already in a terminal state
784
+ if (this.state !== WebRtcState.DISCONNECTED &&
785
+ this.state !== WebRtcState.ERROR &&
786
+ this.state !== WebRtcState.IDLE) {
787
+ this.#dispatch(WebRtcFsmEvent.DISCONNECT);
788
+ }
770
789
  // Check if auto-reconnect is enabled
771
790
  if (!this.#config.autoReconnect) {
772
791
  this.#debug("Auto-reconnect disabled, not attempting reconnection");
@@ -781,6 +800,21 @@ export class WebRtcManager {
781
800
  });
782
801
  return;
783
802
  }
803
+ // Determine strategy for this attempt (next attempt number is current + 1)
804
+ const nextAttempt = this.#reconnectAttempts + 1;
805
+ const strategy = nextAttempt <= 2 ? "ice-restart" : "full";
806
+ // Check shouldReconnect callback if provided
807
+ if (this.#config.shouldReconnect) {
808
+ const shouldProceed = this.#config.shouldReconnect?.({
809
+ attempt: nextAttempt,
810
+ maxAttempts,
811
+ strategy,
812
+ });
813
+ if (!shouldProceed) {
814
+ this.#debug("Reconnection suppressed by shouldReconnect callback");
815
+ return;
816
+ }
817
+ }
784
818
  // Transition to RECONNECTING state
785
819
  this.#dispatch(WebRtcFsmEvent.RECONNECTING);
786
820
  // Attempt reconnection with exponential backoff
@@ -863,6 +897,7 @@ export class WebRtcManager {
863
897
  this.#pubsub.publish(WebRtcManager.EVENT_DATA_CHANNEL_CLOSE, dc);
864
898
  this.#dataChannels.delete(dc.label);
865
899
  };
900
+ // deno-lint-ignore no-explicit-any
866
901
  dc.onerror = (error) => {
867
902
  // Ignore "User-Initiated Abort" errors which occur during intentional close()
868
903
  const isUserAbort = error?.error?.message?.includes("User-Initiated Abort");
package/package.json CHANGED
@@ -1,14 +1,20 @@
1
1
  {
2
2
  "name": "@marianmeres/webrtc",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/mod.js",
6
6
  "types": "dist/mod.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/mod.d.ts",
10
+ "import": "./dist/mod.js"
11
+ }
12
+ },
7
13
  "author": "Marian Meres",
8
14
  "license": "MIT",
9
15
  "dependencies": {
10
- "@marianmeres/fsm": "^2.6.4",
11
- "@marianmeres/pubsub": "^2.4.3"
16
+ "@marianmeres/fsm": "^2.11.0",
17
+ "@marianmeres/pubsub": "^2.4.4"
12
18
  },
13
19
  "repository": {
14
20
  "type": "git",