@abraca/dabra 1.8.2 → 1.9.1

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.
@@ -10426,6 +10426,15 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
10426
10426
  this.objectUrls.delete(key);
10427
10427
  }
10428
10428
  }
10429
+ /**
10430
+ * Clear the 404 negative-cache entry for (docId, uploadId) so the next
10431
+ * getBlobUrl() re-fetches from the server instead of short-circuiting.
10432
+ * Use on explicit user retry or after reconnect, when the file may have
10433
+ * become available since the last 404.
10434
+ */
10435
+ clearNotFound(docId, uploadId) {
10436
+ this._notFound.delete(this.blobKey(docId, uploadId));
10437
+ }
10429
10438
  /** Revoke the object URL and remove the blob from cache. */
10430
10439
  async evictBlob(docId, uploadId) {
10431
10440
  const key = this.blobKey(docId, uploadId);
@@ -10817,6 +10826,19 @@ var E2EOfflineStore = class extends OfflineStore {
10817
10826
  * are fetched on subsequent connects.
10818
10827
  * - After sync, a fresh encrypted snapshot is saved.
10819
10828
  *
10829
+ * Client-side compaction:
10830
+ * - After `compactionThreshold` encrypted updates have been applied in this
10831
+ * session (local + remote), and the doc has been quiescent for
10832
+ * `compactionQuiescenceMs`, the provider merges the whole Y.Doc, encrypts it,
10833
+ * and sends `snapshot:compact` — the server atomically replaces the per-doc
10834
+ * update log with that single compacted blob. The server acknowledges by
10835
+ * broadcasting `snapshot:compacted`, which emits the `"compacted"` event.
10836
+ * - Requires Owner or above (server silently drops non-Owner requests).
10837
+ * - Callers that want a final compaction before teardown should
10838
+ * `await provider.compactNow()` before `destroy()`. `destroy()` does not
10839
+ * compact (it'd race with the socket teardown) — any pending debounce is
10840
+ * cancelled.
10841
+ *
10820
10842
  * Key availability limitation: if the user's WebAuthn key is not in
10821
10843
  * DocKeyManager's in-memory cache and there is no network, E2E docs show
10822
10844
  * empty — the key fetch requires either a cached in-memory key or network.
@@ -10824,6 +10846,11 @@ var E2EOfflineStore = class extends OfflineStore {
10824
10846
  function fromBase64(b64) {
10825
10847
  return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
10826
10848
  }
10849
+ function toBase64(bytes) {
10850
+ let bin = "";
10851
+ for (let i = 0; i < bytes.length; i += 32768) bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
10852
+ return btoa(bin);
10853
+ }
10827
10854
  var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraProvider {
10828
10855
  constructor(configuration) {
10829
10856
  super({
@@ -10833,9 +10860,17 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
10833
10860
  this.docKey = null;
10834
10861
  this.lastSeq = -1;
10835
10862
  this.e2eStore = null;
10863
+ this.updatesSinceCompaction = 0;
10864
+ this.compactionInFlight = false;
10865
+ this.compactionInFlightTimeout = null;
10866
+ this.compactionDebounceTimer = null;
10867
+ this.destroyed = false;
10836
10868
  this.docKeyManager = configuration.docKeyManager;
10837
10869
  this.keystore = configuration.keystore;
10838
10870
  this.e2eClient = configuration.client;
10871
+ this.compactionEnabled = configuration.compactionEnabled !== false;
10872
+ this.compactionThreshold = Math.max(1, configuration.compactionThreshold ?? 50);
10873
+ this.compactionQuiescenceMs = Math.max(0, configuration.compactionQuiescenceMs ?? 2e3);
10839
10874
  this.e2eServerOrigin = E2EAbracadabraProvider.deriveServerOrigin(configuration, configuration.client);
10840
10875
  }
10841
10876
  /** Fetch the doc key from the server (requires WebAuthn if not cached). */
@@ -10847,6 +10882,10 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
10847
10882
  }
10848
10883
  /** Handle stateless messages including e2e_ready and e2e_update. */
10849
10884
  receiveStateless(payload) {
10885
+ if (payload.startsWith("snapshot:compacted ")) {
10886
+ this._handleCompactedBroadcast(payload.slice(19));
10887
+ return;
10888
+ }
10850
10889
  let parsed;
10851
10890
  try {
10852
10891
  parsed = JSON.parse(payload);
@@ -10898,6 +10937,7 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
10898
10937
  const plaintext = await decryptField(encryptedData, key);
10899
10938
  Y.applyUpdate(this.document, plaintext, this);
10900
10939
  this.lastSeq = Math.max(this.lastSeq, seq);
10940
+ this._noteUpdateApplied();
10901
10941
  } catch (e) {
10902
10942
  console.error("[E2EAbracadabraProvider] decryption failed for seq", seq, e);
10903
10943
  }
@@ -10920,8 +10960,93 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
10920
10960
  update: encrypted,
10921
10961
  documentName: this.configuration.name
10922
10962
  });
10963
+ this._noteUpdateApplied();
10964
+ }
10965
+ /**
10966
+ * Force an immediate compaction attempt, bypassing the threshold and
10967
+ * quiescence debounce. Resolves once the `snapshot:compact` frame has been
10968
+ * sent (or rejected locally for missing prerequisites — destroyed, no
10969
+ * doc key, or already in-flight). The server acknowledges via a
10970
+ * `snapshot:compacted` broadcast, which emits the `"compacted"` event.
10971
+ */
10972
+ async compactNow() {
10973
+ if (this.compactionDebounceTimer) {
10974
+ clearTimeout(this.compactionDebounceTimer);
10975
+ this.compactionDebounceTimer = null;
10976
+ }
10977
+ await this._performCompaction();
10978
+ }
10979
+ _noteUpdateApplied() {
10980
+ if (!this.compactionEnabled || this.destroyed) return;
10981
+ this.updatesSinceCompaction += 1;
10982
+ if (this.updatesSinceCompaction < this.compactionThreshold) return;
10983
+ if (this.compactionDebounceTimer) clearTimeout(this.compactionDebounceTimer);
10984
+ this.compactionDebounceTimer = setTimeout(() => {
10985
+ this.compactionDebounceTimer = null;
10986
+ this._performCompaction().catch((e) => {
10987
+ console.error("[E2EAbracadabraProvider] compaction failed:", e);
10988
+ });
10989
+ }, this.compactionQuiescenceMs);
10990
+ }
10991
+ async _performCompaction() {
10992
+ if (this.destroyed) return;
10993
+ if (this.compactionInFlight) return;
10994
+ if (this.updatesSinceCompaction === 0) return;
10995
+ if (!this.synced) return;
10996
+ const key = await this.ensureDocKey();
10997
+ if (!key) return;
10998
+ if (this.destroyed) return;
10999
+ this.compactionInFlight = true;
11000
+ try {
11001
+ const stateVector = Y.encodeStateVector(this.document);
11002
+ const encrypted = await encryptField(Y.encodeStateAsUpdate(this.document), key);
11003
+ if (this.destroyed) {
11004
+ this.compactionInFlight = false;
11005
+ return;
11006
+ }
11007
+ const payload = `snapshot:compact ${JSON.stringify({
11008
+ state_vector: toBase64(stateVector),
11009
+ compacted: toBase64(encrypted)
11010
+ })}`;
11011
+ this.sendStateless(payload);
11012
+ this.compactionInFlightTimeout = setTimeout(() => {
11013
+ this.compactionInFlight = false;
11014
+ this.compactionInFlightTimeout = null;
11015
+ }, 3e4);
11016
+ } catch (e) {
11017
+ this.compactionInFlight = false;
11018
+ throw e;
11019
+ }
11020
+ }
11021
+ _handleCompactedBroadcast(jsonStr) {
11022
+ let parsed = {};
11023
+ try {
11024
+ parsed = JSON.parse(jsonStr);
11025
+ } catch {}
11026
+ if (parsed.doc_id && parsed.doc_id !== this.configuration.name) return;
11027
+ if (this.compactionInFlightTimeout) {
11028
+ clearTimeout(this.compactionInFlightTimeout);
11029
+ this.compactionInFlightTimeout = null;
11030
+ }
11031
+ this.compactionInFlight = false;
11032
+ this.updatesSinceCompaction = 0;
11033
+ const event = {
11034
+ docId: parsed.doc_id ?? this.configuration.name,
11035
+ by: parsed.by
11036
+ };
11037
+ this.emit("compacted", event);
10923
11038
  }
10924
11039
  destroy() {
11040
+ if (this.destroyed) return;
11041
+ this.destroyed = true;
11042
+ if (this.compactionDebounceTimer) {
11043
+ clearTimeout(this.compactionDebounceTimer);
11044
+ this.compactionDebounceTimer = null;
11045
+ }
11046
+ if (this.compactionInFlightTimeout) {
11047
+ clearTimeout(this.compactionInFlightTimeout);
11048
+ this.compactionInFlightTimeout = null;
11049
+ }
10925
11050
  this.e2eStore?.destroy();
10926
11051
  this.e2eStore = null;
10927
11052
  super.destroy();