@milaboratories/pl-tree 1.13.3 → 1.13.5
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 +2 -2
- package/dist/state.cjs +35 -3
- package/dist/state.cjs.map +1 -1
- package/dist/state.d.ts +46 -2
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +35 -3
- package/dist/state.js.map +1 -1
- package/dist/sync.cjs +71 -19
- package/dist/sync.cjs.map +1 -1
- package/dist/sync.d.ts +10 -3
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +71 -19
- package/dist/sync.js.map +1 -1
- package/dist/synchronized_tree.cjs +4 -1
- package/dist/synchronized_tree.cjs.map +1 -1
- package/dist/synchronized_tree.js +4 -1
- package/dist/synchronized_tree.js.map +1 -1
- package/package.json +4 -4
- package/src/state.ts +86 -3
- package/src/sync.ts +96 -23
- package/src/synchronized_tree.ts +1 -1
package/src/state.ts
CHANGED
|
@@ -68,6 +68,49 @@ export type ResourceDataWithFinalState = ResourceData & {
|
|
|
68
68
|
finalState: boolean;
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
+
/** Cross-cycle re-fetch duplication counters, populated by {@link PlTreeState.updateFromResourceData}.
|
|
72
|
+
* Separates genuinely new/changed resources from redundant re-fetches of unchanged ones
|
|
73
|
+
* (the delta-skip opportunity: intra-cycle dedup already happens in the loader, so all waste here
|
|
74
|
+
* is cross-cycle). */
|
|
75
|
+
export interface ResourceUpdateStat {
|
|
76
|
+
/** Resources seen for the first time in this update. */
|
|
77
|
+
resourcesNew: number;
|
|
78
|
+
/** Resources already held whose state actually changed. */
|
|
79
|
+
resourcesChanged: number;
|
|
80
|
+
/** Resources already held that came back unchanged: a pure duplicate re-fetch. */
|
|
81
|
+
resourcesUnchanged: number;
|
|
82
|
+
/** data + KV bytes of the unchanged bucket: wasted downlink. */
|
|
83
|
+
bytesUnchanged: number;
|
|
84
|
+
/** Changed resources whose immutable metadata (type, kind, field set) did not change,
|
|
85
|
+
* i.e. metadata re-streamed even though only a value or flag flipped. */
|
|
86
|
+
metadataStableChanged: number;
|
|
87
|
+
/** Per-resource fetches spent on unchanged resources (BFS only; streaming re-sends them
|
|
88
|
+
* inside one stream, so this stays 0). */
|
|
89
|
+
bfsRequestsWasted: number;
|
|
90
|
+
/** Whether the current load used backend streaming; set by the loader, read here to
|
|
91
|
+
* attribute {@link bfsRequestsWasted}. */
|
|
92
|
+
usedStreaming: boolean;
|
|
93
|
+
// Breakdown of what changed among resourcesChanged (a resource can bump several).
|
|
94
|
+
/** New fields appeared on a held resource. */
|
|
95
|
+
fieldsAdded: number;
|
|
96
|
+
/** Dynamic fields disappeared from a held resource. */
|
|
97
|
+
fieldsRemoved: number;
|
|
98
|
+
/** Existing field value pointer changed (a new value/result was attached). */
|
|
99
|
+
fieldsChanged: number;
|
|
100
|
+
/** KV entries added, changed, or deleted. */
|
|
101
|
+
kvChanged: number;
|
|
102
|
+
/** resourceReady flipped. */
|
|
103
|
+
readyFlips: number;
|
|
104
|
+
/** inputsLocked or outputsLocked transitioned. */
|
|
105
|
+
locksChanged: number;
|
|
106
|
+
/** An error resource was attached. */
|
|
107
|
+
errorsAttached: number;
|
|
108
|
+
/** originalResourceId resolved (duplicate -> original). */
|
|
109
|
+
duplicatesResolved: number;
|
|
110
|
+
/** Held resources that transitioned to final this update. */
|
|
111
|
+
resourcesMarkedFinal: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
71
114
|
/** Never store instances of this class, always get fresh instance from {@link PlTreeState} */
|
|
72
115
|
export class PlTreeResource implements ResourceDataWithFinalState {
|
|
73
116
|
/** Tracks number of other resources referencing this resource. Used to perform garbage collection in tree patching procedure */
|
|
@@ -451,7 +494,11 @@ export class PlTreeState {
|
|
|
451
494
|
return res;
|
|
452
495
|
}
|
|
453
496
|
|
|
454
|
-
updateFromResourceData(
|
|
497
|
+
updateFromResourceData(
|
|
498
|
+
resourceData: ExtendedResourceData[],
|
|
499
|
+
opts: { allowOrphanInputs?: boolean; stat?: ResourceUpdateStat } = {},
|
|
500
|
+
) {
|
|
501
|
+
const { allowOrphanInputs = false, stat } = opts;
|
|
455
502
|
this.checkValid();
|
|
456
503
|
|
|
457
504
|
// All resources for which recount should be incremented, first are aggregated in this list
|
|
@@ -461,6 +508,11 @@ export class PlTreeState {
|
|
|
461
508
|
// patching / creating resources
|
|
462
509
|
for (const rd of resourceData) {
|
|
463
510
|
let resource = this.resources.get(rd.id);
|
|
511
|
+
const held = resource !== undefined;
|
|
512
|
+
let changed = false;
|
|
513
|
+
// Structural/metadata change (new or removed field). type and kind are readonly, so
|
|
514
|
+
// they never change; this flag isolates value/flag-only changes from real metadata churn.
|
|
515
|
+
let metadataChanged = false;
|
|
464
516
|
|
|
465
517
|
const statBeforeMutation = resource?.basicState;
|
|
466
518
|
const unexpectedTransitionError = (reason: string): never => {
|
|
@@ -480,7 +532,6 @@ export class PlTreeState {
|
|
|
480
532
|
if (resource.finalState)
|
|
481
533
|
unexpectedTransitionError("resource state can\t be updated after it is marked as final");
|
|
482
534
|
|
|
483
|
-
let changed = false;
|
|
484
535
|
// updating resource version, even if it was not changed
|
|
485
536
|
resource.version += 1;
|
|
486
537
|
|
|
@@ -494,6 +545,7 @@ export class PlTreeState {
|
|
|
494
545
|
`originalResourceId changed for ${resourceIdToString(resource.id)}`,
|
|
495
546
|
);
|
|
496
547
|
changed = true;
|
|
548
|
+
if (stat) stat.duplicatesResolved++;
|
|
497
549
|
}
|
|
498
550
|
|
|
499
551
|
// error
|
|
@@ -506,6 +558,7 @@ export class PlTreeState {
|
|
|
506
558
|
`error changed for ${resourceIdToString(resource.id)}`,
|
|
507
559
|
);
|
|
508
560
|
changed = true;
|
|
561
|
+
if (stat) stat.errorsAttached++;
|
|
509
562
|
}
|
|
510
563
|
|
|
511
564
|
// updating fields
|
|
@@ -552,6 +605,8 @@ export class PlTreeState {
|
|
|
552
605
|
resource.fieldsMap.set(fd.name, field);
|
|
553
606
|
|
|
554
607
|
changed = true;
|
|
608
|
+
metadataChanged = true;
|
|
609
|
+
if (stat) stat.fieldsAdded++;
|
|
555
610
|
} else {
|
|
556
611
|
// change of old field
|
|
557
612
|
|
|
@@ -596,6 +651,7 @@ export class PlTreeState {
|
|
|
596
651
|
`field ${fd.name} value changed in ${resourceIdToString(resource.id)}`,
|
|
597
652
|
);
|
|
598
653
|
changed = true;
|
|
654
|
+
if (stat) stat.fieldsChanged++;
|
|
599
655
|
}
|
|
600
656
|
|
|
601
657
|
// field error
|
|
@@ -640,6 +696,8 @@ export class PlTreeState {
|
|
|
640
696
|
`dynamic field ${fieldName} removed from ${resourceIdToString(resource!.id)}`,
|
|
641
697
|
);
|
|
642
698
|
fields.delete(fieldName);
|
|
699
|
+
metadataChanged = true;
|
|
700
|
+
if (stat) stat.fieldsRemoved++;
|
|
643
701
|
|
|
644
702
|
if (isNotNullSignedResourceId(field.value)) decrementRefs.push(field.value);
|
|
645
703
|
if (isNotNullSignedResourceId(field.error)) decrementRefs.push(field.error);
|
|
@@ -658,6 +716,7 @@ export class PlTreeState {
|
|
|
658
716
|
`inputs locked for ${resourceIdToString(resource.id)}`,
|
|
659
717
|
);
|
|
660
718
|
changed = true;
|
|
719
|
+
if (stat) stat.locksChanged++;
|
|
661
720
|
}
|
|
662
721
|
|
|
663
722
|
// outputsLocked
|
|
@@ -669,6 +728,7 @@ export class PlTreeState {
|
|
|
669
728
|
`outputs locked for ${resourceIdToString(resource.id)}`,
|
|
670
729
|
);
|
|
671
730
|
changed = true;
|
|
731
|
+
if (stat) stat.locksChanged++;
|
|
672
732
|
}
|
|
673
733
|
|
|
674
734
|
// ready flag
|
|
@@ -684,6 +744,7 @@ export class PlTreeState {
|
|
|
684
744
|
`ready flag changed to ${rd.resourceReady} for ${resourceIdToString(resource.id)}`,
|
|
685
745
|
);
|
|
686
746
|
changed = true;
|
|
747
|
+
if (stat) stat.readyFlips++;
|
|
687
748
|
}
|
|
688
749
|
|
|
689
750
|
// syncing kv
|
|
@@ -695,12 +756,16 @@ export class PlTreeState {
|
|
|
695
756
|
kv.key,
|
|
696
757
|
`kv added for ${resourceIdToString(resource.id)}: ${kv.key}`,
|
|
697
758
|
);
|
|
759
|
+
changed = true;
|
|
760
|
+
if (stat) stat.kvChanged++;
|
|
698
761
|
} else if (Buffer.compare(current, kv.value) !== 0) {
|
|
699
762
|
resource.kv.set(kv.key, kv.value);
|
|
700
763
|
notEmpty(resource.kvChangedPerKey).markChanged(
|
|
701
764
|
kv.key,
|
|
702
765
|
`kv changed for ${resourceIdToString(resource.id)}: ${kv.key}`,
|
|
703
766
|
);
|
|
767
|
+
changed = true;
|
|
768
|
+
if (stat) stat.kvChanged++;
|
|
704
769
|
}
|
|
705
770
|
}
|
|
706
771
|
|
|
@@ -716,6 +781,8 @@ export class PlTreeState {
|
|
|
716
781
|
key,
|
|
717
782
|
`kv deleted for ${resourceIdToString(resource!.id)}: ${key}`,
|
|
718
783
|
);
|
|
784
|
+
changed = true;
|
|
785
|
+
if (stat) stat.kvChanged++;
|
|
719
786
|
}
|
|
720
787
|
});
|
|
721
788
|
}
|
|
@@ -723,7 +790,10 @@ export class PlTreeState {
|
|
|
723
790
|
if (changed) {
|
|
724
791
|
// if resource was changed, updating resource data version
|
|
725
792
|
resource.dataVersion = resource.version;
|
|
726
|
-
if (this.isFinalPredicate(resource))
|
|
793
|
+
if (this.isFinalPredicate(resource)) {
|
|
794
|
+
resource.markFinal();
|
|
795
|
+
if (stat) stat.resourcesMarkedFinal++;
|
|
796
|
+
}
|
|
727
797
|
}
|
|
728
798
|
} else {
|
|
729
799
|
// creating new resource
|
|
@@ -756,6 +826,19 @@ export class PlTreeState {
|
|
|
756
826
|
this.resources.set(resource.id, resource);
|
|
757
827
|
this.resourcesAdded.markChanged(`new resource ${resourceIdToString(resource.id)} added`);
|
|
758
828
|
}
|
|
829
|
+
|
|
830
|
+
if (stat) {
|
|
831
|
+
if (!held) stat.resourcesNew++;
|
|
832
|
+
else if (changed) {
|
|
833
|
+
stat.resourcesChanged++;
|
|
834
|
+
if (!metadataChanged) stat.metadataStableChanged++;
|
|
835
|
+
} else {
|
|
836
|
+
stat.resourcesUnchanged++;
|
|
837
|
+
stat.bytesUnchanged += rd.data?.length ?? 0;
|
|
838
|
+
for (const kv of rd.kv) stat.bytesUnchanged += kv.value.length;
|
|
839
|
+
if (!stat.usedStreaming) stat.bfsRequestsWasted++;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
759
842
|
}
|
|
760
843
|
|
|
761
844
|
// applying refCount increments
|
package/src/sync.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
} from "@milaboratories/pl-client";
|
|
9
9
|
import Denque from "denque";
|
|
10
10
|
import { hasCapability, isNullSignedResourceId } from "@milaboratories/pl-client";
|
|
11
|
-
import type { ExtendedResourceData, PlTreeState } from "./state";
|
|
11
|
+
import type { ExtendedResourceData, PlTreeState, ResourceUpdateStat } from "./state";
|
|
12
12
|
import { ConcurrencyLimitingExecutor, msToHumanReadable } from "@milaboratories/ts-helpers";
|
|
13
13
|
|
|
14
14
|
/** Applied to list of fields in resource data. */
|
|
@@ -70,7 +70,7 @@ export function constructTreeLoadingRequest(
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
export type TreeLoadingStat = {
|
|
73
|
+
export type TreeLoadingStat = ResourceUpdateStat & {
|
|
74
74
|
requests: number;
|
|
75
75
|
roundTrips: number;
|
|
76
76
|
retrievedResources: number;
|
|
@@ -85,6 +85,20 @@ export type TreeLoadingStat = {
|
|
|
85
85
|
stopMarkersSkipped: number;
|
|
86
86
|
/** Number of follow-up resourceTree() calls issued to resolve unknown stop markers. */
|
|
87
87
|
stopMarkerFollowUpRoundTrips: number;
|
|
88
|
+
/** Streaming (resourceTree) path: resourceTree() streams consumed (1, or 2 with a follow-up). */
|
|
89
|
+
streamRounds: number;
|
|
90
|
+
/** Streaming path: resource frames received. */
|
|
91
|
+
resourceFrames: number;
|
|
92
|
+
/** Streaming path: stopMarker frames received (both skipped and follow-up). */
|
|
93
|
+
stopMarkerFrames: number;
|
|
94
|
+
/** Streaming path: stop markers that were not final locally and triggered a follow-up fetch. */
|
|
95
|
+
stopMarkersFollowUp: number;
|
|
96
|
+
/** Streaming path: frames where the backend stopped traversal (final or traverseWasStopped). */
|
|
97
|
+
traverseWasStoppedCount: number;
|
|
98
|
+
/** BFS path: resource states actually requested from the backend this cycle (after intra-cycle dedup). */
|
|
99
|
+
bfsResourcesRequested: number;
|
|
100
|
+
/** BFS path: requested resources that no longer exist (undefined reply). */
|
|
101
|
+
bfsResourcesNotFound: number;
|
|
88
102
|
};
|
|
89
103
|
|
|
90
104
|
export function initialTreeLoadingStat(): TreeLoadingStat {
|
|
@@ -101,23 +115,54 @@ export function initialTreeLoadingStat(): TreeLoadingStat {
|
|
|
101
115
|
millisSpent: 0,
|
|
102
116
|
stopMarkersSkipped: 0,
|
|
103
117
|
stopMarkerFollowUpRoundTrips: 0,
|
|
118
|
+
streamRounds: 0,
|
|
119
|
+
resourceFrames: 0,
|
|
120
|
+
stopMarkerFrames: 0,
|
|
121
|
+
stopMarkersFollowUp: 0,
|
|
122
|
+
traverseWasStoppedCount: 0,
|
|
123
|
+
bfsResourcesRequested: 0,
|
|
124
|
+
bfsResourcesNotFound: 0,
|
|
125
|
+
resourcesNew: 0,
|
|
126
|
+
resourcesChanged: 0,
|
|
127
|
+
resourcesUnchanged: 0,
|
|
128
|
+
bytesUnchanged: 0,
|
|
129
|
+
metadataStableChanged: 0,
|
|
130
|
+
bfsRequestsWasted: 0,
|
|
131
|
+
usedStreaming: false,
|
|
132
|
+
fieldsAdded: 0,
|
|
133
|
+
fieldsRemoved: 0,
|
|
134
|
+
fieldsChanged: 0,
|
|
135
|
+
kvChanged: 0,
|
|
136
|
+
readyFlips: 0,
|
|
137
|
+
locksChanged: 0,
|
|
138
|
+
errorsAttached: 0,
|
|
139
|
+
duplicatesResolved: 0,
|
|
140
|
+
resourcesMarkedFinal: 0,
|
|
104
141
|
};
|
|
105
142
|
}
|
|
106
143
|
|
|
107
144
|
export function formatTreeLoadingStat(stat: TreeLoadingStat): string {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
145
|
+
return `Requests: ${stat.requests}
|
|
146
|
+
Total time: ${msToHumanReadable(stat.millisSpent)}
|
|
147
|
+
Round-trips: ${stat.roundTrips}
|
|
148
|
+
Resources: ${stat.retrievedResources}
|
|
149
|
+
Fields: ${stat.retrievedFields}
|
|
150
|
+
KV: ${stat.retrievedKeyValues}
|
|
151
|
+
Data Bytes: ${stat.retrievedResourceDataBytes}
|
|
152
|
+
KV Bytes: ${stat.retrievedKeyValueBytes}
|
|
153
|
+
Pruned fields: ${stat.prunedFields}
|
|
154
|
+
Final resources skipped: ${stat.finalResourcesSkipped}
|
|
155
|
+
Stop markers skipped: ${stat.stopMarkersSkipped}
|
|
156
|
+
Stop marker follow-up round-trips: ${stat.stopMarkerFollowUpRoundTrips}
|
|
157
|
+
New resources: ${stat.resourcesNew}
|
|
158
|
+
Changed resources: ${stat.resourcesChanged}
|
|
159
|
+
Unchanged (duplicate re-fetch) resources: ${stat.resourcesUnchanged}
|
|
160
|
+
Unchanged bytes (wasted downlink): ${stat.bytesUnchanged}
|
|
161
|
+
Changed with stable metadata: ${stat.metadataStableChanged}
|
|
162
|
+
BFS fetches wasted on unchanged: ${stat.bfsRequestsWasted}
|
|
163
|
+
Used streaming: ${stat.usedStreaming}
|
|
164
|
+
[streaming] rounds: ${stat.streamRounds}, resource frames: ${stat.resourceFrames}, stop-marker frames: ${stat.stopMarkerFrames}, stop->follow-up: ${stat.stopMarkersFollowUp}, traverse-stopped: ${stat.traverseWasStoppedCount}
|
|
165
|
+
[bfs] resources requested: ${stat.bfsResourcesRequested}, not found: ${stat.bfsResourcesNotFound}`;
|
|
121
166
|
}
|
|
122
167
|
|
|
123
168
|
function supportsResourceTreeTraversal(capabilities: readonly string[] = []): boolean {
|
|
@@ -164,6 +209,7 @@ async function loadTreeStateViaBfs(
|
|
|
164
209
|
}
|
|
165
210
|
|
|
166
211
|
requested.add(rid);
|
|
212
|
+
if (stats) stats.bfsResourcesRequested++;
|
|
167
213
|
|
|
168
214
|
pending.push(
|
|
169
215
|
limiter.run(async () => {
|
|
@@ -183,7 +229,10 @@ async function loadTreeStateViaBfs(
|
|
|
183
229
|
roundTripToggle = true;
|
|
184
230
|
}
|
|
185
231
|
|
|
186
|
-
if (resource === undefined)
|
|
232
|
+
if (resource === undefined) {
|
|
233
|
+
if (stats) stats.bfsResourcesNotFound++;
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
187
236
|
if (kv === undefined) throw new Error("Inconsistent replies");
|
|
188
237
|
|
|
189
238
|
return { ...resource, kv };
|
|
@@ -247,15 +296,21 @@ async function processResourceTreeStream(
|
|
|
247
296
|
// should make a decision: has it already loaded the resource or should it be requested for get the latest state?
|
|
248
297
|
for await (const frame of treeItems) {
|
|
249
298
|
if (frame.frameKind === "stopMarker") {
|
|
299
|
+
if (stats) stats.stopMarkerFrames++;
|
|
250
300
|
if (finalResources.has(frame.id)) {
|
|
251
301
|
if (stats) stats.stopMarkersSkipped++;
|
|
252
302
|
continue;
|
|
253
303
|
}
|
|
304
|
+
if (stats) stats.stopMarkersFollowUp++;
|
|
254
305
|
followUpSeeds.push(frame.id);
|
|
255
306
|
continue;
|
|
256
307
|
}
|
|
257
308
|
|
|
258
309
|
// Normal resource frame.
|
|
310
|
+
if (stats) {
|
|
311
|
+
stats.resourceFrames++;
|
|
312
|
+
if (frame.traverseWasStopped) stats.traverseWasStoppedCount++;
|
|
313
|
+
}
|
|
259
314
|
if (finalResources.has(frame.id)) {
|
|
260
315
|
if (stats) stats.finalResourcesSkipped++;
|
|
261
316
|
continue;
|
|
@@ -317,16 +372,30 @@ async function loadTreeStateViaResourceTree(
|
|
|
317
372
|
pruningFunction,
|
|
318
373
|
stats,
|
|
319
374
|
);
|
|
320
|
-
if (stats)
|
|
375
|
+
if (stats) {
|
|
376
|
+
stats.roundTrips++;
|
|
377
|
+
stats.streamRounds++;
|
|
378
|
+
}
|
|
321
379
|
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
380
|
+
// Resolve stop-marker seeds by fetching them (see the note below on why the stop rule is not
|
|
381
|
+
// reapplied). Loop in case a fetch still yields stop markers; dedup fetched ids so shared refs
|
|
382
|
+
// or diamonds cannot loop forever (the id set is finite, so the loop terminates). A referenced
|
|
383
|
+
// resource left unloaded would make updateFromResourceData throw "orphan resource".
|
|
384
|
+
let pendingSeeds = followUpSeeds;
|
|
385
|
+
const fetchedSeeds = new Set<SignedResourceId>();
|
|
386
|
+
while (pendingSeeds.length > 0) {
|
|
387
|
+
const roundSeeds = pendingSeeds.filter((id) => !fetchedSeeds.has(id));
|
|
388
|
+
if (roundSeeds.length === 0) break;
|
|
389
|
+
for (const id of roundSeeds) fetchedSeeds.add(id);
|
|
390
|
+
|
|
391
|
+
// No traverseStopRules here on purpose: the seeds are the stop-marked resources, and the
|
|
392
|
+
// stop rule (finality-based) would re-flag them as stop markers instead of loading their
|
|
393
|
+
// state, leaving resources that reference them as orphans. The retry must fetch them plainly.
|
|
394
|
+
const followUpItems = tx.resourceTree(roundSeeds, {
|
|
326
395
|
includeKv: true,
|
|
327
396
|
fieldFilter,
|
|
328
397
|
});
|
|
329
|
-
const { result: followUpResult } = await processResourceTreeStream(
|
|
398
|
+
const { result: followUpResult, followUpSeeds: nextSeeds } = await processResourceTreeStream(
|
|
330
399
|
followUpItems,
|
|
331
400
|
finalResources,
|
|
332
401
|
pruningFunction,
|
|
@@ -335,11 +404,13 @@ async function loadTreeStateViaResourceTree(
|
|
|
335
404
|
result.push(...followUpResult);
|
|
336
405
|
if (stats) {
|
|
337
406
|
logger?.info?.(
|
|
338
|
-
`loadTreeStateViaResourceTree: follow-up request for ${
|
|
407
|
+
`loadTreeStateViaResourceTree: follow-up request for ${roundSeeds.length} stop-marker seeds: ${JSON.stringify(roundSeeds)}`,
|
|
339
408
|
);
|
|
340
409
|
stats.roundTrips++;
|
|
410
|
+
stats.streamRounds++;
|
|
341
411
|
stats.stopMarkerFollowUpRoundTrips++;
|
|
342
412
|
}
|
|
413
|
+
pendingSeeds = nextSeeds;
|
|
343
414
|
}
|
|
344
415
|
|
|
345
416
|
return result;
|
|
@@ -364,6 +435,8 @@ export async function loadTreeState(
|
|
|
364
435
|
mode === "backend-streaming" ||
|
|
365
436
|
(mode === "auto" && supportsResourceTreeTraversal(capabilities));
|
|
366
437
|
|
|
438
|
+
if (stats) stats.usedStreaming = wantsStreaming && supportsResourceTreeTraversal(capabilities);
|
|
439
|
+
|
|
367
440
|
if (wantsStreaming && !supportsResourceTreeTraversal(capabilities)) {
|
|
368
441
|
const msg =
|
|
369
442
|
"traversalMode=backend-streaming but backend lacks treeFilter:v2 capability; falling back to BFS";
|
package/src/synchronized_tree.ts
CHANGED
|
@@ -276,7 +276,7 @@ export class SynchronizedTreeState {
|
|
|
276
276
|
},
|
|
277
277
|
txOps,
|
|
278
278
|
);
|
|
279
|
-
this.state.updateFromResourceData(data, true);
|
|
279
|
+
this.state.updateFromResourceData(data, { allowOrphanInputs: true, stat: stats });
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
/** Discovery sync for shared-type seeds: re-polls `ListUserResources` (gRPC-only) and
|