@abraca/dabra 1.0.13 → 1.0.15

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.
@@ -1005,9 +1005,20 @@ var EventEmitter = class {
1005
1005
  this.callbacks[event].push(fn);
1006
1006
  return this;
1007
1007
  }
1008
+ once(event, fn) {
1009
+ const wrapper = (...args) => {
1010
+ this.off(event, wrapper);
1011
+ fn.apply(this, args);
1012
+ };
1013
+ return this.on(event, wrapper);
1014
+ }
1008
1015
  emit(event, ...args) {
1009
1016
  const callbacks = this.callbacks[event];
1010
- if (callbacks) callbacks.forEach((callback) => callback.apply(this, args));
1017
+ if (callbacks) for (const callback of callbacks) try {
1018
+ callback.apply(this, args);
1019
+ } catch (err) {
1020
+ console.error(`[EventEmitter] Error in "${event}" listener:`, err);
1021
+ }
1011
1022
  return this;
1012
1023
  }
1013
1024
  off(event, fn) {
@@ -1847,7 +1858,11 @@ var AbracadabraWS = class extends EventEmitter {
1847
1858
  this.resolveConnectionAttempt();
1848
1859
  this.lastMessageReceived = getUnixTime();
1849
1860
  const documentName = new IncomingMessage(event.data).peekVarString();
1850
- this.configuration.providerMap.get(documentName)?.onMessage(event);
1861
+ try {
1862
+ this.configuration.providerMap.get(documentName)?.onMessage(event);
1863
+ } catch (err) {
1864
+ console.error(`[AbracadabraWS] Provider onMessage error for "${documentName}":`, err);
1865
+ }
1851
1866
  }
1852
1867
  resolveConnectionAttempt() {
1853
1868
  if (this.connectionAttempt) {
@@ -2274,6 +2289,10 @@ var AwarenessError = class extends Error {
2274
2289
  }
2275
2290
  };
2276
2291
  var AbracadabraBaseProvider = class extends EventEmitter {
2292
+ /** Current WebSocket connection status. */
2293
+ get connectionStatus() {
2294
+ return this.configuration.websocketProvider.status;
2295
+ }
2277
2296
  constructor(configuration) {
2278
2297
  super();
2279
2298
  this.configuration = {
@@ -2792,7 +2811,9 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
2792
2811
  this.document.on("subdocs", this.boundHandleYSubdocsChange);
2793
2812
  this.on("synced", () => this.flushPendingUpdates());
2794
2813
  this.restorePermissionSnapshot();
2795
- this.ready = this._initFromOfflineStore();
2814
+ this.ready = Promise.race([this._initFromOfflineStore(), new Promise((resolve) => setTimeout(resolve, 5e3))]).catch((err) => {
2815
+ this.emit("error", { message: `Offline store init failed: ${err?.message ?? err}` });
2816
+ });
2796
2817
  }
2797
2818
  /**
2798
2819
  * Extract the server hostname from the provider configuration.
@@ -2910,7 +2931,9 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
2910
2931
  }
2911
2932
  const msg = parsed;
2912
2933
  if (msg.type === "auth_challenge" && msg.challenge && msg.expiresAt) {
2913
- this.handleAuthChallenge(msg.challenge, msg.expiresAt).catch(() => null);
2934
+ this.handleAuthChallenge(msg.challenge, msg.expiresAt).catch((err) => {
2935
+ this.emit("authenticationFailed", { reason: `Auth challenge error: ${err?.message ?? err}` });
2936
+ });
2914
2937
  return;
2915
2938
  }
2916
2939
  if (msg.type === "subdoc_registered" && msg.child_id && msg.parent_id) {
@@ -2952,6 +2975,14 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
2952
2975
  createdAt: Date.now()
2953
2976
  });
2954
2977
  }
2978
+ /** Get a loaded child provider by ID, or null if not yet loaded. */
2979
+ getChild(childId) {
2980
+ return this.childProviders.get(childId) ?? null;
2981
+ }
2982
+ /** Check if a child provider is already loaded. */
2983
+ hasChild(childId) {
2984
+ return this.childProviders.has(childId);
2985
+ }
2955
2986
  /**
2956
2987
  * Create (or return cached) a child AbracadabraProvider for a given
2957
2988
  * child document id. Each child opens its own WebSocket connection because
@@ -3082,6 +3113,17 @@ var AbracadabraClient = class {
3082
3113
  get isAuthenticated() {
3083
3114
  return this._token !== null;
3084
3115
  }
3116
+ /** Check if the current JWT token is present and not expired. */
3117
+ isTokenValid() {
3118
+ if (!this._token) return false;
3119
+ try {
3120
+ const [, payload] = this._token.split(".");
3121
+ const { exp } = JSON.parse(atob(payload));
3122
+ return typeof exp === "number" && exp * 1e3 > Date.now();
3123
+ } catch {
3124
+ return false;
3125
+ }
3126
+ }
3085
3127
  /** Derives ws:// or wss:// URL from the http(s) base URL. */
3086
3128
  get wsUrl() {
3087
3129
  return this.baseUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") + "/ws";
@@ -3175,12 +3217,7 @@ var AbracadabraClient = class {
3175
3217
  }
3176
3218
  /** Get the caller's key envelope for a document (for decrypting the DocKey). */
3177
3219
  async getMyKeyEnvelope(docId) {
3178
- try {
3179
- return await this.request("GET", `/docs/${encodeURIComponent(docId)}/key-envelope`);
3180
- } catch (e) {
3181
- if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return null;
3182
- throw e;
3183
- }
3220
+ return this.requestOrNull("GET", `/docs/${encodeURIComponent(docId)}/key-envelope`);
3184
3221
  }
3185
3222
  /** Upload key envelopes for a document (Owner only). */
3186
3223
  async uploadKeyEnvelopes(docId, opts) {
@@ -3188,12 +3225,7 @@ var AbracadabraClient = class {
3188
3225
  }
3189
3226
  /** Get the X25519 public key for a user. */
3190
3227
  async getUserX25519Key(userId) {
3191
- try {
3192
- return (await this.request("GET", `/users/${encodeURIComponent(userId)}/x25519-key`)).x25519_key;
3193
- } catch (e) {
3194
- if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return null;
3195
- throw e;
3196
- }
3228
+ return (await this.requestOrNull("GET", `/users/${encodeURIComponent(userId)}/x25519-key`))?.x25519_key ?? null;
3197
3229
  }
3198
3230
  /** List all non-revoked keys for a user (Owner/Admin or self). */
3199
3231
  async listUserKeys(userId) {
@@ -3271,14 +3303,15 @@ var AbracadabraClient = class {
3271
3303
  async listEffectivePermissions(docId) {
3272
3304
  try {
3273
3305
  return await this.request("GET", `/docs/${encodeURIComponent(docId)}/effective-permissions`);
3274
- } catch {
3275
- return {
3306
+ } catch (e) {
3307
+ if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return {
3276
3308
  permissions: (await this.listPermissions(docId)).map((p) => ({
3277
3309
  ...p,
3278
3310
  source: "direct"
3279
3311
  })),
3280
3312
  default_role: "viewer"
3281
3313
  };
3314
+ throw e;
3282
3315
  }
3283
3316
  }
3284
3317
  /** Grant or change a user's role on a document (requires Owner). */
@@ -3357,12 +3390,7 @@ var AbracadabraClient = class {
3357
3390
  }
3358
3391
  /** Get the hub space, or null if none is configured. */
3359
3392
  async getHubSpace() {
3360
- try {
3361
- return await this.request("GET", "/spaces/hub", { auth: false });
3362
- } catch (e) {
3363
- if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return null;
3364
- throw e;
3365
- }
3393
+ return this.requestOrNull("GET", "/spaces/hub", { auth: false });
3366
3394
  }
3367
3395
  /** Create a new space (auth required). */
3368
3396
  async createSpace(opts) {
@@ -3405,25 +3433,35 @@ var AbracadabraClient = class {
3405
3433
  if (auth && this._token) headers["Authorization"] = `Bearer ${this._token}`;
3406
3434
  const init = {
3407
3435
  method,
3408
- headers
3436
+ headers,
3437
+ signal: AbortSignal.timeout(3e4)
3409
3438
  };
3410
3439
  if (opts?.body !== void 0) {
3411
3440
  headers["Content-Type"] = "application/json";
3412
3441
  init.body = JSON.stringify(opts.body);
3413
3442
  }
3414
3443
  const res = await this._fetch(`${this.baseUrl}${path}`, init);
3415
- if (!res.ok) throw await this.toError(res);
3444
+ if (!res.ok) throw await this.toError(res, method, path);
3416
3445
  if (res.status === 204) return;
3417
3446
  return res.json();
3418
3447
  }
3419
- async toError(res) {
3448
+ async requestOrNull(method, path, opts) {
3449
+ try {
3450
+ return await this.request(method, path, opts);
3451
+ } catch (e) {
3452
+ if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return null;
3453
+ throw e;
3454
+ }
3455
+ }
3456
+ async toError(res, method, path) {
3420
3457
  let message;
3421
3458
  try {
3422
3459
  message = (await res.json()).error ?? res.statusText;
3423
3460
  } catch {
3424
3461
  message = res.statusText;
3425
3462
  }
3426
- const err = new Error(message);
3463
+ const prefix = method && path ? `${method} ${path}: ` : "";
3464
+ const err = /* @__PURE__ */ new Error(`${prefix}${message} (${res.status})`);
3427
3465
  err.status = res.status;
3428
3466
  return err;
3429
3467
  }
@@ -7442,6 +7480,7 @@ var SearchIndex = class {
7442
7480
  if (!db) return [];
7443
7481
  const queryTrigrams = [...extractTrigrams(query)];
7444
7482
  if (queryTrigrams.length === 0) return [];
7483
+ const maxScoreEntries = limit * 10;
7445
7484
  return new Promise((resolve, reject) => {
7446
7485
  const tx = db.transaction("postings", "readonly");
7447
7486
  const postings = tx.objectStore("postings");
@@ -7451,7 +7490,10 @@ var SearchIndex = class {
7451
7490
  const req = postings.get(trigram);
7452
7491
  req.onsuccess = () => {
7453
7492
  const docIds = req.result ?? [];
7454
- for (const docId of docIds) scores.set(docId, (scores.get(docId) ?? 0) + 1);
7493
+ for (const docId of docIds) {
7494
+ scores.set(docId, (scores.get(docId) ?? 0) + 1);
7495
+ if (scores.size >= maxScoreEntries) break;
7496
+ }
7455
7497
  remaining--;
7456
7498
  if (remaining === 0) resolve([...scores.entries()].map(([docId, score]) => ({
7457
7499
  docId,
@@ -7509,7 +7551,7 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
7509
7551
  this.db = null;
7510
7552
  this.objectUrls = /* @__PURE__ */ new Map();
7511
7553
  this._notFound = /* @__PURE__ */ new Map();
7512
- this._flushing = false;
7554
+ this._flushPromise = null;
7513
7555
  this.origin = serverOrigin;
7514
7556
  this.client = client ?? null;
7515
7557
  this._onlineHandler = () => {
@@ -7609,6 +7651,62 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
7609
7651
  const tx = db.transaction("blobs", "readonly");
7610
7652
  return (await txPromise(tx.objectStore("blobs"), tx.objectStore("blobs").get(key)))?.blob ?? null;
7611
7653
  }
7654
+ /** Return metadata for all cached blobs (for storage stats). */
7655
+ async getAllCachedEntries() {
7656
+ const db = await this.getDb();
7657
+ if (!db) return [];
7658
+ return new Promise((resolve, reject) => {
7659
+ const tx = db.transaction("blobs", "readonly");
7660
+ const store = tx.objectStore("blobs");
7661
+ const keysReq = store.getAllKeys();
7662
+ const valuesReq = store.getAll();
7663
+ tx.oncomplete = () => {
7664
+ const keys = keysReq.result;
7665
+ const values = valuesReq.result;
7666
+ resolve(keys.map((key, i) => {
7667
+ const slashIdx = key.indexOf("/");
7668
+ const docId = key.slice(0, slashIdx);
7669
+ const uploadId = key.slice(slashIdx + 1);
7670
+ const e = values[i];
7671
+ return {
7672
+ docId,
7673
+ uploadId,
7674
+ filename: e.filename,
7675
+ mimeType: e.mime_type,
7676
+ size: e.blob.size,
7677
+ cachedAt: e.cachedAt
7678
+ };
7679
+ }));
7680
+ };
7681
+ tx.onerror = () => reject(tx.error);
7682
+ });
7683
+ }
7684
+ /** Revoke all object URLs and clear the entire blob cache from IDB. */
7685
+ async clearAllBlobs() {
7686
+ for (const url of this.objectUrls.values()) URL.revokeObjectURL(url);
7687
+ this.objectUrls.clear();
7688
+ const db = await this.getDb();
7689
+ if (!db) return;
7690
+ return new Promise((resolve, reject) => {
7691
+ const req = db.transaction("blobs", "readwrite").objectStore("blobs").clear();
7692
+ req.onsuccess = () => resolve();
7693
+ req.onerror = () => reject(req.error);
7694
+ });
7695
+ }
7696
+ /**
7697
+ * Revoke the in-memory object URL without touching the IDB cache.
7698
+ * The next call to getBlobUrl() will re-create a fresh URL from IDB.
7699
+ * Use this when an <img> @error fires — the blob data is fine, only
7700
+ * the object URL reference is stale.
7701
+ */
7702
+ invalidateUrl(docId, uploadId) {
7703
+ const key = this.blobKey(docId, uploadId);
7704
+ const url = this.objectUrls.get(key);
7705
+ if (url) {
7706
+ URL.revokeObjectURL(url);
7707
+ this.objectUrls.delete(key);
7708
+ }
7709
+ }
7612
7710
  /** Revoke the object URL and remove the blob from cache. */
7613
7711
  async evictBlob(docId, uploadId) {
7614
7712
  const key = this.blobKey(docId, uploadId);
@@ -7661,38 +7759,42 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
7661
7759
  * Entries that fail are marked with status "error" and left in the queue.
7662
7760
  */
7663
7761
  async flushQueue() {
7664
- if (this._flushing || !this.client) return;
7665
- this._flushing = true;
7762
+ if (this._flushPromise || !this.client) return;
7763
+ this._flushPromise = this._doFlush();
7666
7764
  try {
7667
- const pending = (await this.getQueue()).filter((e) => e.status === "pending");
7668
- for (const entry of pending) {
7669
- await this._updateQueueEntry(entry.id, { status: "uploading" });
7670
- this.emit("upload:started", {
7765
+ await this._flushPromise;
7766
+ } finally {
7767
+ this._flushPromise = null;
7768
+ }
7769
+ }
7770
+ async _doFlush() {
7771
+ if (!this.client) return;
7772
+ const pending = (await this.getQueue()).filter((e) => e.status === "pending");
7773
+ for (const entry of pending) {
7774
+ await this._updateQueueEntry(entry.id, { status: "uploading" });
7775
+ this.emit("upload:started", {
7776
+ ...entry,
7777
+ status: "uploading"
7778
+ });
7779
+ try {
7780
+ await this.client.upload(entry.docId, entry.file, entry.filename);
7781
+ await this._updateQueueEntry(entry.id, { status: "done" });
7782
+ this.emit("upload:done", {
7671
7783
  ...entry,
7672
- status: "uploading"
7784
+ status: "done"
7785
+ });
7786
+ } catch (err) {
7787
+ const message = err instanceof Error ? err.message : String(err);
7788
+ await this._updateQueueEntry(entry.id, {
7789
+ status: "error",
7790
+ error: message
7791
+ });
7792
+ this.emit("upload:error", {
7793
+ ...entry,
7794
+ status: "error",
7795
+ error: message
7673
7796
  });
7674
- try {
7675
- await this.client.upload(entry.docId, entry.file, entry.filename);
7676
- await this._updateQueueEntry(entry.id, { status: "done" });
7677
- this.emit("upload:done", {
7678
- ...entry,
7679
- status: "done"
7680
- });
7681
- } catch (err) {
7682
- const message = err instanceof Error ? err.message : String(err);
7683
- await this._updateQueueEntry(entry.id, {
7684
- status: "error",
7685
- error: message
7686
- });
7687
- this.emit("upload:error", {
7688
- ...entry,
7689
- status: "error",
7690
- error: message
7691
- });
7692
- }
7693
7797
  }
7694
- } finally {
7695
- this._flushing = false;
7696
7798
  }
7697
7799
  }
7698
7800
  async _updateQueueEntry(id, patch) {
@@ -7740,10 +7842,13 @@ const HKDF_INFO$1 = new TextEncoder().encode("abracadabra-dockey-v1");
7740
7842
  function fromBase64$1(b64) {
7741
7843
  return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
7742
7844
  }
7743
- var DocKeyManager = class {
7845
+ var DocKeyManager = class DocKeyManager {
7744
7846
  constructor() {
7745
7847
  this.cache = /* @__PURE__ */ new Map();
7746
7848
  }
7849
+ static {
7850
+ this.CACHE_TTL = 600 * 1e3;
7851
+ }
7747
7852
  /** Generate a new random AES-256-GCM document key. */
7748
7853
  static async generateDocKey() {
7749
7854
  return crypto.subtle.generateKey({
@@ -7757,7 +7862,7 @@ var DocKeyManager = class {
7757
7862
  */
7758
7863
  async getDocKey(docId, client, keystore) {
7759
7864
  const cached = this.cache.get(docId);
7760
- if (cached) return cached.key;
7865
+ if (cached && Date.now() - cached.fetchedAt < DocKeyManager.CACHE_TTL) return cached.key;
7761
7866
  const envelope = await client.getMyKeyEnvelope(docId);
7762
7867
  if (!envelope) return null;
7763
7868
  const x25519PrivKey = await keystore.getX25519PrivateKey();
@@ -7766,7 +7871,8 @@ var DocKeyManager = class {
7766
7871
  const docKey = await this._unwrapKey(wrapped, x25519PrivKey, docId);
7767
7872
  this.cache.set(docId, {
7768
7873
  key: docKey,
7769
- epoch: envelope.key_epoch
7874
+ epoch: envelope.key_epoch,
7875
+ fetchedAt: Date.now()
7770
7876
  });
7771
7877
  return docKey;
7772
7878
  } finally {
@@ -8262,6 +8368,7 @@ var BackgroundSyncManager = class extends EventEmitter {
8262
8368
  super();
8263
8369
  this.syncStates = /* @__PURE__ */ new Map();
8264
8370
  this._destroyed = false;
8371
+ this._initPromise = null;
8265
8372
  this.rootProvider = rootProvider;
8266
8373
  this.client = client;
8267
8374
  this.fileBlobStore = fileBlobStore ?? null;
@@ -8277,18 +8384,36 @@ var BackgroundSyncManager = class extends EventEmitter {
8277
8384
  this.persistence = new BackgroundSyncPersistence(serverOrigin);
8278
8385
  this.semaphore = new Semaphore(this.opts.concurrency);
8279
8386
  }
8387
+ /**
8388
+ * Load persisted sync states from IndexedDB into the in-memory map.
8389
+ * Called automatically by syncAll() / syncDoc(); safe to call concurrently.
8390
+ */
8391
+ async init() {
8392
+ if (!this._initPromise) this._initPromise = this._loadPersistedStates();
8393
+ return this._initPromise;
8394
+ }
8395
+ async _loadPersistedStates() {
8396
+ try {
8397
+ const states = await this.persistence.getAllStates();
8398
+ for (const state of states) this.syncStates.set(state.docId, state);
8399
+ } catch {}
8400
+ }
8280
8401
  /** Sync all documents in the root tree. */
8281
8402
  async syncAll() {
8282
8403
  if (this._destroyed) return;
8404
+ await this.init();
8283
8405
  const treeMap = this.rootProvider.document.getMap("doc-tree");
8284
8406
  const entries = Array.from(treeMap.entries());
8285
8407
  if (entries.length === 0) return;
8408
+ const updatedAtMap = /* @__PURE__ */ new Map();
8409
+ for (const [docId, v] of entries) updatedAtMap.set(docId, v?.updatedAt ?? v?.createdAt ?? 0);
8286
8410
  this._prefetchCovers(entries).catch(() => null);
8287
8411
  const queue = this._buildQueue(entries);
8288
- await Promise.all(queue.map((docId) => this._syncWithSemaphore(docId)));
8412
+ await Promise.all(queue.map((docId) => this._syncWithSemaphore(docId, updatedAtMap.get(docId) ?? 0)));
8289
8413
  }
8290
8414
  /** Sync a single document by ID. */
8291
8415
  async syncDoc(docId) {
8416
+ await this.init();
8292
8417
  const state = await this._doSyncDoc(docId);
8293
8418
  this.syncStates.set(docId, state);
8294
8419
  await this.persistence.setState(state).catch(() => null);
@@ -8339,8 +8464,16 @@ var BackgroundSyncManager = class extends EventEmitter {
8339
8464
  items.sort((a, b) => b.priority - a.priority);
8340
8465
  return items.map((i) => i.docId);
8341
8466
  }
8342
- async _syncWithSemaphore(docId) {
8467
+ async _syncWithSemaphore(docId, updatedAt) {
8343
8468
  if (this._destroyed) return;
8469
+ const existing = this.syncStates.get(docId);
8470
+ if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced >= updatedAt) {
8471
+ this.emit("stateChanged", {
8472
+ docId,
8473
+ state: existing
8474
+ });
8475
+ return;
8476
+ }
8344
8477
  await this.semaphore.acquire();
8345
8478
  try {
8346
8479
  const state = await this._doSyncDoc(docId);
@@ -8366,8 +8499,8 @@ var BackgroundSyncManager = class extends EventEmitter {
8366
8499
  docId,
8367
8500
  state: syncing
8368
8501
  });
8502
+ let isE2E = false;
8369
8503
  try {
8370
- let isE2E = false;
8371
8504
  try {
8372
8505
  isE2E = (await this.client.getDocEncryption(docId)).mode === "e2e";
8373
8506
  } catch {}
@@ -8380,7 +8513,7 @@ var BackgroundSyncManager = class extends EventEmitter {
8380
8513
  status: "error",
8381
8514
  lastSynced: this.syncStates.get(docId)?.lastSynced ?? null,
8382
8515
  error,
8383
- isE2E: false
8516
+ isE2E
8384
8517
  };
8385
8518
  }
8386
8519
  }
@@ -8513,6 +8646,7 @@ var SignalingSocket = class extends EventEmitter {
8513
8646
  this.connectionAttempt = null;
8514
8647
  this.localPeerId = null;
8515
8648
  this.isConnected = false;
8649
+ this._connectPromise = null;
8516
8650
  this.config = {
8517
8651
  url: configuration.url,
8518
8652
  token: configuration.token,
@@ -8532,6 +8666,15 @@ var SignalingSocket = class extends EventEmitter {
8532
8666
  }
8533
8667
  async connect() {
8534
8668
  if (this.isConnected) return;
8669
+ if (this._connectPromise) return this._connectPromise;
8670
+ this._connectPromise = this._doConnect();
8671
+ try {
8672
+ await this._connectPromise;
8673
+ } finally {
8674
+ this._connectPromise = null;
8675
+ }
8676
+ }
8677
+ async _doConnect() {
8535
8678
  if (this.cancelRetry) {
8536
8679
  this.cancelRetry();
8537
8680
  this.cancelRetry = void 0;
@@ -8845,11 +8988,12 @@ var DataChannelRouter = class extends EventEmitter {
8845
8988
  */
8846
8989
  async send(name, data) {
8847
8990
  const channel = this.channels.get(name);
8848
- if (!channel || channel.readyState !== "open") return;
8991
+ if (!channel || channel.readyState !== "open") return false;
8849
8992
  if (this.encryptor?.isEstablished && !this.plaintextChannels.has(name)) {
8850
8993
  const encrypted = await this.encryptor.encrypt(data);
8851
8994
  channel.send(encrypted);
8852
8995
  } else channel.send(data);
8996
+ return true;
8853
8997
  }
8854
8998
  registerChannel(channel) {
8855
8999
  channel.binaryType = "arraybuffer";
@@ -9017,6 +9161,7 @@ var YjsDataChannel = class {
9017
9161
  this.document = document;
9018
9162
  this.awareness = awareness;
9019
9163
  this.router = router;
9164
+ this.isSynced = false;
9020
9165
  this.docUpdateHandler = null;
9021
9166
  this.awarenessUpdateHandler = null;
9022
9167
  this.channelOpenHandler = null;
@@ -9253,6 +9398,10 @@ var FileTransferChannel = class extends EventEmitter {
9253
9398
  try {
9254
9399
  meta = JSON.parse(json);
9255
9400
  } catch {
9401
+ this.emit("receiveError", {
9402
+ transferId: "unknown",
9403
+ error: "Malformed START message: invalid JSON"
9404
+ });
9256
9405
  return;
9257
9406
  }
9258
9407
  this.receives.set(meta.transferId, {
@@ -9959,7 +10108,9 @@ var ManualSignaling = class extends EventEmitter {
9959
10108
  type: "offer",
9960
10109
  sdp: offerBlob.sdp
9961
10110
  }));
9962
- for (const c of offerBlob.candidates) await this.pc.addIceCandidate(new RTCIceCandidate(JSON.parse(c)));
10111
+ for (const c of offerBlob.candidates) try {
10112
+ await this.pc.addIceCandidate(new RTCIceCandidate(JSON.parse(c)));
10113
+ } catch {}
9963
10114
  const answer = await this.pc.createAnswer();
9964
10115
  await this.pc.setLocalDescription(answer);
9965
10116
  await gatheringComplete;