@afterrealism/dendri-client 2.3.7 → 2.4.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.
@@ -133,7 +133,10 @@ declare enum ServerMessageType {
133
133
  RoomPeers = "ROOM-PEERS",// List of peers in a room.
134
134
  HostMigrate = "HOST-MIGRATE",// Host migration notification.
135
135
  PresenceUpdate = "PRESENCE-UPDATE",// Presence data broadcast.
136
- KeyExchange = "KEY-EXCHANGE"
136
+ KeyExchange = "KEY-EXCHANGE",// E2E encryption key exchange for relay.
137
+ ConnectRequest = "CONNECT-REQUEST",// Address exchange for Connection Reversal (H1)
138
+ DcutrConnect = "DCUTR-CONNECT",// DCUtR hole punch address exchange (H2)
139
+ DcutrSync = "DCUTR-SYNC"
137
140
  }
138
141
  declare enum TransportMode {
139
142
  WebRTC = "webrtc",
@@ -162,6 +165,12 @@ declare enum ConnectionQuality {
162
165
  Poor = "poor",
163
166
  Unknown = "unknown"
164
167
  }
168
+ declare enum TopicClass {
169
+ /** Reliable, persisted, replayed — data integrity critical (Yjs updates, RPC) */
170
+ Persistent = "persistent",
171
+ /** Best-effort, not replayed — ephemeral UX state (cursor, scroll, typing indicators) */
172
+ Ephemeral = "ephemeral"
173
+ }
165
174
 
166
175
  interface ServerMessage {
167
176
  type: ServerMessageType;
@@ -262,6 +271,7 @@ declare abstract class DataConnection extends BaseConnection<DataConnectionEvent
262
271
  constructor(peerId: string, provider: Dendri, options: any);
263
272
  /** Called by the Negotiator when the DataChannel is ready. */
264
273
  _initializeDataChannel(dc: RTCDataChannel): void;
274
+ private _applyAdaptiveBuffer;
265
275
  /**
266
276
  * Exposed functionality for users.
267
277
  */
@@ -325,7 +335,7 @@ interface DendriOption {
325
335
  config?: RTCConfiguration;
326
336
  debug?: number;
327
337
  referrerPolicy?: ReferrerPolicy;
328
- /** Auto-fetch TURN credentials from the signaling server's GET /turn endpoint. */
338
+ /** Auto-fetch TURN credentials from the signaling server's GET /{key}/turn-credentials endpoint. */
329
339
  fetchTurnCredentials?: boolean;
330
340
  /** Optional JWT for authenticated connections. */
331
341
  jwt?: string;
@@ -515,6 +525,9 @@ declare class HybridConnection extends EventEmitter<HybridConnectionEvents> {
515
525
  * Respects `autoUpgrade`, `upgradeInterval`, and `maxUpgradeAttempts` options.
516
526
  */
517
527
  private _scheduleUpgrade;
528
+ private _tryConnectionReversal;
529
+ private _gatherSrflxCandidates;
530
+ _dcutrHolePunch(): Promise<boolean>;
518
531
  }
519
532
 
520
533
  declare enum LogLevel {
@@ -648,7 +661,7 @@ declare class DendriOptions implements DendriOption {
648
661
  *
649
662
  * Defaults to {@apilink util.defaultConfig}
650
663
  */
651
- config?: any;
664
+ config?: RTCConfiguration;
652
665
  /**
653
666
  * Set to true `true` if you're using TLS.
654
667
  * :::danger
@@ -658,7 +671,7 @@ declare class DendriOptions implements DendriOption {
658
671
  secure?: boolean;
659
672
  pingInterval?: number;
660
673
  referrerPolicy?: ReferrerPolicy;
661
- logFunction?: (logLevel: LogLevel, ...rest: any[]) => void;
674
+ logFunction?: (logLevel: LogLevel, ...rest: unknown[]) => void;
662
675
  serializers?: SerializerMapping;
663
676
  /** Auto-fetch TURN credentials from the signaling server's GET /turn endpoint. */
664
677
  fetchTurnCredentials?: boolean;
@@ -670,6 +683,12 @@ declare class DendriOptions implements DendriOption {
670
683
  validateMetadata?: (metadata: unknown) => boolean;
671
684
  /** Signaling transport: 'websocket' (default), 'sse', 'polling', or 'auto' (tries WS then SSE then polling) */
672
685
  signalingTransport?: "websocket" | "sse" | "polling" | "auto";
686
+ /**
687
+ * ICE candidate privacy policy.
688
+ * - 'all': RFC 8828 mode 1 — all candidates including host IPs (default)
689
+ * - 'public': RFC 8828 mode 3 — only STUN/TURN (srflx + relay) candidates
690
+ */
691
+ ipPolicy?: "all" | "public";
673
692
  }
674
693
 
675
694
  interface SerializerMapping {
@@ -1375,4 +1394,4 @@ interface DendriStore {
1375
1394
  */
1376
1395
  declare function createDendriStore(input?: CreateDendriStoreInput): DendriStore;
1377
1396
 
1378
- export { AckManager as A, BaseConnectionErrorType as B, type CallOption as C, Dendri as D, type RpcResponse as E, SerializationType as F, ServerMessageType as G, HybridConnection as H, SocketEventType as I, type StoreListener as J, TransportMode as K, LogLevel as L, MediaConnection as M, createDendriStore as N, isRpcRequest as O, type PresenceEvents as P, isRpcResponse as Q, Room as R, type SerializerMapping as S, type TransportEvents as T, type CreateDendriStoreInput as U, DataConnection as a, SignalingTransport as b, type AnswerOption as c, ConnectionQuality as d, ConnectionState as e, ConnectionType as f, DataConnectionErrorType as g, type DendriConnectOption as h, DendriError as i, DendriErrorType as j, type DendriEvents as k, type DendriOption as l, DendriOptions as m, type DendriStore as n, type DendriStoreOptions as o, type DendriStoreSnapshot as p, type HybridConnectionOption as q, type HybridSendOptions as r, PresenceManager as s, type RoomEvents as t, type RoomOptions as u, RpcError as v, RpcErrorCode as w, type RpcHandler as x, RpcManager as y, type RpcRequest as z };
1397
+ export { AckManager as A, BaseConnectionErrorType as B, type CallOption as C, Dendri as D, type RpcResponse as E, SerializationType as F, ServerMessageType as G, HybridConnection as H, SocketEventType as I, type StoreListener as J, type TransportEvents as K, LogLevel as L, MediaConnection as M, TransportMode as N, createDendriStore as O, type PresenceEvents as P, isRpcRequest as Q, Room as R, type SerializerMapping as S, TopicClass as T, isRpcResponse as U, type CreateDendriStoreInput as V, DataConnection as a, SignalingTransport as b, type AnswerOption as c, ConnectionQuality as d, ConnectionState as e, ConnectionType as f, DataConnectionErrorType as g, type DendriConnectOption as h, DendriError as i, DendriErrorType as j, type DendriEvents as k, type DendriOption as l, DendriOptions as m, type DendriStore as n, type DendriStoreOptions as o, type DendriStoreSnapshot as p, type HybridConnectionOption as q, type HybridSendOptions as r, PresenceManager as s, type RoomEvents as t, type RoomOptions as u, RpcError as v, RpcErrorCode as w, type RpcHandler as x, RpcManager as y, type RpcRequest as z };
@@ -133,7 +133,10 @@ declare enum ServerMessageType {
133
133
  RoomPeers = "ROOM-PEERS",// List of peers in a room.
134
134
  HostMigrate = "HOST-MIGRATE",// Host migration notification.
135
135
  PresenceUpdate = "PRESENCE-UPDATE",// Presence data broadcast.
136
- KeyExchange = "KEY-EXCHANGE"
136
+ KeyExchange = "KEY-EXCHANGE",// E2E encryption key exchange for relay.
137
+ ConnectRequest = "CONNECT-REQUEST",// Address exchange for Connection Reversal (H1)
138
+ DcutrConnect = "DCUTR-CONNECT",// DCUtR hole punch address exchange (H2)
139
+ DcutrSync = "DCUTR-SYNC"
137
140
  }
138
141
  declare enum TransportMode {
139
142
  WebRTC = "webrtc",
@@ -162,6 +165,12 @@ declare enum ConnectionQuality {
162
165
  Poor = "poor",
163
166
  Unknown = "unknown"
164
167
  }
168
+ declare enum TopicClass {
169
+ /** Reliable, persisted, replayed — data integrity critical (Yjs updates, RPC) */
170
+ Persistent = "persistent",
171
+ /** Best-effort, not replayed — ephemeral UX state (cursor, scroll, typing indicators) */
172
+ Ephemeral = "ephemeral"
173
+ }
165
174
 
166
175
  interface ServerMessage {
167
176
  type: ServerMessageType;
@@ -262,6 +271,7 @@ declare abstract class DataConnection extends BaseConnection<DataConnectionEvent
262
271
  constructor(peerId: string, provider: Dendri, options: any);
263
272
  /** Called by the Negotiator when the DataChannel is ready. */
264
273
  _initializeDataChannel(dc: RTCDataChannel): void;
274
+ private _applyAdaptiveBuffer;
265
275
  /**
266
276
  * Exposed functionality for users.
267
277
  */
@@ -325,7 +335,7 @@ interface DendriOption {
325
335
  config?: RTCConfiguration;
326
336
  debug?: number;
327
337
  referrerPolicy?: ReferrerPolicy;
328
- /** Auto-fetch TURN credentials from the signaling server's GET /turn endpoint. */
338
+ /** Auto-fetch TURN credentials from the signaling server's GET /{key}/turn-credentials endpoint. */
329
339
  fetchTurnCredentials?: boolean;
330
340
  /** Optional JWT for authenticated connections. */
331
341
  jwt?: string;
@@ -515,6 +525,9 @@ declare class HybridConnection extends EventEmitter<HybridConnectionEvents> {
515
525
  * Respects `autoUpgrade`, `upgradeInterval`, and `maxUpgradeAttempts` options.
516
526
  */
517
527
  private _scheduleUpgrade;
528
+ private _tryConnectionReversal;
529
+ private _gatherSrflxCandidates;
530
+ _dcutrHolePunch(): Promise<boolean>;
518
531
  }
519
532
 
520
533
  declare enum LogLevel {
@@ -648,7 +661,7 @@ declare class DendriOptions implements DendriOption {
648
661
  *
649
662
  * Defaults to {@apilink util.defaultConfig}
650
663
  */
651
- config?: any;
664
+ config?: RTCConfiguration;
652
665
  /**
653
666
  * Set to true `true` if you're using TLS.
654
667
  * :::danger
@@ -658,7 +671,7 @@ declare class DendriOptions implements DendriOption {
658
671
  secure?: boolean;
659
672
  pingInterval?: number;
660
673
  referrerPolicy?: ReferrerPolicy;
661
- logFunction?: (logLevel: LogLevel, ...rest: any[]) => void;
674
+ logFunction?: (logLevel: LogLevel, ...rest: unknown[]) => void;
662
675
  serializers?: SerializerMapping;
663
676
  /** Auto-fetch TURN credentials from the signaling server's GET /turn endpoint. */
664
677
  fetchTurnCredentials?: boolean;
@@ -670,6 +683,12 @@ declare class DendriOptions implements DendriOption {
670
683
  validateMetadata?: (metadata: unknown) => boolean;
671
684
  /** Signaling transport: 'websocket' (default), 'sse', 'polling', or 'auto' (tries WS then SSE then polling) */
672
685
  signalingTransport?: "websocket" | "sse" | "polling" | "auto";
686
+ /**
687
+ * ICE candidate privacy policy.
688
+ * - 'all': RFC 8828 mode 1 — all candidates including host IPs (default)
689
+ * - 'public': RFC 8828 mode 3 — only STUN/TURN (srflx + relay) candidates
690
+ */
691
+ ipPolicy?: "all" | "public";
673
692
  }
674
693
 
675
694
  interface SerializerMapping {
@@ -1375,4 +1394,4 @@ interface DendriStore {
1375
1394
  */
1376
1395
  declare function createDendriStore(input?: CreateDendriStoreInput): DendriStore;
1377
1396
 
1378
- export { AckManager as A, BaseConnectionErrorType as B, type CallOption as C, Dendri as D, type RpcResponse as E, SerializationType as F, ServerMessageType as G, HybridConnection as H, SocketEventType as I, type StoreListener as J, TransportMode as K, LogLevel as L, MediaConnection as M, createDendriStore as N, isRpcRequest as O, type PresenceEvents as P, isRpcResponse as Q, Room as R, type SerializerMapping as S, type TransportEvents as T, type CreateDendriStoreInput as U, DataConnection as a, SignalingTransport as b, type AnswerOption as c, ConnectionQuality as d, ConnectionState as e, ConnectionType as f, DataConnectionErrorType as g, type DendriConnectOption as h, DendriError as i, DendriErrorType as j, type DendriEvents as k, type DendriOption as l, DendriOptions as m, type DendriStore as n, type DendriStoreOptions as o, type DendriStoreSnapshot as p, type HybridConnectionOption as q, type HybridSendOptions as r, PresenceManager as s, type RoomEvents as t, type RoomOptions as u, RpcError as v, RpcErrorCode as w, type RpcHandler as x, RpcManager as y, type RpcRequest as z };
1397
+ export { AckManager as A, BaseConnectionErrorType as B, type CallOption as C, Dendri as D, type RpcResponse as E, SerializationType as F, ServerMessageType as G, HybridConnection as H, SocketEventType as I, type StoreListener as J, type TransportEvents as K, LogLevel as L, MediaConnection as M, TransportMode as N, createDendriStore as O, type PresenceEvents as P, isRpcRequest as Q, Room as R, type SerializerMapping as S, TopicClass as T, isRpcResponse as U, type CreateDendriStoreInput as V, DataConnection as a, SignalingTransport as b, type AnswerOption as c, ConnectionQuality as d, ConnectionState as e, ConnectionType as f, DataConnectionErrorType as g, type DendriConnectOption as h, DendriError as i, DendriErrorType as j, type DendriEvents as k, type DendriOption as l, DendriOptions as m, type DendriStore as n, type DendriStoreOptions as o, type DendriStoreSnapshot as p, type HybridConnectionOption as q, type HybridSendOptions as r, PresenceManager as s, type RoomEvents as t, type RoomOptions as u, RpcError as v, RpcErrorCode as w, type RpcHandler as x, RpcManager as y, type RpcRequest as z };
package/dist/store.cjs CHANGED
@@ -3531,7 +3531,7 @@ var Util = class extends BinaryPackChunker {
3531
3531
  var util = new Util();
3532
3532
 
3533
3533
  // src/api.ts
3534
- var version = "2.3.7";
3534
+ var version = "2.4.0";
3535
3535
  var API = class _API {
3536
3536
  constructor(_options) {
3537
3537
  this._options = _options;
@@ -3568,11 +3568,11 @@ var API = class _API {
3568
3568
  throw new Error(`Could not get an ID from the server.${pathError}`);
3569
3569
  }
3570
3570
  }
3571
- /** Fetch TURN credentials from the signaling server's GET /turn endpoint. */
3571
+ /** Fetch TURN credentials from the signaling server's turn-credentials endpoint. */
3572
3572
  async getTurnCredentials() {
3573
3573
  const protocol = this._options.secure ? "https" : "http";
3574
- const { host, port } = this._options;
3575
- const url = `${protocol}://${host}:${port}/turn`;
3574
+ const { host, port, path, key } = this._options;
3575
+ const url = `${protocol}://${host}:${port}${path}${key}/turn-credentials`;
3576
3576
  try {
3577
3577
  const controller = new AbortController();
3578
3578
  const timeoutId = setTimeout(() => controller.abort(), _API.FETCH_TIMEOUT);
@@ -3701,6 +3701,7 @@ var Negotiator = class {
3701
3701
  }
3702
3702
  connection;
3703
3703
  _pendingCandidates = [];
3704
+ _iceCandidateFilter = null;
3704
3705
  /** Returns a PeerConnection object set up correctly (for data, media). */
3705
3706
  startConnection(options) {
3706
3707
  const peerConnection = this._startPeerConnection();
@@ -3723,6 +3724,13 @@ var Negotiator = class {
3723
3724
  _startPeerConnection() {
3724
3725
  logger_default.log("Creating RTCPeerConnection.");
3725
3726
  const peerConnection = new RTCPeerConnection(this.connection.provider?.options.config);
3727
+ if (this.connection.provider?.options.ipPolicy === "public") {
3728
+ const isPublicCandidate = (c) => {
3729
+ const sdp2 = c.candidate ?? "";
3730
+ return !sdp2.includes("typ host");
3731
+ };
3732
+ this._iceCandidateFilter = isPublicCandidate;
3733
+ }
3726
3734
  this._setupListeners(peerConnection);
3727
3735
  return peerConnection;
3728
3736
  }
@@ -3735,6 +3743,9 @@ var Negotiator = class {
3735
3743
  logger_default.log("Listening for ICE candidates.");
3736
3744
  peerConnection.onicecandidate = (evt) => {
3737
3745
  if (!evt.candidate?.candidate) return;
3746
+ if (this._iceCandidateFilter && !this._iceCandidateFilter(evt.candidate)) {
3747
+ return;
3748
+ }
3738
3749
  logger_default.log(`Received ICE candidates for ${peerId}:`, evt.candidate);
3739
3750
  provider.socket.send({
3740
3751
  type: "CANDIDATE" /* Candidate */,
@@ -4073,6 +4084,7 @@ var DataConnection = class _DataConnection extends BaseConnection {
4073
4084
  this.dataChannel.onopen = () => {
4074
4085
  logger_default.log(`DC#${this.connectionId} dc connection success`);
4075
4086
  this._open = true;
4087
+ this._applyAdaptiveBuffer(dc);
4076
4088
  this.emit("open");
4077
4089
  };
4078
4090
  this.dataChannel.onclose = () => {
@@ -4080,6 +4092,24 @@ var DataConnection = class _DataConnection extends BaseConnection {
4080
4092
  this.close();
4081
4093
  };
4082
4094
  }
4095
+ _applyAdaptiveBuffer(dc) {
4096
+ const pc = this.peerConnection;
4097
+ if (!pc || typeof pc.getStats !== "function") return;
4098
+ pc.getStats().then((stats) => {
4099
+ let rtt = null;
4100
+ stats.forEach((report) => {
4101
+ if (report.type === "candidate-pair" && report.state === "succeeded" && report.currentRoundTripTime) {
4102
+ rtt = report.currentRoundTripTime * 1e3;
4103
+ }
4104
+ });
4105
+ if (rtt !== null) {
4106
+ const bdp = 12.5 * 1024 * 1024 * (rtt / 1e3);
4107
+ const optimal = Math.max(1 * 1024 * 1024, Math.min(32 * 1024 * 1024, Math.ceil(bdp)));
4108
+ dc.bufferedAmountLowThreshold = optimal;
4109
+ }
4110
+ }).catch(() => {
4111
+ });
4112
+ }
4083
4113
  /**
4084
4114
  * Exposed functionality for users.
4085
4115
  */
@@ -4614,7 +4644,17 @@ var HybridConnection = class extends import_eventemitter32.EventEmitter {
4614
4644
  if (this._closed) {
4615
4645
  return;
4616
4646
  }
4617
- this._attemptWebRTC();
4647
+ logger_default.log(
4648
+ `HybridConnection: start peer=${this.peer} iceTimeout=${this._options.iceTimeout ?? 1e4}ms encryptRelay=${this._encryptRelay}`
4649
+ );
4650
+ if (typeof this._provider?.on !== "function") {
4651
+ this._attemptWebRTC();
4652
+ return;
4653
+ }
4654
+ this._tryConnectionReversal().then((direct) => {
4655
+ if (direct) return;
4656
+ this._attemptWebRTC();
4657
+ });
4618
4658
  }
4619
4659
  /** Send data through the best available transport, optionally tagged with a topic. */
4620
4660
  send(data, options) {
@@ -4904,10 +4944,16 @@ var HybridConnection = class extends import_eventemitter32.EventEmitter {
4904
4944
  this._dataConnection = dc;
4905
4945
  this._iceTimer = setTimeout(() => {
4906
4946
  if (this._mode !== "webrtc" /* WebRTC */) {
4947
+ logger_default.warn(
4948
+ `HybridConnection: ICE timeout after ${iceTimeout}ms for ${this.peer}, falling back to relay`
4949
+ );
4907
4950
  this._fallbackToRelay();
4908
4951
  }
4909
4952
  }, iceTimeout);
4910
4953
  this._dataConnection.on("open", () => {
4954
+ logger_default.log(
4955
+ `HybridConnection: WebRTC opened to ${this.peer} (attempt ${this._upgradeAttempts + 1})`
4956
+ );
4911
4957
  this._clearIceTimer();
4912
4958
  this._clearUpgradeTimer();
4913
4959
  this._upgradeAttempts = 0;
@@ -4948,6 +4994,7 @@ var HybridConnection = class extends import_eventemitter32.EventEmitter {
4948
4994
  /** Update the transport mode and emit if changed. */
4949
4995
  _setMode(mode) {
4950
4996
  if (this._mode !== mode) {
4997
+ logger_default.log(`HybridConnection: transport ${this._mode} -> ${mode} for ${this.peer}`);
4951
4998
  this._mode = mode;
4952
4999
  this.emit("transportChanged", mode);
4953
5000
  }
@@ -4988,6 +5035,117 @@ var HybridConnection = class extends import_eventemitter32.EventEmitter {
4988
5035
  this._attemptWebRTC();
4989
5036
  }, interval);
4990
5037
  }
5038
+ async _tryConnectionReversal() {
5039
+ if (typeof this._provider?.on !== "function") return false;
5040
+ try {
5041
+ const resp = await new Promise((resolve, reject) => {
5042
+ const timer = setTimeout(() => reject(new Error("timeout")), 3e3);
5043
+ const handler = (data) => {
5044
+ clearTimeout(timer);
5045
+ this._provider.off("CONNECT-REQUEST" /* ConnectRequest */, handler);
5046
+ resolve(data);
5047
+ };
5048
+ this._provider.on("CONNECT-REQUEST" /* ConnectRequest */, handler);
5049
+ this._provider.socket.send({
5050
+ type: "CONNECT-REQUEST" /* ConnectRequest */,
5051
+ payload: { peer: this.peer }
5052
+ });
5053
+ });
5054
+ const addr = resp?.address;
5055
+ if (!addr) return false;
5056
+ const pc = new RTCPeerConnection(this._provider.options.config);
5057
+ const dc = pc.createDataChannel("probe", { id: 0 });
5058
+ await new Promise((resolve, reject) => {
5059
+ const timer = setTimeout(() => {
5060
+ pc.close();
5061
+ reject(new Error("direct-dial-timeout"));
5062
+ }, 2500);
5063
+ dc.onopen = () => {
5064
+ clearTimeout(timer);
5065
+ resolve();
5066
+ };
5067
+ dc.onerror = () => {
5068
+ clearTimeout(timer);
5069
+ pc.close();
5070
+ reject(new Error("dc-error"));
5071
+ };
5072
+ });
5073
+ this._dataConnection = void 0;
5074
+ this._setMode("webrtc" /* WebRTC */);
5075
+ pc.close();
5076
+ return true;
5077
+ } catch {
5078
+ return false;
5079
+ }
5080
+ }
5081
+ async _gatherSrflxCandidates() {
5082
+ const pc = new RTCPeerConnection({ iceServers: this._provider.options.config?.iceServers });
5083
+ pc.createDataChannel("probe");
5084
+ const offer = await pc.createOffer();
5085
+ await pc.setLocalDescription(offer);
5086
+ const candidates = [];
5087
+ await new Promise((resolve) => {
5088
+ const timer = setTimeout(resolve, 2e3);
5089
+ pc.onicecandidate = (evt) => {
5090
+ if (!evt.candidate) {
5091
+ clearTimeout(timer);
5092
+ resolve();
5093
+ return;
5094
+ }
5095
+ if (!evt.candidate.candidate.includes("typ host")) {
5096
+ candidates.push(evt.candidate.candidate);
5097
+ }
5098
+ };
5099
+ });
5100
+ pc.close();
5101
+ return candidates;
5102
+ }
5103
+ async _dcutrHolePunch() {
5104
+ try {
5105
+ const localAddrs = await this._gatherSrflxCandidates();
5106
+ const t0 = performance.now();
5107
+ const peerAddrs = await new Promise((resolve, reject) => {
5108
+ const timer = setTimeout(() => reject(new Error("dcutr-timeout")), 8e3);
5109
+ const handler = (data) => {
5110
+ clearTimeout(timer);
5111
+ this._provider.off("DCUTR-CONNECT" /* DcutrConnect */, handler);
5112
+ resolve(data?.addresses ?? []);
5113
+ };
5114
+ this._provider.on("DCUTR-CONNECT" /* DcutrConnect */, handler);
5115
+ this._provider.socket.send({
5116
+ type: "DCUTR-CONNECT" /* DcutrConnect */,
5117
+ payload: { addresses: localAddrs }
5118
+ });
5119
+ });
5120
+ const relayRtt = performance.now() - t0;
5121
+ this._provider.socket.send({ type: "DCUTR-SYNC" /* DcutrSync */, payload: {} });
5122
+ await new Promise((r) => setTimeout(r, relayRtt / 2));
5123
+ for (let i = 0; i < Math.min(peerAddrs.length, 4); i++) {
5124
+ try {
5125
+ const pc = new RTCPeerConnection(this._provider.options.config);
5126
+ await new Promise((resolve, reject) => {
5127
+ const timer = setTimeout(() => {
5128
+ pc.close();
5129
+ reject(new Error("dc-dial-timeout"));
5130
+ }, 5e3);
5131
+ const dc = pc.createDataChannel("dcutr");
5132
+ dc.onopen = () => {
5133
+ clearTimeout(timer);
5134
+ resolve();
5135
+ };
5136
+ });
5137
+ this._dataConnection = void 0;
5138
+ this._setMode("webrtc" /* WebRTC */);
5139
+ pc.close();
5140
+ return true;
5141
+ } catch {
5142
+ }
5143
+ }
5144
+ return false;
5145
+ } catch {
5146
+ return false;
5147
+ }
5148
+ }
4991
5149
  };
4992
5150
 
4993
5151
  // src/mediaconnection.ts
@@ -5284,7 +5442,7 @@ var PollingTransport = class _PollingTransport extends SignalingTransport {
5284
5442
  };
5285
5443
 
5286
5444
  // src/socket.ts
5287
- var version2 = "2.3.7";
5445
+ var version2 = "2.4.0";
5288
5446
  var Socket = class _Socket extends SignalingTransport {
5289
5447
  constructor(secure, host, port, path, key, pingInterval = 5e3, jwt) {
5290
5448
  super();
@@ -5326,7 +5484,7 @@ var Socket = class _Socket extends SignalingTransport {
5326
5484
  if (this._jwt) {
5327
5485
  wsUrl += `&jwt=${encodeURIComponent(this._jwt)}`;
5328
5486
  }
5329
- if (!!this._socket || !this._disconnected) {
5487
+ if (this._socket || !this._disconnected) {
5330
5488
  return;
5331
5489
  }
5332
5490
  this._socket = new WebSocket(`${wsUrl}&version=${version2}`);
@@ -5917,7 +6075,7 @@ var Dendri = class _Dendri extends EventEmitterWithError {
5917
6075
  return;
5918
6076
  }
5919
6077
  }
5920
- if (!!userId && !util.validateId(userId)) {
6078
+ if (userId && !util.validateId(userId)) {
5921
6079
  this._delayedAbort("invalid-id" /* InvalidID */, `ID "${userId}" is invalid`);
5922
6080
  return;
5923
6081
  }
@@ -6830,7 +6988,13 @@ var Room = class extends import_eventemitter35.EventEmitter {
6830
6988
  sentViaWebRTC = true;
6831
6989
  }
6832
6990
  }
6833
- if (!sentViaWebRTC && this._peer?.socket) {
6991
+ if (this._dendriOptions?.enableRelay && this._peer?.socket) {
6992
+ this._peer.socket.send({
6993
+ type: "DATA" /* Data */,
6994
+ room: this._roomId,
6995
+ payload: wire
6996
+ });
6997
+ } else if (!sentViaWebRTC && this._peer?.socket) {
6834
6998
  this._peer.socket.send({
6835
6999
  type: "DATA" /* Data */,
6836
7000
  room: this._roomId,
@@ -6864,7 +7028,13 @@ var Room = class extends import_eventemitter35.EventEmitter {
6864
7028
  sentViaWebRTC = true;
6865
7029
  }
6866
7030
  }
6867
- if (!sentViaWebRTC && this._peer?.socket) {
7031
+ if (this._dendriOptions?.enableRelay && this._peer?.socket) {
7032
+ this._peer.socket.send({
7033
+ type: "DATA" /* Data */,
7034
+ room: this._roomId,
7035
+ payload: wire
7036
+ });
7037
+ } else if (!sentViaWebRTC && this._peer?.socket) {
6868
7038
  this._peer.socket.send({
6869
7039
  type: "DATA" /* Data */,
6870
7040
  room: this._roomId,
@@ -7055,8 +7225,8 @@ var Room = class extends import_eventemitter35.EventEmitter {
7055
7225
  const remotePeerId = conn.peer;
7056
7226
  this._connections.set(remotePeerId, conn);
7057
7227
  this._knownPeers.add(remotePeerId);
7228
+ this.emit("peerJoined", remotePeerId);
7058
7229
  conn.on("open", () => {
7059
- this.emit("peerJoined", remotePeerId);
7060
7230
  for (const [peerId, c] of this._connections) {
7061
7231
  if (peerId !== remotePeerId && c.open) {
7062
7232
  c.send({ __room: { type: "peer-joined", peerId: remotePeerId } });
@@ -7378,6 +7548,14 @@ var Room = class extends import_eventemitter35.EventEmitter {
7378
7548
  const conn = this._connections.get(peerId);
7379
7549
  if (conn?.open) {
7380
7550
  conn.send(data);
7551
+ if (this._dendriOptions?.enableRelay && this._peer?.socket) {
7552
+ this._peer.socket.send({
7553
+ type: "DATA" /* Data */,
7554
+ dst: peerId,
7555
+ room: this._roomId,
7556
+ payload: data
7557
+ });
7558
+ }
7381
7559
  return;
7382
7560
  }
7383
7561
  if (!this._isHost && this._hostId) {