@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
|
@@ -10456,6 +10456,15 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
10456
10456
|
this.objectUrls.delete(key);
|
|
10457
10457
|
}
|
|
10458
10458
|
}
|
|
10459
|
+
/**
|
|
10460
|
+
* Clear the 404 negative-cache entry for (docId, uploadId) so the next
|
|
10461
|
+
* getBlobUrl() re-fetches from the server instead of short-circuiting.
|
|
10462
|
+
* Use on explicit user retry or after reconnect, when the file may have
|
|
10463
|
+
* become available since the last 404.
|
|
10464
|
+
*/
|
|
10465
|
+
clearNotFound(docId, uploadId) {
|
|
10466
|
+
this._notFound.delete(this.blobKey(docId, uploadId));
|
|
10467
|
+
}
|
|
10459
10468
|
/** Revoke the object URL and remove the blob from cache. */
|
|
10460
10469
|
async evictBlob(docId, uploadId) {
|
|
10461
10470
|
const key = this.blobKey(docId, uploadId);
|
|
@@ -10856,6 +10865,19 @@ var E2EOfflineStore = class extends OfflineStore {
|
|
|
10856
10865
|
* are fetched on subsequent connects.
|
|
10857
10866
|
* - After sync, a fresh encrypted snapshot is saved.
|
|
10858
10867
|
*
|
|
10868
|
+
* Client-side compaction:
|
|
10869
|
+
* - After `compactionThreshold` encrypted updates have been applied in this
|
|
10870
|
+
* session (local + remote), and the doc has been quiescent for
|
|
10871
|
+
* `compactionQuiescenceMs`, the provider merges the whole Y.Doc, encrypts it,
|
|
10872
|
+
* and sends `snapshot:compact` — the server atomically replaces the per-doc
|
|
10873
|
+
* update log with that single compacted blob. The server acknowledges by
|
|
10874
|
+
* broadcasting `snapshot:compacted`, which emits the `"compacted"` event.
|
|
10875
|
+
* - Requires Owner or above (server silently drops non-Owner requests).
|
|
10876
|
+
* - Callers that want a final compaction before teardown should
|
|
10877
|
+
* `await provider.compactNow()` before `destroy()`. `destroy()` does not
|
|
10878
|
+
* compact (it'd race with the socket teardown) — any pending debounce is
|
|
10879
|
+
* cancelled.
|
|
10880
|
+
*
|
|
10859
10881
|
* Key availability limitation: if the user's WebAuthn key is not in
|
|
10860
10882
|
* DocKeyManager's in-memory cache and there is no network, E2E docs show
|
|
10861
10883
|
* empty — the key fetch requires either a cached in-memory key or network.
|
|
@@ -10863,6 +10885,11 @@ var E2EOfflineStore = class extends OfflineStore {
|
|
|
10863
10885
|
function fromBase64(b64) {
|
|
10864
10886
|
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
10865
10887
|
}
|
|
10888
|
+
function toBase64(bytes) {
|
|
10889
|
+
let bin = "";
|
|
10890
|
+
for (let i = 0; i < bytes.length; i += 32768) bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
10891
|
+
return btoa(bin);
|
|
10892
|
+
}
|
|
10866
10893
|
var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraProvider {
|
|
10867
10894
|
constructor(configuration) {
|
|
10868
10895
|
super({
|
|
@@ -10872,9 +10899,17 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10872
10899
|
this.docKey = null;
|
|
10873
10900
|
this.lastSeq = -1;
|
|
10874
10901
|
this.e2eStore = null;
|
|
10902
|
+
this.updatesSinceCompaction = 0;
|
|
10903
|
+
this.compactionInFlight = false;
|
|
10904
|
+
this.compactionInFlightTimeout = null;
|
|
10905
|
+
this.compactionDebounceTimer = null;
|
|
10906
|
+
this.destroyed = false;
|
|
10875
10907
|
this.docKeyManager = configuration.docKeyManager;
|
|
10876
10908
|
this.keystore = configuration.keystore;
|
|
10877
10909
|
this.e2eClient = configuration.client;
|
|
10910
|
+
this.compactionEnabled = configuration.compactionEnabled !== false;
|
|
10911
|
+
this.compactionThreshold = Math.max(1, configuration.compactionThreshold ?? 50);
|
|
10912
|
+
this.compactionQuiescenceMs = Math.max(0, configuration.compactionQuiescenceMs ?? 2e3);
|
|
10878
10913
|
this.e2eServerOrigin = E2EAbracadabraProvider.deriveServerOrigin(configuration, configuration.client);
|
|
10879
10914
|
}
|
|
10880
10915
|
/** Fetch the doc key from the server (requires WebAuthn if not cached). */
|
|
@@ -10886,6 +10921,10 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10886
10921
|
}
|
|
10887
10922
|
/** Handle stateless messages including e2e_ready and e2e_update. */
|
|
10888
10923
|
receiveStateless(payload) {
|
|
10924
|
+
if (payload.startsWith("snapshot:compacted ")) {
|
|
10925
|
+
this._handleCompactedBroadcast(payload.slice(19));
|
|
10926
|
+
return;
|
|
10927
|
+
}
|
|
10889
10928
|
let parsed;
|
|
10890
10929
|
try {
|
|
10891
10930
|
parsed = JSON.parse(payload);
|
|
@@ -10937,6 +10976,7 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10937
10976
|
const plaintext = await decryptField(encryptedData, key);
|
|
10938
10977
|
yjs.applyUpdate(this.document, plaintext, this);
|
|
10939
10978
|
this.lastSeq = Math.max(this.lastSeq, seq);
|
|
10979
|
+
this._noteUpdateApplied();
|
|
10940
10980
|
} catch (e) {
|
|
10941
10981
|
console.error("[E2EAbracadabraProvider] decryption failed for seq", seq, e);
|
|
10942
10982
|
}
|
|
@@ -10959,8 +10999,93 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10959
10999
|
update: encrypted,
|
|
10960
11000
|
documentName: this.configuration.name
|
|
10961
11001
|
});
|
|
11002
|
+
this._noteUpdateApplied();
|
|
11003
|
+
}
|
|
11004
|
+
/**
|
|
11005
|
+
* Force an immediate compaction attempt, bypassing the threshold and
|
|
11006
|
+
* quiescence debounce. Resolves once the `snapshot:compact` frame has been
|
|
11007
|
+
* sent (or rejected locally for missing prerequisites — destroyed, no
|
|
11008
|
+
* doc key, or already in-flight). The server acknowledges via a
|
|
11009
|
+
* `snapshot:compacted` broadcast, which emits the `"compacted"` event.
|
|
11010
|
+
*/
|
|
11011
|
+
async compactNow() {
|
|
11012
|
+
if (this.compactionDebounceTimer) {
|
|
11013
|
+
clearTimeout(this.compactionDebounceTimer);
|
|
11014
|
+
this.compactionDebounceTimer = null;
|
|
11015
|
+
}
|
|
11016
|
+
await this._performCompaction();
|
|
11017
|
+
}
|
|
11018
|
+
_noteUpdateApplied() {
|
|
11019
|
+
if (!this.compactionEnabled || this.destroyed) return;
|
|
11020
|
+
this.updatesSinceCompaction += 1;
|
|
11021
|
+
if (this.updatesSinceCompaction < this.compactionThreshold) return;
|
|
11022
|
+
if (this.compactionDebounceTimer) clearTimeout(this.compactionDebounceTimer);
|
|
11023
|
+
this.compactionDebounceTimer = setTimeout(() => {
|
|
11024
|
+
this.compactionDebounceTimer = null;
|
|
11025
|
+
this._performCompaction().catch((e) => {
|
|
11026
|
+
console.error("[E2EAbracadabraProvider] compaction failed:", e);
|
|
11027
|
+
});
|
|
11028
|
+
}, this.compactionQuiescenceMs);
|
|
11029
|
+
}
|
|
11030
|
+
async _performCompaction() {
|
|
11031
|
+
if (this.destroyed) return;
|
|
11032
|
+
if (this.compactionInFlight) return;
|
|
11033
|
+
if (this.updatesSinceCompaction === 0) return;
|
|
11034
|
+
if (!this.synced) return;
|
|
11035
|
+
const key = await this.ensureDocKey();
|
|
11036
|
+
if (!key) return;
|
|
11037
|
+
if (this.destroyed) return;
|
|
11038
|
+
this.compactionInFlight = true;
|
|
11039
|
+
try {
|
|
11040
|
+
const stateVector = yjs.encodeStateVector(this.document);
|
|
11041
|
+
const encrypted = await encryptField(yjs.encodeStateAsUpdate(this.document), key);
|
|
11042
|
+
if (this.destroyed) {
|
|
11043
|
+
this.compactionInFlight = false;
|
|
11044
|
+
return;
|
|
11045
|
+
}
|
|
11046
|
+
const payload = `snapshot:compact ${JSON.stringify({
|
|
11047
|
+
state_vector: toBase64(stateVector),
|
|
11048
|
+
compacted: toBase64(encrypted)
|
|
11049
|
+
})}`;
|
|
11050
|
+
this.sendStateless(payload);
|
|
11051
|
+
this.compactionInFlightTimeout = setTimeout(() => {
|
|
11052
|
+
this.compactionInFlight = false;
|
|
11053
|
+
this.compactionInFlightTimeout = null;
|
|
11054
|
+
}, 3e4);
|
|
11055
|
+
} catch (e) {
|
|
11056
|
+
this.compactionInFlight = false;
|
|
11057
|
+
throw e;
|
|
11058
|
+
}
|
|
11059
|
+
}
|
|
11060
|
+
_handleCompactedBroadcast(jsonStr) {
|
|
11061
|
+
let parsed = {};
|
|
11062
|
+
try {
|
|
11063
|
+
parsed = JSON.parse(jsonStr);
|
|
11064
|
+
} catch {}
|
|
11065
|
+
if (parsed.doc_id && parsed.doc_id !== this.configuration.name) return;
|
|
11066
|
+
if (this.compactionInFlightTimeout) {
|
|
11067
|
+
clearTimeout(this.compactionInFlightTimeout);
|
|
11068
|
+
this.compactionInFlightTimeout = null;
|
|
11069
|
+
}
|
|
11070
|
+
this.compactionInFlight = false;
|
|
11071
|
+
this.updatesSinceCompaction = 0;
|
|
11072
|
+
const event = {
|
|
11073
|
+
docId: parsed.doc_id ?? this.configuration.name,
|
|
11074
|
+
by: parsed.by
|
|
11075
|
+
};
|
|
11076
|
+
this.emit("compacted", event);
|
|
10962
11077
|
}
|
|
10963
11078
|
destroy() {
|
|
11079
|
+
if (this.destroyed) return;
|
|
11080
|
+
this.destroyed = true;
|
|
11081
|
+
if (this.compactionDebounceTimer) {
|
|
11082
|
+
clearTimeout(this.compactionDebounceTimer);
|
|
11083
|
+
this.compactionDebounceTimer = null;
|
|
11084
|
+
}
|
|
11085
|
+
if (this.compactionInFlightTimeout) {
|
|
11086
|
+
clearTimeout(this.compactionInFlightTimeout);
|
|
11087
|
+
this.compactionInFlightTimeout = null;
|
|
11088
|
+
}
|
|
10964
11089
|
this.e2eStore?.destroy();
|
|
10965
11090
|
this.e2eStore = null;
|
|
10966
11091
|
super.destroy();
|