@abraca/dabra 1.0.1 → 1.0.3

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