@abraca/dabra 1.0.2 → 1.0.4

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.
@@ -1908,7 +1908,7 @@ var AbracadabraWS = class extends EventEmitter {
1908
1908
  if (this.connectionAttempt) this.rejectConnectionAttempt();
1909
1909
  this.status = WebSocketStatus.Disconnected;
1910
1910
  this.emit("status", { status: WebSocketStatus.Disconnected });
1911
- const isRateLimited = event?.code === 4429;
1911
+ const isRateLimited = event?.code === 4429 || event === 4429;
1912
1912
  this.emit("disconnect", { event });
1913
1913
  if (isRateLimited) this.emit("rateLimited");
1914
1914
  if (!this.cancelWebsocketRetry && this.shouldConnect) {
@@ -3365,6 +3365,18 @@ var AbracadabraClient = class {
3365
3365
  async serverInfo() {
3366
3366
  return this.request("GET", "/info", { auth: false });
3367
3367
  }
3368
+ /**
3369
+ * Fetch ICE server configuration for WebRTC peer connections.
3370
+ * Falls back to default Google STUN server if the endpoint is unavailable.
3371
+ * No auth required.
3372
+ */
3373
+ async getIceServers() {
3374
+ try {
3375
+ return (await this.request("GET", "/ice-servers", { auth: false })).iceServers;
3376
+ } catch {
3377
+ return [{ urls: "stun:stun.l.google.com:19302" }];
3378
+ }
3379
+ }
3368
3380
  async request(method, path, opts) {
3369
3381
  const auth = opts?.auth ?? true;
3370
3382
  const headers = {};
@@ -4163,7 +4175,7 @@ const hexes = /* @__PURE__ */ Array.from({ length: 256 }, (_, i) => i.toString(1
4163
4175
  * Convert byte array to hex string. Uses built-in function, when available.
4164
4176
  * @example bytesToHex(Uint8Array.from([0xca, 0xfe, 0x01, 0x23])) // 'cafe0123'
4165
4177
  */
4166
- function bytesToHex(bytes) {
4178
+ function bytesToHex$1(bytes) {
4167
4179
  abytes(bytes);
4168
4180
  if (hasHexBuiltin) return bytes.toHex();
4169
4181
  let hex = "";
@@ -4187,7 +4199,7 @@ function asciiToBase16(ch) {
4187
4199
  * Convert hex string to byte array. Uses built-in function, when available.
4188
4200
  * @example hexToBytes('cafe0123') // Uint8Array.from([0xca, 0xfe, 0x01, 0x23])
4189
4201
  */
4190
- function hexToBytes(hex) {
4202
+ function hexToBytes$1(hex) {
4191
4203
  if (typeof hex !== "string") throw new Error("hex string expected, got " + typeof hex);
4192
4204
  if (hasHexBuiltin) return Uint8Array.fromHex(hex);
4193
4205
  const hl = hex.length;
@@ -4682,15 +4694,15 @@ function hexToNumber(hex) {
4682
4694
  return hex === "" ? _0n$5 : BigInt("0x" + hex);
4683
4695
  }
4684
4696
  function bytesToNumberBE(bytes) {
4685
- return hexToNumber(bytesToHex(bytes));
4697
+ return hexToNumber(bytesToHex$1(bytes));
4686
4698
  }
4687
4699
  function bytesToNumberLE(bytes) {
4688
- return hexToNumber(bytesToHex(copyBytes(abytes(bytes)).reverse()));
4700
+ return hexToNumber(bytesToHex$1(copyBytes(abytes(bytes)).reverse()));
4689
4701
  }
4690
4702
  function numberToBytesBE(n, len) {
4691
4703
  anumber(len);
4692
4704
  n = abignumber(n);
4693
- const res = hexToBytes(n.toString(16).padStart(len * 2, "0"));
4705
+ const res = hexToBytes$1(n.toString(16).padStart(len * 2, "0"));
4694
4706
  if (res.length !== len) throw new Error("number too large");
4695
4707
  return res;
4696
4708
  }
@@ -5652,7 +5664,7 @@ function edwards(params, extraOpts = {}) {
5652
5664
  });
5653
5665
  }
5654
5666
  static fromHex(hex, zip215 = false) {
5655
- return Point.fromBytes(hexToBytes(hex), zip215);
5667
+ return Point.fromBytes(hexToBytes$1(hex), zip215);
5656
5668
  }
5657
5669
  get x() {
5658
5670
  return this.toAffine().x;
@@ -5753,7 +5765,7 @@ function edwards(params, extraOpts = {}) {
5753
5765
  return bytes;
5754
5766
  }
5755
5767
  toHex() {
5756
- return bytesToHex(this.toBytes());
5768
+ return bytesToHex$1(this.toBytes());
5757
5769
  }
5758
5770
  toString() {
5759
5771
  return `<Point ${this.is0() ? "ZERO" : this.toHex()}>`;
@@ -5799,7 +5811,7 @@ var PrimeEdwardsPoint = class {
5799
5811
  return this.ep.toAffine(invertedZ);
5800
5812
  }
5801
5813
  toHex() {
5802
- return bytesToHex(this.toBytes());
5814
+ return bytesToHex$1(this.toBytes());
5803
5815
  }
5804
5816
  toString() {
5805
5817
  return this.toHex();
@@ -6832,7 +6844,7 @@ var _RistrettoPoint = class _RistrettoPoint extends PrimeEdwardsPoint {
6832
6844
  * @param hex Ristretto-encoded 32 bytes. Not every 32-byte string is valid ristretto encoding
6833
6845
  */
6834
6846
  static fromHex(hex) {
6835
- return _RistrettoPoint.fromBytes(hexToBytes(hex));
6847
+ return _RistrettoPoint.fromBytes(hexToBytes$1(hex));
6836
6848
  }
6837
6849
  /**
6838
6850
  * Encodes ristretto point to Uint8Array.
@@ -8447,17 +8459,1186 @@ var BackgroundSyncManager = class extends EventEmitter {
8447
8459
  }
8448
8460
  };
8449
8461
 
8462
+ //#endregion
8463
+ //#region packages/provider/src/webrtc/SignalingSocket.ts
8464
+ var SignalingSocket = class extends EventEmitter {
8465
+ constructor(configuration) {
8466
+ super();
8467
+ this.ws = null;
8468
+ this.wsHandlers = {};
8469
+ this.shouldConnect = true;
8470
+ this.connectionAttempt = null;
8471
+ this.localPeerId = null;
8472
+ this.isConnected = false;
8473
+ this.config = {
8474
+ url: configuration.url,
8475
+ token: configuration.token,
8476
+ delay: configuration.delay ?? 1e3,
8477
+ factor: configuration.factor ?? 2,
8478
+ minDelay: configuration.minDelay ?? 1e3,
8479
+ maxDelay: configuration.maxDelay ?? 3e4,
8480
+ jitter: configuration.jitter ?? true,
8481
+ maxAttempts: configuration.maxAttempts ?? 0,
8482
+ WebSocketPolyfill: configuration.WebSocketPolyfill ?? WebSocket
8483
+ };
8484
+ if (configuration.autoConnect !== false) this.connect();
8485
+ }
8486
+ async getToken() {
8487
+ if (typeof this.config.token === "function") return await this.config.token();
8488
+ return this.config.token;
8489
+ }
8490
+ async connect() {
8491
+ if (this.isConnected) return;
8492
+ if (this.cancelRetry) {
8493
+ this.cancelRetry();
8494
+ this.cancelRetry = void 0;
8495
+ }
8496
+ this.shouldConnect = true;
8497
+ let cancelAttempt = false;
8498
+ const retryPromise = (0, _lifeomic_attempt.retry)(() => this.createConnection(), {
8499
+ delay: this.config.delay,
8500
+ initialDelay: 0,
8501
+ factor: this.config.factor,
8502
+ maxAttempts: this.config.maxAttempts,
8503
+ minDelay: this.config.minDelay,
8504
+ maxDelay: this.config.maxDelay,
8505
+ jitter: this.config.jitter,
8506
+ timeout: 0,
8507
+ beforeAttempt: (context) => {
8508
+ if (!this.shouldConnect || cancelAttempt) context.abort();
8509
+ }
8510
+ }).catch((error) => {
8511
+ if (error && error.code !== "ATTEMPT_ABORTED") throw error;
8512
+ });
8513
+ this.cancelRetry = () => {
8514
+ cancelAttempt = true;
8515
+ };
8516
+ return retryPromise;
8517
+ }
8518
+ async createConnection() {
8519
+ this.cleanup();
8520
+ const token = await this.getToken();
8521
+ const separator = this.config.url.includes("?") ? "&" : "?";
8522
+ const url = `${this.config.url}${separator}token=${encodeURIComponent(token)}`;
8523
+ const ws = new this.config.WebSocketPolyfill(url);
8524
+ return new Promise((resolve, reject) => {
8525
+ const onOpen = () => {
8526
+ this.isConnected = true;
8527
+ this.sendRaw({ type: "join" });
8528
+ };
8529
+ const onMessage = (event) => {
8530
+ const data = typeof event === "string" ? event : typeof event.data === "string" ? event.data : null;
8531
+ if (!data) return;
8532
+ let msg;
8533
+ try {
8534
+ msg = JSON.parse(data);
8535
+ } catch {
8536
+ return;
8537
+ }
8538
+ this.handleMessage(msg, resolve);
8539
+ };
8540
+ const onClose = (event) => {
8541
+ const wasConnected = this.isConnected;
8542
+ this.isConnected = false;
8543
+ this.localPeerId = null;
8544
+ if (this.connectionAttempt) {
8545
+ this.connectionAttempt = null;
8546
+ reject(/* @__PURE__ */ new Error(`Signaling WebSocket closed: ${event?.code}`));
8547
+ }
8548
+ this.emit("disconnected");
8549
+ if (!wasConnected) return;
8550
+ if (this.shouldConnect && !this.cancelRetry) setTimeout(() => this.connect(), this.config.delay);
8551
+ };
8552
+ const onError = (err) => {
8553
+ if (this.connectionAttempt) {
8554
+ this.connectionAttempt = null;
8555
+ reject(err);
8556
+ }
8557
+ };
8558
+ this.wsHandlers = {
8559
+ open: onOpen,
8560
+ message: onMessage,
8561
+ close: onClose,
8562
+ error: onError
8563
+ };
8564
+ for (const [name, handler] of Object.entries(this.wsHandlers)) ws.addEventListener(name, handler);
8565
+ this.ws = ws;
8566
+ this.connectionAttempt = {
8567
+ resolve,
8568
+ reject
8569
+ };
8570
+ });
8571
+ }
8572
+ handleMessage(msg, resolveConnection) {
8573
+ switch (msg.type) {
8574
+ case "welcome":
8575
+ this.localPeerId = msg.peer_id;
8576
+ if (this.connectionAttempt) {
8577
+ this.connectionAttempt = null;
8578
+ resolveConnection?.();
8579
+ }
8580
+ this.emit("welcome", {
8581
+ peerId: msg.peer_id,
8582
+ peers: msg.peers
8583
+ });
8584
+ break;
8585
+ case "joined":
8586
+ this.emit("joined", {
8587
+ peerId: msg.peer_id,
8588
+ userId: msg.user_id,
8589
+ muted: msg.muted,
8590
+ video: msg.video,
8591
+ screen: msg.screen,
8592
+ name: msg.name,
8593
+ color: msg.color
8594
+ });
8595
+ break;
8596
+ case "left":
8597
+ this.emit("left", { peerId: msg.peer_id });
8598
+ break;
8599
+ case "offer":
8600
+ this.emit("offer", {
8601
+ from: msg.from,
8602
+ sdp: msg.sdp
8603
+ });
8604
+ break;
8605
+ case "answer":
8606
+ this.emit("answer", {
8607
+ from: msg.from,
8608
+ sdp: msg.sdp
8609
+ });
8610
+ break;
8611
+ case "ice":
8612
+ this.emit("ice", {
8613
+ from: msg.from,
8614
+ candidate: msg.candidate
8615
+ });
8616
+ break;
8617
+ case "mute":
8618
+ this.emit("mute", {
8619
+ peerId: msg.peer_id,
8620
+ muted: msg.muted
8621
+ });
8622
+ break;
8623
+ case "media-state":
8624
+ this.emit("media-state", {
8625
+ peerId: msg.peer_id,
8626
+ video: msg.video,
8627
+ screen: msg.screen
8628
+ });
8629
+ break;
8630
+ case "profile":
8631
+ this.emit("profile", {
8632
+ peerId: msg.peer_id,
8633
+ name: msg.name,
8634
+ color: msg.color
8635
+ });
8636
+ break;
8637
+ case "ping":
8638
+ this.sendRaw({ type: "pong" });
8639
+ break;
8640
+ case "error":
8641
+ this.emit("error", {
8642
+ code: msg.code,
8643
+ message: msg.message
8644
+ });
8645
+ break;
8646
+ }
8647
+ }
8648
+ sendRaw(msg) {
8649
+ if (this.ws?.readyState === 1) this.ws.send(JSON.stringify(msg));
8650
+ }
8651
+ sendOffer(to, sdp) {
8652
+ this.sendRaw({
8653
+ type: "offer",
8654
+ to,
8655
+ sdp
8656
+ });
8657
+ }
8658
+ sendAnswer(to, sdp) {
8659
+ this.sendRaw({
8660
+ type: "answer",
8661
+ to,
8662
+ sdp
8663
+ });
8664
+ }
8665
+ sendIce(to, candidate) {
8666
+ this.sendRaw({
8667
+ type: "ice",
8668
+ to,
8669
+ candidate
8670
+ });
8671
+ }
8672
+ sendMute(muted) {
8673
+ this.sendRaw({
8674
+ type: "mute",
8675
+ muted
8676
+ });
8677
+ }
8678
+ sendMediaState(video, screen) {
8679
+ this.sendRaw({
8680
+ type: "media-state",
8681
+ video,
8682
+ screen
8683
+ });
8684
+ }
8685
+ sendProfile(name, color) {
8686
+ this.sendRaw({
8687
+ type: "profile",
8688
+ name,
8689
+ color
8690
+ });
8691
+ }
8692
+ sendLeave() {
8693
+ this.sendRaw({ type: "leave" });
8694
+ }
8695
+ disconnect() {
8696
+ this.shouldConnect = false;
8697
+ this.sendLeave();
8698
+ if (this.cancelRetry) {
8699
+ this.cancelRetry();
8700
+ this.cancelRetry = void 0;
8701
+ }
8702
+ this.cleanup();
8703
+ }
8704
+ destroy() {
8705
+ this.disconnect();
8706
+ this.removeAllListeners();
8707
+ }
8708
+ cleanup() {
8709
+ if (!this.ws) return;
8710
+ for (const [name, handler] of Object.entries(this.wsHandlers)) this.ws.removeEventListener(name, handler);
8711
+ this.wsHandlers = {};
8712
+ try {
8713
+ if (this.ws.readyState !== 3) this.ws.close();
8714
+ } catch {}
8715
+ this.ws = null;
8716
+ this.isConnected = false;
8717
+ this.localPeerId = null;
8718
+ }
8719
+ };
8720
+
8721
+ //#endregion
8722
+ //#region packages/provider/src/webrtc/types.ts
8723
+ /** Data channel file transfer message type discriminators (first byte). */
8724
+ const FILE_MSG = {
8725
+ START: 1,
8726
+ CHUNK: 2,
8727
+ COMPLETE: 3,
8728
+ CANCEL: 4
8729
+ };
8730
+ /** Data channel Y.js message type discriminators (first byte). */
8731
+ const YJS_MSG = {
8732
+ SYNC: 0,
8733
+ UPDATE: 1
8734
+ };
8735
+ const CHANNEL_NAMES = {
8736
+ YJS_SYNC: "yjs-sync",
8737
+ AWARENESS: "awareness",
8738
+ FILE_TRANSFER: "file-transfer",
8739
+ CUSTOM: "custom"
8740
+ };
8741
+ const DEFAULT_ICE_SERVERS = [{ urls: "stun:stun.l.google.com:19302" }];
8742
+ const DEFAULT_FILE_CHUNK_SIZE = 16384;
8743
+ /** UUID v4 transfer ID length when encoded as raw bytes. */
8744
+ const TRANSFER_ID_BYTES = 16;
8745
+ /** SHA-256 hash length in bytes. */
8746
+ const SHA256_BYTES = 32;
8747
+
8748
+ //#endregion
8749
+ //#region packages/provider/src/webrtc/DataChannelRouter.ts
8750
+ var DataChannelRouter = class extends EventEmitter {
8751
+ constructor(connection) {
8752
+ super();
8753
+ this.connection = connection;
8754
+ this.channels = /* @__PURE__ */ new Map();
8755
+ this.connection.ondatachannel = (event) => {
8756
+ this.registerChannel(event.channel);
8757
+ };
8758
+ }
8759
+ /** Create a named data channel (initiator side). */
8760
+ createChannel(name, options) {
8761
+ const channel = this.connection.createDataChannel(name, options);
8762
+ this.registerChannel(channel);
8763
+ return channel;
8764
+ }
8765
+ /** Create the standard set of channels for Abracadabra WebRTC. */
8766
+ createDefaultChannels(opts) {
8767
+ if (opts.enableDocSync) this.createChannel(CHANNEL_NAMES.YJS_SYNC, { ordered: true });
8768
+ if (opts.enableAwareness) this.createChannel(CHANNEL_NAMES.AWARENESS, {
8769
+ ordered: false,
8770
+ maxRetransmits: 0
8771
+ });
8772
+ if (opts.enableFileTransfer) this.createChannel(CHANNEL_NAMES.FILE_TRANSFER, { ordered: true });
8773
+ }
8774
+ getChannel(name) {
8775
+ return this.channels.get(name) ?? null;
8776
+ }
8777
+ isOpen(name) {
8778
+ return this.channels.get(name)?.readyState === "open";
8779
+ }
8780
+ registerChannel(channel) {
8781
+ channel.binaryType = "arraybuffer";
8782
+ this.channels.set(channel.label, channel);
8783
+ channel.onopen = () => {
8784
+ this.emit("channelOpen", {
8785
+ name: channel.label,
8786
+ channel
8787
+ });
8788
+ };
8789
+ channel.onclose = () => {
8790
+ this.emit("channelClose", { name: channel.label });
8791
+ this.channels.delete(channel.label);
8792
+ };
8793
+ channel.onmessage = (event) => {
8794
+ this.emit("channelMessage", {
8795
+ name: channel.label,
8796
+ data: event.data
8797
+ });
8798
+ };
8799
+ channel.onerror = (event) => {
8800
+ this.emit("channelError", {
8801
+ name: channel.label,
8802
+ error: event
8803
+ });
8804
+ };
8805
+ if (channel.readyState === "open") this.emit("channelOpen", {
8806
+ name: channel.label,
8807
+ channel
8808
+ });
8809
+ }
8810
+ close() {
8811
+ for (const channel of this.channels.values()) try {
8812
+ channel.close();
8813
+ } catch {}
8814
+ this.channels.clear();
8815
+ }
8816
+ destroy() {
8817
+ this.close();
8818
+ this.connection.ondatachannel = null;
8819
+ this.removeAllListeners();
8820
+ }
8821
+ };
8822
+
8823
+ //#endregion
8824
+ //#region packages/provider/src/webrtc/PeerConnection.ts
8825
+ var PeerConnection = class extends EventEmitter {
8826
+ constructor(peerId, iceServers) {
8827
+ super();
8828
+ this.pendingCandidates = [];
8829
+ this.hasRemoteDescription = false;
8830
+ this.peerId = peerId;
8831
+ this.connection = new RTCPeerConnection({ iceServers });
8832
+ this.router = new DataChannelRouter(this.connection);
8833
+ this.connection.onicecandidate = (event) => {
8834
+ if (event.candidate) this.emit("iceCandidate", {
8835
+ peerId: this.peerId,
8836
+ candidate: JSON.stringify(event.candidate.toJSON())
8837
+ });
8838
+ };
8839
+ this.connection.oniceconnectionstatechange = () => {
8840
+ const state = this.connection.iceConnectionState;
8841
+ this.emit("iceStateChange", {
8842
+ peerId: this.peerId,
8843
+ state
8844
+ });
8845
+ if (state === "failed") this.emit("iceFailed", { peerId: this.peerId });
8846
+ };
8847
+ this.connection.onconnectionstatechange = () => {
8848
+ this.emit("connectionStateChange", {
8849
+ peerId: this.peerId,
8850
+ state: this.connection.connectionState
8851
+ });
8852
+ };
8853
+ }
8854
+ get connectionState() {
8855
+ return this.connection.connectionState;
8856
+ }
8857
+ get iceConnectionState() {
8858
+ return this.connection.iceConnectionState;
8859
+ }
8860
+ /** Create an SDP offer (initiator side). */
8861
+ async createOffer(iceRestart = false) {
8862
+ const offer = await this.connection.createOffer(iceRestart ? { iceRestart: true } : void 0);
8863
+ await this.connection.setLocalDescription(offer);
8864
+ return JSON.stringify(this.connection.localDescription?.toJSON());
8865
+ }
8866
+ /** Set a remote offer and create an answer (receiver side). Returns the SDP answer. */
8867
+ async setRemoteOffer(sdp) {
8868
+ const offer = JSON.parse(sdp);
8869
+ await this.connection.setRemoteDescription(new RTCSessionDescription(offer));
8870
+ this.hasRemoteDescription = true;
8871
+ await this.flushPendingCandidates();
8872
+ const answer = await this.connection.createAnswer();
8873
+ await this.connection.setLocalDescription(answer);
8874
+ return JSON.stringify(this.connection.localDescription?.toJSON());
8875
+ }
8876
+ /** Set the remote answer (initiator side). */
8877
+ async setRemoteAnswer(sdp) {
8878
+ const answer = JSON.parse(sdp);
8879
+ await this.connection.setRemoteDescription(new RTCSessionDescription(answer));
8880
+ this.hasRemoteDescription = true;
8881
+ await this.flushPendingCandidates();
8882
+ }
8883
+ /** Add a remote ICE candidate. Queues if remote description not yet set. */
8884
+ async addIceCandidate(candidateJson) {
8885
+ const candidate = JSON.parse(candidateJson);
8886
+ if (!this.hasRemoteDescription) {
8887
+ this.pendingCandidates.push(candidate);
8888
+ return;
8889
+ }
8890
+ await this.connection.addIceCandidate(new RTCIceCandidate(candidate));
8891
+ }
8892
+ async flushPendingCandidates() {
8893
+ for (const candidate of this.pendingCandidates) await this.connection.addIceCandidate(new RTCIceCandidate(candidate));
8894
+ this.pendingCandidates = [];
8895
+ }
8896
+ close() {
8897
+ this.router.close();
8898
+ try {
8899
+ this.connection.close();
8900
+ } catch {}
8901
+ }
8902
+ destroy() {
8903
+ this.router.destroy();
8904
+ this.connection.onicecandidate = null;
8905
+ this.connection.oniceconnectionstatechange = null;
8906
+ this.connection.onconnectionstatechange = null;
8907
+ try {
8908
+ this.connection.close();
8909
+ } catch {}
8910
+ this.removeAllListeners();
8911
+ }
8912
+ };
8913
+
8914
+ //#endregion
8915
+ //#region packages/provider/src/webrtc/YjsDataChannel.ts
8916
+ /**
8917
+ * Handles Y.js document sync and awareness over WebRTC data channels.
8918
+ *
8919
+ * Uses the same y-protocols/sync encoding as the WebSocket provider but
8920
+ * transported over RTCDataChannel instead. A unique origin is used to
8921
+ * prevent echo loops with the server-based provider.
8922
+ */
8923
+ var YjsDataChannel = class {
8924
+ constructor(document, awareness, router) {
8925
+ this.document = document;
8926
+ this.awareness = awareness;
8927
+ this.router = router;
8928
+ this.docUpdateHandler = null;
8929
+ this.awarenessUpdateHandler = null;
8930
+ this.channelOpenHandler = null;
8931
+ this.channelMessageHandler = null;
8932
+ }
8933
+ /** Start listening for Y.js updates and data channel messages. */
8934
+ attach() {
8935
+ this.docUpdateHandler = (update, origin) => {
8936
+ if (origin === this) return;
8937
+ const channel = this.router.getChannel(CHANNEL_NAMES.YJS_SYNC);
8938
+ if (!channel || channel.readyState !== "open") return;
8939
+ const encoder = createEncoder();
8940
+ writeVarUint(encoder, YJS_MSG.UPDATE);
8941
+ writeVarUint8Array(encoder, update);
8942
+ channel.send(toUint8Array(encoder));
8943
+ };
8944
+ this.document.on("update", this.docUpdateHandler);
8945
+ if (this.awareness) {
8946
+ this.awarenessUpdateHandler = ({ added, updated, removed }, _origin) => {
8947
+ const channel = this.router.getChannel(CHANNEL_NAMES.AWARENESS);
8948
+ if (!channel || channel.readyState !== "open") return;
8949
+ const changedClients = added.concat(updated).concat(removed);
8950
+ const update = encodeAwarenessUpdate(this.awareness, changedClients);
8951
+ channel.send(update);
8952
+ };
8953
+ this.awareness.on("update", this.awarenessUpdateHandler);
8954
+ }
8955
+ this.channelMessageHandler = ({ name, data }) => {
8956
+ if (name === CHANNEL_NAMES.YJS_SYNC) this.handleSyncMessage(data);
8957
+ else if (name === CHANNEL_NAMES.AWARENESS) this.handleAwarenessMessage(data);
8958
+ };
8959
+ this.router.on("channelMessage", this.channelMessageHandler);
8960
+ this.channelOpenHandler = ({ name }) => {
8961
+ if (name === CHANNEL_NAMES.YJS_SYNC) this.sendSyncStep1();
8962
+ else if (name === CHANNEL_NAMES.AWARENESS && this.awareness) {
8963
+ const channel = this.router.getChannel(CHANNEL_NAMES.AWARENESS);
8964
+ if (channel?.readyState === "open") {
8965
+ const update = encodeAwarenessUpdate(this.awareness, Array.from(this.awareness.getStates().keys()));
8966
+ channel.send(update);
8967
+ }
8968
+ }
8969
+ };
8970
+ this.router.on("channelOpen", this.channelOpenHandler);
8971
+ if (this.router.isOpen(CHANNEL_NAMES.YJS_SYNC)) this.sendSyncStep1();
8972
+ if (this.awareness && this.router.isOpen(CHANNEL_NAMES.AWARENESS)) {
8973
+ const channel = this.router.getChannel(CHANNEL_NAMES.AWARENESS);
8974
+ if (channel?.readyState === "open") {
8975
+ const update = encodeAwarenessUpdate(this.awareness, Array.from(this.awareness.getStates().keys()));
8976
+ channel.send(update);
8977
+ }
8978
+ }
8979
+ }
8980
+ /** Stop listening and clean up handlers. */
8981
+ detach() {
8982
+ if (this.docUpdateHandler) {
8983
+ this.document.off("update", this.docUpdateHandler);
8984
+ this.docUpdateHandler = null;
8985
+ }
8986
+ if (this.awarenessUpdateHandler && this.awareness) {
8987
+ this.awareness.off("update", this.awarenessUpdateHandler);
8988
+ this.awarenessUpdateHandler = null;
8989
+ }
8990
+ if (this.channelMessageHandler) {
8991
+ this.router.off("channelMessage", this.channelMessageHandler);
8992
+ this.channelMessageHandler = null;
8993
+ }
8994
+ if (this.channelOpenHandler) {
8995
+ this.router.off("channelOpen", this.channelOpenHandler);
8996
+ this.channelOpenHandler = null;
8997
+ }
8998
+ this.isSynced = false;
8999
+ }
9000
+ destroy() {
9001
+ this.detach();
9002
+ }
9003
+ sendSyncStep1() {
9004
+ const channel = this.router.getChannel(CHANNEL_NAMES.YJS_SYNC);
9005
+ if (!channel || channel.readyState !== "open") return;
9006
+ const encoder = createEncoder();
9007
+ writeVarUint(encoder, YJS_MSG.SYNC);
9008
+ writeSyncStep1(encoder, this.document);
9009
+ channel.send(toUint8Array(encoder));
9010
+ }
9011
+ handleSyncMessage(data) {
9012
+ const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
9013
+ const decoder = createDecoder(buf);
9014
+ const msgType = readVarUint(decoder);
9015
+ if (msgType === YJS_MSG.SYNC) {
9016
+ const encoder = createEncoder();
9017
+ const syncMessageType = readSyncMessage(decoder, encoder, this.document, this);
9018
+ if (length(encoder) > 0) {
9019
+ const responseEncoder = createEncoder();
9020
+ writeVarUint(responseEncoder, YJS_MSG.SYNC);
9021
+ writeUint8Array(responseEncoder, toUint8Array(encoder));
9022
+ const channel = this.router.getChannel(CHANNEL_NAMES.YJS_SYNC);
9023
+ if (channel?.readyState === "open") channel.send(toUint8Array(responseEncoder));
9024
+ }
9025
+ if (syncMessageType === messageYjsSyncStep2) this.isSynced = true;
9026
+ } else if (msgType === YJS_MSG.UPDATE) {
9027
+ const update = readVarUint8Array(decoder);
9028
+ yjs.applyUpdate(this.document, update, this);
9029
+ }
9030
+ }
9031
+ handleAwarenessMessage(data) {
9032
+ if (!this.awareness) return;
9033
+ const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
9034
+ applyAwarenessUpdate(this.awareness, buf, this);
9035
+ }
9036
+ };
9037
+
9038
+ //#endregion
9039
+ //#region packages/provider/src/webrtc/FileTransferChannel.ts
9040
+ /**
9041
+ * Handle for tracking a file transfer in progress.
9042
+ */
9043
+ var FileTransferHandle = class extends EventEmitter {
9044
+ constructor(transferId) {
9045
+ super();
9046
+ this.progress = 0;
9047
+ this.status = "pending";
9048
+ this.abortController = new AbortController();
9049
+ this.transferId = transferId;
9050
+ }
9051
+ cancel() {
9052
+ this.status = "cancelled";
9053
+ this.abortController.abort();
9054
+ this.emit("cancelled");
9055
+ }
9056
+ get signal() {
9057
+ return this.abortController.signal;
9058
+ }
9059
+ /** @internal */
9060
+ _setProgress(p) {
9061
+ this.progress = p;
9062
+ this.emit("progress", p);
9063
+ }
9064
+ /** @internal */
9065
+ _setStatus(s) {
9066
+ this.status = s;
9067
+ }
9068
+ };
9069
+ /**
9070
+ * Chunked binary file transfer over a dedicated WebRTC data channel.
9071
+ */
9072
+ var FileTransferChannel = class extends EventEmitter {
9073
+ constructor(router, chunkSize) {
9074
+ super();
9075
+ this.router = router;
9076
+ this.receives = /* @__PURE__ */ new Map();
9077
+ this.channelMessageHandler = null;
9078
+ this.chunkSize = chunkSize ?? DEFAULT_FILE_CHUNK_SIZE;
9079
+ this.channelMessageHandler = ({ name, data }) => {
9080
+ if (name === CHANNEL_NAMES.FILE_TRANSFER) this.handleMessage(data);
9081
+ };
9082
+ this.router.on("channelMessage", this.channelMessageHandler);
9083
+ }
9084
+ /** Send a file to a peer. Returns a handle for tracking progress. */
9085
+ async send(file, filename) {
9086
+ const transferId = generateTransferId();
9087
+ const handle = new FileTransferHandle(transferId);
9088
+ const transferIdBytes = hexToBytes(transferId);
9089
+ const totalSize = file.size;
9090
+ const totalChunks = Math.ceil(totalSize / this.chunkSize);
9091
+ const meta = {
9092
+ transferId,
9093
+ filename,
9094
+ mimeType: file instanceof File ? file.type : "application/octet-stream",
9095
+ totalSize,
9096
+ chunkSize: this.chunkSize,
9097
+ totalChunks
9098
+ };
9099
+ const channel = this.router.getChannel(CHANNEL_NAMES.FILE_TRANSFER);
9100
+ if (!channel || channel.readyState !== "open") {
9101
+ handle._setStatus("error");
9102
+ handle.emit("error", /* @__PURE__ */ new Error("File transfer channel not open"));
9103
+ return handle;
9104
+ }
9105
+ const startMsg = new Uint8Array(1 + new TextEncoder().encode(JSON.stringify(meta)).length);
9106
+ startMsg[0] = FILE_MSG.START;
9107
+ startMsg.set(new TextEncoder().encode(JSON.stringify(meta)), 1);
9108
+ channel.send(startMsg);
9109
+ handle._setStatus("sending");
9110
+ const arrayBuffer = await file.arrayBuffer();
9111
+ const fileBytes = new Uint8Array(arrayBuffer);
9112
+ const hashBuffer = await crypto.subtle.digest("SHA-256", fileBytes);
9113
+ const hashBytes = new Uint8Array(hashBuffer);
9114
+ for (let i = 0; i < totalChunks; i++) {
9115
+ if (handle.signal.aborted) {
9116
+ const cancelMsg = new Uint8Array(1 + TRANSFER_ID_BYTES);
9117
+ cancelMsg[0] = FILE_MSG.CANCEL;
9118
+ cancelMsg.set(transferIdBytes, 1);
9119
+ channel.send(cancelMsg);
9120
+ return handle;
9121
+ }
9122
+ const offset = i * this.chunkSize;
9123
+ const chunk = fileBytes.slice(offset, Math.min(offset + this.chunkSize, totalSize));
9124
+ const msg = new Uint8Array(1 + TRANSFER_ID_BYTES + 4 + chunk.length);
9125
+ msg[0] = FILE_MSG.CHUNK;
9126
+ msg.set(transferIdBytes, 1);
9127
+ new DataView(msg.buffer).setUint32(1 + TRANSFER_ID_BYTES, i, false);
9128
+ msg.set(chunk, 1 + TRANSFER_ID_BYTES + 4);
9129
+ while (channel.bufferedAmount > this.chunkSize * 4) {
9130
+ await new Promise((resolve) => setTimeout(resolve, 10));
9131
+ if (handle.signal.aborted) return handle;
9132
+ }
9133
+ channel.send(msg);
9134
+ handle._setProgress((i + 1) / totalChunks);
9135
+ }
9136
+ const completeMsg = new Uint8Array(1 + TRANSFER_ID_BYTES + SHA256_BYTES);
9137
+ completeMsg[0] = FILE_MSG.COMPLETE;
9138
+ completeMsg.set(transferIdBytes, 1);
9139
+ completeMsg.set(hashBytes, 1 + TRANSFER_ID_BYTES);
9140
+ channel.send(completeMsg);
9141
+ handle._setStatus("complete");
9142
+ handle.emit("complete");
9143
+ return handle;
9144
+ }
9145
+ handleMessage(data) {
9146
+ const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
9147
+ if (buf.length < 1) return;
9148
+ switch (buf[0]) {
9149
+ case FILE_MSG.START:
9150
+ this.handleStart(buf);
9151
+ break;
9152
+ case FILE_MSG.CHUNK:
9153
+ this.handleChunk(buf);
9154
+ break;
9155
+ case FILE_MSG.COMPLETE:
9156
+ this.handleComplete(buf);
9157
+ break;
9158
+ case FILE_MSG.CANCEL:
9159
+ this.handleCancel(buf);
9160
+ break;
9161
+ }
9162
+ }
9163
+ handleStart(buf) {
9164
+ const json = new TextDecoder().decode(buf.slice(1));
9165
+ let meta;
9166
+ try {
9167
+ meta = JSON.parse(json);
9168
+ } catch {
9169
+ return;
9170
+ }
9171
+ this.receives.set(meta.transferId, {
9172
+ meta,
9173
+ chunks: new Array(meta.totalChunks).fill(null),
9174
+ receivedCount: 0
9175
+ });
9176
+ this.emit("receiveStart", meta);
9177
+ }
9178
+ handleChunk(buf) {
9179
+ if (buf.length < 1 + TRANSFER_ID_BYTES + 4) return;
9180
+ const transferId = bytesToHex(buf.slice(1, 1 + TRANSFER_ID_BYTES));
9181
+ const chunkIndex = new DataView(buf.buffer, buf.byteOffset).getUint32(1 + TRANSFER_ID_BYTES, false);
9182
+ const chunkData = buf.slice(1 + TRANSFER_ID_BYTES + 4);
9183
+ const state = this.receives.get(transferId);
9184
+ if (!state) return;
9185
+ if (chunkIndex < state.chunks.length && !state.chunks[chunkIndex]) {
9186
+ state.chunks[chunkIndex] = chunkData;
9187
+ state.receivedCount++;
9188
+ const progress = state.receivedCount / state.meta.totalChunks;
9189
+ this.emit("receiveProgress", {
9190
+ transferId,
9191
+ received: state.receivedCount,
9192
+ total: state.meta.totalChunks,
9193
+ progress
9194
+ });
9195
+ }
9196
+ }
9197
+ async handleComplete(buf) {
9198
+ if (buf.length < 1 + TRANSFER_ID_BYTES + SHA256_BYTES) return;
9199
+ const transferId = bytesToHex(buf.slice(1, 1 + TRANSFER_ID_BYTES));
9200
+ const expectedHash = buf.slice(1 + TRANSFER_ID_BYTES, 1 + TRANSFER_ID_BYTES + SHA256_BYTES);
9201
+ const state = this.receives.get(transferId);
9202
+ if (!state) return;
9203
+ const totalSize = state.meta.totalSize;
9204
+ const assembled = new Uint8Array(totalSize);
9205
+ let offset = 0;
9206
+ for (let i = 0; i < state.chunks.length; i++) {
9207
+ const chunk = state.chunks[i];
9208
+ if (!chunk) {
9209
+ this.emit("receiveError", {
9210
+ transferId,
9211
+ error: `Missing chunk ${i}`
9212
+ });
9213
+ this.receives.delete(transferId);
9214
+ return;
9215
+ }
9216
+ assembled.set(chunk, offset);
9217
+ offset += chunk.length;
9218
+ }
9219
+ const actualHashBuffer = await crypto.subtle.digest("SHA-256", assembled);
9220
+ if (!constantTimeEqual(expectedHash, new Uint8Array(actualHashBuffer))) {
9221
+ this.emit("receiveError", {
9222
+ transferId,
9223
+ error: "SHA-256 integrity check failed"
9224
+ });
9225
+ this.receives.delete(transferId);
9226
+ return;
9227
+ }
9228
+ const blob = new Blob([assembled], { type: state.meta.mimeType });
9229
+ this.emit("receiveComplete", {
9230
+ transferId,
9231
+ blob,
9232
+ filename: state.meta.filename,
9233
+ mimeType: state.meta.mimeType,
9234
+ size: state.meta.totalSize
9235
+ });
9236
+ this.receives.delete(transferId);
9237
+ }
9238
+ handleCancel(buf) {
9239
+ if (buf.length < 1 + TRANSFER_ID_BYTES) return;
9240
+ const transferId = bytesToHex(buf.slice(1, 1 + TRANSFER_ID_BYTES));
9241
+ this.receives.delete(transferId);
9242
+ this.emit("receiveCancelled", { transferId });
9243
+ }
9244
+ destroy() {
9245
+ if (this.channelMessageHandler) {
9246
+ this.router.off("channelMessage", this.channelMessageHandler);
9247
+ this.channelMessageHandler = null;
9248
+ }
9249
+ this.receives.clear();
9250
+ this.removeAllListeners();
9251
+ }
9252
+ };
9253
+ function generateTransferId() {
9254
+ const bytes = new Uint8Array(TRANSFER_ID_BYTES);
9255
+ crypto.getRandomValues(bytes);
9256
+ return bytesToHex(bytes);
9257
+ }
9258
+ function bytesToHex(bytes) {
9259
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
9260
+ }
9261
+ function hexToBytes(hex) {
9262
+ const bytes = new Uint8Array(hex.length / 2);
9263
+ for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
9264
+ return bytes;
9265
+ }
9266
+ function constantTimeEqual(a, b) {
9267
+ if (a.length !== b.length) return false;
9268
+ let result = 0;
9269
+ for (let i = 0; i < a.length; i++) result |= a[i] ^ b[i];
9270
+ return result === 0;
9271
+ }
9272
+
9273
+ //#endregion
9274
+ //#region packages/provider/src/webrtc/AbracadabraWebRTC.ts
9275
+ const HAS_RTC = typeof globalThis.RTCPeerConnection !== "undefined";
9276
+ /**
9277
+ * Optional WebRTC provider for peer-to-peer Y.js sync, awareness, and file transfer.
9278
+ *
9279
+ * Uses the server's signaling endpoint (`/ws/:doc_id/signaling`) for connection
9280
+ * negotiation, then establishes direct data channels between peers. Designed to
9281
+ * work alongside `AbracadabraProvider` — the server remains the persistence layer,
9282
+ * while WebRTC provides low-latency P2P sync.
9283
+ *
9284
+ * Falls back to a no-op when `RTCPeerConnection` is unavailable (e.g. Node.js).
9285
+ */
9286
+ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9287
+ constructor(configuration) {
9288
+ super();
9289
+ this.signaling = null;
9290
+ this.peerConnections = /* @__PURE__ */ new Map();
9291
+ this.yjsChannels = /* @__PURE__ */ new Map();
9292
+ this.fileChannels = /* @__PURE__ */ new Map();
9293
+ this.peers = /* @__PURE__ */ new Map();
9294
+ this.localPeerId = null;
9295
+ this.isConnected = false;
9296
+ const doc = configuration.document ?? null;
9297
+ const awareness = configuration.awareness ?? null;
9298
+ this.config = {
9299
+ docId: configuration.docId,
9300
+ url: configuration.url,
9301
+ token: configuration.token,
9302
+ document: doc,
9303
+ awareness,
9304
+ iceServers: configuration.iceServers ?? DEFAULT_ICE_SERVERS,
9305
+ displayName: configuration.displayName ?? null,
9306
+ color: configuration.color ?? null,
9307
+ enableDocSync: configuration.enableDocSync ?? !!doc,
9308
+ enableAwarenessSync: configuration.enableAwarenessSync ?? !!awareness,
9309
+ enableFileTransfer: configuration.enableFileTransfer ?? false,
9310
+ fileChunkSize: configuration.fileChunkSize ?? 16384,
9311
+ WebSocketPolyfill: configuration.WebSocketPolyfill
9312
+ };
9313
+ if (configuration.autoConnect !== false && HAS_RTC) this.connect();
9314
+ }
9315
+ /**
9316
+ * Create an AbracadabraWebRTC instance from an existing provider,
9317
+ * reusing its document, awareness, URL, and token.
9318
+ */
9319
+ static fromProvider(provider, options) {
9320
+ const config = provider.configuration;
9321
+ const httpUrl = (config.websocketProvider?.url ?? config.url ?? "").replace(/^wss:/, "https:").replace(/^ws:/, "http:");
9322
+ return new AbracadabraWebRTC({
9323
+ docId: config.name,
9324
+ url: httpUrl,
9325
+ token: config.token,
9326
+ document: provider.document,
9327
+ awareness: provider.awareness,
9328
+ ...options
9329
+ });
9330
+ }
9331
+ async connect() {
9332
+ if (!HAS_RTC) return;
9333
+ if (this.isConnected) return;
9334
+ this.signaling = new SignalingSocket({
9335
+ url: this.buildSignalingUrl(),
9336
+ token: this.config.token,
9337
+ autoConnect: false,
9338
+ WebSocketPolyfill: this.config.WebSocketPolyfill
9339
+ });
9340
+ this.signaling.on("welcome", (data) => {
9341
+ this.localPeerId = data.peerId;
9342
+ this.isConnected = true;
9343
+ this.emit("connected");
9344
+ if (this.config.displayName && this.config.color) this.signaling.sendProfile(this.config.displayName, this.config.color);
9345
+ for (const peer of data.peers) {
9346
+ this.addPeer(peer);
9347
+ if (this.localPeerId < peer.peer_id) this.initiateConnection(peer.peer_id);
9348
+ }
9349
+ });
9350
+ this.signaling.on("joined", (peer) => {
9351
+ this.addPeer(peer);
9352
+ this.emit("peerJoined", peer);
9353
+ if (this.localPeerId < peer.peer_id) this.initiateConnection(peer.peer_id);
9354
+ });
9355
+ this.signaling.on("left", ({ peerId }) => {
9356
+ this.removePeer(peerId);
9357
+ this.emit("peerLeft", { peerId });
9358
+ });
9359
+ this.signaling.on("offer", async ({ from, sdp }) => {
9360
+ await this.handleOffer(from, sdp);
9361
+ });
9362
+ this.signaling.on("answer", async ({ from, sdp }) => {
9363
+ const pc = this.peerConnections.get(from);
9364
+ if (pc) await pc.setRemoteAnswer(sdp);
9365
+ });
9366
+ this.signaling.on("ice", async ({ from, candidate }) => {
9367
+ const pc = this.peerConnections.get(from);
9368
+ if (pc) await pc.addIceCandidate(candidate);
9369
+ });
9370
+ this.signaling.on("mute", ({ peerId, muted }) => {
9371
+ const peer = this.peers.get(peerId);
9372
+ if (peer) {
9373
+ peer.muted = muted;
9374
+ this.emit("peerMuted", {
9375
+ peerId,
9376
+ muted
9377
+ });
9378
+ }
9379
+ });
9380
+ this.signaling.on("media-state", ({ peerId, video, screen }) => {
9381
+ const peer = this.peers.get(peerId);
9382
+ if (peer) {
9383
+ peer.video = video;
9384
+ peer.screen = screen;
9385
+ this.emit("peerMediaState", {
9386
+ peerId,
9387
+ video,
9388
+ screen
9389
+ });
9390
+ }
9391
+ });
9392
+ this.signaling.on("profile", ({ peerId, name, color }) => {
9393
+ const peer = this.peers.get(peerId);
9394
+ if (peer) {
9395
+ peer.name = name;
9396
+ peer.color = color;
9397
+ this.emit("peerProfile", {
9398
+ peerId,
9399
+ name,
9400
+ color
9401
+ });
9402
+ }
9403
+ });
9404
+ this.signaling.on("disconnected", () => {
9405
+ this.isConnected = false;
9406
+ this.localPeerId = null;
9407
+ this.removeAllPeers();
9408
+ this.emit("disconnected");
9409
+ });
9410
+ this.signaling.on("error", (err) => {
9411
+ this.emit("signalingError", err);
9412
+ });
9413
+ await this.signaling.connect();
9414
+ }
9415
+ disconnect() {
9416
+ if (!HAS_RTC) return;
9417
+ this.removeAllPeers();
9418
+ if (this.signaling) {
9419
+ this.signaling.disconnect();
9420
+ this.signaling = null;
9421
+ }
9422
+ this.isConnected = false;
9423
+ this.localPeerId = null;
9424
+ }
9425
+ destroy() {
9426
+ this.disconnect();
9427
+ if (this.signaling) {
9428
+ this.signaling.destroy();
9429
+ this.signaling = null;
9430
+ }
9431
+ this.removeAllListeners();
9432
+ }
9433
+ setMuted(muted) {
9434
+ this.signaling?.sendMute(muted);
9435
+ }
9436
+ setMediaState(video, screen) {
9437
+ this.signaling?.sendMediaState(video, screen);
9438
+ }
9439
+ setProfile(name, color) {
9440
+ this.signaling?.sendProfile(name, color);
9441
+ }
9442
+ /**
9443
+ * Send a file to a specific peer. Returns a handle for tracking progress.
9444
+ */
9445
+ async sendFile(peerId, file, filename) {
9446
+ const fc = this.fileChannels.get(peerId);
9447
+ if (!fc) return null;
9448
+ return fc.send(file, filename);
9449
+ }
9450
+ /**
9451
+ * Send a file to all connected peers. Returns an array of handles.
9452
+ */
9453
+ async broadcastFile(file, filename) {
9454
+ const handles = [];
9455
+ for (const [peerId, fc] of this.fileChannels) {
9456
+ const handle = await fc.send(file, filename);
9457
+ handles.push(handle);
9458
+ }
9459
+ return handles;
9460
+ }
9461
+ /**
9462
+ * Send a custom string message to a specific peer via a data channel.
9463
+ */
9464
+ sendCustomMessage(peerId, payload) {
9465
+ const pc = this.peerConnections.get(peerId);
9466
+ if (!pc) return;
9467
+ let channel = pc.router.getChannel("custom");
9468
+ if (!channel || channel.readyState !== "open") {
9469
+ channel = pc.router.createChannel("custom", { ordered: true });
9470
+ channel.onopen = () => {
9471
+ channel.send(payload);
9472
+ };
9473
+ return;
9474
+ }
9475
+ channel.send(payload);
9476
+ }
9477
+ /**
9478
+ * Send a custom string message to all connected peers.
9479
+ */
9480
+ broadcastCustomMessage(payload) {
9481
+ for (const peerId of this.peerConnections.keys()) this.sendCustomMessage(peerId, payload);
9482
+ }
9483
+ addPeer(info) {
9484
+ this.peers.set(info.peer_id, {
9485
+ ...info,
9486
+ connectionState: "new"
9487
+ });
9488
+ }
9489
+ removePeer(peerId) {
9490
+ this.peers.delete(peerId);
9491
+ const yjs = this.yjsChannels.get(peerId);
9492
+ if (yjs) {
9493
+ yjs.destroy();
9494
+ this.yjsChannels.delete(peerId);
9495
+ }
9496
+ const fc = this.fileChannels.get(peerId);
9497
+ if (fc) {
9498
+ fc.destroy();
9499
+ this.fileChannels.delete(peerId);
9500
+ }
9501
+ const pc = this.peerConnections.get(peerId);
9502
+ if (pc) {
9503
+ pc.destroy();
9504
+ this.peerConnections.delete(peerId);
9505
+ }
9506
+ }
9507
+ removeAllPeers() {
9508
+ for (const peerId of [...this.peers.keys()]) this.removePeer(peerId);
9509
+ }
9510
+ createPeerConnection(peerId) {
9511
+ const pc = new PeerConnection(peerId, this.config.iceServers);
9512
+ pc.on("iceCandidate", ({ peerId, candidate }) => {
9513
+ this.signaling?.sendIce(peerId, candidate);
9514
+ });
9515
+ pc.on("iceFailed", async ({ peerId }) => {
9516
+ try {
9517
+ const sdp = await pc.createOffer(true);
9518
+ this.signaling?.sendOffer(peerId, sdp);
9519
+ } catch {
9520
+ this.removePeer(peerId);
9521
+ }
9522
+ });
9523
+ pc.on("connectionStateChange", ({ peerId, state }) => {
9524
+ const peer = this.peers.get(peerId);
9525
+ if (peer) peer.connectionState = state;
9526
+ if (state === "disconnected" || state === "closed") {}
9527
+ this.emit("peerConnectionState", {
9528
+ peerId,
9529
+ state
9530
+ });
9531
+ });
9532
+ pc.router.on("channelMessage", ({ name, data }) => {
9533
+ if (name === "custom") {
9534
+ const payload = typeof data === "string" ? data : new TextDecoder().decode(data);
9535
+ this.emit("customMessage", {
9536
+ peerId,
9537
+ payload
9538
+ });
9539
+ }
9540
+ });
9541
+ this.peerConnections.set(peerId, pc);
9542
+ this.attachDataHandlers(peerId, pc);
9543
+ return pc;
9544
+ }
9545
+ attachDataHandlers(peerId, pc) {
9546
+ if (this.config.document && this.config.enableDocSync) {
9547
+ const yjs = new YjsDataChannel(this.config.document, this.config.enableAwarenessSync ? this.config.awareness : null, pc.router);
9548
+ yjs.attach();
9549
+ this.yjsChannels.set(peerId, yjs);
9550
+ }
9551
+ if (this.config.enableFileTransfer) {
9552
+ const fc = new FileTransferChannel(pc.router, this.config.fileChunkSize);
9553
+ fc.on("receiveStart", (meta) => {
9554
+ this.emit("fileReceiveStart", {
9555
+ peerId,
9556
+ ...meta
9557
+ });
9558
+ });
9559
+ fc.on("receiveProgress", (data) => {
9560
+ this.emit("fileReceiveProgress", {
9561
+ peerId,
9562
+ ...data
9563
+ });
9564
+ });
9565
+ fc.on("receiveComplete", (data) => {
9566
+ this.emit("fileReceiveComplete", {
9567
+ peerId,
9568
+ ...data
9569
+ });
9570
+ });
9571
+ fc.on("receiveError", (data) => {
9572
+ this.emit("fileReceiveError", {
9573
+ peerId,
9574
+ ...data
9575
+ });
9576
+ });
9577
+ fc.on("receiveCancelled", (data) => {
9578
+ this.emit("fileReceiveCancelled", {
9579
+ peerId,
9580
+ ...data
9581
+ });
9582
+ });
9583
+ this.fileChannels.set(peerId, fc);
9584
+ }
9585
+ }
9586
+ async initiateConnection(peerId) {
9587
+ const pc = this.createPeerConnection(peerId);
9588
+ pc.router.createDefaultChannels({
9589
+ enableDocSync: this.config.enableDocSync,
9590
+ enableAwareness: this.config.enableAwarenessSync,
9591
+ enableFileTransfer: this.config.enableFileTransfer
9592
+ });
9593
+ try {
9594
+ const sdp = await pc.createOffer();
9595
+ this.signaling?.sendOffer(peerId, sdp);
9596
+ } catch {
9597
+ this.removePeer(peerId);
9598
+ }
9599
+ }
9600
+ async handleOffer(from, sdp) {
9601
+ const existing = this.peerConnections.get(from);
9602
+ if (existing) {
9603
+ existing.destroy();
9604
+ this.yjsChannels.get(from)?.destroy();
9605
+ this.yjsChannels.delete(from);
9606
+ this.fileChannels.get(from)?.destroy();
9607
+ this.fileChannels.delete(from);
9608
+ this.peerConnections.delete(from);
9609
+ }
9610
+ const pc = this.createPeerConnection(from);
9611
+ try {
9612
+ const answerSdp = await pc.setRemoteOffer(sdp);
9613
+ this.signaling?.sendAnswer(from, answerSdp);
9614
+ } catch {
9615
+ this.removePeer(from);
9616
+ }
9617
+ }
9618
+ buildSignalingUrl() {
9619
+ let base = this.config.url;
9620
+ while (base.endsWith("/")) base = base.slice(0, -1);
9621
+ base = base.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
9622
+ return `${base}/ws/${encodeURIComponent(this.config.docId)}/signaling`;
9623
+ }
9624
+ };
9625
+
8450
9626
  //#endregion
8451
9627
  exports.AbracadabraBaseProvider = AbracadabraBaseProvider;
8452
9628
  exports.AbracadabraClient = AbracadabraClient;
8453
9629
  exports.AbracadabraProvider = AbracadabraProvider;
8454
9630
  exports.AbracadabraWS = AbracadabraWS;
9631
+ exports.AbracadabraWebRTC = AbracadabraWebRTC;
8455
9632
  exports.AuthMessageType = AuthMessageType;
8456
9633
  exports.AwarenessError = AwarenessError;
8457
9634
  exports.BackgroundSyncManager = BackgroundSyncManager;
8458
9635
  exports.BackgroundSyncPersistence = BackgroundSyncPersistence;
9636
+ exports.CHANNEL_NAMES = CHANNEL_NAMES;
8459
9637
  exports.ConnectionTimeout = ConnectionTimeout;
8460
9638
  exports.CryptoIdentityKeystore = CryptoIdentityKeystore;
9639
+ exports.DEFAULT_FILE_CHUNK_SIZE = DEFAULT_FILE_CHUNK_SIZE;
9640
+ exports.DEFAULT_ICE_SERVERS = DEFAULT_ICE_SERVERS;
9641
+ exports.DataChannelRouter = DataChannelRouter;
8461
9642
  exports.DocKeyManager = DocKeyManager;
8462
9643
  exports.DocumentCache = DocumentCache;
8463
9644
  exports.E2EAbracadabraProvider = E2EAbracadabraProvider;
@@ -8465,18 +9646,23 @@ exports.E2EOfflineStore = E2EOfflineStore;
8465
9646
  exports.EncryptedYMap = EncryptedYMap;
8466
9647
  exports.EncryptedYText = EncryptedYText;
8467
9648
  exports.FileBlobStore = FileBlobStore;
9649
+ exports.FileTransferChannel = FileTransferChannel;
9650
+ exports.FileTransferHandle = FileTransferHandle;
8468
9651
  exports.Forbidden = Forbidden;
8469
9652
  exports.HocuspocusProvider = HocuspocusProvider;
8470
9653
  exports.HocuspocusProviderWebsocket = HocuspocusProviderWebsocket;
8471
9654
  exports.MessageTooBig = MessageTooBig;
8472
9655
  exports.MessageType = MessageType;
8473
9656
  exports.OfflineStore = OfflineStore;
9657
+ exports.PeerConnection = PeerConnection;
8474
9658
  exports.ResetConnection = ResetConnection;
8475
9659
  exports.SearchIndex = SearchIndex;
9660
+ exports.SignalingSocket = SignalingSocket;
8476
9661
  exports.SubdocMessage = SubdocMessage;
8477
9662
  exports.Unauthorized = Unauthorized;
8478
9663
  exports.WebSocketStatus = WebSocketStatus;
8479
9664
  exports.WsReadyStates = WsReadyStates;
9665
+ exports.YjsDataChannel = YjsDataChannel;
8480
9666
  exports.attachUpdatedAtObserver = attachUpdatedAtObserver;
8481
9667
  exports.awarenessStatesToArray = awarenessStatesToArray;
8482
9668
  exports.decryptField = decryptField;