@abraca/dabra 1.3.4 → 1.6.0
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 +160 -36
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +160 -36
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +86 -9
- package/package.json +1 -1
- package/src/AbracadabraBaseProvider.ts +15 -0
- package/src/AbracadabraClient.ts +55 -0
- package/src/AbracadabraProvider.ts +56 -0
- package/src/BackgroundSyncManager.ts +44 -30
- package/src/TreeTimestamps.ts +47 -16
- package/src/types.ts +37 -0
|
@@ -2317,6 +2317,7 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2317
2317
|
onAwarenessUpdate: () => null,
|
|
2318
2318
|
onAwarenessChange: () => null,
|
|
2319
2319
|
onStateless: () => null,
|
|
2320
|
+
onServerError: () => null,
|
|
2320
2321
|
onUnsyncedChanges: () => null
|
|
2321
2322
|
};
|
|
2322
2323
|
this.isSynced = false;
|
|
@@ -2348,6 +2349,7 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2348
2349
|
this.on("awarenessUpdate", this.configuration.onAwarenessUpdate);
|
|
2349
2350
|
this.on("awarenessChange", this.configuration.onAwarenessChange);
|
|
2350
2351
|
this.on("stateless", this.configuration.onStateless);
|
|
2352
|
+
this.on("serverError", this.configuration.onServerError);
|
|
2351
2353
|
this.on("unsyncedChanges", this.configuration.onUnsyncedChanges);
|
|
2352
2354
|
this.on("authenticated", this.configuration.onAuthenticated);
|
|
2353
2355
|
this.on("authenticationFailed", this.configuration.onAuthenticationFailed);
|
|
@@ -2463,6 +2465,19 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2463
2465
|
if (state) this.emit("synced", { state });
|
|
2464
2466
|
}
|
|
2465
2467
|
receiveStateless(payload) {
|
|
2468
|
+
try {
|
|
2469
|
+
const parsed = JSON.parse(payload);
|
|
2470
|
+
if (parsed?.type === "error" && parsed.source && parsed.code) {
|
|
2471
|
+
const { source, code, message } = parsed;
|
|
2472
|
+
console.warn(`[Abracadabra] Server error: ${source} (${code}) — ${message}`);
|
|
2473
|
+
this.emit("serverError", {
|
|
2474
|
+
source,
|
|
2475
|
+
code,
|
|
2476
|
+
message: message ?? ""
|
|
2477
|
+
});
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
} catch {}
|
|
2466
2481
|
this.emit("stateless", { payload });
|
|
2467
2482
|
}
|
|
2468
2483
|
async connect() {
|
|
@@ -2808,6 +2823,9 @@ function isValidDocId(id) {
|
|
|
2808
2823
|
* refreshed from the server on every reconnect.
|
|
2809
2824
|
*/
|
|
2810
2825
|
var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
2826
|
+
static {
|
|
2827
|
+
this.MAX_CHILDREN = 20;
|
|
2828
|
+
}
|
|
2811
2829
|
constructor(configuration) {
|
|
2812
2830
|
const resolved = { ...configuration };
|
|
2813
2831
|
const client = configuration.client ?? null;
|
|
@@ -2819,6 +2837,8 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2819
2837
|
this.effectiveRole = null;
|
|
2820
2838
|
this.childProviders = /* @__PURE__ */ new Map();
|
|
2821
2839
|
this.pendingLoads = /* @__PURE__ */ new Map();
|
|
2840
|
+
this.childAccessTimes = /* @__PURE__ */ new Map();
|
|
2841
|
+
this.pinnedChildren = /* @__PURE__ */ new Set();
|
|
2822
2842
|
this.boundHandleYSubdocsChange = this.handleYSubdocsChange.bind(this);
|
|
2823
2843
|
this._client = client;
|
|
2824
2844
|
this.abracadabraConfig = configuration;
|
|
@@ -3013,7 +3033,10 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
3013
3033
|
*/
|
|
3014
3034
|
loadChild(childId) {
|
|
3015
3035
|
if (!isValidDocId(childId)) return Promise.reject(/* @__PURE__ */ new Error(`loadChild: "${childId}" is not a valid document ID (must be a UUID). If this node was created with an older version of the app, delete it and recreate it.`));
|
|
3016
|
-
if (this.childProviders.has(childId))
|
|
3036
|
+
if (this.childProviders.has(childId)) {
|
|
3037
|
+
this.childAccessTimes.set(childId, Date.now());
|
|
3038
|
+
return Promise.resolve(this.childProviders.get(childId));
|
|
3039
|
+
}
|
|
3017
3040
|
if (this.pendingLoads.has(childId)) return this.pendingLoads.get(childId);
|
|
3018
3041
|
const load = this._doLoadChild(childId);
|
|
3019
3042
|
this.pendingLoads.set(childId, load);
|
|
@@ -3038,6 +3061,8 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
3038
3061
|
});
|
|
3039
3062
|
childProvider.attach();
|
|
3040
3063
|
this.childProviders.set(childId, childProvider);
|
|
3064
|
+
this.childAccessTimes.set(childId, Date.now());
|
|
3065
|
+
this.evictLRU();
|
|
3041
3066
|
this.emit("subdocLoaded", {
|
|
3042
3067
|
childId,
|
|
3043
3068
|
provider: childProvider
|
|
@@ -3049,6 +3074,44 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
3049
3074
|
if (provider) {
|
|
3050
3075
|
provider.destroy();
|
|
3051
3076
|
this.childProviders.delete(childId);
|
|
3077
|
+
this.childAccessTimes.delete(childId);
|
|
3078
|
+
this.pinnedChildren.delete(childId);
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Mark a child as pinned so LRU eviction will not remove it.
|
|
3083
|
+
* Use this when a document is actively being viewed by the user.
|
|
3084
|
+
*/
|
|
3085
|
+
pinChild(childId) {
|
|
3086
|
+
this.pinnedChildren.add(childId);
|
|
3087
|
+
}
|
|
3088
|
+
/**
|
|
3089
|
+
* Unpin a child, allowing LRU eviction to reclaim it when capacity is exceeded.
|
|
3090
|
+
*/
|
|
3091
|
+
unpinChild(childId) {
|
|
3092
|
+
this.pinnedChildren.delete(childId);
|
|
3093
|
+
this.evictLRU();
|
|
3094
|
+
}
|
|
3095
|
+
/**
|
|
3096
|
+
* Evict least-recently-used unpinned child providers until the cache is
|
|
3097
|
+
* at or below MAX_CHILDREN.
|
|
3098
|
+
*/
|
|
3099
|
+
evictLRU() {
|
|
3100
|
+
if (this.childProviders.size <= AbracadabraProvider.MAX_CHILDREN) return;
|
|
3101
|
+
const evictable = [];
|
|
3102
|
+
for (const [id] of this.childProviders) {
|
|
3103
|
+
if (this.pinnedChildren.has(id)) continue;
|
|
3104
|
+
evictable.push({
|
|
3105
|
+
id,
|
|
3106
|
+
accessTime: this.childAccessTimes.get(id) ?? 0
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
evictable.sort((a, b) => a.accessTime - b.accessTime);
|
|
3110
|
+
let toEvict = this.childProviders.size - AbracadabraProvider.MAX_CHILDREN;
|
|
3111
|
+
for (const entry of evictable) {
|
|
3112
|
+
if (toEvict <= 0) break;
|
|
3113
|
+
this.unloadChild(entry.id);
|
|
3114
|
+
toEvict--;
|
|
3052
3115
|
}
|
|
3053
3116
|
}
|
|
3054
3117
|
/** Return all currently-loaded child providers. */
|
|
@@ -3108,6 +3171,8 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
3108
3171
|
const childIds = [...this.childProviders.keys()];
|
|
3109
3172
|
for (const provider of this.childProviders.values()) provider.destroy();
|
|
3110
3173
|
this.childProviders.clear();
|
|
3174
|
+
this.childAccessTimes.clear();
|
|
3175
|
+
this.pinnedChildren.clear();
|
|
3111
3176
|
const wsProviderMap = this.configuration.websocketProvider?.configuration?.providerMap;
|
|
3112
3177
|
if (wsProviderMap) for (const childId of childIds) wsProviderMap.delete(childId);
|
|
3113
3178
|
this.offlineStore?.destroy();
|
|
@@ -3550,6 +3615,34 @@ var AbracadabraClient = class {
|
|
|
3550
3615
|
async adminStorageRepair() {
|
|
3551
3616
|
return this.request("POST", "/admin/storage/repair");
|
|
3552
3617
|
}
|
|
3618
|
+
/** List snapshot metadata for a document. */
|
|
3619
|
+
async listSnapshots(docId, opts) {
|
|
3620
|
+
const params = new URLSearchParams();
|
|
3621
|
+
if (opts?.limit != null) params.set("limit", String(opts.limit));
|
|
3622
|
+
if (opts?.offset != null) params.set("offset", String(opts.offset));
|
|
3623
|
+
const qs = params.toString();
|
|
3624
|
+
return (await this.request("GET", `/docs/${encodeURIComponent(docId)}/snapshots${qs ? `?${qs}` : ""}`)).snapshots;
|
|
3625
|
+
}
|
|
3626
|
+
/** Fetch a single snapshot including its base64-encoded data blob. */
|
|
3627
|
+
async getSnapshot(docId, version) {
|
|
3628
|
+
return this.request("GET", `/docs/${encodeURIComponent(docId)}/snapshots/${version}`);
|
|
3629
|
+
}
|
|
3630
|
+
/** Create a manual snapshot of the current document state. */
|
|
3631
|
+
async createSnapshot(docId, opts) {
|
|
3632
|
+
return this.request("POST", `/docs/${encodeURIComponent(docId)}/snapshots`, { body: opts ?? {} });
|
|
3633
|
+
}
|
|
3634
|
+
/** Delete a specific snapshot version. Requires manage permission. */
|
|
3635
|
+
async deleteSnapshot(docId, version) {
|
|
3636
|
+
await this.request("DELETE", `/docs/${encodeURIComponent(docId)}/snapshots/${version}`);
|
|
3637
|
+
}
|
|
3638
|
+
/** Restore a snapshot by merging it forward into the current document state. */
|
|
3639
|
+
async restoreSnapshot(docId, version) {
|
|
3640
|
+
return this.request("POST", `/docs/${encodeURIComponent(docId)}/snapshots/${version}/restore`, { body: {} });
|
|
3641
|
+
}
|
|
3642
|
+
/** Fork a snapshot into a new document. */
|
|
3643
|
+
async forkSnapshot(docId, version) {
|
|
3644
|
+
return this.request("POST", `/docs/${encodeURIComponent(docId)}/snapshots/${version}/fork`, { body: {} });
|
|
3645
|
+
}
|
|
3553
3646
|
/** Health check — no auth required. */
|
|
3554
3647
|
async health() {
|
|
3555
3648
|
return this.request("GET", "/health", { auth: false });
|
|
@@ -10878,35 +10971,57 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10878
10971
|
* This propagates "last edited" timestamps to all peers via the root CRDT,
|
|
10879
10972
|
* without requiring any server-side changes.
|
|
10880
10973
|
*
|
|
10881
|
-
*
|
|
10882
|
-
*
|
|
10974
|
+
* A trailing-edge throttle (default 5 s) limits writes to avoid CRDT bloat
|
|
10975
|
+
* on the root doc during rapid typing.
|
|
10883
10976
|
*/
|
|
10884
10977
|
/**
|
|
10885
|
-
* Attach an observer that writes `updatedAt
|
|
10886
|
-
*
|
|
10887
|
-
*
|
|
10978
|
+
* Attach an observer that writes `updatedAt` to the root doc-tree entry for
|
|
10979
|
+
* `childDocId` whenever the child doc receives a non-offline update.
|
|
10980
|
+
*
|
|
10981
|
+
* Writes are throttled: the first qualifying update records the timestamp;
|
|
10982
|
+
* a trailing-edge timer flushes it to the tree map after `throttleMs`.
|
|
10888
10983
|
*
|
|
10889
|
-
* @param treeMap
|
|
10890
|
-
* @param childDocId
|
|
10891
|
-
* @param childDoc
|
|
10984
|
+
* @param treeMap The root doc's "doc-tree" Y.Map.
|
|
10985
|
+
* @param childDocId The child document's UUID (key in treeMap).
|
|
10986
|
+
* @param childDoc The child Y.Doc to observe.
|
|
10892
10987
|
* @param offlineStore The child provider's OfflineStore (used to detect
|
|
10893
10988
|
* offline-replay origins and skip them). Pass null when
|
|
10894
10989
|
* the offline store is disabled.
|
|
10895
|
-
* @
|
|
10896
|
-
|
|
10897
|
-
function
|
|
10898
|
-
|
|
10899
|
-
|
|
10990
|
+
* @param options Optional config. `throttleMs` controls the write
|
|
10991
|
+
* interval (default 5000).
|
|
10992
|
+
* @returns Cleanup function — call on provider destroy. Flushes
|
|
10993
|
+
* any pending write before detaching.
|
|
10994
|
+
*/
|
|
10995
|
+
function attachUpdatedAtObserver(treeMap, childDocId, childDoc, offlineStore, options) {
|
|
10996
|
+
const throttleMs = options?.throttleMs ?? 5e3;
|
|
10997
|
+
let latestTs = 0;
|
|
10998
|
+
let timer = null;
|
|
10999
|
+
function flush() {
|
|
11000
|
+
if (latestTs === 0) return;
|
|
11001
|
+
const ts = latestTs;
|
|
11002
|
+
latestTs = 0;
|
|
11003
|
+
timer = null;
|
|
10900
11004
|
const raw = treeMap.get(childDocId);
|
|
10901
11005
|
if (!raw) return;
|
|
10902
11006
|
const entry = raw instanceof yjs.Map ? raw.toJSON() : raw;
|
|
10903
11007
|
treeMap.set(childDocId, {
|
|
10904
11008
|
...entry,
|
|
10905
|
-
updatedAt:
|
|
11009
|
+
updatedAt: ts
|
|
10906
11010
|
});
|
|
10907
11011
|
}
|
|
11012
|
+
function handler(_update, origin) {
|
|
11013
|
+
if (offlineStore !== null && origin === offlineStore) return;
|
|
11014
|
+
latestTs = Date.now();
|
|
11015
|
+
if (timer === null) timer = setTimeout(flush, throttleMs);
|
|
11016
|
+
}
|
|
10908
11017
|
childDoc.on("update", handler);
|
|
10909
|
-
return () =>
|
|
11018
|
+
return () => {
|
|
11019
|
+
childDoc.off("update", handler);
|
|
11020
|
+
if (timer !== null) {
|
|
11021
|
+
clearTimeout(timer);
|
|
11022
|
+
flush();
|
|
11023
|
+
}
|
|
11024
|
+
};
|
|
10910
11025
|
}
|
|
10911
11026
|
|
|
10912
11027
|
//#endregion
|
|
@@ -11176,7 +11291,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11176
11291
|
async _syncWithSemaphore(docId, updatedAt) {
|
|
11177
11292
|
if (this._destroyed) return true;
|
|
11178
11293
|
const existing = this.syncStates.get(docId);
|
|
11179
|
-
if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced >= updatedAt) {
|
|
11294
|
+
if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced + 200 >= updatedAt) {
|
|
11180
11295
|
this.emit("stateChanged", {
|
|
11181
11296
|
docId,
|
|
11182
11297
|
state: existing
|
|
@@ -11228,6 +11343,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11228
11343
|
}
|
|
11229
11344
|
}
|
|
11230
11345
|
async _syncNonE2EDoc(docId) {
|
|
11346
|
+
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
11231
11347
|
const alreadyCached = this.rootProvider.children.has(docId);
|
|
11232
11348
|
if (!alreadyCached) {
|
|
11233
11349
|
if ((this.rootProvider.configuration?.websocketProvider?.configuration?.providerMap)?.has(docId)) return {
|
|
@@ -11238,27 +11354,33 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11238
11354
|
};
|
|
11239
11355
|
}
|
|
11240
11356
|
const childProvider = await this.rootProvider.loadChild(docId);
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
|
|
11357
|
+
try {
|
|
11358
|
+
await childProvider.ready;
|
|
11359
|
+
await this._waitForSynced(childProvider);
|
|
11360
|
+
{
|
|
11361
|
+
const treeEntry = treeMap.get(docId);
|
|
11362
|
+
this.emit("docSynced", {
|
|
11363
|
+
docId,
|
|
11364
|
+
document: childProvider.document,
|
|
11365
|
+
label: treeEntry?.label ?? "",
|
|
11366
|
+
meta: treeEntry?.meta
|
|
11367
|
+
});
|
|
11368
|
+
}
|
|
11369
|
+
if (this.opts.prefetchFiles && this.fileBlobStore) this._prefetchDocFiles(docId, childProvider.document).catch(() => null);
|
|
11370
|
+
const treeEntry = treeMap.get(docId);
|
|
11371
|
+
const treeUpdatedAt = treeEntry?.updatedAt ?? treeEntry?.createdAt ?? 0;
|
|
11372
|
+
return {
|
|
11246
11373
|
docId,
|
|
11247
|
-
|
|
11248
|
-
|
|
11249
|
-
|
|
11250
|
-
}
|
|
11374
|
+
status: "synced",
|
|
11375
|
+
lastSynced: Math.max(Date.now(), treeUpdatedAt),
|
|
11376
|
+
isE2E: false
|
|
11377
|
+
};
|
|
11378
|
+
} finally {
|
|
11379
|
+
if (!alreadyCached) this.rootProvider.unloadChild(docId);
|
|
11251
11380
|
}
|
|
11252
|
-
if (this.opts.prefetchFiles && this.fileBlobStore) this._prefetchDocFiles(docId, childProvider.document).catch(() => null);
|
|
11253
|
-
if (!alreadyCached) this.rootProvider.unloadChild(docId);
|
|
11254
|
-
return {
|
|
11255
|
-
docId,
|
|
11256
|
-
status: "synced",
|
|
11257
|
-
lastSynced: Date.now(),
|
|
11258
|
-
isE2E: false
|
|
11259
|
-
};
|
|
11260
11381
|
}
|
|
11261
11382
|
async _syncE2EDoc(docId) {
|
|
11383
|
+
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
11262
11384
|
const docKeyManager = this.rootProvider.abracadabraConfig?.docKeyManager;
|
|
11263
11385
|
const keystore = this.rootProvider.abracadabraConfig?.keystore;
|
|
11264
11386
|
if (!docKeyManager || !keystore) return {
|
|
@@ -11279,7 +11401,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11279
11401
|
await childProvider.ready;
|
|
11280
11402
|
await this._waitForSynced(childProvider);
|
|
11281
11403
|
{
|
|
11282
|
-
const treeEntry =
|
|
11404
|
+
const treeEntry = treeMap.get(docId);
|
|
11283
11405
|
this.emit("docSynced", {
|
|
11284
11406
|
docId,
|
|
11285
11407
|
document: childDoc,
|
|
@@ -11288,10 +11410,12 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11288
11410
|
});
|
|
11289
11411
|
}
|
|
11290
11412
|
if (this.opts.prefetchFiles && this.fileBlobStore) this._prefetchDocFiles(docId, childDoc).catch(() => null);
|
|
11413
|
+
const treeEntry = treeMap.get(docId);
|
|
11414
|
+
const treeUpdatedAt = treeEntry?.updatedAt ?? treeEntry?.createdAt ?? 0;
|
|
11291
11415
|
return {
|
|
11292
11416
|
docId,
|
|
11293
11417
|
status: "synced",
|
|
11294
|
-
lastSynced: Date.now(),
|
|
11418
|
+
lastSynced: Math.max(Date.now(), treeUpdatedAt),
|
|
11295
11419
|
isE2E: true
|
|
11296
11420
|
};
|
|
11297
11421
|
} finally {
|