@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
|
@@ -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();
|
|
@@ -10906,35 +10971,57 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10906
10971
|
* This propagates "last edited" timestamps to all peers via the root CRDT,
|
|
10907
10972
|
* without requiring any server-side changes.
|
|
10908
10973
|
*
|
|
10909
|
-
*
|
|
10910
|
-
*
|
|
10974
|
+
* A trailing-edge throttle (default 5 s) limits writes to avoid CRDT bloat
|
|
10975
|
+
* on the root doc during rapid typing.
|
|
10911
10976
|
*/
|
|
10912
10977
|
/**
|
|
10913
|
-
* Attach an observer that writes `updatedAt
|
|
10914
|
-
*
|
|
10915
|
-
*
|
|
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`.
|
|
10916
10983
|
*
|
|
10917
|
-
* @param treeMap
|
|
10918
|
-
* @param childDocId
|
|
10919
|
-
* @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.
|
|
10920
10987
|
* @param offlineStore The child provider's OfflineStore (used to detect
|
|
10921
10988
|
* offline-replay origins and skip them). Pass null when
|
|
10922
10989
|
* the offline store is disabled.
|
|
10923
|
-
* @
|
|
10924
|
-
|
|
10925
|
-
function
|
|
10926
|
-
|
|
10927
|
-
|
|
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;
|
|
10928
11004
|
const raw = treeMap.get(childDocId);
|
|
10929
11005
|
if (!raw) return;
|
|
10930
11006
|
const entry = raw instanceof yjs.Map ? raw.toJSON() : raw;
|
|
10931
11007
|
treeMap.set(childDocId, {
|
|
10932
11008
|
...entry,
|
|
10933
|
-
updatedAt:
|
|
11009
|
+
updatedAt: ts
|
|
10934
11010
|
});
|
|
10935
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
|
+
}
|
|
10936
11017
|
childDoc.on("update", handler);
|
|
10937
|
-
return () =>
|
|
11018
|
+
return () => {
|
|
11019
|
+
childDoc.off("update", handler);
|
|
11020
|
+
if (timer !== null) {
|
|
11021
|
+
clearTimeout(timer);
|
|
11022
|
+
flush();
|
|
11023
|
+
}
|
|
11024
|
+
};
|
|
10938
11025
|
}
|
|
10939
11026
|
|
|
10940
11027
|
//#endregion
|
|
@@ -11204,7 +11291,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11204
11291
|
async _syncWithSemaphore(docId, updatedAt) {
|
|
11205
11292
|
if (this._destroyed) return true;
|
|
11206
11293
|
const existing = this.syncStates.get(docId);
|
|
11207
|
-
if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced >= updatedAt) {
|
|
11294
|
+
if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced + 200 >= updatedAt) {
|
|
11208
11295
|
this.emit("stateChanged", {
|
|
11209
11296
|
docId,
|
|
11210
11297
|
state: existing
|
|
@@ -11256,6 +11343,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11256
11343
|
}
|
|
11257
11344
|
}
|
|
11258
11345
|
async _syncNonE2EDoc(docId) {
|
|
11346
|
+
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
11259
11347
|
const alreadyCached = this.rootProvider.children.has(docId);
|
|
11260
11348
|
if (!alreadyCached) {
|
|
11261
11349
|
if ((this.rootProvider.configuration?.websocketProvider?.configuration?.providerMap)?.has(docId)) return {
|
|
@@ -11266,27 +11354,33 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11266
11354
|
};
|
|
11267
11355
|
}
|
|
11268
11356
|
const childProvider = await this.rootProvider.loadChild(docId);
|
|
11269
|
-
|
|
11270
|
-
|
|
11271
|
-
|
|
11272
|
-
|
|
11273
|
-
|
|
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 {
|
|
11274
11373
|
docId,
|
|
11275
|
-
|
|
11276
|
-
|
|
11277
|
-
|
|
11278
|
-
}
|
|
11374
|
+
status: "synced",
|
|
11375
|
+
lastSynced: Math.max(Date.now(), treeUpdatedAt),
|
|
11376
|
+
isE2E: false
|
|
11377
|
+
};
|
|
11378
|
+
} finally {
|
|
11379
|
+
if (!alreadyCached) this.rootProvider.unloadChild(docId);
|
|
11279
11380
|
}
|
|
11280
|
-
if (this.opts.prefetchFiles && this.fileBlobStore) this._prefetchDocFiles(docId, childProvider.document).catch(() => null);
|
|
11281
|
-
if (!alreadyCached) this.rootProvider.unloadChild(docId);
|
|
11282
|
-
return {
|
|
11283
|
-
docId,
|
|
11284
|
-
status: "synced",
|
|
11285
|
-
lastSynced: Date.now(),
|
|
11286
|
-
isE2E: false
|
|
11287
|
-
};
|
|
11288
11381
|
}
|
|
11289
11382
|
async _syncE2EDoc(docId) {
|
|
11383
|
+
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
11290
11384
|
const docKeyManager = this.rootProvider.abracadabraConfig?.docKeyManager;
|
|
11291
11385
|
const keystore = this.rootProvider.abracadabraConfig?.keystore;
|
|
11292
11386
|
if (!docKeyManager || !keystore) return {
|
|
@@ -11307,7 +11401,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11307
11401
|
await childProvider.ready;
|
|
11308
11402
|
await this._waitForSynced(childProvider);
|
|
11309
11403
|
{
|
|
11310
|
-
const treeEntry =
|
|
11404
|
+
const treeEntry = treeMap.get(docId);
|
|
11311
11405
|
this.emit("docSynced", {
|
|
11312
11406
|
docId,
|
|
11313
11407
|
document: childDoc,
|
|
@@ -11316,10 +11410,12 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11316
11410
|
});
|
|
11317
11411
|
}
|
|
11318
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;
|
|
11319
11415
|
return {
|
|
11320
11416
|
docId,
|
|
11321
11417
|
status: "synced",
|
|
11322
|
-
lastSynced: Date.now(),
|
|
11418
|
+
lastSynced: Math.max(Date.now(), treeUpdatedAt),
|
|
11323
11419
|
isE2E: true
|
|
11324
11420
|
};
|
|
11325
11421
|
} finally {
|