@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.
package/dist/dendri.cjs CHANGED
@@ -3533,7 +3533,7 @@ var Logger = class {
3533
3533
  var logger_default = new Logger();
3534
3534
 
3535
3535
  // src/api.ts
3536
- var version = "2.3.7";
3536
+ var version = "2.4.0";
3537
3537
  var API = class _API {
3538
3538
  constructor(_options) {
3539
3539
  this._options = _options;
@@ -3570,11 +3570,11 @@ var API = class _API {
3570
3570
  throw new Error(`Could not get an ID from the server.${pathError}`);
3571
3571
  }
3572
3572
  }
3573
- /** Fetch TURN credentials from the signaling server's GET /turn endpoint. */
3573
+ /** Fetch TURN credentials from the signaling server's turn-credentials endpoint. */
3574
3574
  async getTurnCredentials() {
3575
3575
  const protocol = this._options.secure ? "https" : "http";
3576
- const { host, port } = this._options;
3577
- const url = `${protocol}://${host}:${port}/turn`;
3576
+ const { host, port, path, key } = this._options;
3577
+ const url = `${protocol}://${host}:${port}${path}${key}/turn-credentials`;
3578
3578
  try {
3579
3579
  const controller = new AbortController();
3580
3580
  const timeoutId = setTimeout(() => controller.abort(), _API.FETCH_TIMEOUT);
@@ -3683,6 +3683,9 @@ var ServerMessageType = /* @__PURE__ */ ((ServerMessageType2) => {
3683
3683
  ServerMessageType2["HostMigrate"] = "HOST-MIGRATE";
3684
3684
  ServerMessageType2["PresenceUpdate"] = "PRESENCE-UPDATE";
3685
3685
  ServerMessageType2["KeyExchange"] = "KEY-EXCHANGE";
3686
+ ServerMessageType2["ConnectRequest"] = "CONNECT-REQUEST";
3687
+ ServerMessageType2["DcutrConnect"] = "DCUTR-CONNECT";
3688
+ ServerMessageType2["DcutrSync"] = "DCUTR-SYNC";
3686
3689
  return ServerMessageType2;
3687
3690
  })(ServerMessageType || {});
3688
3691
  var TransportMode = /* @__PURE__ */ ((TransportMode2) => {
@@ -3708,6 +3711,11 @@ var ConnectionQuality = /* @__PURE__ */ ((ConnectionQuality2) => {
3708
3711
  ConnectionQuality2["Unknown"] = "unknown";
3709
3712
  return ConnectionQuality2;
3710
3713
  })(ConnectionQuality || {});
3714
+ var TopicClass = /* @__PURE__ */ ((TopicClass2) => {
3715
+ TopicClass2["Persistent"] = "persistent";
3716
+ TopicClass2["Ephemeral"] = "ephemeral";
3717
+ return TopicClass2;
3718
+ })(TopicClass || {});
3711
3719
 
3712
3720
  // src/dendriError.ts
3713
3721
  var import_eventemitter3 = __toESM(require_eventemitter3(), 1);
@@ -3796,6 +3804,7 @@ var Negotiator = class {
3796
3804
  }
3797
3805
  connection;
3798
3806
  _pendingCandidates = [];
3807
+ _iceCandidateFilter = null;
3799
3808
  /** Returns a PeerConnection object set up correctly (for data, media). */
3800
3809
  startConnection(options) {
3801
3810
  const peerConnection = this._startPeerConnection();
@@ -3818,6 +3827,13 @@ var Negotiator = class {
3818
3827
  _startPeerConnection() {
3819
3828
  logger_default.log("Creating RTCPeerConnection.");
3820
3829
  const peerConnection = new RTCPeerConnection(this.connection.provider?.options.config);
3830
+ if (this.connection.provider?.options.ipPolicy === "public") {
3831
+ const isPublicCandidate = (c) => {
3832
+ const sdp2 = c.candidate ?? "";
3833
+ return !sdp2.includes("typ host");
3834
+ };
3835
+ this._iceCandidateFilter = isPublicCandidate;
3836
+ }
3821
3837
  this._setupListeners(peerConnection);
3822
3838
  return peerConnection;
3823
3839
  }
@@ -3830,6 +3846,9 @@ var Negotiator = class {
3830
3846
  logger_default.log("Listening for ICE candidates.");
3831
3847
  peerConnection.onicecandidate = (evt) => {
3832
3848
  if (!evt.candidate?.candidate) return;
3849
+ if (this._iceCandidateFilter && !this._iceCandidateFilter(evt.candidate)) {
3850
+ return;
3851
+ }
3833
3852
  logger_default.log(`Received ICE candidates for ${peerId}:`, evt.candidate);
3834
3853
  provider.socket.send({
3835
3854
  type: "CANDIDATE" /* Candidate */,
@@ -4168,6 +4187,7 @@ var DataConnection = class _DataConnection extends BaseConnection {
4168
4187
  this.dataChannel.onopen = () => {
4169
4188
  logger_default.log(`DC#${this.connectionId} dc connection success`);
4170
4189
  this._open = true;
4190
+ this._applyAdaptiveBuffer(dc);
4171
4191
  this.emit("open");
4172
4192
  };
4173
4193
  this.dataChannel.onclose = () => {
@@ -4175,6 +4195,24 @@ var DataConnection = class _DataConnection extends BaseConnection {
4175
4195
  this.close();
4176
4196
  };
4177
4197
  }
4198
+ _applyAdaptiveBuffer(dc) {
4199
+ const pc = this.peerConnection;
4200
+ if (!pc || typeof pc.getStats !== "function") return;
4201
+ pc.getStats().then((stats) => {
4202
+ let rtt = null;
4203
+ stats.forEach((report) => {
4204
+ if (report.type === "candidate-pair" && report.state === "succeeded" && report.currentRoundTripTime) {
4205
+ rtt = report.currentRoundTripTime * 1e3;
4206
+ }
4207
+ });
4208
+ if (rtt !== null) {
4209
+ const bdp = 12.5 * 1024 * 1024 * (rtt / 1e3);
4210
+ const optimal = Math.max(1 * 1024 * 1024, Math.min(32 * 1024 * 1024, Math.ceil(bdp)));
4211
+ dc.bufferedAmountLowThreshold = optimal;
4212
+ }
4213
+ }).catch(() => {
4214
+ });
4215
+ }
4178
4216
  /**
4179
4217
  * Exposed functionality for users.
4180
4218
  */
@@ -4709,7 +4747,17 @@ var HybridConnection = class extends import_eventemitter32.EventEmitter {
4709
4747
  if (this._closed) {
4710
4748
  return;
4711
4749
  }
4712
- this._attemptWebRTC();
4750
+ logger_default.log(
4751
+ `HybridConnection: start peer=${this.peer} iceTimeout=${this._options.iceTimeout ?? 1e4}ms encryptRelay=${this._encryptRelay}`
4752
+ );
4753
+ if (typeof this._provider?.on !== "function") {
4754
+ this._attemptWebRTC();
4755
+ return;
4756
+ }
4757
+ this._tryConnectionReversal().then((direct) => {
4758
+ if (direct) return;
4759
+ this._attemptWebRTC();
4760
+ });
4713
4761
  }
4714
4762
  /** Send data through the best available transport, optionally tagged with a topic. */
4715
4763
  send(data, options) {
@@ -4999,10 +5047,16 @@ var HybridConnection = class extends import_eventemitter32.EventEmitter {
4999
5047
  this._dataConnection = dc;
5000
5048
  this._iceTimer = setTimeout(() => {
5001
5049
  if (this._mode !== "webrtc" /* WebRTC */) {
5050
+ logger_default.warn(
5051
+ `HybridConnection: ICE timeout after ${iceTimeout}ms for ${this.peer}, falling back to relay`
5052
+ );
5002
5053
  this._fallbackToRelay();
5003
5054
  }
5004
5055
  }, iceTimeout);
5005
5056
  this._dataConnection.on("open", () => {
5057
+ logger_default.log(
5058
+ `HybridConnection: WebRTC opened to ${this.peer} (attempt ${this._upgradeAttempts + 1})`
5059
+ );
5006
5060
  this._clearIceTimer();
5007
5061
  this._clearUpgradeTimer();
5008
5062
  this._upgradeAttempts = 0;
@@ -5043,6 +5097,7 @@ var HybridConnection = class extends import_eventemitter32.EventEmitter {
5043
5097
  /** Update the transport mode and emit if changed. */
5044
5098
  _setMode(mode) {
5045
5099
  if (this._mode !== mode) {
5100
+ logger_default.log(`HybridConnection: transport ${this._mode} -> ${mode} for ${this.peer}`);
5046
5101
  this._mode = mode;
5047
5102
  this.emit("transportChanged", mode);
5048
5103
  }
@@ -5083,6 +5138,117 @@ var HybridConnection = class extends import_eventemitter32.EventEmitter {
5083
5138
  this._attemptWebRTC();
5084
5139
  }, interval);
5085
5140
  }
5141
+ async _tryConnectionReversal() {
5142
+ if (typeof this._provider?.on !== "function") return false;
5143
+ try {
5144
+ const resp = await new Promise((resolve, reject) => {
5145
+ const timer = setTimeout(() => reject(new Error("timeout")), 3e3);
5146
+ const handler = (data) => {
5147
+ clearTimeout(timer);
5148
+ this._provider.off("CONNECT-REQUEST" /* ConnectRequest */, handler);
5149
+ resolve(data);
5150
+ };
5151
+ this._provider.on("CONNECT-REQUEST" /* ConnectRequest */, handler);
5152
+ this._provider.socket.send({
5153
+ type: "CONNECT-REQUEST" /* ConnectRequest */,
5154
+ payload: { peer: this.peer }
5155
+ });
5156
+ });
5157
+ const addr = resp?.address;
5158
+ if (!addr) return false;
5159
+ const pc = new RTCPeerConnection(this._provider.options.config);
5160
+ const dc = pc.createDataChannel("probe", { id: 0 });
5161
+ await new Promise((resolve, reject) => {
5162
+ const timer = setTimeout(() => {
5163
+ pc.close();
5164
+ reject(new Error("direct-dial-timeout"));
5165
+ }, 2500);
5166
+ dc.onopen = () => {
5167
+ clearTimeout(timer);
5168
+ resolve();
5169
+ };
5170
+ dc.onerror = () => {
5171
+ clearTimeout(timer);
5172
+ pc.close();
5173
+ reject(new Error("dc-error"));
5174
+ };
5175
+ });
5176
+ this._dataConnection = void 0;
5177
+ this._setMode("webrtc" /* WebRTC */);
5178
+ pc.close();
5179
+ return true;
5180
+ } catch {
5181
+ return false;
5182
+ }
5183
+ }
5184
+ async _gatherSrflxCandidates() {
5185
+ const pc = new RTCPeerConnection({ iceServers: this._provider.options.config?.iceServers });
5186
+ pc.createDataChannel("probe");
5187
+ const offer = await pc.createOffer();
5188
+ await pc.setLocalDescription(offer);
5189
+ const candidates = [];
5190
+ await new Promise((resolve) => {
5191
+ const timer = setTimeout(resolve, 2e3);
5192
+ pc.onicecandidate = (evt) => {
5193
+ if (!evt.candidate) {
5194
+ clearTimeout(timer);
5195
+ resolve();
5196
+ return;
5197
+ }
5198
+ if (!evt.candidate.candidate.includes("typ host")) {
5199
+ candidates.push(evt.candidate.candidate);
5200
+ }
5201
+ };
5202
+ });
5203
+ pc.close();
5204
+ return candidates;
5205
+ }
5206
+ async _dcutrHolePunch() {
5207
+ try {
5208
+ const localAddrs = await this._gatherSrflxCandidates();
5209
+ const t0 = performance.now();
5210
+ const peerAddrs = await new Promise((resolve, reject) => {
5211
+ const timer = setTimeout(() => reject(new Error("dcutr-timeout")), 8e3);
5212
+ const handler = (data) => {
5213
+ clearTimeout(timer);
5214
+ this._provider.off("DCUTR-CONNECT" /* DcutrConnect */, handler);
5215
+ resolve(data?.addresses ?? []);
5216
+ };
5217
+ this._provider.on("DCUTR-CONNECT" /* DcutrConnect */, handler);
5218
+ this._provider.socket.send({
5219
+ type: "DCUTR-CONNECT" /* DcutrConnect */,
5220
+ payload: { addresses: localAddrs }
5221
+ });
5222
+ });
5223
+ const relayRtt = performance.now() - t0;
5224
+ this._provider.socket.send({ type: "DCUTR-SYNC" /* DcutrSync */, payload: {} });
5225
+ await new Promise((r) => setTimeout(r, relayRtt / 2));
5226
+ for (let i = 0; i < Math.min(peerAddrs.length, 4); i++) {
5227
+ try {
5228
+ const pc = new RTCPeerConnection(this._provider.options.config);
5229
+ await new Promise((resolve, reject) => {
5230
+ const timer = setTimeout(() => {
5231
+ pc.close();
5232
+ reject(new Error("dc-dial-timeout"));
5233
+ }, 5e3);
5234
+ const dc = pc.createDataChannel("dcutr");
5235
+ dc.onopen = () => {
5236
+ clearTimeout(timer);
5237
+ resolve();
5238
+ };
5239
+ });
5240
+ this._dataConnection = void 0;
5241
+ this._setMode("webrtc" /* WebRTC */);
5242
+ pc.close();
5243
+ return true;
5244
+ } catch {
5245
+ }
5246
+ }
5247
+ return false;
5248
+ } catch {
5249
+ return false;
5250
+ }
5251
+ }
5086
5252
  };
5087
5253
 
5088
5254
  // src/mediaconnection.ts
@@ -5379,7 +5545,7 @@ var PollingTransport = class _PollingTransport extends SignalingTransport {
5379
5545
  };
5380
5546
 
5381
5547
  // src/socket.ts
5382
- var version2 = "2.3.7";
5548
+ var version2 = "2.4.0";
5383
5549
  var Socket = class _Socket extends SignalingTransport {
5384
5550
  constructor(secure, host, port, path, key, pingInterval = 5e3, jwt) {
5385
5551
  super();
@@ -5421,7 +5587,7 @@ var Socket = class _Socket extends SignalingTransport {
5421
5587
  if (this._jwt) {
5422
5588
  wsUrl += `&jwt=${encodeURIComponent(this._jwt)}`;
5423
5589
  }
5424
- if (!!this._socket || !this._disconnected) {
5590
+ if (this._socket || !this._disconnected) {
5425
5591
  return;
5426
5592
  }
5427
5593
  this._socket = new WebSocket(`${wsUrl}&version=${version2}`);
@@ -6012,7 +6178,7 @@ var Dendri = class _Dendri extends EventEmitterWithError {
6012
6178
  return;
6013
6179
  }
6014
6180
  }
6015
- if (!!userId && !util.validateId(userId)) {
6181
+ if (userId && !util.validateId(userId)) {
6016
6182
  this._delayedAbort("invalid-id" /* InvalidID */, `ID "${userId}" is invalid`);
6017
6183
  return;
6018
6184
  }
@@ -8791,7 +8957,13 @@ var Room = class extends import_eventemitter35.EventEmitter {
8791
8957
  sentViaWebRTC = true;
8792
8958
  }
8793
8959
  }
8794
- if (!sentViaWebRTC && this._peer?.socket) {
8960
+ if (this._dendriOptions?.enableRelay && this._peer?.socket) {
8961
+ this._peer.socket.send({
8962
+ type: "DATA" /* Data */,
8963
+ room: this._roomId,
8964
+ payload: wire
8965
+ });
8966
+ } else if (!sentViaWebRTC && this._peer?.socket) {
8795
8967
  this._peer.socket.send({
8796
8968
  type: "DATA" /* Data */,
8797
8969
  room: this._roomId,
@@ -8825,7 +8997,13 @@ var Room = class extends import_eventemitter35.EventEmitter {
8825
8997
  sentViaWebRTC = true;
8826
8998
  }
8827
8999
  }
8828
- if (!sentViaWebRTC && this._peer?.socket) {
9000
+ if (this._dendriOptions?.enableRelay && this._peer?.socket) {
9001
+ this._peer.socket.send({
9002
+ type: "DATA" /* Data */,
9003
+ room: this._roomId,
9004
+ payload: wire
9005
+ });
9006
+ } else if (!sentViaWebRTC && this._peer?.socket) {
8829
9007
  this._peer.socket.send({
8830
9008
  type: "DATA" /* Data */,
8831
9009
  room: this._roomId,
@@ -9016,8 +9194,8 @@ var Room = class extends import_eventemitter35.EventEmitter {
9016
9194
  const remotePeerId = conn.peer;
9017
9195
  this._connections.set(remotePeerId, conn);
9018
9196
  this._knownPeers.add(remotePeerId);
9197
+ this.emit("peerJoined", remotePeerId);
9019
9198
  conn.on("open", () => {
9020
- this.emit("peerJoined", remotePeerId);
9021
9199
  for (const [peerId, c] of this._connections) {
9022
9200
  if (peerId !== remotePeerId && c.open) {
9023
9201
  c.send({ __room: { type: "peer-joined", peerId: remotePeerId } });
@@ -9339,6 +9517,14 @@ var Room = class extends import_eventemitter35.EventEmitter {
9339
9517
  const conn = this._connections.get(peerId);
9340
9518
  if (conn?.open) {
9341
9519
  conn.send(data);
9520
+ if (this._dendriOptions?.enableRelay && this._peer?.socket) {
9521
+ this._peer.socket.send({
9522
+ type: "DATA" /* Data */,
9523
+ dst: peerId,
9524
+ room: this._roomId,
9525
+ payload: data
9526
+ });
9527
+ }
9342
9528
  return;
9343
9529
  }
9344
9530
  if (!this._isHost && this._hostId) {
@@ -9403,7 +9589,7 @@ var DendriServerAPI = class {
9403
9589
  }
9404
9590
  /** Get TURN credentials */
9405
9591
  async getTurnCredentials() {
9406
- const res = await fetch(`${this._baseUrl}/turn`);
9592
+ const res = await fetch(`${this._baseUrl}/${this._key}/turn-credentials`);
9407
9593
  if (!res.ok) throw new Error(`TURN credentials failed: ${res.status}`);
9408
9594
  return res.json();
9409
9595
  }
@@ -9712,6 +9898,7 @@ exports.ServerMessageType = ServerMessageType;
9712
9898
  exports.SignalingTransport = SignalingTransport;
9713
9899
  exports.SocketEventType = SocketEventType;
9714
9900
  exports.StreamConnection = StreamConnection;
9901
+ exports.TopicClass = TopicClass;
9715
9902
  exports.TopicManager = TopicManager;
9716
9903
  exports.TransportMode = TransportMode;
9717
9904
  exports.createDendriStore = createDendriStore;