@abraca/dabra 2.0.5 → 2.0.8
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/index.d.ts
CHANGED
|
@@ -1089,7 +1089,28 @@ declare class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
1089
1089
|
private childAccessTimes;
|
|
1090
1090
|
/** Pinned children that must not be evicted (e.g. actively viewed docs) */
|
|
1091
1091
|
private pinnedChildren;
|
|
1092
|
-
/**
|
|
1092
|
+
/**
|
|
1093
|
+
* Children explicitly marked as transient (e.g. by a background indexer
|
|
1094
|
+
* that scans the whole tree). They:
|
|
1095
|
+
* - never count toward the LRU `maxChildren` budget
|
|
1096
|
+
* - never cause eviction of OTHER children when they're added
|
|
1097
|
+
* - are eligible for LRU eviction themselves only after they get
|
|
1098
|
+
* unmarked or explicitly unloaded
|
|
1099
|
+
*
|
|
1100
|
+
* The motivating case: `useSearchIndex` / `useFileIndex` walk every doc
|
|
1101
|
+
* in the tree calling `loadChild`. With a default cap of 20 and a tree
|
|
1102
|
+
* of 76+ subdocs, the LRU silently evicted whichever subdoc the UI was
|
|
1103
|
+
* actually showing — a CLOSE storm and dead awareness for the user.
|
|
1104
|
+
* Transient loads sidestep that pool entirely.
|
|
1105
|
+
*/
|
|
1106
|
+
private transientChildren;
|
|
1107
|
+
/**
|
|
1108
|
+
* Default cap on simultaneously cached child providers (excluding
|
|
1109
|
+
* transient ones — see `transientChildren`). Configurable per-instance
|
|
1110
|
+
* via `maxChildren`. Bumped from 20 → 64 because real apps routinely
|
|
1111
|
+
* have trees with dozens of subdocs being touched and 20 caused silent
|
|
1112
|
+
* subdoc eviction (with awareness loss) on perfectly normal load.
|
|
1113
|
+
*/
|
|
1093
1114
|
private static readonly DEFAULT_MAX_CHILDREN;
|
|
1094
1115
|
private readonly maxChildren;
|
|
1095
1116
|
private abracadabraConfig;
|
|
@@ -1166,10 +1187,21 @@ declare class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
1166
1187
|
hasChild(childId: string): boolean;
|
|
1167
1188
|
/**
|
|
1168
1189
|
* Create (or return cached) a child AbracadabraProvider for a given
|
|
1169
|
-
* child document id. Each child
|
|
1170
|
-
*
|
|
1171
|
-
|
|
1172
|
-
|
|
1190
|
+
* child document id. Each child shares the parent's WebSocket via
|
|
1191
|
+
* multiplexing.
|
|
1192
|
+
*
|
|
1193
|
+
* `evictable` (default `true`) controls whether the child enters the
|
|
1194
|
+
* LRU pool. Pass `evictable: false` for transient loads — typically
|
|
1195
|
+
* batch indexers (search, file extraction) that touch every doc in
|
|
1196
|
+
* the tree and would otherwise blow out the cache, silently evicting
|
|
1197
|
+
* subdocs the UI is actively using. Transient children are excluded
|
|
1198
|
+
* from the LRU budget AND don't trigger eviction of other children.
|
|
1199
|
+
* Callers MUST pair `loadChild(id, { evictable: false })` with an
|
|
1200
|
+
* explicit `unloadChild(id)` once they're done.
|
|
1201
|
+
*/
|
|
1202
|
+
loadChild(childId: string, options?: {
|
|
1203
|
+
evictable?: boolean;
|
|
1204
|
+
}): Promise<AbracadabraProvider>;
|
|
1173
1205
|
private _doLoadChild;
|
|
1174
1206
|
unloadChild(childId: string): void;
|
|
1175
1207
|
/**
|
|
@@ -1182,8 +1214,11 @@ declare class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
1182
1214
|
*/
|
|
1183
1215
|
unpinChild(childId: string): void;
|
|
1184
1216
|
/**
|
|
1185
|
-
* Evict least-recently-used unpinned child providers
|
|
1186
|
-
* at or below MAX_CHILDREN.
|
|
1217
|
+
* Evict least-recently-used unpinned, non-transient child providers
|
|
1218
|
+
* until the cache is at or below MAX_CHILDREN. Transient children
|
|
1219
|
+
* (loaded with `{ evictable: false }`) are excluded from both the
|
|
1220
|
+
* budget *and* the eviction list — they're invisible to the LRU and
|
|
1221
|
+
* only go away when their loader explicitly calls `unloadChild`.
|
|
1187
1222
|
*/
|
|
1188
1223
|
private evictLRU;
|
|
1189
1224
|
/** Return all currently-loaded child providers. */
|
|
@@ -2280,6 +2315,15 @@ declare class AbracadabraBaseProvider extends EventEmitter {
|
|
|
2280
2315
|
isSynced: boolean;
|
|
2281
2316
|
unsyncedChanges: number;
|
|
2282
2317
|
isAuthenticated: boolean;
|
|
2318
|
+
/**
|
|
2319
|
+
* True once this provider has received at least one Authenticated frame
|
|
2320
|
+
* on the current socket lifetime. Used by `permissionDeniedHandler` to
|
|
2321
|
+
* tell apart "the entry doc is wrong / unreachable" (first frame is a
|
|
2322
|
+
* denial → config error, give up) from "a mid-session write was denied"
|
|
2323
|
+
* (server keeps the read subscription alive → don't kill the socket).
|
|
2324
|
+
* Reset on close.
|
|
2325
|
+
*/
|
|
2326
|
+
private _hasEverAuthenticated;
|
|
2283
2327
|
/** Current WebSocket connection status. */
|
|
2284
2328
|
get connectionStatus(): WebSocketStatus;
|
|
2285
2329
|
authorizedScope: AuthorizedScope | undefined;
|
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,32 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
486
519
|
);
|
|
487
520
|
}
|
|
488
521
|
|
|
522
|
+
// Self-load: caller asked for the doc this provider already owns.
|
|
523
|
+
// Return `this` directly. Without this guard we'd build a sibling
|
|
524
|
+
// AbracadabraProvider with the same `name` and call attach() on the
|
|
525
|
+
// shared AbracadabraWS. The wsp's providerMap is keyed by name, so
|
|
526
|
+
// the sibling silently REPLACES the root in the routing table — its
|
|
527
|
+
// outgoing messages still reach the wire (send doesn't read the
|
|
528
|
+
// map), but inbound frames addressed to that doc go to the sibling.
|
|
529
|
+
// When the borrow ends and the sibling detaches, providerMap is
|
|
530
|
+
// wiped of that name and the root is left orphaned: the UI still
|
|
531
|
+
// reports `connected` + `synced`, but no awareness/sync messages
|
|
532
|
+
// are received. Returning `this` is intentionally not added to
|
|
533
|
+
// `childProviders` so LRU eviction never destroys the root, and
|
|
534
|
+
// `unloadChild(self)` becomes a safe no-op (see below).
|
|
535
|
+
if (childId === this.configuration.name) {
|
|
536
|
+
return Promise.resolve(this);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const evictable = options.evictable !== false;
|
|
540
|
+
|
|
489
541
|
if (this.childProviders.has(childId)) {
|
|
490
542
|
this.childAccessTimes.set(childId, Date.now());
|
|
543
|
+
// A second load call promotes a child *out* of transient mode —
|
|
544
|
+
// once a UI surface wants it, it's no longer "just for indexing"
|
|
545
|
+
// and should be governed by the normal LRU budget. We never go
|
|
546
|
+
// the other way (UI-loaded → transient) implicitly.
|
|
547
|
+
if (evictable) this.transientChildren.delete(childId);
|
|
491
548
|
return Promise.resolve(this.childProviders.get(childId)!);
|
|
492
549
|
}
|
|
493
550
|
|
|
@@ -497,13 +554,16 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
497
554
|
return this.pendingLoads.get(childId)!;
|
|
498
555
|
}
|
|
499
556
|
|
|
500
|
-
const load = this._doLoadChild(childId);
|
|
557
|
+
const load = this._doLoadChild(childId, evictable);
|
|
501
558
|
this.pendingLoads.set(childId, load);
|
|
502
559
|
load.finally(() => this.pendingLoads.delete(childId));
|
|
503
560
|
return load;
|
|
504
561
|
}
|
|
505
562
|
|
|
506
|
-
private async _doLoadChild(
|
|
563
|
+
private async _doLoadChild(
|
|
564
|
+
childId: string,
|
|
565
|
+
evictable: boolean,
|
|
566
|
+
): Promise<AbracadabraProvider> {
|
|
507
567
|
const childDoc = new Y.Doc({ guid: childId });
|
|
508
568
|
|
|
509
569
|
// Fire-and-forget: tell the server this child belongs to the parent.
|
|
@@ -535,9 +595,13 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
535
595
|
|
|
536
596
|
this.childProviders.set(childId, childProvider);
|
|
537
597
|
this.childAccessTimes.set(childId, Date.now());
|
|
598
|
+
if (!evictable) this.transientChildren.add(childId);
|
|
538
599
|
|
|
539
|
-
// Evict least-recently-used children if over capacity
|
|
540
|
-
|
|
600
|
+
// Evict least-recently-used children if over capacity. A transient
|
|
601
|
+
// load doesn't count toward the budget AND shouldn't kick others
|
|
602
|
+
// out — it's an indexer scanning the tree, not the UI subscribing
|
|
603
|
+
// to a new doc.
|
|
604
|
+
if (evictable) this.evictLRU();
|
|
541
605
|
|
|
542
606
|
this.emit("subdocLoaded", { childId, provider: childProvider });
|
|
543
607
|
|
|
@@ -551,6 +615,7 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
551
615
|
this.childProviders.delete(childId);
|
|
552
616
|
this.childAccessTimes.delete(childId);
|
|
553
617
|
this.pinnedChildren.delete(childId);
|
|
618
|
+
this.transientChildren.delete(childId);
|
|
554
619
|
}
|
|
555
620
|
}
|
|
556
621
|
|
|
@@ -572,20 +637,31 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
572
637
|
}
|
|
573
638
|
|
|
574
639
|
/**
|
|
575
|
-
* Evict least-recently-used unpinned child providers
|
|
576
|
-
* at or below MAX_CHILDREN.
|
|
640
|
+
* Evict least-recently-used unpinned, non-transient child providers
|
|
641
|
+
* until the cache is at or below MAX_CHILDREN. Transient children
|
|
642
|
+
* (loaded with `{ evictable: false }`) are excluded from both the
|
|
643
|
+
* budget *and* the eviction list — they're invisible to the LRU and
|
|
644
|
+
* only go away when their loader explicitly calls `unloadChild`.
|
|
577
645
|
*/
|
|
578
646
|
private evictLRU() {
|
|
579
|
-
|
|
647
|
+
// Count only non-transient children toward the budget. A search
|
|
648
|
+
// indexer that pinned 100 transient children must NOT cause the
|
|
649
|
+
// LRU to evict the 1 doc the user's editor is actively viewing.
|
|
650
|
+
let nonTransientCount = 0;
|
|
651
|
+
for (const id of this.childProviders.keys()) {
|
|
652
|
+
if (!this.transientChildren.has(id)) nonTransientCount++;
|
|
653
|
+
}
|
|
654
|
+
if (nonTransientCount <= this.maxChildren) return;
|
|
580
655
|
|
|
581
656
|
const evictable: Array<{ id: string; accessTime: number }> = [];
|
|
582
657
|
for (const [id] of this.childProviders) {
|
|
583
658
|
if (this.pinnedChildren.has(id)) continue;
|
|
659
|
+
if (this.transientChildren.has(id)) continue;
|
|
584
660
|
evictable.push({ id, accessTime: this.childAccessTimes.get(id) ?? 0 });
|
|
585
661
|
}
|
|
586
662
|
evictable.sort((a, b) => a.accessTime - b.accessTime);
|
|
587
663
|
|
|
588
|
-
let toEvict =
|
|
664
|
+
let toEvict = nonTransientCount - this.maxChildren;
|
|
589
665
|
for (const entry of evictable) {
|
|
590
666
|
if (toEvict <= 0) break;
|
|
591
667
|
this.unloadChild(entry.id);
|