@abraca/dabra 1.0.14 → 1.0.16

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 = {
@@ -2707,6 +2726,22 @@ var OfflineStore = class {
2707
2726
  const tx = db.transaction("meta", "readwrite");
2708
2727
  await txPromise$2(tx.objectStore("meta"), tx.objectStore("meta").put(value, `meta:${key}`));
2709
2728
  }
2729
+ /**
2730
+ * Clear all stored data (updates, snapshots, state vectors, subdoc queue).
2731
+ * The database itself is kept but emptied.
2732
+ */
2733
+ async clearAll() {
2734
+ const db = await this.getDb();
2735
+ if (!db) return;
2736
+ const storeNames = Array.from(db.objectStoreNames);
2737
+ if (storeNames.length === 0) return;
2738
+ const tx = db.transaction(storeNames, "readwrite");
2739
+ await Promise.all(storeNames.map((name) => new Promise((resolve, reject) => {
2740
+ const req = tx.objectStore(name).clear();
2741
+ req.onsuccess = () => resolve();
2742
+ req.onerror = () => reject(req.error);
2743
+ })));
2744
+ }
2710
2745
  destroy() {
2711
2746
  this._destroyed = true;
2712
2747
  this.db = null;
@@ -2792,7 +2827,9 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
2792
2827
  this.document.on("subdocs", this.boundHandleYSubdocsChange);
2793
2828
  this.on("synced", () => this.flushPendingUpdates());
2794
2829
  this.restorePermissionSnapshot();
2795
- this.ready = this._initFromOfflineStore();
2830
+ this.ready = Promise.race([this._initFromOfflineStore(), new Promise((resolve) => setTimeout(resolve, 5e3))]).catch((err) => {
2831
+ this.emit("error", { message: `Offline store init failed: ${err?.message ?? err}` });
2832
+ });
2796
2833
  }
2797
2834
  /**
2798
2835
  * Extract the server hostname from the provider configuration.
@@ -2910,7 +2947,9 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
2910
2947
  }
2911
2948
  const msg = parsed;
2912
2949
  if (msg.type === "auth_challenge" && msg.challenge && msg.expiresAt) {
2913
- this.handleAuthChallenge(msg.challenge, msg.expiresAt).catch(() => null);
2950
+ this.handleAuthChallenge(msg.challenge, msg.expiresAt).catch((err) => {
2951
+ this.emit("authenticationFailed", { reason: `Auth challenge error: ${err?.message ?? err}` });
2952
+ });
2914
2953
  return;
2915
2954
  }
2916
2955
  if (msg.type === "subdoc_registered" && msg.child_id && msg.parent_id) {
@@ -2952,6 +2991,14 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
2952
2991
  createdAt: Date.now()
2953
2992
  });
2954
2993
  }
2994
+ /** Get a loaded child provider by ID, or null if not yet loaded. */
2995
+ getChild(childId) {
2996
+ return this.childProviders.get(childId) ?? null;
2997
+ }
2998
+ /** Check if a child provider is already loaded. */
2999
+ hasChild(childId) {
3000
+ return this.childProviders.has(childId);
3001
+ }
2955
3002
  /**
2956
3003
  * Create (or return cached) a child AbracadabraProvider for a given
2957
3004
  * child document id. Each child opens its own WebSocket connection because
@@ -3082,6 +3129,17 @@ var AbracadabraClient = class {
3082
3129
  get isAuthenticated() {
3083
3130
  return this._token !== null;
3084
3131
  }
3132
+ /** Check if the current JWT token is present and not expired. */
3133
+ isTokenValid() {
3134
+ if (!this._token) return false;
3135
+ try {
3136
+ const [, payload] = this._token.split(".");
3137
+ const { exp } = JSON.parse(atob(payload));
3138
+ return typeof exp === "number" && exp * 1e3 > Date.now();
3139
+ } catch {
3140
+ return false;
3141
+ }
3142
+ }
3085
3143
  /** Derives ws:// or wss:// URL from the http(s) base URL. */
3086
3144
  get wsUrl() {
3087
3145
  return this.baseUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") + "/ws";
@@ -3175,12 +3233,7 @@ var AbracadabraClient = class {
3175
3233
  }
3176
3234
  /** Get the caller's key envelope for a document (for decrypting the DocKey). */
3177
3235
  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
- }
3236
+ return this.requestOrNull("GET", `/docs/${encodeURIComponent(docId)}/key-envelope`);
3184
3237
  }
3185
3238
  /** Upload key envelopes for a document (Owner only). */
3186
3239
  async uploadKeyEnvelopes(docId, opts) {
@@ -3188,12 +3241,7 @@ var AbracadabraClient = class {
3188
3241
  }
3189
3242
  /** Get the X25519 public key for a user. */
3190
3243
  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
- }
3244
+ return (await this.requestOrNull("GET", `/users/${encodeURIComponent(userId)}/x25519-key`))?.x25519_key ?? null;
3197
3245
  }
3198
3246
  /** List all non-revoked keys for a user (Owner/Admin or self). */
3199
3247
  async listUserKeys(userId) {
@@ -3271,14 +3319,15 @@ var AbracadabraClient = class {
3271
3319
  async listEffectivePermissions(docId) {
3272
3320
  try {
3273
3321
  return await this.request("GET", `/docs/${encodeURIComponent(docId)}/effective-permissions`);
3274
- } catch {
3275
- return {
3322
+ } catch (e) {
3323
+ if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return {
3276
3324
  permissions: (await this.listPermissions(docId)).map((p) => ({
3277
3325
  ...p,
3278
3326
  source: "direct"
3279
3327
  })),
3280
3328
  default_role: "viewer"
3281
3329
  };
3330
+ throw e;
3282
3331
  }
3283
3332
  }
3284
3333
  /** Grant or change a user's role on a document (requires Owner). */
@@ -3357,12 +3406,7 @@ var AbracadabraClient = class {
3357
3406
  }
3358
3407
  /** Get the hub space, or null if none is configured. */
3359
3408
  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
- }
3409
+ return this.requestOrNull("GET", "/spaces/hub", { auth: false });
3366
3410
  }
3367
3411
  /** Create a new space (auth required). */
3368
3412
  async createSpace(opts) {
@@ -3405,25 +3449,35 @@ var AbracadabraClient = class {
3405
3449
  if (auth && this._token) headers["Authorization"] = `Bearer ${this._token}`;
3406
3450
  const init = {
3407
3451
  method,
3408
- headers
3452
+ headers,
3453
+ signal: AbortSignal.timeout(3e4)
3409
3454
  };
3410
3455
  if (opts?.body !== void 0) {
3411
3456
  headers["Content-Type"] = "application/json";
3412
3457
  init.body = JSON.stringify(opts.body);
3413
3458
  }
3414
3459
  const res = await this._fetch(`${this.baseUrl}${path}`, init);
3415
- if (!res.ok) throw await this.toError(res);
3460
+ if (!res.ok) throw await this.toError(res, method, path);
3416
3461
  if (res.status === 204) return;
3417
3462
  return res.json();
3418
3463
  }
3419
- async toError(res) {
3464
+ async requestOrNull(method, path, opts) {
3465
+ try {
3466
+ return await this.request(method, path, opts);
3467
+ } catch (e) {
3468
+ if (typeof e === "object" && e !== null && "status" in e && e.status === 404) return null;
3469
+ throw e;
3470
+ }
3471
+ }
3472
+ async toError(res, method, path) {
3420
3473
  let message;
3421
3474
  try {
3422
3475
  message = (await res.json()).error ?? res.statusText;
3423
3476
  } catch {
3424
3477
  message = res.statusText;
3425
3478
  }
3426
- const err = new Error(message);
3479
+ const prefix = method && path ? `${method} ${path}: ` : "";
3480
+ const err = /* @__PURE__ */ new Error(`${prefix}${message} (${res.status})`);
3427
3481
  err.status = res.status;
3428
3482
  return err;
3429
3483
  }
@@ -7442,6 +7496,7 @@ var SearchIndex = class {
7442
7496
  if (!db) return [];
7443
7497
  const queryTrigrams = [...extractTrigrams(query)];
7444
7498
  if (queryTrigrams.length === 0) return [];
7499
+ const maxScoreEntries = limit * 10;
7445
7500
  return new Promise((resolve, reject) => {
7446
7501
  const tx = db.transaction("postings", "readonly");
7447
7502
  const postings = tx.objectStore("postings");
@@ -7451,7 +7506,10 @@ var SearchIndex = class {
7451
7506
  const req = postings.get(trigram);
7452
7507
  req.onsuccess = () => {
7453
7508
  const docIds = req.result ?? [];
7454
- for (const docId of docIds) scores.set(docId, (scores.get(docId) ?? 0) + 1);
7509
+ for (const docId of docIds) {
7510
+ scores.set(docId, (scores.get(docId) ?? 0) + 1);
7511
+ if (scores.size >= maxScoreEntries) break;
7512
+ }
7455
7513
  remaining--;
7456
7514
  if (remaining === 0) resolve([...scores.entries()].map(([docId, score]) => ({
7457
7515
  docId,
@@ -7509,7 +7567,7 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
7509
7567
  this.db = null;
7510
7568
  this.objectUrls = /* @__PURE__ */ new Map();
7511
7569
  this._notFound = /* @__PURE__ */ new Map();
7512
- this._flushing = false;
7570
+ this._flushPromise = null;
7513
7571
  this.origin = serverOrigin;
7514
7572
  this.client = client ?? null;
7515
7573
  this._onlineHandler = () => {
@@ -7651,6 +7709,20 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
7651
7709
  req.onerror = () => reject(req.error);
7652
7710
  });
7653
7711
  }
7712
+ /**
7713
+ * Revoke the in-memory object URL without touching the IDB cache.
7714
+ * The next call to getBlobUrl() will re-create a fresh URL from IDB.
7715
+ * Use this when an <img> @error fires — the blob data is fine, only
7716
+ * the object URL reference is stale.
7717
+ */
7718
+ invalidateUrl(docId, uploadId) {
7719
+ const key = this.blobKey(docId, uploadId);
7720
+ const url = this.objectUrls.get(key);
7721
+ if (url) {
7722
+ URL.revokeObjectURL(url);
7723
+ this.objectUrls.delete(key);
7724
+ }
7725
+ }
7654
7726
  /** Revoke the object URL and remove the blob from cache. */
7655
7727
  async evictBlob(docId, uploadId) {
7656
7728
  const key = this.blobKey(docId, uploadId);
@@ -7703,38 +7775,42 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
7703
7775
  * Entries that fail are marked with status "error" and left in the queue.
7704
7776
  */
7705
7777
  async flushQueue() {
7706
- if (this._flushing || !this.client) return;
7707
- this._flushing = true;
7778
+ if (this._flushPromise || !this.client) return;
7779
+ this._flushPromise = this._doFlush();
7708
7780
  try {
7709
- const pending = (await this.getQueue()).filter((e) => e.status === "pending");
7710
- for (const entry of pending) {
7711
- await this._updateQueueEntry(entry.id, { status: "uploading" });
7712
- this.emit("upload:started", {
7781
+ await this._flushPromise;
7782
+ } finally {
7783
+ this._flushPromise = null;
7784
+ }
7785
+ }
7786
+ async _doFlush() {
7787
+ if (!this.client) return;
7788
+ const pending = (await this.getQueue()).filter((e) => e.status === "pending");
7789
+ for (const entry of pending) {
7790
+ await this._updateQueueEntry(entry.id, { status: "uploading" });
7791
+ this.emit("upload:started", {
7792
+ ...entry,
7793
+ status: "uploading"
7794
+ });
7795
+ try {
7796
+ await this.client.upload(entry.docId, entry.file, entry.filename);
7797
+ await this._updateQueueEntry(entry.id, { status: "done" });
7798
+ this.emit("upload:done", {
7713
7799
  ...entry,
7714
- status: "uploading"
7800
+ status: "done"
7801
+ });
7802
+ } catch (err) {
7803
+ const message = err instanceof Error ? err.message : String(err);
7804
+ await this._updateQueueEntry(entry.id, {
7805
+ status: "error",
7806
+ error: message
7807
+ });
7808
+ this.emit("upload:error", {
7809
+ ...entry,
7810
+ status: "error",
7811
+ error: message
7715
7812
  });
7716
- try {
7717
- await this.client.upload(entry.docId, entry.file, entry.filename);
7718
- await this._updateQueueEntry(entry.id, { status: "done" });
7719
- this.emit("upload:done", {
7720
- ...entry,
7721
- status: "done"
7722
- });
7723
- } catch (err) {
7724
- const message = err instanceof Error ? err.message : String(err);
7725
- await this._updateQueueEntry(entry.id, {
7726
- status: "error",
7727
- error: message
7728
- });
7729
- this.emit("upload:error", {
7730
- ...entry,
7731
- status: "error",
7732
- error: message
7733
- });
7734
- }
7735
7813
  }
7736
- } finally {
7737
- this._flushing = false;
7738
7814
  }
7739
7815
  }
7740
7816
  async _updateQueueEntry(id, patch) {
@@ -7782,10 +7858,13 @@ const HKDF_INFO$1 = new TextEncoder().encode("abracadabra-dockey-v1");
7782
7858
  function fromBase64$1(b64) {
7783
7859
  return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
7784
7860
  }
7785
- var DocKeyManager = class {
7861
+ var DocKeyManager = class DocKeyManager {
7786
7862
  constructor() {
7787
7863
  this.cache = /* @__PURE__ */ new Map();
7788
7864
  }
7865
+ static {
7866
+ this.CACHE_TTL = 600 * 1e3;
7867
+ }
7789
7868
  /** Generate a new random AES-256-GCM document key. */
7790
7869
  static async generateDocKey() {
7791
7870
  return crypto.subtle.generateKey({
@@ -7799,7 +7878,7 @@ var DocKeyManager = class {
7799
7878
  */
7800
7879
  async getDocKey(docId, client, keystore) {
7801
7880
  const cached = this.cache.get(docId);
7802
- if (cached) return cached.key;
7881
+ if (cached && Date.now() - cached.fetchedAt < DocKeyManager.CACHE_TTL) return cached.key;
7803
7882
  const envelope = await client.getMyKeyEnvelope(docId);
7804
7883
  if (!envelope) return null;
7805
7884
  const x25519PrivKey = await keystore.getX25519PrivateKey();
@@ -7808,7 +7887,8 @@ var DocKeyManager = class {
7808
7887
  const docKey = await this._unwrapKey(wrapped, x25519PrivKey, docId);
7809
7888
  this.cache.set(docId, {
7810
7889
  key: docKey,
7811
- epoch: envelope.key_epoch
7890
+ epoch: envelope.key_epoch,
7891
+ fetchedAt: Date.now()
7812
7892
  });
7813
7893
  return docKey;
7814
7894
  } finally {
@@ -8304,6 +8384,7 @@ var BackgroundSyncManager = class extends EventEmitter {
8304
8384
  super();
8305
8385
  this.syncStates = /* @__PURE__ */ new Map();
8306
8386
  this._destroyed = false;
8387
+ this._initPromise = null;
8307
8388
  this.rootProvider = rootProvider;
8308
8389
  this.client = client;
8309
8390
  this.fileBlobStore = fileBlobStore ?? null;
@@ -8319,18 +8400,36 @@ var BackgroundSyncManager = class extends EventEmitter {
8319
8400
  this.persistence = new BackgroundSyncPersistence(serverOrigin);
8320
8401
  this.semaphore = new Semaphore(this.opts.concurrency);
8321
8402
  }
8403
+ /**
8404
+ * Load persisted sync states from IndexedDB into the in-memory map.
8405
+ * Called automatically by syncAll() / syncDoc(); safe to call concurrently.
8406
+ */
8407
+ async init() {
8408
+ if (!this._initPromise) this._initPromise = this._loadPersistedStates();
8409
+ return this._initPromise;
8410
+ }
8411
+ async _loadPersistedStates() {
8412
+ try {
8413
+ const states = await this.persistence.getAllStates();
8414
+ for (const state of states) this.syncStates.set(state.docId, state);
8415
+ } catch {}
8416
+ }
8322
8417
  /** Sync all documents in the root tree. */
8323
8418
  async syncAll() {
8324
8419
  if (this._destroyed) return;
8420
+ await this.init();
8325
8421
  const treeMap = this.rootProvider.document.getMap("doc-tree");
8326
8422
  const entries = Array.from(treeMap.entries());
8327
8423
  if (entries.length === 0) return;
8424
+ const updatedAtMap = /* @__PURE__ */ new Map();
8425
+ for (const [docId, v] of entries) updatedAtMap.set(docId, v?.updatedAt ?? v?.createdAt ?? 0);
8328
8426
  this._prefetchCovers(entries).catch(() => null);
8329
8427
  const queue = this._buildQueue(entries);
8330
- await Promise.all(queue.map((docId) => this._syncWithSemaphore(docId)));
8428
+ await Promise.all(queue.map((docId) => this._syncWithSemaphore(docId, updatedAtMap.get(docId) ?? 0)));
8331
8429
  }
8332
8430
  /** Sync a single document by ID. */
8333
8431
  async syncDoc(docId) {
8432
+ await this.init();
8334
8433
  const state = await this._doSyncDoc(docId);
8335
8434
  this.syncStates.set(docId, state);
8336
8435
  await this.persistence.setState(state).catch(() => null);
@@ -8355,6 +8454,32 @@ var BackgroundSyncManager = class extends EventEmitter {
8355
8454
  }, intervalMs);
8356
8455
  return () => clearInterval(handle);
8357
8456
  }
8457
+ /**
8458
+ * Clear all offline document data and sync state.
8459
+ * Opens each document's OfflineStore and clears its contents, then
8460
+ * resets the background sync persistence. After calling this, all
8461
+ * documents will need to be re-synced.
8462
+ */
8463
+ async clearAllSyncedData() {
8464
+ const docIds = new Set(this.syncStates.keys());
8465
+ const treeMap = this.rootProvider.document.getMap("doc-tree");
8466
+ for (const docId of treeMap.keys()) docIds.add(docId);
8467
+ let serverOrigin;
8468
+ try {
8469
+ serverOrigin = new URL(this.client.baseUrl ?? "").hostname;
8470
+ } catch {}
8471
+ const clearPromises = Array.from(docIds).map(async (docId) => {
8472
+ try {
8473
+ const store = new OfflineStore(docId, serverOrigin);
8474
+ await store.clearAll();
8475
+ store.destroy();
8476
+ } catch {}
8477
+ });
8478
+ await Promise.all(clearPromises);
8479
+ for (const docId of docIds) await this.persistence.deleteState(docId).catch(() => null);
8480
+ this.syncStates.clear();
8481
+ this._initPromise = null;
8482
+ }
8358
8483
  destroy() {
8359
8484
  this._destroyed = true;
8360
8485
  this.removeAllListeners();
@@ -8381,8 +8506,16 @@ var BackgroundSyncManager = class extends EventEmitter {
8381
8506
  items.sort((a, b) => b.priority - a.priority);
8382
8507
  return items.map((i) => i.docId);
8383
8508
  }
8384
- async _syncWithSemaphore(docId) {
8509
+ async _syncWithSemaphore(docId, updatedAt) {
8385
8510
  if (this._destroyed) return;
8511
+ const existing = this.syncStates.get(docId);
8512
+ if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced >= updatedAt) {
8513
+ this.emit("stateChanged", {
8514
+ docId,
8515
+ state: existing
8516
+ });
8517
+ return;
8518
+ }
8386
8519
  await this.semaphore.acquire();
8387
8520
  try {
8388
8521
  const state = await this._doSyncDoc(docId);
@@ -8408,8 +8541,8 @@ var BackgroundSyncManager = class extends EventEmitter {
8408
8541
  docId,
8409
8542
  state: syncing
8410
8543
  });
8544
+ let isE2E = false;
8411
8545
  try {
8412
- let isE2E = false;
8413
8546
  try {
8414
8547
  isE2E = (await this.client.getDocEncryption(docId)).mode === "e2e";
8415
8548
  } catch {}
@@ -8422,7 +8555,7 @@ var BackgroundSyncManager = class extends EventEmitter {
8422
8555
  status: "error",
8423
8556
  lastSynced: this.syncStates.get(docId)?.lastSynced ?? null,
8424
8557
  error,
8425
- isE2E: false
8558
+ isE2E
8426
8559
  };
8427
8560
  }
8428
8561
  }
@@ -8555,6 +8688,7 @@ var SignalingSocket = class extends EventEmitter {
8555
8688
  this.connectionAttempt = null;
8556
8689
  this.localPeerId = null;
8557
8690
  this.isConnected = false;
8691
+ this._connectPromise = null;
8558
8692
  this.config = {
8559
8693
  url: configuration.url,
8560
8694
  token: configuration.token,
@@ -8574,6 +8708,15 @@ var SignalingSocket = class extends EventEmitter {
8574
8708
  }
8575
8709
  async connect() {
8576
8710
  if (this.isConnected) return;
8711
+ if (this._connectPromise) return this._connectPromise;
8712
+ this._connectPromise = this._doConnect();
8713
+ try {
8714
+ await this._connectPromise;
8715
+ } finally {
8716
+ this._connectPromise = null;
8717
+ }
8718
+ }
8719
+ async _doConnect() {
8577
8720
  if (this.cancelRetry) {
8578
8721
  this.cancelRetry();
8579
8722
  this.cancelRetry = void 0;
@@ -8887,11 +9030,12 @@ var DataChannelRouter = class extends EventEmitter {
8887
9030
  */
8888
9031
  async send(name, data) {
8889
9032
  const channel = this.channels.get(name);
8890
- if (!channel || channel.readyState !== "open") return;
9033
+ if (!channel || channel.readyState !== "open") return false;
8891
9034
  if (this.encryptor?.isEstablished && !this.plaintextChannels.has(name)) {
8892
9035
  const encrypted = await this.encryptor.encrypt(data);
8893
9036
  channel.send(encrypted);
8894
9037
  } else channel.send(data);
9038
+ return true;
8895
9039
  }
8896
9040
  registerChannel(channel) {
8897
9041
  channel.binaryType = "arraybuffer";
@@ -9059,6 +9203,7 @@ var YjsDataChannel = class {
9059
9203
  this.document = document;
9060
9204
  this.awareness = awareness;
9061
9205
  this.router = router;
9206
+ this.isSynced = false;
9062
9207
  this.docUpdateHandler = null;
9063
9208
  this.awarenessUpdateHandler = null;
9064
9209
  this.channelOpenHandler = null;
@@ -9295,6 +9440,10 @@ var FileTransferChannel = class extends EventEmitter {
9295
9440
  try {
9296
9441
  meta = JSON.parse(json);
9297
9442
  } catch {
9443
+ this.emit("receiveError", {
9444
+ transferId: "unknown",
9445
+ error: "Malformed START message: invalid JSON"
9446
+ });
9298
9447
  return;
9299
9448
  }
9300
9449
  this.receives.set(meta.transferId, {
@@ -10001,7 +10150,9 @@ var ManualSignaling = class extends EventEmitter {
10001
10150
  type: "offer",
10002
10151
  sdp: offerBlob.sdp
10003
10152
  }));
10004
- for (const c of offerBlob.candidates) await this.pc.addIceCandidate(new RTCIceCandidate(JSON.parse(c)));
10153
+ for (const c of offerBlob.candidates) try {
10154
+ await this.pc.addIceCandidate(new RTCIceCandidate(JSON.parse(c)));
10155
+ } catch {}
10005
10156
  const answer = await this.pc.createAnswer();
10006
10157
  await this.pc.setLocalDescription(answer);
10007
10158
  await gatheringComplete;