@abraca/dabra 1.5.0 → 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 +132 -36
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +132 -36
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +42 -9
- package/package.json +1 -1
- package/src/AbracadabraBaseProvider.ts +15 -0
- package/src/AbracadabraProvider.ts +56 -0
- package/src/BackgroundSyncManager.ts +44 -30
- package/src/TreeTimestamps.ts +47 -16
- package/src/types.ts +6 -0
|
@@ -2287,6 +2287,7 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2287
2287
|
onAwarenessUpdate: () => null,
|
|
2288
2288
|
onAwarenessChange: () => null,
|
|
2289
2289
|
onStateless: () => null,
|
|
2290
|
+
onServerError: () => null,
|
|
2290
2291
|
onUnsyncedChanges: () => null
|
|
2291
2292
|
};
|
|
2292
2293
|
this.isSynced = false;
|
|
@@ -2318,6 +2319,7 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2318
2319
|
this.on("awarenessUpdate", this.configuration.onAwarenessUpdate);
|
|
2319
2320
|
this.on("awarenessChange", this.configuration.onAwarenessChange);
|
|
2320
2321
|
this.on("stateless", this.configuration.onStateless);
|
|
2322
|
+
this.on("serverError", this.configuration.onServerError);
|
|
2321
2323
|
this.on("unsyncedChanges", this.configuration.onUnsyncedChanges);
|
|
2322
2324
|
this.on("authenticated", this.configuration.onAuthenticated);
|
|
2323
2325
|
this.on("authenticationFailed", this.configuration.onAuthenticationFailed);
|
|
@@ -2433,6 +2435,19 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2433
2435
|
if (state) this.emit("synced", { state });
|
|
2434
2436
|
}
|
|
2435
2437
|
receiveStateless(payload) {
|
|
2438
|
+
try {
|
|
2439
|
+
const parsed = JSON.parse(payload);
|
|
2440
|
+
if (parsed?.type === "error" && parsed.source && parsed.code) {
|
|
2441
|
+
const { source, code, message } = parsed;
|
|
2442
|
+
console.warn(`[Abracadabra] Server error: ${source} (${code}) — ${message}`);
|
|
2443
|
+
this.emit("serverError", {
|
|
2444
|
+
source,
|
|
2445
|
+
code,
|
|
2446
|
+
message: message ?? ""
|
|
2447
|
+
});
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
} catch {}
|
|
2436
2451
|
this.emit("stateless", { payload });
|
|
2437
2452
|
}
|
|
2438
2453
|
async connect() {
|
|
@@ -2778,6 +2793,9 @@ function isValidDocId(id) {
|
|
|
2778
2793
|
* refreshed from the server on every reconnect.
|
|
2779
2794
|
*/
|
|
2780
2795
|
var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
2796
|
+
static {
|
|
2797
|
+
this.MAX_CHILDREN = 20;
|
|
2798
|
+
}
|
|
2781
2799
|
constructor(configuration) {
|
|
2782
2800
|
const resolved = { ...configuration };
|
|
2783
2801
|
const client = configuration.client ?? null;
|
|
@@ -2789,6 +2807,8 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2789
2807
|
this.effectiveRole = null;
|
|
2790
2808
|
this.childProviders = /* @__PURE__ */ new Map();
|
|
2791
2809
|
this.pendingLoads = /* @__PURE__ */ new Map();
|
|
2810
|
+
this.childAccessTimes = /* @__PURE__ */ new Map();
|
|
2811
|
+
this.pinnedChildren = /* @__PURE__ */ new Set();
|
|
2792
2812
|
this.boundHandleYSubdocsChange = this.handleYSubdocsChange.bind(this);
|
|
2793
2813
|
this._client = client;
|
|
2794
2814
|
this.abracadabraConfig = configuration;
|
|
@@ -2983,7 +3003,10 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2983
3003
|
*/
|
|
2984
3004
|
loadChild(childId) {
|
|
2985
3005
|
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.`));
|
|
2986
|
-
if (this.childProviders.has(childId))
|
|
3006
|
+
if (this.childProviders.has(childId)) {
|
|
3007
|
+
this.childAccessTimes.set(childId, Date.now());
|
|
3008
|
+
return Promise.resolve(this.childProviders.get(childId));
|
|
3009
|
+
}
|
|
2987
3010
|
if (this.pendingLoads.has(childId)) return this.pendingLoads.get(childId);
|
|
2988
3011
|
const load = this._doLoadChild(childId);
|
|
2989
3012
|
this.pendingLoads.set(childId, load);
|
|
@@ -3008,6 +3031,8 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
3008
3031
|
});
|
|
3009
3032
|
childProvider.attach();
|
|
3010
3033
|
this.childProviders.set(childId, childProvider);
|
|
3034
|
+
this.childAccessTimes.set(childId, Date.now());
|
|
3035
|
+
this.evictLRU();
|
|
3011
3036
|
this.emit("subdocLoaded", {
|
|
3012
3037
|
childId,
|
|
3013
3038
|
provider: childProvider
|
|
@@ -3019,6 +3044,44 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
3019
3044
|
if (provider) {
|
|
3020
3045
|
provider.destroy();
|
|
3021
3046
|
this.childProviders.delete(childId);
|
|
3047
|
+
this.childAccessTimes.delete(childId);
|
|
3048
|
+
this.pinnedChildren.delete(childId);
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Mark a child as pinned so LRU eviction will not remove it.
|
|
3053
|
+
* Use this when a document is actively being viewed by the user.
|
|
3054
|
+
*/
|
|
3055
|
+
pinChild(childId) {
|
|
3056
|
+
this.pinnedChildren.add(childId);
|
|
3057
|
+
}
|
|
3058
|
+
/**
|
|
3059
|
+
* Unpin a child, allowing LRU eviction to reclaim it when capacity is exceeded.
|
|
3060
|
+
*/
|
|
3061
|
+
unpinChild(childId) {
|
|
3062
|
+
this.pinnedChildren.delete(childId);
|
|
3063
|
+
this.evictLRU();
|
|
3064
|
+
}
|
|
3065
|
+
/**
|
|
3066
|
+
* Evict least-recently-used unpinned child providers until the cache is
|
|
3067
|
+
* at or below MAX_CHILDREN.
|
|
3068
|
+
*/
|
|
3069
|
+
evictLRU() {
|
|
3070
|
+
if (this.childProviders.size <= AbracadabraProvider.MAX_CHILDREN) return;
|
|
3071
|
+
const evictable = [];
|
|
3072
|
+
for (const [id] of this.childProviders) {
|
|
3073
|
+
if (this.pinnedChildren.has(id)) continue;
|
|
3074
|
+
evictable.push({
|
|
3075
|
+
id,
|
|
3076
|
+
accessTime: this.childAccessTimes.get(id) ?? 0
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
evictable.sort((a, b) => a.accessTime - b.accessTime);
|
|
3080
|
+
let toEvict = this.childProviders.size - AbracadabraProvider.MAX_CHILDREN;
|
|
3081
|
+
for (const entry of evictable) {
|
|
3082
|
+
if (toEvict <= 0) break;
|
|
3083
|
+
this.unloadChild(entry.id);
|
|
3084
|
+
toEvict--;
|
|
3022
3085
|
}
|
|
3023
3086
|
}
|
|
3024
3087
|
/** Return all currently-loaded child providers. */
|
|
@@ -3078,6 +3141,8 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
3078
3141
|
const childIds = [...this.childProviders.keys()];
|
|
3079
3142
|
for (const provider of this.childProviders.values()) provider.destroy();
|
|
3080
3143
|
this.childProviders.clear();
|
|
3144
|
+
this.childAccessTimes.clear();
|
|
3145
|
+
this.pinnedChildren.clear();
|
|
3081
3146
|
const wsProviderMap = this.configuration.websocketProvider?.configuration?.providerMap;
|
|
3082
3147
|
if (wsProviderMap) for (const childId of childIds) wsProviderMap.delete(childId);
|
|
3083
3148
|
this.offlineStore?.destroy();
|
|
@@ -10867,35 +10932,57 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10867
10932
|
* This propagates "last edited" timestamps to all peers via the root CRDT,
|
|
10868
10933
|
* without requiring any server-side changes.
|
|
10869
10934
|
*
|
|
10870
|
-
*
|
|
10871
|
-
*
|
|
10935
|
+
* A trailing-edge throttle (default 5 s) limits writes to avoid CRDT bloat
|
|
10936
|
+
* on the root doc during rapid typing.
|
|
10872
10937
|
*/
|
|
10873
10938
|
/**
|
|
10874
|
-
* Attach an observer that writes `updatedAt
|
|
10875
|
-
*
|
|
10876
|
-
*
|
|
10939
|
+
* Attach an observer that writes `updatedAt` to the root doc-tree entry for
|
|
10940
|
+
* `childDocId` whenever the child doc receives a non-offline update.
|
|
10941
|
+
*
|
|
10942
|
+
* Writes are throttled: the first qualifying update records the timestamp;
|
|
10943
|
+
* a trailing-edge timer flushes it to the tree map after `throttleMs`.
|
|
10877
10944
|
*
|
|
10878
|
-
* @param treeMap
|
|
10879
|
-
* @param childDocId
|
|
10880
|
-
* @param childDoc
|
|
10945
|
+
* @param treeMap The root doc's "doc-tree" Y.Map.
|
|
10946
|
+
* @param childDocId The child document's UUID (key in treeMap).
|
|
10947
|
+
* @param childDoc The child Y.Doc to observe.
|
|
10881
10948
|
* @param offlineStore The child provider's OfflineStore (used to detect
|
|
10882
10949
|
* offline-replay origins and skip them). Pass null when
|
|
10883
10950
|
* the offline store is disabled.
|
|
10884
|
-
* @
|
|
10885
|
-
|
|
10886
|
-
function
|
|
10887
|
-
|
|
10888
|
-
|
|
10951
|
+
* @param options Optional config. `throttleMs` controls the write
|
|
10952
|
+
* interval (default 5000).
|
|
10953
|
+
* @returns Cleanup function — call on provider destroy. Flushes
|
|
10954
|
+
* any pending write before detaching.
|
|
10955
|
+
*/
|
|
10956
|
+
function attachUpdatedAtObserver(treeMap, childDocId, childDoc, offlineStore, options) {
|
|
10957
|
+
const throttleMs = options?.throttleMs ?? 5e3;
|
|
10958
|
+
let latestTs = 0;
|
|
10959
|
+
let timer = null;
|
|
10960
|
+
function flush() {
|
|
10961
|
+
if (latestTs === 0) return;
|
|
10962
|
+
const ts = latestTs;
|
|
10963
|
+
latestTs = 0;
|
|
10964
|
+
timer = null;
|
|
10889
10965
|
const raw = treeMap.get(childDocId);
|
|
10890
10966
|
if (!raw) return;
|
|
10891
10967
|
const entry = raw instanceof Y.Map ? raw.toJSON() : raw;
|
|
10892
10968
|
treeMap.set(childDocId, {
|
|
10893
10969
|
...entry,
|
|
10894
|
-
updatedAt:
|
|
10970
|
+
updatedAt: ts
|
|
10895
10971
|
});
|
|
10896
10972
|
}
|
|
10973
|
+
function handler(_update, origin) {
|
|
10974
|
+
if (offlineStore !== null && origin === offlineStore) return;
|
|
10975
|
+
latestTs = Date.now();
|
|
10976
|
+
if (timer === null) timer = setTimeout(flush, throttleMs);
|
|
10977
|
+
}
|
|
10897
10978
|
childDoc.on("update", handler);
|
|
10898
|
-
return () =>
|
|
10979
|
+
return () => {
|
|
10980
|
+
childDoc.off("update", handler);
|
|
10981
|
+
if (timer !== null) {
|
|
10982
|
+
clearTimeout(timer);
|
|
10983
|
+
flush();
|
|
10984
|
+
}
|
|
10985
|
+
};
|
|
10899
10986
|
}
|
|
10900
10987
|
|
|
10901
10988
|
//#endregion
|
|
@@ -11165,7 +11252,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11165
11252
|
async _syncWithSemaphore(docId, updatedAt) {
|
|
11166
11253
|
if (this._destroyed) return true;
|
|
11167
11254
|
const existing = this.syncStates.get(docId);
|
|
11168
|
-
if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced >= updatedAt) {
|
|
11255
|
+
if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced + 200 >= updatedAt) {
|
|
11169
11256
|
this.emit("stateChanged", {
|
|
11170
11257
|
docId,
|
|
11171
11258
|
state: existing
|
|
@@ -11217,6 +11304,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11217
11304
|
}
|
|
11218
11305
|
}
|
|
11219
11306
|
async _syncNonE2EDoc(docId) {
|
|
11307
|
+
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
11220
11308
|
const alreadyCached = this.rootProvider.children.has(docId);
|
|
11221
11309
|
if (!alreadyCached) {
|
|
11222
11310
|
if ((this.rootProvider.configuration?.websocketProvider?.configuration?.providerMap)?.has(docId)) return {
|
|
@@ -11227,27 +11315,33 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11227
11315
|
};
|
|
11228
11316
|
}
|
|
11229
11317
|
const childProvider = await this.rootProvider.loadChild(docId);
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11318
|
+
try {
|
|
11319
|
+
await childProvider.ready;
|
|
11320
|
+
await this._waitForSynced(childProvider);
|
|
11321
|
+
{
|
|
11322
|
+
const treeEntry = treeMap.get(docId);
|
|
11323
|
+
this.emit("docSynced", {
|
|
11324
|
+
docId,
|
|
11325
|
+
document: childProvider.document,
|
|
11326
|
+
label: treeEntry?.label ?? "",
|
|
11327
|
+
meta: treeEntry?.meta
|
|
11328
|
+
});
|
|
11329
|
+
}
|
|
11330
|
+
if (this.opts.prefetchFiles && this.fileBlobStore) this._prefetchDocFiles(docId, childProvider.document).catch(() => null);
|
|
11331
|
+
const treeEntry = treeMap.get(docId);
|
|
11332
|
+
const treeUpdatedAt = treeEntry?.updatedAt ?? treeEntry?.createdAt ?? 0;
|
|
11333
|
+
return {
|
|
11235
11334
|
docId,
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
}
|
|
11335
|
+
status: "synced",
|
|
11336
|
+
lastSynced: Math.max(Date.now(), treeUpdatedAt),
|
|
11337
|
+
isE2E: false
|
|
11338
|
+
};
|
|
11339
|
+
} finally {
|
|
11340
|
+
if (!alreadyCached) this.rootProvider.unloadChild(docId);
|
|
11240
11341
|
}
|
|
11241
|
-
if (this.opts.prefetchFiles && this.fileBlobStore) this._prefetchDocFiles(docId, childProvider.document).catch(() => null);
|
|
11242
|
-
if (!alreadyCached) this.rootProvider.unloadChild(docId);
|
|
11243
|
-
return {
|
|
11244
|
-
docId,
|
|
11245
|
-
status: "synced",
|
|
11246
|
-
lastSynced: Date.now(),
|
|
11247
|
-
isE2E: false
|
|
11248
|
-
};
|
|
11249
11342
|
}
|
|
11250
11343
|
async _syncE2EDoc(docId) {
|
|
11344
|
+
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
11251
11345
|
const docKeyManager = this.rootProvider.abracadabraConfig?.docKeyManager;
|
|
11252
11346
|
const keystore = this.rootProvider.abracadabraConfig?.keystore;
|
|
11253
11347
|
if (!docKeyManager || !keystore) return {
|
|
@@ -11268,7 +11362,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11268
11362
|
await childProvider.ready;
|
|
11269
11363
|
await this._waitForSynced(childProvider);
|
|
11270
11364
|
{
|
|
11271
|
-
const treeEntry =
|
|
11365
|
+
const treeEntry = treeMap.get(docId);
|
|
11272
11366
|
this.emit("docSynced", {
|
|
11273
11367
|
docId,
|
|
11274
11368
|
document: childDoc,
|
|
@@ -11277,10 +11371,12 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11277
11371
|
});
|
|
11278
11372
|
}
|
|
11279
11373
|
if (this.opts.prefetchFiles && this.fileBlobStore) this._prefetchDocFiles(docId, childDoc).catch(() => null);
|
|
11374
|
+
const treeEntry = treeMap.get(docId);
|
|
11375
|
+
const treeUpdatedAt = treeEntry?.updatedAt ?? treeEntry?.createdAt ?? 0;
|
|
11280
11376
|
return {
|
|
11281
11377
|
docId,
|
|
11282
11378
|
status: "synced",
|
|
11283
|
-
lastSynced: Date.now(),
|
|
11379
|
+
lastSynced: Math.max(Date.now(), treeUpdatedAt),
|
|
11284
11380
|
isE2E: true
|
|
11285
11381
|
};
|
|
11286
11382
|
} finally {
|