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