@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.
- package/dist/abracadabra-provider.cjs +125 -0
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +125 -0
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +60 -1
- package/package.json +1 -1
- package/src/ContentManager.ts +160 -0
- package/src/DocConverters.ts +1707 -0
- package/src/DocTypes.ts +618 -0
- package/src/DocUtils.ts +89 -0
- package/src/DocumentManager.ts +342 -0
- package/src/E2EAbracadabraProvider.ts +189 -0
- package/src/FileBlobStore.ts +10 -0
- package/src/MetaManager.ts +100 -0
- package/src/TreeManager.ts +429 -0
- package/src/types.ts +8 -0
|
@@ -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();
|