@afterrealism/dendri-client 2.3.7 → 2.5.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/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.5.0";
3535
3535
  var API = class _API {
3536
3536
  constructor(_options) {
3537
3537
  this._options = _options;
@@ -3544,6 +3544,7 @@ var API = class _API {
3544
3544
  const url = new URL(`${protocol}://${host}:${port}${path}${key}/${method}`);
3545
3545
  url.searchParams.set("ts", `${Date.now()}${Math.random()}`);
3546
3546
  url.searchParams.set("version", version);
3547
+ if (this._options.apiKey) url.searchParams.set("api_key", this._options.apiKey);
3547
3548
  const controller = new AbortController();
3548
3549
  const timeoutId = setTimeout(() => controller.abort(), _API.FETCH_TIMEOUT);
3549
3550
  return fetch(url.href, {
@@ -3568,15 +3569,16 @@ var API = class _API {
3568
3569
  throw new Error(`Could not get an ID from the server.${pathError}`);
3569
3570
  }
3570
3571
  }
3571
- /** Fetch TURN credentials from the signaling server's GET /turn endpoint. */
3572
+ /** Fetch TURN credentials from the signaling server's turn-credentials endpoint. */
3572
3573
  async getTurnCredentials() {
3573
3574
  const protocol = this._options.secure ? "https" : "http";
3574
- const { host, port } = this._options;
3575
- const url = `${protocol}://${host}:${port}/turn`;
3575
+ const { host, port, path, key } = this._options;
3576
+ const url = new URL(`${protocol}://${host}:${port}${path}${key}/turn-credentials`);
3577
+ if (this._options.apiKey) url.searchParams.set("api_key", this._options.apiKey);
3576
3578
  try {
3577
3579
  const controller = new AbortController();
3578
3580
  const timeoutId = setTimeout(() => controller.abort(), _API.FETCH_TIMEOUT);
3579
- const response = await fetch(url, {
3581
+ const response = await fetch(url.href, {
3580
3582
  referrerPolicy: this._options.referrerPolicy,
3581
3583
  signal: controller.signal
3582
3584
  }).finally(() => clearTimeout(timeoutId));
@@ -3701,6 +3703,7 @@ var Negotiator = class {
3701
3703
  }
3702
3704
  connection;
3703
3705
  _pendingCandidates = [];
3706
+ _iceCandidateFilter = null;
3704
3707
  /** Returns a PeerConnection object set up correctly (for data, media). */
3705
3708
  startConnection(options) {
3706
3709
  const peerConnection = this._startPeerConnection();
@@ -3723,6 +3726,13 @@ var Negotiator = class {
3723
3726
  _startPeerConnection() {
3724
3727
  logger_default.log("Creating RTCPeerConnection.");
3725
3728
  const peerConnection = new RTCPeerConnection(this.connection.provider?.options.config);
3729
+ if (this.connection.provider?.options.ipPolicy === "public") {
3730
+ const isPublicCandidate = (c) => {
3731
+ const sdp2 = c.candidate ?? "";
3732
+ return !sdp2.includes("typ host");
3733
+ };
3734
+ this._iceCandidateFilter = isPublicCandidate;
3735
+ }
3726
3736
  this._setupListeners(peerConnection);
3727
3737
  return peerConnection;
3728
3738
  }
@@ -3735,6 +3745,9 @@ var Negotiator = class {
3735
3745
  logger_default.log("Listening for ICE candidates.");
3736
3746
  peerConnection.onicecandidate = (evt) => {
3737
3747
  if (!evt.candidate?.candidate) return;
3748
+ if (this._iceCandidateFilter && !this._iceCandidateFilter(evt.candidate)) {
3749
+ return;
3750
+ }
3738
3751
  logger_default.log(`Received ICE candidates for ${peerId}:`, evt.candidate);
3739
3752
  provider.socket.send({
3740
3753
  type: "CANDIDATE" /* Candidate */,
@@ -4073,6 +4086,7 @@ var DataConnection = class _DataConnection extends BaseConnection {
4073
4086
  this.dataChannel.onopen = () => {
4074
4087
  logger_default.log(`DC#${this.connectionId} dc connection success`);
4075
4088
  this._open = true;
4089
+ this._applyAdaptiveBuffer(dc);
4076
4090
  this.emit("open");
4077
4091
  };
4078
4092
  this.dataChannel.onclose = () => {
@@ -4080,6 +4094,24 @@ var DataConnection = class _DataConnection extends BaseConnection {
4080
4094
  this.close();
4081
4095
  };
4082
4096
  }
4097
+ _applyAdaptiveBuffer(dc) {
4098
+ const pc = this.peerConnection;
4099
+ if (!pc || typeof pc.getStats !== "function") return;
4100
+ pc.getStats().then((stats) => {
4101
+ let rtt = null;
4102
+ stats.forEach((report) => {
4103
+ if (report.type === "candidate-pair" && report.state === "succeeded" && report.currentRoundTripTime) {
4104
+ rtt = report.currentRoundTripTime * 1e3;
4105
+ }
4106
+ });
4107
+ if (rtt !== null) {
4108
+ const bdp = 12.5 * 1024 * 1024 * (rtt / 1e3);
4109
+ const optimal = Math.max(1 * 1024 * 1024, Math.min(32 * 1024 * 1024, Math.ceil(bdp)));
4110
+ dc.bufferedAmountLowThreshold = optimal;
4111
+ }
4112
+ }).catch(() => {
4113
+ });
4114
+ }
4083
4115
  /**
4084
4116
  * Exposed functionality for users.
4085
4117
  */
@@ -4614,7 +4646,17 @@ var HybridConnection = class extends import_eventemitter32.EventEmitter {
4614
4646
  if (this._closed) {
4615
4647
  return;
4616
4648
  }
4617
- this._attemptWebRTC();
4649
+ logger_default.log(
4650
+ `HybridConnection: start peer=${this.peer} iceTimeout=${this._options.iceTimeout ?? 1e4}ms encryptRelay=${this._encryptRelay}`
4651
+ );
4652
+ if (typeof this._provider?.on !== "function") {
4653
+ this._attemptWebRTC();
4654
+ return;
4655
+ }
4656
+ this._tryConnectionReversal().then((direct) => {
4657
+ if (direct) return;
4658
+ this._attemptWebRTC();
4659
+ });
4618
4660
  }
4619
4661
  /** Send data through the best available transport, optionally tagged with a topic. */
4620
4662
  send(data, options) {
@@ -4904,10 +4946,16 @@ var HybridConnection = class extends import_eventemitter32.EventEmitter {
4904
4946
  this._dataConnection = dc;
4905
4947
  this._iceTimer = setTimeout(() => {
4906
4948
  if (this._mode !== "webrtc" /* WebRTC */) {
4949
+ logger_default.warn(
4950
+ `HybridConnection: ICE timeout after ${iceTimeout}ms for ${this.peer}, falling back to relay`
4951
+ );
4907
4952
  this._fallbackToRelay();
4908
4953
  }
4909
4954
  }, iceTimeout);
4910
4955
  this._dataConnection.on("open", () => {
4956
+ logger_default.log(
4957
+ `HybridConnection: WebRTC opened to ${this.peer} (attempt ${this._upgradeAttempts + 1})`
4958
+ );
4911
4959
  this._clearIceTimer();
4912
4960
  this._clearUpgradeTimer();
4913
4961
  this._upgradeAttempts = 0;
@@ -4948,6 +4996,7 @@ var HybridConnection = class extends import_eventemitter32.EventEmitter {
4948
4996
  /** Update the transport mode and emit if changed. */
4949
4997
  _setMode(mode) {
4950
4998
  if (this._mode !== mode) {
4999
+ logger_default.log(`HybridConnection: transport ${this._mode} -> ${mode} for ${this.peer}`);
4951
5000
  this._mode = mode;
4952
5001
  this.emit("transportChanged", mode);
4953
5002
  }
@@ -4988,6 +5037,117 @@ var HybridConnection = class extends import_eventemitter32.EventEmitter {
4988
5037
  this._attemptWebRTC();
4989
5038
  }, interval);
4990
5039
  }
5040
+ async _tryConnectionReversal() {
5041
+ if (typeof this._provider?.on !== "function") return false;
5042
+ try {
5043
+ const resp = await new Promise((resolve, reject) => {
5044
+ const timer = setTimeout(() => reject(new Error("timeout")), 3e3);
5045
+ const handler = (data) => {
5046
+ clearTimeout(timer);
5047
+ this._provider.off("CONNECT-REQUEST" /* ConnectRequest */, handler);
5048
+ resolve(data);
5049
+ };
5050
+ this._provider.on("CONNECT-REQUEST" /* ConnectRequest */, handler);
5051
+ this._provider.socket.send({
5052
+ type: "CONNECT-REQUEST" /* ConnectRequest */,
5053
+ payload: { peer: this.peer }
5054
+ });
5055
+ });
5056
+ const addr = resp?.address;
5057
+ if (!addr) return false;
5058
+ const pc = new RTCPeerConnection(this._provider.options.config);
5059
+ const dc = pc.createDataChannel("probe", { id: 0 });
5060
+ await new Promise((resolve, reject) => {
5061
+ const timer = setTimeout(() => {
5062
+ pc.close();
5063
+ reject(new Error("direct-dial-timeout"));
5064
+ }, 2500);
5065
+ dc.onopen = () => {
5066
+ clearTimeout(timer);
5067
+ resolve();
5068
+ };
5069
+ dc.onerror = () => {
5070
+ clearTimeout(timer);
5071
+ pc.close();
5072
+ reject(new Error("dc-error"));
5073
+ };
5074
+ });
5075
+ this._dataConnection = void 0;
5076
+ this._setMode("webrtc" /* WebRTC */);
5077
+ pc.close();
5078
+ return true;
5079
+ } catch {
5080
+ return false;
5081
+ }
5082
+ }
5083
+ async _gatherSrflxCandidates() {
5084
+ const pc = new RTCPeerConnection({ iceServers: this._provider.options.config?.iceServers });
5085
+ pc.createDataChannel("probe");
5086
+ const offer = await pc.createOffer();
5087
+ await pc.setLocalDescription(offer);
5088
+ const candidates = [];
5089
+ await new Promise((resolve) => {
5090
+ const timer = setTimeout(resolve, 2e3);
5091
+ pc.onicecandidate = (evt) => {
5092
+ if (!evt.candidate) {
5093
+ clearTimeout(timer);
5094
+ resolve();
5095
+ return;
5096
+ }
5097
+ if (!evt.candidate.candidate.includes("typ host")) {
5098
+ candidates.push(evt.candidate.candidate);
5099
+ }
5100
+ };
5101
+ });
5102
+ pc.close();
5103
+ return candidates;
5104
+ }
5105
+ async _dcutrHolePunch() {
5106
+ try {
5107
+ const localAddrs = await this._gatherSrflxCandidates();
5108
+ const t0 = performance.now();
5109
+ const peerAddrs = await new Promise((resolve, reject) => {
5110
+ const timer = setTimeout(() => reject(new Error("dcutr-timeout")), 8e3);
5111
+ const handler = (data) => {
5112
+ clearTimeout(timer);
5113
+ this._provider.off("DCUTR-CONNECT" /* DcutrConnect */, handler);
5114
+ resolve(data?.addresses ?? []);
5115
+ };
5116
+ this._provider.on("DCUTR-CONNECT" /* DcutrConnect */, handler);
5117
+ this._provider.socket.send({
5118
+ type: "DCUTR-CONNECT" /* DcutrConnect */,
5119
+ payload: { addresses: localAddrs }
5120
+ });
5121
+ });
5122
+ const relayRtt = performance.now() - t0;
5123
+ this._provider.socket.send({ type: "DCUTR-SYNC" /* DcutrSync */, payload: {} });
5124
+ await new Promise((r) => setTimeout(r, relayRtt / 2));
5125
+ for (let i = 0; i < Math.min(peerAddrs.length, 4); i++) {
5126
+ try {
5127
+ const pc = new RTCPeerConnection(this._provider.options.config);
5128
+ await new Promise((resolve, reject) => {
5129
+ const timer = setTimeout(() => {
5130
+ pc.close();
5131
+ reject(new Error("dc-dial-timeout"));
5132
+ }, 5e3);
5133
+ const dc = pc.createDataChannel("dcutr");
5134
+ dc.onopen = () => {
5135
+ clearTimeout(timer);
5136
+ resolve();
5137
+ };
5138
+ });
5139
+ this._dataConnection = void 0;
5140
+ this._setMode("webrtc" /* WebRTC */);
5141
+ pc.close();
5142
+ return true;
5143
+ } catch {
5144
+ }
5145
+ }
5146
+ return false;
5147
+ } catch {
5148
+ return false;
5149
+ }
5150
+ }
4991
5151
  };
4992
5152
 
4993
5153
  // src/mediaconnection.ts
@@ -5144,17 +5304,29 @@ var PollingTransport = class _PollingTransport extends SignalingTransport {
5144
5304
  _heartbeatTimer;
5145
5305
  _lastSeq = 0;
5146
5306
  _baseUrl;
5307
+ _key;
5308
+ _jwt;
5309
+ _apiKey;
5147
5310
  _pingInterval;
5148
5311
  _abortController;
5149
5312
  /** Backoff schedule base delays in milliseconds. */
5150
5313
  static BACKOFF_SCHEDULE = [0, 1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
5151
5314
  /** Random jitter range in milliseconds (applied as +/-). */
5152
5315
  static BACKOFF_JITTER = 500;
5153
- constructor(secure, host, port, path, _key, pingInterval = 5e3, _jwt) {
5316
+ constructor(secure, host, port, path, key, pingInterval = 5e3, jwt, apiKey) {
5154
5317
  super();
5155
5318
  const protocol = secure ? "https://" : "http://";
5156
5319
  this._baseUrl = `${protocol + host}:${port}${path}`;
5157
5320
  this._pingInterval = pingInterval;
5321
+ this._key = key;
5322
+ this._jwt = jwt;
5323
+ this._apiKey = apiKey;
5324
+ }
5325
+ /** Append the shared auth params (key, jwt, api_key) so every HTTP call authenticates like the WS transport. */
5326
+ _applyAuthParams(params) {
5327
+ params.set("key", this._key);
5328
+ if (this._jwt) params.set("jwt", this._jwt);
5329
+ if (this._apiKey) params.set("api_key", this._apiKey);
5158
5330
  }
5159
5331
  get reconnectAttempt() {
5160
5332
  return this._reconnectAttempt;
@@ -5182,6 +5354,7 @@ var PollingTransport = class _PollingTransport extends SignalingTransport {
5182
5354
  id: this._id,
5183
5355
  token: this._token
5184
5356
  });
5357
+ this._applyAuthParams(params);
5185
5358
  if (this._lastSeq > 0) params.set("last_seq", String(this._lastSeq));
5186
5359
  const response = await fetch(`${this._baseUrl}http/poll?${params}`, {
5187
5360
  signal: this._abortController.signal
@@ -5220,6 +5393,7 @@ var PollingTransport = class _PollingTransport extends SignalingTransport {
5220
5393
  id: this._id,
5221
5394
  token: this._token
5222
5395
  });
5396
+ this._applyAuthParams(params);
5223
5397
  try {
5224
5398
  await fetch(`${this._baseUrl}http/send?${params}`, {
5225
5399
  method: "POST",
@@ -5284,13 +5458,16 @@ var PollingTransport = class _PollingTransport extends SignalingTransport {
5284
5458
  };
5285
5459
 
5286
5460
  // src/socket.ts
5287
- var version2 = "2.3.7";
5461
+ var version2 = "2.5.0";
5288
5462
  var Socket = class _Socket extends SignalingTransport {
5289
- constructor(secure, host, port, path, key, pingInterval = 5e3, jwt) {
5463
+ constructor(secure, host, port, path, key, pingInterval = 5e3, jwt, apiKey) {
5290
5464
  super();
5291
5465
  this.pingInterval = pingInterval;
5292
5466
  const wsProtocol = secure ? "wss://" : "ws://";
5293
5467
  this._baseUrl = `${wsProtocol + host}:${port}${path}dendri?key=${key}`;
5468
+ if (apiKey) {
5469
+ this._baseUrl += `&api_key=${encodeURIComponent(apiKey)}`;
5470
+ }
5294
5471
  this._jwt = jwt;
5295
5472
  }
5296
5473
  pingInterval;
@@ -5326,7 +5503,7 @@ var Socket = class _Socket extends SignalingTransport {
5326
5503
  if (this._jwt) {
5327
5504
  wsUrl += `&jwt=${encodeURIComponent(this._jwt)}`;
5328
5505
  }
5329
- if (!!this._socket || !this._disconnected) {
5506
+ if (this._socket || !this._disconnected) {
5330
5507
  return;
5331
5508
  }
5332
5509
  this._socket = new WebSocket(`${wsUrl}&version=${version2}`);
@@ -5596,18 +5773,28 @@ var SSETransport = class _SSETransport extends SignalingTransport {
5596
5773
  _heartbeatTimer;
5597
5774
  _lastSeq = 0;
5598
5775
  _baseUrl;
5776
+ _key;
5599
5777
  _jwt;
5778
+ _apiKey;
5600
5779
  _pingInterval;
5601
5780
  /** Backoff schedule base delays in milliseconds. */
5602
5781
  static BACKOFF_SCHEDULE = [0, 1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
5603
5782
  /** Random jitter range in milliseconds (applied as +/-). */
5604
5783
  static BACKOFF_JITTER = 500;
5605
- constructor(secure, host, port, path, _key, pingInterval = 5e3, jwt) {
5784
+ constructor(secure, host, port, path, key, pingInterval = 5e3, jwt, apiKey) {
5606
5785
  super();
5607
5786
  const protocol = secure ? "https://" : "http://";
5608
5787
  this._baseUrl = `${protocol + host}:${port}${path}`;
5609
5788
  this._pingInterval = pingInterval;
5789
+ this._key = key;
5610
5790
  this._jwt = jwt;
5791
+ this._apiKey = apiKey;
5792
+ }
5793
+ /** Append the shared auth params (key, jwt, api_key) so every HTTP call authenticates like the WS transport. */
5794
+ _applyAuthParams(params) {
5795
+ params.set("key", this._key);
5796
+ if (this._jwt) params.set("jwt", this._jwt);
5797
+ if (this._apiKey) params.set("api_key", this._apiKey);
5611
5798
  }
5612
5799
  get reconnectAttempt() {
5613
5800
  return this._reconnectAttempt;
@@ -5625,10 +5812,9 @@ var SSETransport = class _SSETransport extends SignalingTransport {
5625
5812
  if (this._disconnected) return;
5626
5813
  const params = new URLSearchParams({
5627
5814
  id: this._id,
5628
- token: this._token,
5629
- key: "dendri"
5815
+ token: this._token
5630
5816
  });
5631
- if (this._jwt) params.set("jwt", this._jwt);
5817
+ this._applyAuthParams(params);
5632
5818
  if (this._lastSeq > 0) params.set("last_seq", String(this._lastSeq));
5633
5819
  const url = `${this._baseUrl}http/sse?${params}`;
5634
5820
  try {
@@ -5702,6 +5888,7 @@ var SSETransport = class _SSETransport extends SignalingTransport {
5702
5888
  id: this._id,
5703
5889
  token: this._token
5704
5890
  });
5891
+ this._applyAuthParams(params);
5705
5892
  try {
5706
5893
  await fetch(`${this._baseUrl}http/send?${params}`, {
5707
5894
  method: "POST",
@@ -5774,6 +5961,28 @@ var SSETransport = class _SSETransport extends SignalingTransport {
5774
5961
  };
5775
5962
 
5776
5963
  // src/dendri.ts
5964
+ function parseServerUrl(url) {
5965
+ let parsed;
5966
+ try {
5967
+ parsed = new URL(url);
5968
+ } catch {
5969
+ throw new Error(
5970
+ `Invalid Dendri "url" option: "${url}". Expected a full URL like "wss://signal.example.com".`
5971
+ );
5972
+ }
5973
+ const secure = parsed.protocol === "wss:" || parsed.protocol === "https:";
5974
+ if (!secure && parsed.protocol !== "ws:" && parsed.protocol !== "http:") {
5975
+ throw new Error(
5976
+ `Invalid Dendri "url" protocol: "${parsed.protocol}". Use wss://, ws://, https://, or http://.`
5977
+ );
5978
+ }
5979
+ return {
5980
+ host: parsed.hostname,
5981
+ port: parsed.port ? Number(parsed.port) : secure ? 443 : 80,
5982
+ secure,
5983
+ path: parsed.pathname || "/"
5984
+ };
5985
+ }
5777
5986
  var Dendri = class _Dendri extends EventEmitterWithError {
5778
5987
  static DEFAULT_KEY = "dendri";
5779
5988
  _serializers = {
@@ -5864,6 +6073,9 @@ var Dendri = class _Dendri extends EventEmitterWithError {
5864
6073
  } else if (id) {
5865
6074
  userId = id.toString();
5866
6075
  }
6076
+ if (providedOptions?.url) {
6077
+ providedOptions = { ...parseServerUrl(providedOptions.url), ...providedOptions };
6078
+ }
5867
6079
  const normalizedOptions = {
5868
6080
  debug: 0,
5869
6081
  // 1: Errors, 2: Warnings, 3: All logs
@@ -5917,7 +6129,7 @@ var Dendri = class _Dendri extends EventEmitterWithError {
5917
6129
  return;
5918
6130
  }
5919
6131
  }
5920
- if (!!userId && !util.validateId(userId)) {
6132
+ if (userId && !util.validateId(userId)) {
5921
6133
  this._delayedAbort("invalid-id" /* InvalidID */, `ID "${userId}" is invalid`);
5922
6134
  return;
5923
6135
  }
@@ -5982,7 +6194,8 @@ var Dendri = class _Dendri extends EventEmitterWithError {
5982
6194
  this._options.path,
5983
6195
  this._options.key,
5984
6196
  this._options.pingInterval,
5985
- this._options.jwt
6197
+ this._options.jwt,
6198
+ this._options.apiKey
5986
6199
  ) : transport === "polling" ? new PollingTransport(
5987
6200
  this._options.secure ?? false,
5988
6201
  this._options.host,
@@ -5990,7 +6203,8 @@ var Dendri = class _Dendri extends EventEmitterWithError {
5990
6203
  this._options.path,
5991
6204
  this._options.key,
5992
6205
  this._options.pingInterval,
5993
- this._options.jwt
6206
+ this._options.jwt,
6207
+ this._options.apiKey
5994
6208
  ) : new Socket(
5995
6209
  this._options.secure ?? false,
5996
6210
  this._options.host,
@@ -5998,7 +6212,8 @@ var Dendri = class _Dendri extends EventEmitterWithError {
5998
6212
  this._options.path,
5999
6213
  this._options.key,
6000
6214
  this._options.pingInterval,
6001
- this._options.jwt
6215
+ this._options.jwt,
6216
+ this._options.apiKey
6002
6217
  );
6003
6218
  socket.on("message" /* Message */, (data) => {
6004
6219
  this._handleMessage(data);
@@ -6830,7 +7045,13 @@ var Room = class extends import_eventemitter35.EventEmitter {
6830
7045
  sentViaWebRTC = true;
6831
7046
  }
6832
7047
  }
6833
- if (!sentViaWebRTC && this._peer?.socket) {
7048
+ if (this._dendriOptions?.enableRelay && this._peer?.socket) {
7049
+ this._peer.socket.send({
7050
+ type: "DATA" /* Data */,
7051
+ room: this._roomId,
7052
+ payload: wire
7053
+ });
7054
+ } else if (!sentViaWebRTC && this._peer?.socket) {
6834
7055
  this._peer.socket.send({
6835
7056
  type: "DATA" /* Data */,
6836
7057
  room: this._roomId,
@@ -6864,7 +7085,13 @@ var Room = class extends import_eventemitter35.EventEmitter {
6864
7085
  sentViaWebRTC = true;
6865
7086
  }
6866
7087
  }
6867
- if (!sentViaWebRTC && this._peer?.socket) {
7088
+ if (this._dendriOptions?.enableRelay && this._peer?.socket) {
7089
+ this._peer.socket.send({
7090
+ type: "DATA" /* Data */,
7091
+ room: this._roomId,
7092
+ payload: wire
7093
+ });
7094
+ } else if (!sentViaWebRTC && this._peer?.socket) {
6868
7095
  this._peer.socket.send({
6869
7096
  type: "DATA" /* Data */,
6870
7097
  room: this._roomId,
@@ -7055,8 +7282,8 @@ var Room = class extends import_eventemitter35.EventEmitter {
7055
7282
  const remotePeerId = conn.peer;
7056
7283
  this._connections.set(remotePeerId, conn);
7057
7284
  this._knownPeers.add(remotePeerId);
7285
+ this.emit("peerJoined", remotePeerId);
7058
7286
  conn.on("open", () => {
7059
- this.emit("peerJoined", remotePeerId);
7060
7287
  for (const [peerId, c] of this._connections) {
7061
7288
  if (peerId !== remotePeerId && c.open) {
7062
7289
  c.send({ __room: { type: "peer-joined", peerId: remotePeerId } });
@@ -7378,6 +7605,14 @@ var Room = class extends import_eventemitter35.EventEmitter {
7378
7605
  const conn = this._connections.get(peerId);
7379
7606
  if (conn?.open) {
7380
7607
  conn.send(data);
7608
+ if (this._dendriOptions?.enableRelay && this._peer?.socket) {
7609
+ this._peer.socket.send({
7610
+ type: "DATA" /* Data */,
7611
+ dst: peerId,
7612
+ room: this._roomId,
7613
+ payload: data
7614
+ });
7615
+ }
7381
7616
  return;
7382
7617
  }
7383
7618
  if (!this._isHost && this._hostId) {