@abraca/dabra 2.0.5 → 2.0.7
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/package.json +1 -1
- package/src/AbracadabraProvider.ts +72 -13
package/package.json
CHANGED
|
@@ -123,8 +123,29 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
123
123
|
private childAccessTimes = new Map<string, number>();
|
|
124
124
|
/** Pinned children that must not be evicted (e.g. actively viewed docs) */
|
|
125
125
|
private pinnedChildren = new Set<string>();
|
|
126
|
-
/**
|
|
127
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Children explicitly marked as transient (e.g. by a background indexer
|
|
128
|
+
* that scans the whole tree). They:
|
|
129
|
+
* - never count toward the LRU `maxChildren` budget
|
|
130
|
+
* - never cause eviction of OTHER children when they're added
|
|
131
|
+
* - are eligible for LRU eviction themselves only after they get
|
|
132
|
+
* unmarked or explicitly unloaded
|
|
133
|
+
*
|
|
134
|
+
* The motivating case: `useSearchIndex` / `useFileIndex` walk every doc
|
|
135
|
+
* in the tree calling `loadChild`. With a default cap of 20 and a tree
|
|
136
|
+
* of 76+ subdocs, the LRU silently evicted whichever subdoc the UI was
|
|
137
|
+
* actually showing — a CLOSE storm and dead awareness for the user.
|
|
138
|
+
* Transient loads sidestep that pool entirely.
|
|
139
|
+
*/
|
|
140
|
+
private transientChildren = new Set<string>();
|
|
141
|
+
/**
|
|
142
|
+
* Default cap on simultaneously cached child providers (excluding
|
|
143
|
+
* transient ones — see `transientChildren`). Configurable per-instance
|
|
144
|
+
* via `maxChildren`. Bumped from 20 → 64 because real apps routinely
|
|
145
|
+
* have trees with dozens of subdocs being touched and 20 caused silent
|
|
146
|
+
* subdoc eviction (with awareness loss) on perfectly normal load.
|
|
147
|
+
*/
|
|
148
|
+
private static readonly DEFAULT_MAX_CHILDREN = 64;
|
|
128
149
|
private readonly maxChildren: number;
|
|
129
150
|
|
|
130
151
|
private abracadabraConfig: AbracadabraProviderConfiguration;
|
|
@@ -473,10 +494,22 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
473
494
|
|
|
474
495
|
/**
|
|
475
496
|
* Create (or return cached) a child AbracadabraProvider for a given
|
|
476
|
-
* child document id. Each child
|
|
477
|
-
*
|
|
497
|
+
* child document id. Each child shares the parent's WebSocket via
|
|
498
|
+
* multiplexing.
|
|
499
|
+
*
|
|
500
|
+
* `evictable` (default `true`) controls whether the child enters the
|
|
501
|
+
* LRU pool. Pass `evictable: false` for transient loads — typically
|
|
502
|
+
* batch indexers (search, file extraction) that touch every doc in
|
|
503
|
+
* the tree and would otherwise blow out the cache, silently evicting
|
|
504
|
+
* subdocs the UI is actively using. Transient children are excluded
|
|
505
|
+
* from the LRU budget AND don't trigger eviction of other children.
|
|
506
|
+
* Callers MUST pair `loadChild(id, { evictable: false })` with an
|
|
507
|
+
* explicit `unloadChild(id)` once they're done.
|
|
478
508
|
*/
|
|
479
|
-
loadChild(
|
|
509
|
+
loadChild(
|
|
510
|
+
childId: string,
|
|
511
|
+
options: { evictable?: boolean } = {},
|
|
512
|
+
): Promise<AbracadabraProvider> {
|
|
480
513
|
if (!isValidDocId(childId)) {
|
|
481
514
|
return Promise.reject(
|
|
482
515
|
new Error(
|
|
@@ -486,8 +519,15 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
486
519
|
);
|
|
487
520
|
}
|
|
488
521
|
|
|
522
|
+
const evictable = options.evictable !== false;
|
|
523
|
+
|
|
489
524
|
if (this.childProviders.has(childId)) {
|
|
490
525
|
this.childAccessTimes.set(childId, Date.now());
|
|
526
|
+
// A second load call promotes a child *out* of transient mode —
|
|
527
|
+
// once a UI surface wants it, it's no longer "just for indexing"
|
|
528
|
+
// and should be governed by the normal LRU budget. We never go
|
|
529
|
+
// the other way (UI-loaded → transient) implicitly.
|
|
530
|
+
if (evictable) this.transientChildren.delete(childId);
|
|
491
531
|
return Promise.resolve(this.childProviders.get(childId)!);
|
|
492
532
|
}
|
|
493
533
|
|
|
@@ -497,13 +537,16 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
497
537
|
return this.pendingLoads.get(childId)!;
|
|
498
538
|
}
|
|
499
539
|
|
|
500
|
-
const load = this._doLoadChild(childId);
|
|
540
|
+
const load = this._doLoadChild(childId, evictable);
|
|
501
541
|
this.pendingLoads.set(childId, load);
|
|
502
542
|
load.finally(() => this.pendingLoads.delete(childId));
|
|
503
543
|
return load;
|
|
504
544
|
}
|
|
505
545
|
|
|
506
|
-
private async _doLoadChild(
|
|
546
|
+
private async _doLoadChild(
|
|
547
|
+
childId: string,
|
|
548
|
+
evictable: boolean,
|
|
549
|
+
): Promise<AbracadabraProvider> {
|
|
507
550
|
const childDoc = new Y.Doc({ guid: childId });
|
|
508
551
|
|
|
509
552
|
// Fire-and-forget: tell the server this child belongs to the parent.
|
|
@@ -535,9 +578,13 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
535
578
|
|
|
536
579
|
this.childProviders.set(childId, childProvider);
|
|
537
580
|
this.childAccessTimes.set(childId, Date.now());
|
|
581
|
+
if (!evictable) this.transientChildren.add(childId);
|
|
538
582
|
|
|
539
|
-
// Evict least-recently-used children if over capacity
|
|
540
|
-
|
|
583
|
+
// Evict least-recently-used children if over capacity. A transient
|
|
584
|
+
// load doesn't count toward the budget AND shouldn't kick others
|
|
585
|
+
// out — it's an indexer scanning the tree, not the UI subscribing
|
|
586
|
+
// to a new doc.
|
|
587
|
+
if (evictable) this.evictLRU();
|
|
541
588
|
|
|
542
589
|
this.emit("subdocLoaded", { childId, provider: childProvider });
|
|
543
590
|
|
|
@@ -551,6 +598,7 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
551
598
|
this.childProviders.delete(childId);
|
|
552
599
|
this.childAccessTimes.delete(childId);
|
|
553
600
|
this.pinnedChildren.delete(childId);
|
|
601
|
+
this.transientChildren.delete(childId);
|
|
554
602
|
}
|
|
555
603
|
}
|
|
556
604
|
|
|
@@ -572,20 +620,31 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
572
620
|
}
|
|
573
621
|
|
|
574
622
|
/**
|
|
575
|
-
* Evict least-recently-used unpinned child providers
|
|
576
|
-
* at or below MAX_CHILDREN.
|
|
623
|
+
* Evict least-recently-used unpinned, non-transient child providers
|
|
624
|
+
* until the cache is at or below MAX_CHILDREN. Transient children
|
|
625
|
+
* (loaded with `{ evictable: false }`) are excluded from both the
|
|
626
|
+
* budget *and* the eviction list — they're invisible to the LRU and
|
|
627
|
+
* only go away when their loader explicitly calls `unloadChild`.
|
|
577
628
|
*/
|
|
578
629
|
private evictLRU() {
|
|
579
|
-
|
|
630
|
+
// Count only non-transient children toward the budget. A search
|
|
631
|
+
// indexer that pinned 100 transient children must NOT cause the
|
|
632
|
+
// LRU to evict the 1 doc the user's editor is actively viewing.
|
|
633
|
+
let nonTransientCount = 0;
|
|
634
|
+
for (const id of this.childProviders.keys()) {
|
|
635
|
+
if (!this.transientChildren.has(id)) nonTransientCount++;
|
|
636
|
+
}
|
|
637
|
+
if (nonTransientCount <= this.maxChildren) return;
|
|
580
638
|
|
|
581
639
|
const evictable: Array<{ id: string; accessTime: number }> = [];
|
|
582
640
|
for (const [id] of this.childProviders) {
|
|
583
641
|
if (this.pinnedChildren.has(id)) continue;
|
|
642
|
+
if (this.transientChildren.has(id)) continue;
|
|
584
643
|
evictable.push({ id, accessTime: this.childAccessTimes.get(id) ?? 0 });
|
|
585
644
|
}
|
|
586
645
|
evictable.sort((a, b) => a.accessTime - b.accessTime);
|
|
587
646
|
|
|
588
|
-
let toEvict =
|
|
647
|
+
let toEvict = nonTransientCount - this.maxChildren;
|
|
589
648
|
for (const entry of evictable) {
|
|
590
649
|
if (toEvict <= 0) break;
|
|
591
650
|
this.unloadChild(entry.id);
|