@abraca/dabra 1.8.1 → 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 +129 -3
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +129 -3
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +61 -1
- package/package.json +1 -1
- package/src/AbracadabraBaseProvider.ts +3 -3
- 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 +9 -0
|
@@ -2438,12 +2438,13 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2438
2438
|
try {
|
|
2439
2439
|
const parsed = JSON.parse(payload);
|
|
2440
2440
|
if (parsed?.type === "error" && parsed.source && parsed.code) {
|
|
2441
|
-
const { source, code, message } = parsed;
|
|
2442
|
-
console.warn(`[Abracadabra] Server error: ${source} (${code}) — ${message}
|
|
2441
|
+
const { source, code, message, meta } = parsed;
|
|
2442
|
+
console.warn(`[Abracadabra] Server error: ${source} (${code}) — ${message}`, meta ?? "");
|
|
2443
2443
|
this.emit("serverError", {
|
|
2444
2444
|
source,
|
|
2445
2445
|
code,
|
|
2446
|
-
message: message ?? ""
|
|
2446
|
+
message: message ?? "",
|
|
2447
|
+
meta
|
|
2447
2448
|
});
|
|
2448
2449
|
return;
|
|
2449
2450
|
}
|
|
@@ -10425,6 +10426,15 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
10425
10426
|
this.objectUrls.delete(key);
|
|
10426
10427
|
}
|
|
10427
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
|
+
}
|
|
10428
10438
|
/** Revoke the object URL and remove the blob from cache. */
|
|
10429
10439
|
async evictBlob(docId, uploadId) {
|
|
10430
10440
|
const key = this.blobKey(docId, uploadId);
|
|
@@ -10816,6 +10826,19 @@ var E2EOfflineStore = class extends OfflineStore {
|
|
|
10816
10826
|
* are fetched on subsequent connects.
|
|
10817
10827
|
* - After sync, a fresh encrypted snapshot is saved.
|
|
10818
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
|
+
*
|
|
10819
10842
|
* Key availability limitation: if the user's WebAuthn key is not in
|
|
10820
10843
|
* DocKeyManager's in-memory cache and there is no network, E2E docs show
|
|
10821
10844
|
* empty — the key fetch requires either a cached in-memory key or network.
|
|
@@ -10823,6 +10846,11 @@ var E2EOfflineStore = class extends OfflineStore {
|
|
|
10823
10846
|
function fromBase64(b64) {
|
|
10824
10847
|
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
10825
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
|
+
}
|
|
10826
10854
|
var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraProvider {
|
|
10827
10855
|
constructor(configuration) {
|
|
10828
10856
|
super({
|
|
@@ -10832,9 +10860,17 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10832
10860
|
this.docKey = null;
|
|
10833
10861
|
this.lastSeq = -1;
|
|
10834
10862
|
this.e2eStore = null;
|
|
10863
|
+
this.updatesSinceCompaction = 0;
|
|
10864
|
+
this.compactionInFlight = false;
|
|
10865
|
+
this.compactionInFlightTimeout = null;
|
|
10866
|
+
this.compactionDebounceTimer = null;
|
|
10867
|
+
this.destroyed = false;
|
|
10835
10868
|
this.docKeyManager = configuration.docKeyManager;
|
|
10836
10869
|
this.keystore = configuration.keystore;
|
|
10837
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);
|
|
10838
10874
|
this.e2eServerOrigin = E2EAbracadabraProvider.deriveServerOrigin(configuration, configuration.client);
|
|
10839
10875
|
}
|
|
10840
10876
|
/** Fetch the doc key from the server (requires WebAuthn if not cached). */
|
|
@@ -10846,6 +10882,10 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10846
10882
|
}
|
|
10847
10883
|
/** Handle stateless messages including e2e_ready and e2e_update. */
|
|
10848
10884
|
receiveStateless(payload) {
|
|
10885
|
+
if (payload.startsWith("snapshot:compacted ")) {
|
|
10886
|
+
this._handleCompactedBroadcast(payload.slice(19));
|
|
10887
|
+
return;
|
|
10888
|
+
}
|
|
10849
10889
|
let parsed;
|
|
10850
10890
|
try {
|
|
10851
10891
|
parsed = JSON.parse(payload);
|
|
@@ -10897,6 +10937,7 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10897
10937
|
const plaintext = await decryptField(encryptedData, key);
|
|
10898
10938
|
Y.applyUpdate(this.document, plaintext, this);
|
|
10899
10939
|
this.lastSeq = Math.max(this.lastSeq, seq);
|
|
10940
|
+
this._noteUpdateApplied();
|
|
10900
10941
|
} catch (e) {
|
|
10901
10942
|
console.error("[E2EAbracadabraProvider] decryption failed for seq", seq, e);
|
|
10902
10943
|
}
|
|
@@ -10919,8 +10960,93 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10919
10960
|
update: encrypted,
|
|
10920
10961
|
documentName: this.configuration.name
|
|
10921
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);
|
|
10922
11038
|
}
|
|
10923
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
|
+
}
|
|
10924
11050
|
this.e2eStore?.destroy();
|
|
10925
11051
|
this.e2eStore = null;
|
|
10926
11052
|
super.destroy();
|